## 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>
270 lines
11 KiB
PHP
270 lines
11 KiB
PHP
<?php
|
||
|
||
namespace app\chukebao\controller;
|
||
|
||
use library\ResponseHelper;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 模块 G · 转移申请(需求七 §7.7 · 需求九 §9.1)
|
||
* 铁律:触客宝仅「申请」,执行须存客宝审批;不在前端直调即时 transfer。
|
||
* 统一写入存客宝真源表 ck_wechat_account_transfer(与 cunkebao WechatAccountTransferController 同表,
|
||
* 故触客宝提交的申请可在存客宝 H5 审批列表直接出现并由其执行 allot)。
|
||
*/
|
||
class TransferController extends BaseController
|
||
{
|
||
private const SUPPORTED_TYPES = ['session', 'wechat_account', 'contact_group'];
|
||
|
||
/** 数字微信号账号 id → wechatId(整号转移以 wechatId 为 targetId 口径,与 allot 一致) */
|
||
private function resolveWechatId(int $accountId): string
|
||
{
|
||
return (string)Db::table('s2_wechat_account')->where('id', $accountId)->value('wechatId');
|
||
}
|
||
|
||
private function createApply($type)
|
||
{
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$applicantUserId = (int)$this->getUserInfo('id');
|
||
$applicantName = (string)($this->getUserInfo('username') ?: '');
|
||
$toAccountId = trim((string)$this->request->param('toAccountId', $this->request->param('toKefuId', '')));
|
||
$reason = (string)$this->request->param('reason', '');
|
||
|
||
// targetId 口径:wechat_account → wechatId;session → wechatFriendId;contact_group → groupId
|
||
if ($type === 'wechat_account') {
|
||
$accId = (int)$this->request->param('wechatAccountId', 0);
|
||
$targetId = $this->resolveWechatId($accId);
|
||
if ($targetId === '') {
|
||
return ResponseHelper::error('微信号不存在,无法转移', 400);
|
||
}
|
||
} elseif ($type === 'contact_group') {
|
||
$groupType = (int)$this->request->param('groupType', 1);
|
||
if ($groupType !== 1) {
|
||
return ResponseHelper::error('仅好友分组支持转移申请', 400);
|
||
}
|
||
$wechatAccountId = (int)$this->request->param('wechatAccountId', 0);
|
||
if ($wechatAccountId <= 0) {
|
||
return ResponseHelper::error('wechatAccountId 缺失', 400);
|
||
}
|
||
$targetId = (string)(int)$this->request->param('groupId', 0);
|
||
} else {
|
||
$targetId = (string)$this->request->param('wechatFriendId', 0);
|
||
}
|
||
if ($toAccountId === '' || $targetId === '' || $targetId === '0') {
|
||
return ResponseHelper::error('参数缺失(目标客服/对象)', 400);
|
||
}
|
||
|
||
// 校验目标客服归属同公司
|
||
$toAccount = Db::table('s2_company_account')
|
||
->where('id', $toAccountId)
|
||
->where('departmentId', $companyId)
|
||
->field('id, userName, realName, nickname')
|
||
->find();
|
||
$toAccountName = $toAccount
|
||
? (($toAccount['realName'] ?: $toAccount['nickname'] ?: $toAccount['userName']) ?: '')
|
||
: '';
|
||
|
||
$affected = 0;
|
||
try {
|
||
if ($type === 'session') {
|
||
$affected = 1;
|
||
} elseif ($type === 'wechat_account') {
|
||
$affected = (int)Db::table('s2_wechat_friend')->where('ownerWechatId', $targetId)->count();
|
||
} elseif ($type === 'contact_group') {
|
||
$wechatAccountId = (int)$this->request->param('wechatAccountId', 0);
|
||
$ownerWechatId = $this->resolveWechatId($wechatAccountId);
|
||
if ($ownerWechatId === '') {
|
||
return ResponseHelper::error('微信号不存在,无法发起分组转移', 400);
|
||
}
|
||
$affected = (int)Db::table('s2_wechat_friend')
|
||
->where('ownerWechatId', $ownerWechatId)
|
||
->where('groupIds', (int)$targetId)
|
||
->where('isDeleted', 0)
|
||
->count();
|
||
}
|
||
} catch (\Throwable $e) {
|
||
}
|
||
|
||
if ($type === 'contact_group' && $affected <= 0) {
|
||
return ResponseHelper::error('本分组暂无好友,无法发起转移', 400);
|
||
}
|
||
|
||
$now = time();
|
||
$applyId = Db::name('wechat_account_transfer')->insertGetId([
|
||
'companyId' => $companyId,
|
||
'type' => $type,
|
||
'targetId' => $targetId,
|
||
'targetName' => (string)$this->request->param('targetName', ''),
|
||
'toAccountId' => $toAccountId,
|
||
'toAccountName' => $toAccountName,
|
||
'applicantUserId' => $applicantUserId,
|
||
'applicantName' => $applicantName,
|
||
'reason' => $this->buildReason($type, $reason),
|
||
'affectedFriendCount' => $affected,
|
||
'status' => 'pending',
|
||
'createTime' => $now,
|
||
'updateTime' => $now,
|
||
]);
|
||
|
||
return ResponseHelper::success([
|
||
'applyId' => (int)$applyId,
|
||
'status' => 'pending',
|
||
'affectedFriendCount' => $affected,
|
||
], '转移申请已提交,等待存客宝审批');
|
||
}
|
||
|
||
/** POST /v1/kefu/wechat-account/transfer/apply */
|
||
public function applyWechatAccount()
|
||
{
|
||
return $this->createApply('wechat_account');
|
||
}
|
||
|
||
/** POST /v1/kefu/session/transfer/apply */
|
||
public function applySession()
|
||
{
|
||
return $this->createApply('session');
|
||
}
|
||
|
||
/** POST /v1/kefu/transfer/apply */
|
||
public function apply()
|
||
{
|
||
$type = trim((string)$this->request->param('type', ''));
|
||
if (!in_array($type, self::SUPPORTED_TYPES, true)) {
|
||
return ResponseHelper::error('转移类型不支持', 400);
|
||
}
|
||
return $this->createApply($type);
|
||
}
|
||
|
||
/**
|
||
* GET /v1/kefu/transfer/list
|
||
* 我的转移申请(客服看自己提交的;管理员可看本项目全部 pending)
|
||
*/
|
||
public function list()
|
||
{
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$kefuId = (int)$this->getUserInfo('id');
|
||
$isAdmin = (int)($this->getUserInfo('isAdmin') ?? 0) === 1;
|
||
$status = trim((string)$this->request->param('status', ''));
|
||
$page = max(1, (int)$this->request->param('page', 1));
|
||
$limit = min(100, max(1, (int)$this->request->param('limit', 20)));
|
||
|
||
$query = Db::name('wechat_account_transfer')->where('companyId', $companyId);
|
||
if (!$isAdmin) {
|
||
$query->where('applicantUserId', $kefuId);
|
||
}
|
||
if ($status !== '' && $status !== 'all') {
|
||
$query->where('status', $status);
|
||
}
|
||
|
||
$total = (int)$query->count();
|
||
$rows = $query
|
||
->order('id desc')
|
||
->page($page, $limit)
|
||
->select();
|
||
|
||
$list = [];
|
||
foreach ($rows ?: [] as $row) {
|
||
$list[] = [
|
||
'id' => (int)$row['id'],
|
||
'type' => (string)($row['type'] ?? ''),
|
||
'targetId' => (string)($row['targetId'] ?? ''),
|
||
'targetName' => (string)($row['targetName'] ?? ''),
|
||
'toAccountId' => (string)($row['toAccountId'] ?? ''),
|
||
'toAccountName' => (string)($row['toAccountName'] ?? ''),
|
||
'applicantUserId' => (int)($row['applicantUserId'] ?? 0),
|
||
'applicantName' => (string)($row['applicantName'] ?? ''),
|
||
'reason' => (string)($row['reason'] ?? ''),
|
||
'affectedFriendCount' => (int)($row['affectedFriendCount'] ?? 0),
|
||
'status' => (string)($row['status'] ?? 'pending'),
|
||
'reviewComment' => (string)($row['reviewComment'] ?? ''),
|
||
'createTime' => (int)($row['createTime'] ?? 0),
|
||
'updateTime' => (int)($row['updateTime'] ?? 0),
|
||
];
|
||
}
|
||
|
||
$pendingQuery = Db::name('wechat_account_transfer')
|
||
->where('companyId', $companyId)
|
||
->where('status', 'pending');
|
||
if (!$isAdmin) {
|
||
$pendingQuery->where('applicantUserId', $kefuId);
|
||
}
|
||
$pendingCount = (int)$pendingQuery->count();
|
||
|
||
return ResponseHelper::success([
|
||
'list' => $list,
|
||
'total' => $total,
|
||
'pendingCount' => $pendingCount,
|
||
]);
|
||
}
|
||
|
||
/** GET /v1/kefu/wechat-account/transfer/status?applyId= */
|
||
public function status()
|
||
{
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$applyId = (int)$this->request->param('applyId', 0);
|
||
if (!$applyId) {
|
||
return ResponseHelper::error('applyId 缺失', 400);
|
||
}
|
||
$row = Db::name('wechat_account_transfer')
|
||
->where('companyId', $companyId)
|
||
->where('id', $applyId)
|
||
->find();
|
||
if (!$row) {
|
||
return ResponseHelper::error('申请不存在', 404);
|
||
}
|
||
return ResponseHelper::success([
|
||
'applyId' => (int)$row['id'],
|
||
'status' => $row['status'],
|
||
'reason' => $row['reviewComment'] ?? '',
|
||
'executedCount' => (int)($row['executedCount'] ?? 0),
|
||
]);
|
||
}
|
||
|
||
/** GET /v1/kefu/wechatGroup/transfer/preview */
|
||
public function groupPreview()
|
||
{
|
||
$groupType = (int)$this->request->param('groupType', 1);
|
||
$groupId = (int)$this->request->param('groupId', 0);
|
||
$wechatAccountId = (int)$this->request->param('wechatAccountId', 0);
|
||
if ($groupType !== 1) {
|
||
return ResponseHelper::error('仅好友分组支持预览', 400);
|
||
}
|
||
if ($groupId <= 0 || $wechatAccountId <= 0) {
|
||
return ResponseHelper::error('参数缺失(groupId/wechatAccountId)', 400);
|
||
}
|
||
$ownerWechatId = $this->resolveWechatId($wechatAccountId);
|
||
if ($ownerWechatId === '') {
|
||
return ResponseHelper::error('微信号不存在', 404);
|
||
}
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$groupName = (string)Db::table('s2_chat_groups')
|
||
->where('companyId', $companyId)
|
||
->where('id', $groupId)
|
||
->value('groupName');
|
||
$count = (int)Db::table('s2_wechat_friend')
|
||
->where('ownerWechatId', $ownerWechatId)
|
||
->where('groupIds', $groupId)
|
||
->where('isDeleted', 0)
|
||
->count();
|
||
return ResponseHelper::success([
|
||
'groupId' => $groupId,
|
||
'groupType' => 1,
|
||
'groupName' => $groupName ?: '未命名分组',
|
||
'count' => $count,
|
||
]);
|
||
}
|
||
|
||
private function buildReason(string $type, string $reason): string
|
||
{
|
||
if ($type !== 'contact_group') {
|
||
return $reason;
|
||
}
|
||
$payload = [
|
||
'rawReason' => $reason,
|
||
'groupType' => (int)$this->request->param('groupType', 1),
|
||
'groupId' => (int)$this->request->param('groupId', 0),
|
||
'wechatAccountId' => (int)$this->request->param('wechatAccountId', 0),
|
||
];
|
||
return json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||
}
|
||
}
|