## GitHub 同步说明
| 项 | 内容 |
|:---|:---|
| 仓库 | https://github.com/fnvtk/cunkebao_v3 |
| 分支 | develop |
| 方向 | 本地 → GitHub(单向 push) |
| 是否拉取远程 | 否(未 fetch / pull / merge) |
| 是否改本地文件 | 否(仅 git add/commit,未编辑任何源文件正文) |
| 基准 commit | 0643e78a |
| 变更规模 | 709 files, +26436 / -3407 lines |
## 目录变更统计
| 目录 | 文件数 | 说明 |
|:---|---:|:---|
| 开发文档/ | 404 | 四端进行中/已完成需求、接口文档、官网需求、部署脚本 |
| Server/ | 95 | 后端 API、迁移脚本、触客宝/超管/存客宝服务 |
| Cunkebao/ | 85 | 移动端 H5、设备绑定、客服、转号审批等 |
| Touchkebao/ | 68 | PC 工作台、AI 获客、算力中心、Glass UI |
| SuperAdmin/ | 33 | 超管项目中心、算力计费、内容库 |
| 官网/ | 19 | 新增存客宝官网静态页(index/pricing/solutions 等) |
| .cursor/ | 3 | Cursor 规则与 Skill |
| .obsidian/ | 2 | Obsidian 工作区配置 |
## 重点新增/更新(本地为准)
- 官网/: 完整静态站点(index、pricing、manual、solutions、api)
- 开发文档/1、需求/修改/*_进行中_20260530.md(存客宝/触客宝/AI数智员工)
- 开发文档/1、需求/官网需求.md
- 开发文档/5、接口/开发进度_接口文档.md 等接口文档迭代
- Touchkebao Glass UI 与 P0 收尾、通道获客算力
- SuperAdmin 算力计费规则、项目工作台
## 刻意未上传(本地保留)
- node_modules/
- .DS_Store、.smart-env/
- .开发文档_nested_git_backup/(旧嵌套 git 备份)
- **/__pycache__/
## 验收
GitHub 浏览: https://github.com/fnvtk/cunkebao_v3/tree/develop
Co-authored-by: Cursor <cursoragent@cursor.com>
716 lines
25 KiB
PHP
716 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* 开放对接平台 · 领域只读 API
|
||
*
|
||
* 5 大领域,均要求 Authorization: Bearer <openToken>:
|
||
* GET /v1/open/scenes ← 场景类型字典(含计划下的统计)
|
||
* GET /v1/open/leads ← 客资明细列表(当前计划下)
|
||
* GET /v1/open/wechats ← 微信号列表(companyId 隔离)
|
||
* GET /v1/open/devices ← 设备列表(companyId 隔离)
|
||
* GET /v1/open/stats ← 平台关键指标
|
||
*
|
||
* 设计原则:
|
||
* - 严格按 JWT.companyId 过滤数据,禁止跨租户读取
|
||
* - 默认分页(page/limit),limit 上限 200
|
||
* - 字段精简,只暴露对接方需要的关键列
|
||
*
|
||
* 文档:开发文档/5、接口/08-存客宝开放对接平台/03-领域接口表.md
|
||
*
|
||
* @package app\common\controller
|
||
*/
|
||
|
||
namespace app\common\controller;
|
||
|
||
use app\common\service\DeviceTaskConfigService;
|
||
use app\common\service\OpenPlatformService;
|
||
use app\cunkebao\service\ContentItemMediaService;
|
||
use library\ResponseHelper;
|
||
use think\Controller;
|
||
use think\Db;
|
||
use think\facade\Request;
|
||
|
||
class OpenPlatformController extends Controller
|
||
{
|
||
/**
|
||
* 统一鉴权 + 返回上下文,失败抛 401
|
||
*/
|
||
private function ctx(): array
|
||
{
|
||
$ctx = OpenPlatformService::resolveContext();
|
||
if (!$ctx) {
|
||
ResponseHelper::unauthorized('未授权或 Token 已过期')->send();
|
||
exit;
|
||
}
|
||
return $ctx;
|
||
}
|
||
|
||
/**
|
||
* 平台健康检查(不鉴权,公开)
|
||
* GET /v1/open/health
|
||
*/
|
||
public function health()
|
||
{
|
||
return ResponseHelper::success([
|
||
'service' => 'cunkebao-open-platform',
|
||
'version' => '1.0',
|
||
'time' => time(),
|
||
'datetime' => date('Y-m-d H:i:s'),
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 当前 Token 解析(调试用,等价 whoami)
|
||
* GET /v1/open/me
|
||
*/
|
||
public function me()
|
||
{
|
||
$ctx = $this->ctx();
|
||
return ResponseHelper::success([
|
||
'planId' => $ctx['planId'],
|
||
'planName' => $ctx['planName'],
|
||
'companyId' => $ctx['companyId'],
|
||
'apiKey' => substr((string) $ctx['apiKey'], 0, 6) . '****',
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 场景类型字典(含本计划下的统计)
|
||
* GET /v1/open/scenes
|
||
*/
|
||
public function scenes()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$scenes = Db::name('plan_scene')
|
||
->field('id,name,image,sort')
|
||
->where(['status' => 1, 'deleteTime' => 0])
|
||
->order('sort DESC, id ASC')
|
||
->select();
|
||
|
||
if (empty($scenes)) {
|
||
return ResponseHelper::success([], 'ok');
|
||
}
|
||
|
||
$sceneIds = array_column($scenes, 'id');
|
||
$stats = Db::name('customer_acquisition_task')->alias('ac')
|
||
->join('task_customer tc', 'tc.task_id = ac.id')
|
||
->where([
|
||
['ac.companyId', '=', $ctx['companyId']],
|
||
['ac.deleteTime', '=', 0],
|
||
['ac.sceneId', 'in', $sceneIds],
|
||
])
|
||
->field([
|
||
'ac.sceneId',
|
||
Db::raw('COUNT(1) as allNum'),
|
||
Db::raw("SUM(CASE WHEN tc.status IN (1,2,3,4) THEN 1 ELSE 0 END) as addNum"),
|
||
Db::raw("SUM(CASE WHEN tc.status = 4 THEN 1 ELSE 0 END) as passNum"),
|
||
])
|
||
->group('ac.sceneId')
|
||
->select();
|
||
|
||
$statsMap = [];
|
||
foreach ($stats as $r) {
|
||
$sid = is_array($r) ? ($r['sceneId'] ?? 0) : ($r->sceneId ?? 0);
|
||
$statsMap[(int) $sid] = [
|
||
'allNum' => (int) (is_array($r) ? ($r['allNum'] ?? 0) : ($r->allNum ?? 0)),
|
||
'addNum' => (int) (is_array($r) ? ($r['addNum'] ?? 0) : ($r->addNum ?? 0)),
|
||
'passNum' => (int) (is_array($r) ? ($r['passNum'] ?? 0) : ($r->passNum ?? 0)),
|
||
];
|
||
}
|
||
|
||
foreach ($scenes as &$row) {
|
||
$s = $statsMap[(int) $row['id']] ?? ['allNum' => 0, 'addNum' => 0, 'passNum' => 0];
|
||
$row['allNum'] = $s['allNum'];
|
||
$row['addNum'] = $s['addNum'];
|
||
$row['passNum'] = $s['passNum'];
|
||
}
|
||
unset($row);
|
||
|
||
return ResponseHelper::success($scenes, 'ok');
|
||
}
|
||
|
||
/**
|
||
* 当前 Token 所属计划下的客资列表
|
||
* GET /v1/open/leads?page=1&limit=50&status=&startTime=&endTime=
|
||
*/
|
||
public function leads()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
$status = Request::param('status');
|
||
$startTime = intval(Request::param('startTime', 0));
|
||
$endTime = intval(Request::param('endTime', 0));
|
||
|
||
$where = [
|
||
['task_id', '=', $ctx['planId']],
|
||
];
|
||
if ($status !== null && $status !== '') {
|
||
$where[] = ['status', '=', intval($status)];
|
||
}
|
||
if ($startTime > 0 && $endTime > 0) {
|
||
$where[] = ['createTime', 'between', [$startTime, $endTime]];
|
||
}
|
||
|
||
$query = Db::name('task_customer')->where($where);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,task_id,channelId,phone,name,source,remark,tags,siteTags,status,createTime,updateTime')
|
||
->order('createTime DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
$list = array_map(function ($r) {
|
||
$r['tags'] = json_decode($r['tags'] ?? '[]', true) ?: [];
|
||
$r['siteTags'] = json_decode($r['siteTags'] ?? '[]', true) ?: [];
|
||
return $r;
|
||
}, $rows);
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $list,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 当前公司下的微信号
|
||
* GET /v1/open/wechats?page=1&limit=50&alive=1
|
||
*/
|
||
public function wechats()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
$alive = Request::param('alive');
|
||
|
||
// 先按公司过滤设备账号(沿用 StatsController 的 join 路径)
|
||
$accountIds = Db::table('s2_company_account')
|
||
->where('departmentId', $ctx['companyId'])
|
||
->column('id');
|
||
|
||
if (empty($accountIds)) {
|
||
return ResponseHelper::success(['total' => 0, 'list' => []], 'ok');
|
||
}
|
||
|
||
$query = Db::table('s2_wechat_account')->whereIn('deviceAccountId', $accountIds);
|
||
if ($alive !== null && $alive !== '') {
|
||
$query->where('wechatAlive', intval($alive));
|
||
}
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,wechatId,nickname,avatar as headImage,wechatAlive,deviceAccountId,createTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $rows,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 当前公司下的设备列表(轻量版)
|
||
* GET /v1/open/devices?page=1&limit=50
|
||
*/
|
||
public function devices()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
|
||
$query = Db::table('s2_company_account')
|
||
->where('departmentId', $ctx['companyId']);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,userName,realName,memo,alive,lastLoginTime,createTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $rows,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 按 IMEI / 设备序列号 / accountId 解析绑定用手机 accountId
|
||
* GET /v1/open/devices/resolve?imei=bc8bf95f...
|
||
*/
|
||
public function resolveDevice()
|
||
{
|
||
$ctx = $this->ctx();
|
||
// §7.4 支持按 accountId / imei / deviceIdMd5 解析(白名单:必须命中已登记设备)
|
||
$imei = trim((string) Request::param('imei', ''));
|
||
if ($imei === '') {
|
||
$imei = trim((string) Request::param('accountId', ''));
|
||
}
|
||
if ($imei === '') {
|
||
$imei = trim((string) Request::param('keyword', ''));
|
||
}
|
||
if ($imei === '') {
|
||
return ResponseHelper::error('accountId / imei 必填', 400);
|
||
}
|
||
|
||
$resolved = OpenPlatformService::resolveAccountByIdentifier((int) $ctx['companyId'], $imei);
|
||
if (!$resolved || empty($resolved['accountId'])) {
|
||
// 未登记设备:禁止绑定,统一文案,供扫码端直接展示
|
||
return ResponseHelper::error('无法绑定,请联系管理员', 403, [
|
||
'bindable' => false,
|
||
'reason' => 'NOT_REGISTERED',
|
||
]);
|
||
}
|
||
|
||
$resolved['bindable'] = true;
|
||
return ResponseHelper::success($resolved, 'ok');
|
||
}
|
||
|
||
/**
|
||
* 按存客宝手机号列出该公司下可绑定的工作手机
|
||
* GET /v1/open/terminal/devices-by-phone?phone=13800138000
|
||
*/
|
||
public function devicesByPhone()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$phone = trim((string) Request::param('phone', ''));
|
||
if ($phone === '') {
|
||
return ResponseHelper::error('phone 必填', 400);
|
||
}
|
||
|
||
$list = OpenPlatformService::listDevicesByPhone((int) $ctx['companyId'], $phone);
|
||
return ResponseHelper::success([
|
||
'total' => count($list),
|
||
'list' => $list,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 登记 AI数智员工终端 IMEI(deviceIdMd5)供 VIP 自动登录
|
||
* POST /v1/open/terminal/register-imei { accountId, deviceIdMd5 }
|
||
*/
|
||
public function registerTerminalImei()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$deviceIdMd5 = trim((string) Request::param('deviceIdMd5', ''));
|
||
$accountId = (int) Request::param('accountId', 0);
|
||
|
||
if ($deviceIdMd5 === '' || $accountId <= 0) {
|
||
return ResponseHelper::error('accountId 与 deviceIdMd5 必填', 400);
|
||
}
|
||
|
||
// §7 白名单铁律:accountId 必须对应已登记的 ck_device,否则禁止绑定(不得扫即注册)
|
||
$resolved = OpenPlatformService::resolveAccountByIdentifier((int) $ctx['companyId'], (string) $accountId);
|
||
if (!$resolved || (int) ($resolved['accountId'] ?? 0) !== $accountId) {
|
||
return ResponseHelper::error('无法绑定,请联系管理员', 403, [
|
||
'bindable' => false,
|
||
'reason' => 'NOT_REGISTERED',
|
||
]);
|
||
}
|
||
|
||
$ok = OpenPlatformService::registerTerminalImei((int) $ctx['companyId'], $accountId, $deviceIdMd5);
|
||
if (!$ok) {
|
||
return ResponseHelper::error('无法绑定,请联系管理员', 403, [
|
||
'bindable' => false,
|
||
'reason' => 'NOT_REGISTERED',
|
||
]);
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'accountId' => $accountId,
|
||
'deviceIdMd5' => strtolower($deviceIdMd5),
|
||
'imei' => strtolower($deviceIdMd5),
|
||
'bindable' => true,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 校验终端 deviceIdMd5 与 accountId 是否已绑定(自动登录前置)
|
||
* GET /v1/open/terminal/verify?deviceIdMd5=&accountId=
|
||
*/
|
||
public function verifyTerminal()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$deviceIdMd5 = trim((string) Request::param('deviceIdMd5', ''));
|
||
$accountId = (int) Request::param('accountId', 0);
|
||
|
||
if ($deviceIdMd5 === '' || $accountId <= 0) {
|
||
return ResponseHelper::error('deviceIdMd5 与 accountId 必填', 400);
|
||
}
|
||
|
||
$matched = OpenPlatformService::verifyTerminalDeviceMatch(
|
||
(int) $ctx['companyId'],
|
||
$deviceIdMd5,
|
||
$accountId
|
||
);
|
||
|
||
if (!$matched) {
|
||
// 也尝试直接用 deviceIdMd5 作为 IMEI 解析
|
||
$resolved = OpenPlatformService::resolveAccountByIdentifier((int) $ctx['companyId'], $deviceIdMd5);
|
||
$matched = $resolved && (int) ($resolved['accountId'] ?? 0) === $accountId;
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'matched' => $matched,
|
||
'accountId' => $accountId,
|
||
'deviceIdMd5' => strtolower($deviceIdMd5),
|
||
], $matched ? 'ok' : '未匹配');
|
||
}
|
||
|
||
// =========================================================
|
||
// v1.1 新增领域:流量池(V2)
|
||
// =========================================================
|
||
|
||
/**
|
||
* 流量池成员列表(V2)
|
||
* GET /v1/open/pool/list?page=&limit=&keyword=
|
||
*
|
||
* 字段口径与 TrafficPoolV2Controller.getPoolList 保持一致(companyId 过滤 + 软删除排除)
|
||
*/
|
||
public function poolList()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
$keyword = trim((string) Request::param('keyword', ''));
|
||
|
||
$where = [
|
||
['companyId', '=', $ctx['companyId']],
|
||
['deleteTime', '=', 0],
|
||
];
|
||
if ($keyword !== '') {
|
||
$where[] = ['mobile|wechatId|nickname', 'like', '%' . $keyword . '%'];
|
||
}
|
||
|
||
$query = Db::name('traffic_pool')->where($where);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,companyId,identifierType,mobile,wechatId,nickname,avatar,createTime,updateTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $rows,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 流量池成员详情(V2)
|
||
* GET /v1/open/pool/detail?id=
|
||
*/
|
||
public function poolDetail()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$id = intval(Request::param('id', 0));
|
||
if ($id <= 0) {
|
||
return ResponseHelper::error('id 不能为空', 400);
|
||
}
|
||
$row = Db::name('traffic_pool')
|
||
->where('id', $id)
|
||
->where('companyId', $ctx['companyId'])
|
||
->where('deleteTime', 0)
|
||
->find();
|
||
if (!$row) {
|
||
return ResponseHelper::error('未找到或不属于当前公司', 404);
|
||
}
|
||
return ResponseHelper::success($row, 'ok');
|
||
}
|
||
|
||
// =========================================================
|
||
// v1.1 新增领域:内容库
|
||
// =========================================================
|
||
|
||
/**
|
||
* 内容库列表
|
||
* GET /v1/open/content/library?page=&limit=
|
||
*
|
||
* 字段口径与 ContentLibraryController.getList 一致(companyId 过滤 + 未删除)
|
||
*/
|
||
public function contentLibrary()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
|
||
$where = [
|
||
['companyId', '=', $ctx['companyId']],
|
||
['isDel', '=', 0],
|
||
];
|
||
$query = Db::name('content_library')->where($where);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,companyId,name,type,description,createTime,updateTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $rows,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 内容库素材列表
|
||
* GET /v1/open/content/items?libraryId=&page=&limit=
|
||
*/
|
||
public function contentItems()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$libraryId = intval(Request::param('libraryId', 0));
|
||
if ($libraryId <= 0) {
|
||
return ResponseHelper::error('libraryId 不能为空', 400);
|
||
}
|
||
// 校验内容库归属
|
||
$lib = Db::name('content_library')
|
||
->where('id', $libraryId)
|
||
->where('companyId', $ctx['companyId'])
|
||
->where('isDel', 0)
|
||
->find();
|
||
if (!$lib) {
|
||
return ResponseHelper::error('内容库不存在或不属于当前公司', 404);
|
||
}
|
||
|
||
$pager = OpenPlatformService::resolvePager();
|
||
$contentType = Request::param('contentType', '');
|
||
|
||
$where = [
|
||
['libraryId', '=', $libraryId],
|
||
['isDel', '=', 0],
|
||
];
|
||
if ($contentType !== '' && $contentType !== null) {
|
||
$where[] = ['contentType', '=', intval($contentType)];
|
||
}
|
||
|
||
$query = Db::name('content_item')->where($where);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,libraryId,contentType,title,content,type,resUrls,ossUrls,urls,coverImage,createTime,updateTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
$list = array_map(function ($row) {
|
||
return ContentItemMediaService::formatForOpenApi(is_array($row) ? $row : $row->toArray());
|
||
}, $rows ?: []);
|
||
|
||
return ResponseHelper::success([
|
||
'libraryId' => $libraryId,
|
||
'libraryName' => $lib['name'] ?? '',
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $list,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 设备任务开关(工作手机 Agent / 外部系统只读拉取)
|
||
* GET /v1/open/devices/task-config?deviceId=&imei=
|
||
*/
|
||
public function deviceTaskConfig()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$companyId = (int)$ctx['companyId'];
|
||
$deviceId = intval(Request::param('deviceId', 0));
|
||
|
||
if ($deviceId <= 0) {
|
||
$imei = trim((string)Request::param('imei', ''));
|
||
if ($imei === '') {
|
||
$imei = trim((string)Request::param('keyword', ''));
|
||
}
|
||
if ($imei !== '') {
|
||
$resolved = OpenPlatformService::resolveAccountByIdentifier($companyId, $imei);
|
||
$deviceId = (int)($resolved['deviceId'] ?? 0);
|
||
}
|
||
}
|
||
|
||
if ($deviceId <= 0) {
|
||
return ResponseHelper::error('deviceId 或 imei 必填', 400);
|
||
}
|
||
|
||
try {
|
||
DeviceTaskConfigService::assertDeviceExists($deviceId, $companyId);
|
||
$row = DeviceTaskConfigService::getOrCreate($deviceId, $companyId);
|
||
|
||
return ResponseHelper::success([
|
||
'deviceId' => $deviceId,
|
||
'companyId' => $companyId,
|
||
'features' => DeviceTaskConfigService::toFeatures($row),
|
||
'switches' => [
|
||
'autoAddFriend' => (int)($row['autoAddFriend'] ?? 0),
|
||
'autoCustomerDev' => (int)($row['autoCustomerDev'] ?? 0),
|
||
'autoReply' => (int)($row['autoReply'] ?? 0),
|
||
'momentsSync' => (int)($row['momentsSync'] ?? 0),
|
||
'aiChat' => (int)($row['aiChat'] ?? 0),
|
||
],
|
||
], 'ok');
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||
}
|
||
}
|
||
|
||
// =========================================================
|
||
// v1.1 新增领域:算力(tokens)
|
||
// =========================================================
|
||
|
||
/**
|
||
* 算力余额
|
||
* GET /v1/open/tokens/balance
|
||
*
|
||
* 注意:现有 tokens_company 是 (companyId, userId) 双键。
|
||
* 开放平台没有"当前用户"概念,按 companyId 汇总余额。
|
||
*/
|
||
public function tokensBalance()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$balance = (int) Db::name('tokens_company')
|
||
->where('companyId', $ctx['companyId'])
|
||
->sum('tokens');
|
||
|
||
// 今日 / 本月消耗(type=0 减少)
|
||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||
$todayUsed = (int) Db::name('tokens_record')
|
||
->where('companyId', $ctx['companyId'])
|
||
->where('type', 0)
|
||
->whereBetween('createTime', [$start, $end])
|
||
->sum('tokens');
|
||
|
||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||
$monthEnd = strtotime(date('Y-m-t 23:59:59'));
|
||
$monthUsed = (int) Db::name('tokens_record')
|
||
->where('companyId', $ctx['companyId'])
|
||
->where('type', 0)
|
||
->whereBetween('createTime', [$monthStart, $monthEnd])
|
||
->sum('tokens');
|
||
|
||
$totalRecharged = (int) Db::name('tokens_record')
|
||
->where('companyId', $ctx['companyId'])
|
||
->where('type', 1)
|
||
->sum('tokens');
|
||
|
||
return ResponseHelper::success([
|
||
'companyId' => $ctx['companyId'],
|
||
'remainingTokens' => $balance,
|
||
'todayUsed' => $todayUsed,
|
||
'monthUsed' => $monthUsed,
|
||
'totalRecharged' => $totalRecharged,
|
||
'timestamp' => time(),
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 算力使用明细(支持类型/时间过滤)
|
||
* GET /v1/open/tokens/usage?page=&limit=&type=&startTime=&endTime=
|
||
*
|
||
* type: 0=消费 1=充值/分配(来自 tokens_record.type)
|
||
*/
|
||
public function tokensUsage()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$pager = OpenPlatformService::resolvePager();
|
||
$type = Request::param('type');
|
||
$startTime = intval(Request::param('startTime', 0));
|
||
$endTime = intval(Request::param('endTime', 0));
|
||
|
||
$where = [['companyId', '=', $ctx['companyId']]];
|
||
if ($type !== null && $type !== '') {
|
||
$where[] = ['type', '=', intval($type)];
|
||
}
|
||
if ($startTime > 0 && $endTime > 0) {
|
||
$where[] = ['createTime', 'between', [$startTime, $endTime]];
|
||
}
|
||
|
||
$query = Db::name('tokens_record')->where($where);
|
||
$total = (clone $query)->count();
|
||
$rows = $query
|
||
->field('id,companyId,userId,type,form,tokens,balanceTokens,remarks,createTime')
|
||
->order('id DESC')
|
||
->page($pager['page'], $pager['limit'])
|
||
->select();
|
||
|
||
return ResponseHelper::success([
|
||
'total' => $total,
|
||
'page' => $pager['page'],
|
||
'limit' => $pager['limit'],
|
||
'list' => $rows,
|
||
], 'ok');
|
||
}
|
||
|
||
/**
|
||
* 平台关键指标(设备 / 微信 / 客资 / 计划 / 今日新增 / + v1.1 算力)
|
||
* GET /v1/open/stats
|
||
*/
|
||
public function stats()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$companyId = $ctx['companyId'];
|
||
|
||
$accountIds = Db::table('s2_company_account')
|
||
->where('departmentId', $companyId)
|
||
->column('id');
|
||
|
||
$deviceNum = count($accountIds);
|
||
$wechatNum = 0;
|
||
$aliveWechatNum = 0;
|
||
if (!empty($accountIds)) {
|
||
$wechatNum = (int) Db::table('s2_wechat_account')->whereIn('deviceAccountId', $accountIds)->count();
|
||
$aliveWechatNum = (int) Db::table('s2_wechat_account')
|
||
->whereIn('deviceAccountId', $accountIds)
|
||
->where('wechatAlive', 1)
|
||
->count();
|
||
}
|
||
|
||
$planNum = (int) Db::name('customer_acquisition_task')
|
||
->where(['companyId' => $companyId, 'deleteTime' => 0, 'status' => 1])
|
||
->count();
|
||
|
||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||
|
||
$todayNewCustomers = (int) Db::name('customer_acquisition_task')->alias('ac')
|
||
->join('task_customer tc', 'tc.task_id = ac.id')
|
||
->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
|
||
->whereBetween('tc.createTime', [$start, $end])
|
||
->count();
|
||
|
||
$totalCustomers = (int) Db::name('customer_acquisition_task')->alias('ac')
|
||
->join('task_customer tc', 'tc.task_id = ac.id')
|
||
->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
|
||
->count();
|
||
|
||
$onlineRate = $wechatNum > 0
|
||
? round($aliveWechatNum / $wechatNum * 100, 1) . '%'
|
||
: '0%';
|
||
|
||
// v1.1:算力概览(不动 TokensController,复用 tokens_company / tokens_record 表)
|
||
$tokensRemaining = (int) Db::name('tokens_company')->where('companyId', $companyId)->sum('tokens');
|
||
$tokensTodayUsed = (int) Db::name('tokens_record')
|
||
->where('companyId', $companyId)
|
||
->where('type', 0)
|
||
->whereBetween('createTime', [$start, $end])
|
||
->sum('tokens');
|
||
|
||
return ResponseHelper::success([
|
||
'companyId' => $companyId,
|
||
'deviceNum' => $deviceNum,
|
||
'wechatNum' => $wechatNum,
|
||
'aliveWechatNum' => $aliveWechatNum,
|
||
'wechatOnlineRate' => $onlineRate,
|
||
'planNum' => $planNum,
|
||
'totalCustomers' => $totalCustomers,
|
||
'todayNewCustomers' => $todayNewCustomers,
|
||
'tokensRemaining' => $tokensRemaining,
|
||
'tokensTodayUsed' => $tokensTodayUsed,
|
||
'timestamp' => time(),
|
||
], 'ok');
|
||
}
|
||
}
|