Files
cunkebao_v3/Server/application/chukebao/controller/TransferController.php
Manus AI 5182529a58 sync(本地→GitHub): 2026-05-30 21:41 全量单向同步
## 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>
2026-05-30 21:41:40 +08:00

270 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 → wechatIdsession → wechatFriendIdcontact_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);
}
}