Files
cunkebao_v3/Server/application/chukebao/controller/WechatAccountController.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

243 lines
8.4 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 app\cunkebao\service\WechatAccountValuationService;
use library\ResponseHelper;
use think\Db;
/**
* 模块 G2 · 微信号用户管理 & 账号估值(需求九)
* 账号估值优先取存客宝真源 WechatAccountValuationService失败回退本地聚合。
*/
class WechatAccountController extends BaseController
{
private function accountId()
{
return (int)$this->request->param('id', 0);
}
/** 数字微信号账号 id → wechatId 字符串(估值真源以 wechatId 为口径) */
private function resolveWechatId(int $accountId): string
{
return (string)Db::table('s2_wechat_account')->where('id', $accountId)->value('wechatId');
}
/**
* 取账号估值:优先存客宝真源,失败回退本地聚合
*/
private function getValuation(int $accountId, int $friendCount): array
{
$companyId = (int)$this->getUserInfo('companyId');
$wechatId = $this->resolveWechatId($accountId);
if ($wechatId !== '') {
try {
$data = (new WechatAccountValuationService())->getValuation($wechatId, $companyId, false);
if (!empty($data)) {
$data['_source'] = 'cunkebao';
return $data;
}
} catch (\Throwable $e) {
// 回退本地聚合
}
}
return $this->buildValuation($accountId, $friendCount);
}
/**
* 管理页聚合
* GET /v1/kefu/wechat-account/<id>/manage
*/
public function manage()
{
$id = $this->accountId();
if (!$id) {
return ResponseHelper::error('id 缺失', 400);
}
$acc = Db::table('s2_wechat_account')->alias('wa')
->join(['s2_device' => 'd'], 'wa.currentDeviceId = d.id', 'LEFT')
->where('wa.id', $id)
->field(['wa.id', 'wa.nickname', 'wa.alias', 'wa.wechatId as wxid', 'wa.avatar', 'wa.wechatAliveTime', 'd.imei'])
->find();
if (!$acc) {
return ResponseHelper::error('微信号不存在', 404);
}
$friendCount = Db::table('s2_wechat_friend')
->where('wechatAccountId', $id)
->where('isDeleted', 0)
->count();
$acc['online'] = !empty($acc['wechatAliveTime']) && $acc['wechatAliveTime'] > (time() - 600);
$acc['deviceName'] = $acc['imei'] ?? '';
$acc['friendCount'] = $friendCount;
$acc['valuation'] = $this->getValuation($id, $friendCount);
return ResponseHelper::success($acc);
}
/**
* 账号估值best-effort 聚合;真源在存客宝)
* GET /v1/kefu/wechat-account/<id>/valuation
*/
public function valuation()
{
$id = $this->accountId();
if (!$id) {
return ResponseHelper::error('id 缺失', 400);
}
$friendCount = Db::table('s2_wechat_friend')
->where('wechatAccountId', $id)
->where('isDeleted', 0)
->count();
return ResponseHelper::success($this->getValuation($id, $friendCount));
}
private function buildValuation($id, $friendCount)
{
// 活跃好友30 天内有互动passTime 近似);高价值:暂以有手机号近似
$active = Db::table('s2_wechat_friend')
->where('wechatAccountId', $id)
->where('isDeleted', 0)
->where('updateTime', '>', time() - 86400 * 30)
->count();
$highValue = Db::table('s2_wechat_friend')
->where('wechatAccountId', $id)
->where('isDeleted', 0)
->where('phone', '<>', '')
->count();
$score = $friendCount > 0
? min(100, (int)round(($active / max(1, $friendCount)) * 60 + min(40, $friendCount / 10)))
: 0;
$level = $score >= 80 ? 'S' : ($score >= 60 ? 'A' : ($score >= 40 ? 'B' : ($score > 0 ? 'C' : '-')));
$position = $friendCount >= 300 ? '主力号' : ($friendCount >= 50 ? '养号' : '测试号');
return [
'accountValueScore' => $score,
'accountLevel' => $level,
'highValueFriendCount' => $highValue,
'activeFriendCount' => $active,
'projectPosition' => $position,
'sourceSummary' => '',
'evaluatedAt' => date('Y-m-d H:i:s'),
'planLineId' => 0,
'_estimated' => true,
];
}
/**
* 本号好友分页
* GET /v1/kefu/wechat-account/<id>/friends
*/
public function friends()
{
$id = $this->accountId();
if (!$id) {
return ResponseHelper::error('id 缺失', 400);
}
$companyId = (int)$this->getUserInfo('companyId');
$page = (int)$this->request->param('page', 1);
$limit = (int)$this->request->param('limit', 20);
$keyword = $this->request->param('keyword', '');
$query = Db::table('s2_wechat_friend')->alias('f')
->leftJoin(
'ck_traffic_pool_company t',
't.wechatFriendId=f.id AND t.companyId=' . $companyId . ' AND t.isDel=0'
)
->leftJoin(['s2_company_account' => 'ca'], 'ca.id=f.accountId', 'LEFT')
->where('f.wechatAccountId', $id)
->where('f.isDeleted', 0);
if ($keyword !== '') {
$query->where('f.nickname|f.conRemark|f.alias', 'like', '%' . $keyword . '%');
}
$total = (clone $query)->count();
$list = $query->field([
'f.id', 'f.nickname', 'f.conRemark', 'f.alias', 'f.wechatId', 'f.avatar',
'f.labels', 'f.phone', 'f.createTime', 'f.updateTime', 'f.accountId',
't.rfmScore', 't.lastInteractTime', 't.lastMsgTime', 't.id as poolCompanyId',
'ca.realName as kefuName', 'ca.userName as kefuUserName',
])
->page($page, $limit)
->orderRaw(
'COALESCE(t.lastMsgTime,t.lastInteractTime,f.updateTime,0) DESC, COALESCE(t.rfmScore,0) DESC'
)
->select();
$list = $this->enrichFriendsWithPoolTags($list ?: [], $companyId);
return ResponseHelper::success(['list' => $list, 'total' => $total]);
}
/**
* 合并微信 labels + 流量池标签,补齐 RFM 等级
*/
private function enrichFriendsWithPoolTags(array $list, int $companyId): array
{
if (empty($list)) {
return [];
}
$poolIds = [];
foreach ($list as $row) {
if (!empty($row['poolCompanyId'])) {
$poolIds[] = (int)$row['poolCompanyId'];
}
}
$poolIds = array_values(array_unique($poolIds));
$tagsByPool = [];
if (!empty($poolIds)) {
$tagRows = Db::name('traffic_pool_tag')
->where('poolCompanyId', 'in', $poolIds)
->where('isDel', 0)
->field('poolCompanyId, tagName')
->select();
foreach ($tagRows ?: [] as $tr) {
$pid = (int)$tr['poolCompanyId'];
$name = trim((string)($tr['tagName'] ?? ''));
if ($name === '') {
continue;
}
$tagsByPool[$pid][] = $name;
}
}
foreach ($list as &$v) {
$wxLabels = !empty($v['labels']) ? (json_decode($v['labels'], true) ?: []) : [];
if (!is_array($wxLabels)) {
$wxLabels = [];
}
$poolTags = $tagsByPool[(int)($v['poolCompanyId'] ?? 0)] ?? [];
$v['labels'] = array_values(array_unique(array_merge($wxLabels, $poolTags)));
$v['remark'] = $v['conRemark'];
$score = (int)($v['rfmScore'] ?? 0);
$v['rfmScore'] = $score;
$v['rfmLevel'] = $this->rfmLevelFromScore($score);
$kefu = trim((string)($v['kefuName'] ?? ''));
if ($kefu === '') {
$kefu = trim((string)($v['kefuUserName'] ?? ''));
}
$v['kefuName'] = $kefu;
unset($v['kefuUserName'], $v['poolCompanyId']);
}
unset($v);
return $list;
}
private function rfmLevelFromScore(int $score): string
{
if ($score >= 80) {
return 'S';
}
if ($score >= 60) {
return 'A';
}
if ($score >= 40) {
return 'B';
}
return $score > 0 ? 'C' : '-';
}
}