## GitHub 同步说明 | 项 | 内容 | |:---|:---| | 仓库 | https://github.com/fnvtk/cunkebao_v3 | | 分支 | develop | | 方向 | 本地 → GitHub(单向 push) | | 是否拉取远程 | 否(未 fetch / pull / merge) | | 是否改本地源文件 | 否(仅 git add/commit,未编辑业务/文档正文) | | 远程基准 | origin/develop @b5b40e8b| | 本批工作区变更 | 999 files, +47622 / -4007 lines | ## 一并推送的本地已有 commit(此前未 push) 1.3aa7ecf8deploy: 官网 ckb.quwanzhi.com 宝塔上线 DNS+SSL+验收文档 2.16a70045fix(官网): 桌面导航仅保留一个留资 CTA 并重新上线 3.dbe7b7aafix(官网): Hero 改为主 CTA 按钮并去掉重复 1200+ 数据条 4.ac82726echore(部署): 五端上线验收46项并统一留资与缓存版本 ## 本 commit 目录统计 | 目录 | 文件数 | 说明 | |:---|---:|:---| | 开发文档/ | 639 | 10、项目管理、未完成项跨仓汇总、五端需求/部署/接口 | | Server/ | 129 | 后端 API、迁移、部署相关 | | Touchkebao/ | 80 | PC 工作台、获客、算力 | | 官网/ | 61 | ckb.quwanzhi.com 静态站与 CTA 迭代 | | Cunkebao/ | 49 | 移动端场景/设置/流量池 | | SuperAdmin/ | 26 | 超管后台 | | .cursor/ | 11 | 规则与 Skill | | 其他 | 14 | docker-compose、.obsidian、.codegraph 等 | ## 重点文件(本地为准) - 开发文档/1、需求/修改/未完成项与跨仓验收汇总.md - 开发文档/10、项目管理/账号密码与登录环境一览.md - 开发文档/10、项目管理/(新建项目管理目录与截图) - 官网/ 全站 + 宝塔上线文档 - 五端上线验收 46 项(见 ac82726e) ## 刻意未上传 - 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>
579 lines
22 KiB
PHP
579 lines
22 KiB
PHP
<?php
|
||
|
||
namespace app\superadmin\service;
|
||
|
||
use app\common\model\User as UserModel;
|
||
use app\common\service\CompanyFeatureSwitchService;
|
||
use app\common\service\TrafficPoolSystemIdentifierService;
|
||
use app\cunkebao\service\PlanSceneCatalogService;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 超管 · 以项目为中心的工作台(只读存客宝/触客宝同源数据表,不改 cunkebao/chukebao 代码)
|
||
*/
|
||
class ProjectWorkspaceService
|
||
{
|
||
public function getWorkspace(int $projectId): array
|
||
{
|
||
$ctx = ProjectContextService::resolveByProjectId($projectId);
|
||
$companyId = $ctx['companyId'];
|
||
|
||
$master = Db::name('users')
|
||
->where('companyId', $companyId)
|
||
->where('isAdmin', UserModel::ADMIN_STP)
|
||
->where('deleteTime', 0)
|
||
->field('id,account,username,phone,s2_accountId,status')
|
||
->find();
|
||
|
||
$deviceStats = $this->deviceStats($companyId);
|
||
$assetStats = $this->assetStats($companyId);
|
||
$wechatStats = $this->wechatStats($companyId);
|
||
|
||
return array_merge($ctx, [
|
||
'master' => $master ?: null,
|
||
'deviceStats' => $deviceStats,
|
||
'assetStats' => $assetStats,
|
||
'wechatStats' => $wechatStats,
|
||
'contentCount' => (int) Db::name('content_library')->where('companyId', $companyId)->where('isDel', 0)->count(),
|
||
'planCount' => (int) Db::name('customer_acquisition_task')->where('companyId', $companyId)->where('deleteTime', 0)->count(),
|
||
'subUserCount' => max(0, (int) Db::name('users')->where('companyId', $companyId)->where('deleteTime', 0)->count()
|
||
- (ProjectContextService::primaryMasterUserId($companyId) > 0 ? 1 : 0)),
|
||
]);
|
||
}
|
||
|
||
public function assetStats(int $companyId): array
|
||
{
|
||
$rowQuery = Db::name('traffic_pool_company')->alias('tpc')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
TrafficPoolSystemIdentifierService::applyExcludeToCompanyQuery($rowQuery, 'tpc');
|
||
$row = $rowQuery->field([
|
||
'COUNT(*) as customerTotal',
|
||
'COALESCE(SUM(totalOrderAmount),0) as totalConsumption',
|
||
'COALESCE(SUM(totalOrderCount),0) as totalOrders',
|
||
'COALESCE(AVG(rfmScore),0) as avgRfmScore',
|
||
'COALESCE(SUM(rfmScore),0) as rfmValuation',
|
||
])
|
||
->find();
|
||
|
||
$rfmTypesQuery = Db::name('traffic_pool_company')->alias('tpc')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0)
|
||
->where('tpc.rfmType', '<>', '');
|
||
TrafficPoolSystemIdentifierService::applyExcludeToCompanyQuery($rfmTypesQuery, 'tpc');
|
||
$rfmTypes = $rfmTypesQuery
|
||
->field('tpc.rfmType, COUNT(*) as cnt')
|
||
->group('tpc.rfmType')
|
||
->order('cnt', 'desc')
|
||
->limit(8)
|
||
->select();
|
||
|
||
return [
|
||
'customerTotal' => (int) ($row['customerTotal'] ?? 0),
|
||
'totalConsumption' => round((float) ($row['totalConsumption'] ?? 0), 2),
|
||
'totalOrders' => (int) ($row['totalOrders'] ?? 0),
|
||
'avgRfmScore' => round((float) ($row['avgRfmScore'] ?? 0), 1),
|
||
'rfmValuation' => (int) ($row['rfmValuation'] ?? 0),
|
||
'rfmDistribution' => $rfmTypes ?: [],
|
||
];
|
||
}
|
||
|
||
public function deviceStats(int $companyId): array
|
||
{
|
||
$total = (int) Db::name('device')->where('companyId', $companyId)->where('deleteTime', 0)->count();
|
||
$online = (int) Db::name('device')->where('companyId', $companyId)->where('deleteTime', 0)->where('alive', 1)->count();
|
||
|
||
return ['total' => $total, 'online' => $online, 'offline' => $total - $online];
|
||
}
|
||
|
||
public function wechatStats(int $companyId): array
|
||
{
|
||
$rows = Db::name('device_wechat_login')
|
||
->alias('d')
|
||
->join('device dev', 'dev.id = d.deviceId')
|
||
->where('dev.companyId', $companyId)
|
||
->where('dev.deleteTime', 0)
|
||
->field('d.wechatId, d.alive, d.deviceId, dev.memo as deviceName')
|
||
->order('d.id', 'desc')
|
||
->limit(500)
|
||
->select();
|
||
|
||
$loggedIn = 0;
|
||
$map = [];
|
||
foreach ($rows ?: [] as $r) {
|
||
if (!isset($map[$r['wechatId']])) {
|
||
$map[$r['wechatId']] = $r;
|
||
if ((int) $r['alive'] === 1) {
|
||
$loggedIn++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'total' => count($map),
|
||
'loggedIn' => $loggedIn,
|
||
'list' => array_values($map),
|
||
];
|
||
}
|
||
|
||
public function listTraffic(int $projectId, int $page, int $limit, string $keyword = '', string $sortBy = 'rfmScore'): array
|
||
{
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
|
||
$query = Db::name('traffic_pool_company')->alias('tpc')
|
||
->join('traffic_pool tp', 'tp.id = tpc.poolId')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
TrafficPoolSystemIdentifierService::applyExcludeToPoolQuery($query);
|
||
|
||
if ($keyword !== '') {
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($q) use ($like) {
|
||
$q->whereLike('tp.nickname', $like)
|
||
->whereOr('tpc.phone', 'like', $like)
|
||
->whereOr('tpc.realName', 'like', $like);
|
||
});
|
||
}
|
||
|
||
$total = (clone $query)->count('tpc.id');
|
||
$allowedSort = ['rfmScore', 'totalOrderAmount', 'createTime', 'lastInteractTime'];
|
||
if (!in_array($sortBy, $allowedSort, true)) {
|
||
$sortBy = 'rfmScore';
|
||
}
|
||
|
||
$rows = $query
|
||
->field('tpc.id,tpc.identifier,tpc.phone,tpc.realName,tpc.rfmScore,tpc.rfmType,tpc.totalOrderAmount,tpc.totalOrderCount,tpc.createTime,tp.nickname,tp.wechatId,tp.avatar')
|
||
->order($sortBy, 'desc')
|
||
->page($page, $limit)
|
||
->select();
|
||
|
||
return ['list' => $rows ?: [], 'total' => $total, 'page' => $page, 'limit' => $limit];
|
||
}
|
||
|
||
public function listContentLibrary(int $projectId, int $page, int $limit): array
|
||
{
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$query = Db::name('content_library')->where('companyId', $companyId)->where('isDel', 0);
|
||
$total = (clone $query)->count('id');
|
||
$list = $query->field('id,name,status,createTime,updateTime')->order('id', 'desc')->page($page, $limit)->select();
|
||
|
||
$libraryIds = array_column($list ?: [], 'id');
|
||
$itemCounts = [];
|
||
if (!empty($libraryIds)) {
|
||
$counts = Db::name('content_item')
|
||
->field('libraryId, COUNT(*) as count')
|
||
->whereIn('libraryId', $libraryIds)
|
||
->where('isDel', 0)
|
||
->group('libraryId')
|
||
->select();
|
||
foreach ($counts ?: [] as $row) {
|
||
$itemCounts[(int) $row['libraryId']] = (int) $row['count'];
|
||
}
|
||
}
|
||
|
||
foreach ($list ?: [] as &$row) {
|
||
$row['itemCount'] = $itemCounts[(int) $row['id']] ?? 0;
|
||
}
|
||
unset($row);
|
||
|
||
return ['list' => $list ?: [], 'total' => $total, 'page' => $page, 'limit' => $limit];
|
||
}
|
||
|
||
public function saveContentLibrary(int $projectId, array $param): array
|
||
{
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$master = $this->resolveMasterUser($companyId);
|
||
$userId = (int) $master['id'];
|
||
$now = time();
|
||
|
||
$id = (int) ($param['id'] ?? 0);
|
||
$name = trim((string) ($param['name'] ?? ''));
|
||
$status = isset($param['status']) ? (int) $param['status'] : null;
|
||
|
||
if ($id > 0) {
|
||
$library = Db::name('content_library')
|
||
->where('id', $id)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->find();
|
||
if (!$library) {
|
||
throw new \Exception('内容库不存在', 404);
|
||
}
|
||
|
||
$update = ['updateTime' => $now];
|
||
if ($name !== '') {
|
||
$dup = Db::name('content_library')
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->where('name', $name)
|
||
->where('id', '<>', $id)
|
||
->count();
|
||
if ($dup > 0) {
|
||
throw new \Exception('内容库名称已存在', 400);
|
||
}
|
||
$update['name'] = $name;
|
||
}
|
||
if ($status !== null) {
|
||
$update['status'] = $status ? 1 : 0;
|
||
}
|
||
|
||
Db::name('content_library')->where('id', $id)->update($update);
|
||
|
||
return ['id' => $id, 'action' => 'update'];
|
||
}
|
||
|
||
if ($name === '') {
|
||
throw new \Exception('内容库名称不能为空', 400);
|
||
}
|
||
|
||
$exists = Db::name('content_library')
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->where('name', $name)
|
||
->count();
|
||
if ($exists > 0) {
|
||
throw new \Exception('内容库名称已存在', 400);
|
||
}
|
||
|
||
$newId = Db::name('content_library')->insertGetId([
|
||
'name' => $name,
|
||
'sourceType' => 1,
|
||
'sourceFriends' => '[]',
|
||
'sourceGroups' => '[]',
|
||
'groupMembers' => '[]',
|
||
'catchType' => '[]',
|
||
'devices' => '[]',
|
||
'keywordInclude' => '[]',
|
||
'keywordExclude' => '[]',
|
||
'aiEnabled' => 0,
|
||
'aiPrompt' => '',
|
||
'timeEnabled' => 0,
|
||
'formType' => 1,
|
||
'status' => $status !== null ? ($status ? 1 : 0) : 0,
|
||
'userId' => $userId,
|
||
'companyId' => $companyId,
|
||
'createTime' => $now,
|
||
'updateTime' => $now,
|
||
'isDel' => 0,
|
||
]);
|
||
|
||
return ['id' => $newId, 'action' => 'create'];
|
||
}
|
||
|
||
/**
|
||
* 微信号好友迁移(对标 cunkebao PostTransferFriends,注入项目 companyId + 主账号 userId)
|
||
*/
|
||
public function migrateWechatFriends(int $projectId, array $param): array
|
||
{
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$master = $this->resolveMasterUser($companyId);
|
||
$userId = (int) $master['id'];
|
||
|
||
$wechatId = trim((string) ($param['wechatId'] ?? ''));
|
||
$inherit = !empty($param['inherit']);
|
||
$greeting = trim((string) ($param['greeting'] ?? ''));
|
||
$firstMessage = trim((string) ($param['firstMessage'] ?? ''));
|
||
$devices = $param['devices'] ?? [];
|
||
|
||
if ($wechatId === '') {
|
||
throw new \Exception('迁移的微信不能为空', 400);
|
||
}
|
||
if (empty($devices) || !is_array($devices)) {
|
||
throw new \Exception('迁移的设备不能为空', 400);
|
||
}
|
||
if ($greeting === '') {
|
||
throw new \Exception('打招呼不能为空', 400);
|
||
}
|
||
|
||
$wechat = Db::name('wechat_customer')->alias('wc')
|
||
->join('wechat_account wa', 'wc.wechatId = wa.wechatId')
|
||
->where(['wc.wechatId' => $wechatId])
|
||
->field('wa.*')
|
||
->find();
|
||
if (empty($wechat)) {
|
||
throw new \Exception('该微信不存在', 404);
|
||
}
|
||
|
||
$deviceIds = Db::name('device')
|
||
->where(['companyId' => $companyId, 'deleteTime' => 0])
|
||
->whereIn('id', array_map('intval', $devices))
|
||
->column('id');
|
||
if (empty($deviceIds)) {
|
||
throw new \Exception('所选设备不属于本项目', 400);
|
||
}
|
||
|
||
$sceneConf = [
|
||
'enabled' => true,
|
||
'posters' => [
|
||
'id' => 'poster-3',
|
||
'name' => '点击咨询',
|
||
'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif',
|
||
],
|
||
];
|
||
$reqConf = [
|
||
'device' => $deviceIds,
|
||
'startTime' => '09:00',
|
||
'endTime' => '18:00',
|
||
'remarkType' => 'phone',
|
||
'addFriendInterval' => 60,
|
||
'greeting' => $greeting ?: ('我是' . $wechat['nickname'] . '的新号,请通过'),
|
||
];
|
||
$msgConf = [];
|
||
if ($firstMessage !== '') {
|
||
$msgConf = [[
|
||
'day' => 0,
|
||
'messages' => [[
|
||
'id' => 1,
|
||
'type' => 'text',
|
||
'content' => $firstMessage,
|
||
'intervalUnit' => 'seconds',
|
||
'sendInterval' => 5,
|
||
]],
|
||
]];
|
||
}
|
||
|
||
$createAddFriendPlan = app('app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller');
|
||
$taskId = Db::name('customer_acquisition_task')->insertGetId([
|
||
'name' => '迁移好友(' . $wechat['nickname'] . ')',
|
||
'sceneId' => 10,
|
||
'sceneConf' => json_encode($sceneConf, JSON_UNESCAPED_UNICODE),
|
||
'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE),
|
||
'tagConf' => json_encode([]),
|
||
'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE),
|
||
'userId' => $userId,
|
||
'companyId' => $companyId,
|
||
'status' => 0,
|
||
'createTime' => time(),
|
||
'apiKey' => $createAddFriendPlan->generateApiKey(),
|
||
]);
|
||
|
||
$friends = Db::table('s2_wechat_friend')
|
||
->where(['ownerWechatId' => $wechatId])
|
||
->field('wechatId,alias,phone,labels,conRemark')
|
||
->group('wechatId')
|
||
->order('id DESC')
|
||
->select();
|
||
|
||
$friendRows = $friends ? (is_array($friends) ? $friends : $friends->toArray()) : [];
|
||
$batchSize = 1000;
|
||
$inserted = 0;
|
||
|
||
for ($i = 0, $total = count($friendRows); $i < $total; $i += $batchSize) {
|
||
$batchRows = array_slice($friendRows, $i, $batchSize);
|
||
if (empty($batchRows)) {
|
||
continue;
|
||
}
|
||
$newData = [];
|
||
foreach ($batchRows as $row) {
|
||
if (!empty($row['phone'])) {
|
||
$phone = $row['phone'];
|
||
} elseif (!empty($row['alias'])) {
|
||
$phone = $row['alias'];
|
||
} else {
|
||
$phone = $row['wechatId'];
|
||
}
|
||
$tags = !empty($row['labels']) ? json_decode($row['labels'], true) : [];
|
||
$newData[] = [
|
||
'task_id' => $taskId,
|
||
'name' => '',
|
||
'source' => '迁移好友(' . $wechat['nickname'] . ')',
|
||
'phone' => $phone,
|
||
'remark' => $inherit ? ($row['conRemark'] ?? '') : '',
|
||
'tags' => $inherit ? json_encode($tags ?: [], JSON_UNESCAPED_UNICODE) : json_encode([]),
|
||
'siteTags' => json_encode([]),
|
||
'status' => 0,
|
||
'createTime' => time(),
|
||
];
|
||
}
|
||
Db::name('task_customer')->insertAll($newData);
|
||
$inserted += count($newData);
|
||
}
|
||
|
||
return ['taskId' => $taskId, 'friendCount' => $inserted];
|
||
}
|
||
|
||
private function resolveMasterUser(int $companyId): array
|
||
{
|
||
$master = Db::name('users')
|
||
->where('companyId', $companyId)
|
||
->where('isAdmin', UserModel::ADMIN_STP)
|
||
->where('deleteTime', 0)
|
||
->field('id,account,username,phone,s2_accountId')
|
||
->find();
|
||
if (!$master) {
|
||
throw new \Exception('项目未配置主账号', 400);
|
||
}
|
||
|
||
return $master;
|
||
}
|
||
|
||
public function listPlanScenes(int $projectId): array
|
||
{
|
||
ProjectContextService::resolveByProjectId($projectId);
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
|
||
$rowsRaw = Db::name('plan_scene')->where('deleteTime', 0)->order('sort DESC, id ASC')->select();
|
||
$rows = $rowsRaw ? (is_array($rowsRaw) ? $rowsRaw : $rowsRaw->toArray()) : [];
|
||
$byId = [];
|
||
foreach ($rows as $row) {
|
||
$byId[(int) $row['id']] = $row;
|
||
}
|
||
|
||
$planCounts = Db::name('customer_acquisition_task')
|
||
->where('companyId', $companyId)
|
||
->where('deleteTime', 0)
|
||
->field('sceneId, COUNT(*) as cnt')
|
||
->group('sceneId')
|
||
->select();
|
||
$countMap = [];
|
||
foreach ($planCounts ?: [] as $p) {
|
||
$countMap[(int) $p['sceneId']] = (int) $p['cnt'];
|
||
}
|
||
|
||
$overrideMap = CompanyFeatureSwitchService::getOverrideMap(
|
||
(int) $companyId,
|
||
CompanyFeatureSwitchService::TYPE_SCENE
|
||
);
|
||
|
||
$list = [];
|
||
foreach (PlanSceneCatalogService::productionCatalog() as $def) {
|
||
$id = (int) $def['id'];
|
||
$row = $byId[$id] ?? [];
|
||
$globalStatus = (int) ($row['status'] ?? PlanSceneCatalogService::getDefaultStatus($id));
|
||
$effective = array_key_exists($id, $overrideMap) ? (int) $overrideMap[$id] : $globalStatus;
|
||
$list[] = [
|
||
'id' => $id,
|
||
'name' => $row['name'] ?? $def['name'],
|
||
'description' => $row['description'] ?? '',
|
||
'status' => $effective,
|
||
'globalStatus' => $globalStatus,
|
||
'planCount' => $countMap[$id] ?? 0,
|
||
'sort' => (int) ($row['sort'] ?? $def['sort']),
|
||
];
|
||
}
|
||
|
||
return ['list' => $list];
|
||
}
|
||
|
||
/**
|
||
* 项目级设置场景开关
|
||
*/
|
||
public function setPlanSceneStatus(int $projectId, int $sceneId, int $status): array
|
||
{
|
||
ProjectContextService::resolveByProjectId($projectId);
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
if (!PlanSceneCatalogService::isProductionReady($sceneId)) {
|
||
throw new \Exception('场景不可用', 400);
|
||
}
|
||
CompanyFeatureSwitchService::setStatus(
|
||
$companyId,
|
||
CompanyFeatureSwitchService::TYPE_SCENE,
|
||
$sceneId,
|
||
$status
|
||
);
|
||
return ['id' => $sceneId, 'status' => $status ? 1 : 0];
|
||
}
|
||
|
||
/**
|
||
* 项目级工作台功能列表(含公司级有效状态)
|
||
*/
|
||
public function listWorkbenchFunctions(int $projectId): array
|
||
{
|
||
ProjectContextService::resolveByProjectId($projectId);
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
|
||
$rows = Db::name('workbench_function')->order('sort ASC, id ASC')->select();
|
||
$rows = $rows ? (is_array($rows) ? $rows : $rows->toArray()) : [];
|
||
|
||
$overrideMap = CompanyFeatureSwitchService::getOverrideMap(
|
||
(int) $companyId,
|
||
CompanyFeatureSwitchService::TYPE_WORKBENCH
|
||
);
|
||
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$id = (int) $row['id'];
|
||
$globalStatus = (int) ($row['status'] ?? 0);
|
||
$effective = array_key_exists($id, $overrideMap) ? (int) $overrideMap[$id] : $globalStatus;
|
||
$list[] = [
|
||
'id' => $id,
|
||
'key' => (string) ($row['key'] ?? ''),
|
||
'title' => (string) ($row['title'] ?? ''),
|
||
'subtitle' => (string) ($row['subtitle'] ?? ''),
|
||
'status' => $effective,
|
||
'globalStatus' => $globalStatus,
|
||
'sort' => (int) ($row['sort'] ?? 0),
|
||
];
|
||
}
|
||
|
||
return ['list' => $list];
|
||
}
|
||
|
||
/**
|
||
* 项目级设置工作台功能开关
|
||
*/
|
||
public function setWorkbenchFunctionStatus(int $projectId, int $functionId, int $status): array
|
||
{
|
||
ProjectContextService::resolveByProjectId($projectId);
|
||
$companyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$exists = Db::name('workbench_function')->where('id', $functionId)->find();
|
||
if (!$exists) {
|
||
throw new \Exception('功能不存在', 404);
|
||
}
|
||
CompanyFeatureSwitchService::setStatus(
|
||
$companyId,
|
||
CompanyFeatureSwitchService::TYPE_WORKBENCH,
|
||
$functionId,
|
||
$status
|
||
);
|
||
return ['id' => $functionId, 'status' => $status ? 1 : 0];
|
||
}
|
||
|
||
public function migrateContentLibrary(int $projectId, int $libraryId, int $targetProjectId): void
|
||
{
|
||
$sourceCompanyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$targetCompanyId = ProjectContextService::resolveCompanyId($targetProjectId);
|
||
|
||
if ($sourceCompanyId === $targetCompanyId) {
|
||
throw new \Exception('源项目与目标项目相同', 400);
|
||
}
|
||
|
||
$updated = Db::name('content_library')
|
||
->where('id', $libraryId)
|
||
->where('companyId', $sourceCompanyId)
|
||
->where('isDel', 0)
|
||
->update(['companyId' => $targetCompanyId, 'updateTime' => time()]);
|
||
|
||
if (!$updated) {
|
||
throw new \Exception('内容库不存在或迁移失败', 404);
|
||
}
|
||
}
|
||
|
||
public function migrateTraffic(int $projectId, array $poolCompanyIds, int $targetProjectId): int
|
||
{
|
||
$sourceCompanyId = ProjectContextService::resolveCompanyId($projectId);
|
||
$targetCompanyId = ProjectContextService::resolveCompanyId($targetProjectId);
|
||
|
||
if ($sourceCompanyId === $targetCompanyId) {
|
||
throw new \Exception('源项目与目标项目相同', 400);
|
||
}
|
||
|
||
if (empty($poolCompanyIds)) {
|
||
throw new \Exception('请选择要迁移的客户', 400);
|
||
}
|
||
|
||
$ids = array_map('intval', $poolCompanyIds);
|
||
return Db::name('traffic_pool_company')
|
||
->whereIn('id', $ids)
|
||
->where('companyId', $sourceCompanyId)
|
||
->where('isDel', 0)
|
||
->update(['companyId' => $targetCompanyId, 'updateTime' => time()]);
|
||
}
|
||
|
||
public function exportTrafficCsv(int $projectId, int $limit = 5000): array
|
||
{
|
||
$data = $this->listTraffic($projectId, 1, min($limit, 5000), '', 'rfmScore');
|
||
return $data['list'];
|
||
}
|
||
}
|