## GitHub 同步说明 | 项 | 内容 | |:---|:---| | 仓库 | https://github.com/fnvtk/cunkebao_v3 | | 分支 | develop | | 方向 | 本地 → GitHub(单向 push) | | 是否拉取远程 | 否(未 fetch / pull / merge) | | 是否改本地源文件 | 否(仅 git add/commit,未编辑任何业务/文档正文) | | 远程基准 | origin/develop @2753265e| | 本批工作区变更 | 563 files, +30324 / -7013 lines | ## 一并推送的本地已有 commit(此前未 push) 1.f7a7c56cchore(触客宝): 全量构建部署 kr-kf 生产并通过线上 smoke 2.8801f5fffeat(触客宝): TKB-120 快捷语整组互拷与 AI 五步整理 3.c396ed35docs(触客宝): 0603 已完成项归档并交付 TKB-117-B 超管存储 4.e4ed53bcfix(部署): SSH 主机密钥变更兼容并完成五端全量上线 ## 本 commit 目录统计 | 目录 | 文件数 | 说明 | |:---|---:|:---| | 开发文档/ | 251 | 触客宝 0603~0608 归档、未完成项、五端账号速查 | | Touchkebao/ | 109 | 微信客服 CustomerList、快捷语、AI 获客等 | | Server/ | 108 | BFF/API、TaskCustomerList 等 | | Cunkebao/ | 53 | 存客宝移动端迭代 | | SuperAdmin/ | 24 | 超管后台 | | 官网/ | 7 | 官网小改 | | 其他 | 21 | .cursor、Moncter、docker-compose 等 | ## 重点文件(本地为准) - Touchkebao/.../CustomerList/index.tsx(微信客户列表) - 开发文档/1、需求/修改/触客宝_进行中_20260608.md - 开发文档/1、需求/修改/未完成项与跨仓验收汇总.md - 开发文档/10、项目管理/线上五端网址与账号密码速查.md - 开发文档/10、项目管理/已完成/触客宝_0607* 归档 - 开发文档/5、接口/02-存客宝对接/触客宝PC接口对接索引.md ## 刻意未上传 - node_modules/、.DS_Store、.smart-env/ - .开发文档_nested_git_backup/ - **/__pycache__/ ## 验收 - 本提交说明: https://github.com/fnvtk/cunkebao_v3/commit/HEAD - 分支: https://github.com/fnvtk/cunkebao_v3/tree/develop Co-authored-by: Cursor <cursoragent@cursor.com>
234 lines
8.7 KiB
PHP
234 lines
8.7 KiB
PHP
<?php
|
||
|
||
namespace app\chukebao\controller;
|
||
|
||
use app\common\service\KefuAvatarSyncService;
|
||
use library\ResponseHelper;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 模块 I · 设置页聚合(需求六 §6.4)
|
||
* 聚合层在 chukebao,内部调 cunkebao/共享表;触客宝不拼多个异构接口。
|
||
*/
|
||
class SettingsKefuController extends BaseController
|
||
{
|
||
/** GET /v1/kefu/settings/profile */
|
||
public function profile()
|
||
{
|
||
$userId = $this->getUserInfo('id');
|
||
$companyId = $this->getUserInfo('companyId');
|
||
$accountId = $this->getUserInfo('s2_accountId');
|
||
|
||
$user = Db::name('users')->where('id', $userId)->find();
|
||
$companyName = Db::name('company')->where('id', $companyId)->value('name');
|
||
|
||
// 该客服管理的微信号
|
||
$wechatAccounts = [];
|
||
if (!empty($accountId)) {
|
||
$ids = Db::table('s2_wechat_friend')
|
||
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||
->group('wechatAccountId')
|
||
->column('wechatAccountId');
|
||
if (!empty($ids)) {
|
||
$wechatAccounts = Db::table('s2_wechat_account')
|
||
->whereIn('id', $ids)
|
||
->field('id,nickname,alias')
|
||
->select();
|
||
}
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'avatar' => $user['avatar'] ?? '',
|
||
'nickname' => $user['username'] ?? '',
|
||
'jobNo' => (string)($user['s2_accountId'] ?? ''),
|
||
'companyId' => $companyId,
|
||
'companyName' => $companyName ?: ('项目 ' . $companyId),
|
||
'wechatAccounts' => $wechatAccounts,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST /v1/kefu/settings/avatar
|
||
* body: { avatar } — 写入 ck_users.avatar 并回写 s2_company_account.avatar
|
||
*/
|
||
public function saveAvatar()
|
||
{
|
||
$userId = (int)$this->getUserInfo('id');
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$accountId = (int)$this->getUserInfo('s2_accountId');
|
||
$avatar = trim((string)$this->request->param('avatar', ''));
|
||
|
||
if ($avatar === '') {
|
||
return ResponseHelper::error('avatar 不能为空');
|
||
}
|
||
|
||
Db::name('users')->where('id', $userId)->update([
|
||
'avatar' => $avatar,
|
||
'updateTime' => time(),
|
||
]);
|
||
|
||
$synced = false;
|
||
if ($accountId > 0) {
|
||
$synced = KefuAvatarSyncService::syncToS2Account($companyId, $accountId, $avatar);
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'avatar' => $avatar,
|
||
'synced' => $synced,
|
||
'accountId' => $accountId,
|
||
], '已保存');
|
||
}
|
||
|
||
/** GET /v1/kefu/settings/ai */
|
||
public function getAi()
|
||
{
|
||
$userId = $this->getUserInfo('id');
|
||
$companyId = $this->getUserInfo('companyId');
|
||
$row = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
|
||
$tk = [];
|
||
if ($row && !empty($row['config'])) {
|
||
$cfg = json_decode($row['config'], true) ?: [];
|
||
$tk = $cfg['touchkebao'] ?? [];
|
||
}
|
||
return ResponseHelper::success([
|
||
'defaultEnabled' => $tk['defaultEnabled'] ?? false,
|
||
'takeoverDelay' => $tk['takeoverDelay'] ?? 30,
|
||
'defaultKbId' => $tk['defaultKbId'] ?? null,
|
||
'kbIds' => array_values(array_map('intval', $tk['kbIds'] ?? [])),
|
||
'contentLibraryIds' => array_values(array_map('intval', $tk['contentLibraryIds'] ?? [])),
|
||
'contentLibraries' => $tk['contentLibraries'] ?? [],
|
||
'costTip' => $tk['costTip'] ?? true,
|
||
]);
|
||
}
|
||
|
||
/** POST /v1/kefu/settings/ai */
|
||
public function saveAi()
|
||
{
|
||
$userId = $this->getUserInfo('id');
|
||
$companyId = $this->getUserInfo('companyId');
|
||
$kbIds = $this->request->param('kbIds/a', []);
|
||
if (!is_array($kbIds)) {
|
||
$kbIds = [];
|
||
}
|
||
$kbIds = array_values(array_unique(array_filter(array_map('intval', $kbIds))));
|
||
|
||
$contentLibraryIds = $this->request->param('contentLibraryIds/a', []);
|
||
if (!is_array($contentLibraryIds)) {
|
||
$contentLibraryIds = [];
|
||
}
|
||
$contentLibraryIds = array_values(array_unique(array_filter(array_map('intval', $contentLibraryIds))));
|
||
|
||
$contentLibraries = $this->request->param('contentLibraries/a', []);
|
||
if (!is_array($contentLibraries)) {
|
||
$contentLibraries = [];
|
||
}
|
||
|
||
$defaultKbId = $this->request->param('defaultKbId', null);
|
||
if ($defaultKbId !== null && $defaultKbId !== '') {
|
||
$defaultKbId = (int)$defaultKbId;
|
||
} else {
|
||
$defaultKbId = null;
|
||
}
|
||
if ($defaultKbId && !empty($kbIds) && !in_array($defaultKbId, $kbIds, true)) {
|
||
$defaultKbId = $kbIds[0];
|
||
}
|
||
if (!$defaultKbId && !empty($kbIds)) {
|
||
$defaultKbId = $kbIds[0];
|
||
}
|
||
|
||
$tk = [
|
||
'defaultEnabled' => (bool)$this->request->param('defaultEnabled', false),
|
||
'takeoverDelay' => (int)$this->request->param('takeoverDelay', 30),
|
||
'defaultKbId' => $defaultKbId,
|
||
'kbIds' => $kbIds,
|
||
'contentLibraryIds' => $contentLibraryIds,
|
||
'contentLibraries' => array_values(array_filter($contentLibraries, function ($row) {
|
||
return is_array($row) && !empty($row['id']);
|
||
})),
|
||
'costTip' => (bool)$this->request->param('costTip', true),
|
||
];
|
||
$row = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
|
||
if ($row) {
|
||
$cfg = json_decode($row['config'], true) ?: [];
|
||
$cfg['touchkebao'] = $tk;
|
||
Db::name('ai_settings')->where('id', $row['id'])
|
||
->update(['config' => json_encode($cfg, 256), 'updateTime' => time()]);
|
||
} else {
|
||
Db::name('ai_settings')->insert([
|
||
'companyId' => $companyId,
|
||
'userId' => $userId,
|
||
'config' => json_encode(['touchkebao' => $tk], 256),
|
||
'createTime' => time(),
|
||
'updateTime' => time(),
|
||
]);
|
||
}
|
||
|
||
// 同步客服当前默认知识库(供旧接口只读)
|
||
if ($defaultKbId) {
|
||
$this->syncKefuCurrentKb($companyId, $userId, (int)$defaultKbId);
|
||
}
|
||
|
||
return ResponseHelper::success(true, '已保存');
|
||
}
|
||
|
||
/** 写入 ck_kb_touchkebao_config 客服级 currentKbId */
|
||
private function syncKefuCurrentKb(int $companyId, int $kefuId, int $kbId): void
|
||
{
|
||
try {
|
||
Db::execute("CREATE TABLE IF NOT EXISTS `ck_kb_touchkebao_config` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`companyId` int(11) NOT NULL DEFAULT 0,
|
||
`kbId` int(11) NOT NULL DEFAULT 0,
|
||
`kbName` varchar(128) NOT NULL DEFAULT '',
|
||
`isDefault` tinyint(1) NOT NULL DEFAULT 0,
|
||
`kefuId` int(11) NOT NULL DEFAULT 0,
|
||
`currentKbId` int(11) NOT NULL DEFAULT 0,
|
||
`enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||
`updateTime` int(11) NOT NULL DEFAULT 0,
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_company` (`companyId`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||
} catch (\Throwable $e) {
|
||
// ignore
|
||
}
|
||
$exist = Db::name('kb_touchkebao_config')
|
||
->where('companyId', $companyId)
|
||
->where('kefuId', $kefuId)
|
||
->find();
|
||
if ($exist) {
|
||
Db::name('kb_touchkebao_config')->where('id', $exist['id'])
|
||
->update(['currentKbId' => $kbId, 'updateTime' => time()]);
|
||
} else {
|
||
Db::name('kb_touchkebao_config')->insert([
|
||
'companyId' => $companyId,
|
||
'kefuId' => $kefuId,
|
||
'currentKbId' => $kbId,
|
||
'enabled' => 0,
|
||
'updateTime' => time(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET /v1/kefu/settings/map-config
|
||
* 地图 JS API Key · 解析优先级:项目(companyId)覆盖 → 超管全局 → Server .env [TENCENT_MAP]
|
||
*/
|
||
public function mapConfig()
|
||
{
|
||
$companyId = (int)$this->getUserInfo('companyId');
|
||
$cfg = \app\common\service\MapConfigService::resolve($companyId);
|
||
$provider = $cfg['provider'] ?? 'amap';
|
||
$key = trim((string)($cfg['key'] ?? ''));
|
||
$securityCode = trim((string)($cfg['securityCode'] ?? ''));
|
||
|
||
// 触客宝端:有 Key 即下发,不在客户端展示「未配置/超管设置」类提示(probe 仅超管后台用)
|
||
return ResponseHelper::success([
|
||
'provider' => $provider,
|
||
'key' => $key,
|
||
'securityCode' => $securityCode,
|
||
'valid' => $key !== '',
|
||
'reason' => '',
|
||
]);
|
||
}
|
||
}
|