From 5a52b56d895789ca7dcee163a306501a0d0d0aa3 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 24 Nov 2025 17:18:49 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/model/WechatAccountModel.php | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Server/application/api/model/WechatAccountModel.php b/Server/application/api/model/WechatAccountModel.php index 919da478..001d3c63 100644 --- a/Server/application/api/model/WechatAccountModel.php +++ b/Server/application/api/model/WechatAccountModel.php @@ -8,26 +8,4 @@ class WechatAccountModel extends Model { // 设置表名 protected $table = 's2_wechat_account'; - - // 定义字段类型 - protected $type = [ - 'healthScore' => 'integer', - 'baseScore' => 'integer', - 'dynamicScore' => 'integer', - 'isModifiedAlias' => 'integer', - 'frequentCount' => 'integer', - 'consecutiveNoFrequentDays' => 'integer', - 'lastFrequentTime' => 'integer', - 'lastNoFrequentTime' => 'integer', - 'scoreUpdateTime' => 'integer', - ]; - - // 允许批量赋值的字段 - protected $field = [ - 'id', 'wechatId', 'alias', 'nickname', 'avatar', 'gender', 'region', 'signature', - 'healthScore', 'baseScore', 'dynamicScore', 'isModifiedAlias', - 'lastFrequentTime', 'frequentCount', 'lastNoFrequentTime', - 'consecutiveNoFrequentDays', 'scoreUpdateTime', - 'createTime', 'updateTime', 'status', 'isDeleted' - ]; } \ No newline at end of file From cd411906630f4108f64dad267a0f4150ead769e3 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 25 Nov 2025 14:52:46 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/chukebao/config/route.php | 2 + .../controller/WechatFriendController.php | 156 ++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/Server/application/chukebao/config/route.php b/Server/application/chukebao/config/route.php index 289e6fb0..56e17f51 100644 --- a/Server/application/chukebao/config/route.php +++ b/Server/application/chukebao/config/route.php @@ -14,6 +14,8 @@ Route::group('v1/', function () { Route::get('list', 'app\chukebao\controller\WechatFriendController@getList'); // 获取好友列表 Route::get('detail', 'app\chukebao\controller\WechatFriendController@getDetail'); // 获取好友详情 Route::post('updateInfo', 'app\chukebao\controller\WechatFriendController@updateFriendInfo'); // 更新好友资料 + // 添加好友任务记录相关接口 + Route::get('addTaskList', 'app\chukebao\controller\WechatFriendController@getAddTaskList'); // 获取添加好友任务记录列表(包含添加者信息、状态、时间等,支持状态筛选,无需传好友ID) }); //群相关 Route::group('wechatChatroom/', function () { diff --git a/Server/application/chukebao/controller/WechatFriendController.php b/Server/application/chukebao/controller/WechatFriendController.php index 644bd0a9..931d8ce6 100644 --- a/Server/application/chukebao/controller/WechatFriendController.php +++ b/Server/application/chukebao/controller/WechatFriendController.php @@ -166,4 +166,160 @@ class WechatFriendController extends BaseController return ResponseHelper::success(['id' => $friendId]); } + + /** + * 获取添加好友任务记录列表(全新功能) + * 返回当前账号的所有添加好友任务记录,无论是否通过都展示 + * 包含:添加者头像、昵称、微信号、添加状态、添加时间、通过时间等信息 + * @return \think\response\Json + */ + public function getAddTaskList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', ''); // 可选:筛选状态 0执行中,1执行成功,2执行失败 + $accountId = $this->getUserInfo('s2_accountId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + // 直接使用operatorAccountId查询添加好友任务记录 + $query = Db::table('s2_friend_task') + ->where('operatorAccountId', $accountId) + ->order('createTime desc'); + + // 如果指定了状态筛选 + if ($status !== '' && $status !== null) { + $query->where('status', $status); + } + + $total = $query->count(); + $tasks = $query->page($page, $limit)->select(); + + // 提取所有任务的phone和wechatId,用于查询好友信息(获取通过时间) + $taskPhones = []; + $taskWechatIds = []; + foreach ($tasks as $task) { + if (!empty($task['phone'])) { + $taskPhones[] = $task['phone']; + } + if (!empty($task['wechatId'])) { + $taskWechatIds[] = $task['wechatId']; + } + } + + // 查询好友信息,获取通过时间 + $friendPassTimeMap = []; + if (!empty($taskPhones) || !empty($taskWechatIds)) { + // 分别通过phone和wechatId查询,确保都能匹配到 + $friendsByPhone = []; + $friendsByWechatId = []; + + if (!empty($taskPhones)) { + $friendsByPhone = Db::table('s2_wechat_friend') + ->where('accountId', $accountId) + ->where('isDeleted', 0) + ->where('phone', 'in', $taskPhones) + ->field('phone,wechatId,passTime,nickname') + ->select(); + } + + if (!empty($taskWechatIds)) { + $friendsByWechatId = Db::table('s2_wechat_friend') + ->where('accountId', $accountId) + ->where('isDeleted', 0) + ->where('wechatId', 'in', $taskWechatIds) + ->field('phone,wechatId,passTime,nickname') + ->select(); + } + + // 合并结果并构建映射表(优先使用phone作为key) + $allFriends = array_merge($friendsByPhone, $friendsByWechatId); + foreach ($allFriends as $friend) { + // 使用phone作为key(如果存在) + if (!empty($friend['phone'])) { + $friendPassTimeMap[$friend['phone']] = [ + 'passTime' => $friend['passTime'] ?? 0, + 'nickname' => $friend['nickname'] ?? '', + ]; + } + // 同时使用wechatId作为key(如果存在且phone为空) + if (!empty($friend['wechatId']) && empty($friend['phone'])) { + $friendPassTimeMap[$friend['wechatId']] = [ + 'passTime' => $friend['passTime'] ?? 0, + 'nickname' => $friend['nickname'] ?? '', + ]; + } + } + } + + // 处理任务数据 + $list = []; + foreach ($tasks as $task) { + $taskKey = !empty($task['phone']) ? $task['phone'] : ($task['wechatId'] ?? ''); + $friendInfo = isset($friendPassTimeMap[$taskKey]) ? $friendPassTimeMap[$taskKey] : null; + + $item = [ + 'taskId' => $task['id'] ?? 0, + 'phone' => $task['phone'] ?? '', + 'wechatId' => $task['wechatId'] ?? '', + // 添加者信息 + 'adder' => [ + 'avatar' => $task['wechatAvatar'] ?? '', // 添加者头像 + 'nickname' => $task['wechatNickname'] ?? '', // 添加者昵称 + 'username' => $task['accountUsername'] ?? '', // 添加者微信号 + 'accountNickname' => $task['accountNickname'] ?? '', // 账号昵称 + 'accountRealName' => $task['accountRealName'] ?? '', // 账号真实姓名 + ], + // 添加状态 + 'status' => [ + 'code' => $task['status'] ?? 0, // 状态码:0执行中,1执行成功,2执行失败 + 'text' => $this->getTaskStatusText($task['status'] ?? 0), // 状态文本 + ], + // 时间信息 + 'time' => [ + 'addTime' => !empty($task['createTime']) ? date('Y-m-d H:i:s', $task['createTime']) : '', // 添加时间 + 'addTimeStamp' => $task['createTime'] ?? 0, // 添加时间戳 + 'updateTime' => !empty($task['updateTime']) ? date('Y-m-d H:i:s', $task['updateTime']) : '', // 更新时间 + 'updateTimeStamp' => $task['updateTime'] ?? 0, // 更新时间戳 + 'passTime' => !empty($friendInfo['passTime']) ? date('Y-m-d H:i:s', $friendInfo['passTime']) : '', // 通过时间 + 'passTimeStamp' => $friendInfo['passTime'] ?? 0, // 通过时间戳 + ], + // 好友信息(如果已通过) + 'friend' => [ + 'nickname' => $friendInfo['nickname'] ?? '', // 好友昵称 + 'isPassed' => !empty($friendInfo['passTime']), // 是否已通过 + ], + // 其他信息 + 'other' => [ + 'msgContent' => $task['msgContent'] ?? '', // 验证消息 + 'remark' => $task['remark'] ?? '', // 备注 + 'from' => $task['from'] ?? '', // 来源 + 'labels' => !empty($task['labels']) ? explode(',', $task['labels']) : [], // 标签 + ] + ]; + + $list[] = $item; + } + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + + /** + * 获取任务状态文本 + * @param int $status 状态码 + * @return string 状态文本 + */ + private function getTaskStatusText($status) + { + $statusMap = [ + 0 => '执行中', + 1 => '执行成功', + 2 => '执行失败', + ]; + + return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态'; + } } \ No newline at end of file From 7e2dd2914d38fbf8aae18f4bc811b9ce27734d63 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 26 Nov 2025 11:17:23 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E5=81=A5=E5=BA=B7=E5=88=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=8F=90=E4=BA=A4=20+=20=E5=BE=AE=E4=BF=A1=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E9=A1=B5=E9=9D=A2=E6=94=B9=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/wechat-accounts/detail/api.ts | 14 + .../mine/wechat-accounts/detail/data.ts | 98 ++ .../wechat-accounts/detail/detail.module.scss | 838 +++++++++++++++--- .../mine/wechat-accounts/detail/index.tsx | 637 ++++++++++--- .../wechat-accounts/list/index.module.scss | 33 + .../mine/wechat-accounts/list/index.tsx | 52 +- .../WechatAccountHealthScoreService.php | 693 ++++++++++++--- Server/application/cunkebao/config/route.php | 8 +- .../controller/ContentLibraryController.php | 11 +- .../wechat/GetWechatMomentsV1Controller.php | 204 +++++ .../wechat/GetWechatOverviewV1Controller.php | 411 +++++++++ .../GetWechatsOnDevicesV1Controller.php | 256 ++++-- Server/sql.sql | 171 +++- 13 files changed, 2945 insertions(+), 481 deletions(-) create mode 100644 Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php create mode 100644 Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 557ae0c9..11998fe6 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -5,6 +5,20 @@ export function getWechatAccountDetail(id: string) { return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET"); } +// 获取微信号概览数据 +export function getWechatAccountOverview(id: string) { + return request("/v1/wechats/overview", { wechatId: id }, "GET"); +} + +// 获取微信号朋友圈列表 +export function getWechatMoments(params: { + wechatId: string; + page?: number; + limit?: number; +}) { + return request("/v1/wechats/moments", params, "GET"); +} + // 获取微信号好友列表 export function getWechatFriends(params: { wechatAccount: string; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts index 20d7a2c9..9245ba9f 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts @@ -1,3 +1,41 @@ +// 概览数据接口 +export interface WechatAccountOverview { + healthScoreAssessment: { + score: number; + dailyLimit: number; + todayAdded: number; + lastAddTime: string; + statusTag: string; + baseComposition?: Array<{ + name: string; + score: number; + formatted: string; + friendCount?: number; + }>; + dynamicRecords?: Array<{ + title?: string; + description?: string; + time?: string; + score?: number; + formatted?: string; + statusTag?: string; + }>; + }; + accountValue: { + value: number; + formatted: string; + }; + todayValueChange: { + change: number; + formatted: string; + isPositive: boolean; + }; + totalFriends: number; + todayNewFriends: number; + highValueChatrooms: number; + todayNewChatrooms: number; +} + export interface WechatAccountSummary { accountAge: string; activityLevel: { @@ -15,12 +53,51 @@ export interface WechatAccountSummary { todayAdded: number; addLimit: number; }; + healthScore?: { + score: number; + lastUpdate?: string; + lastAddTime?: string; + baseScore?: number; + verifiedScore?: number; + friendsScore?: number; + activities?: { + type: string; + time?: string; + score: number; + description?: string; + status?: string; + }[]; + }; + moments?: { + id: string; + date: string; + month: string; + day: string; + content: string; + images?: string[]; + timeAgo?: string; + hasEmoji?: boolean; + }[]; + accountValue?: { + value: number; + todayChange?: number; + }; + friendsCount?: { + total: number; + todayAdded?: number; + }; + groupsCount?: { + total: number; + todayAdded?: number; + }; restrictions: { id: number; level: number; reason: string; date: string; }[]; + // 新增概览数据 + overview?: WechatAccountOverview; } export interface Friend { @@ -39,6 +116,27 @@ export interface Friend { region: string; source: string; notes: string; + value?: number; + valueFormatted?: string; + statusTags?: string[]; +} + +export interface MomentItem { + id: string; + snsId: string; + type: number; + content: string; + resUrls: string[]; + commentList?: any[]; + likeList?: any[]; + createTime: string; + momentEntity?: { + lat?: string; + lng?: string; + location?: string; + picSize?: number; + userName?: string; + }; } export interface WechatFriendDetail { diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index c6309f70..08f297a2 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -143,67 +143,235 @@ } .overview-content { - .info-grid { + // 健康分评估区域 + .health-score-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .health-score-info { + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + } + + // 账号统计卡片网格 + .account-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; - .info-card { - background: linear-gradient(135deg, #e6f7ff, #f0f8ff); + .stat-card { + background: #ffffff; padding: 16px; border-radius: 12px; - border: 1px solid #bae7ff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - transition: all 0.3s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - &:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); - } - - .info-header { + .stat-header { display: flex; + justify-content: space-between; align-items: center; - gap: 8px; margin-bottom: 8px; - .info-icon { - font-size: 16px; - color: #1677ff; - padding: 6px; - background: #e6f7ff; - border-radius: 8px; + .stat-title { + font-size: 14px; + color: #666; } - .info-title { - flex: 1; + .stat-icon-up { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; - .title-text { - font-size: 12px; - font-weight: 600; - color: #1677ff; - margin-bottom: 2px; + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(-45deg); + width: 8px; + height: 8px; + border-top: 2px solid #722ed1; + border-right: 2px solid #722ed1; + } + } + + .stat-icon-plus { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 2px; + background: #52c41a; } - .title-sub { - font-size: 10px; - color: #666; + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 10px; + background: #52c41a; + } + } + + .stat-icon-people { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 7px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #1677ff; + } + + &::after { + content: ''; + position: absolute; + top: 13px; + left: 5px; + width: 10px; + height: 5px; + border-radius: 10px 10px 0 0; + background: #1677ff; + } + } + + .stat-icon-chat { + width: 20px; + height: 20px; + background: #f0f0f0; + border-radius: 50%; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: #fa8c16; } } } - .info-value { - text-align: right; - font-size: 18px; - font-weight: 700; - color: #1677ff; + .stat-value { + font-size: 20px; + font-weight: 600; + color: #333; + } - .value-unit { - font-size: 12px; - color: #666; - margin-left: 4px; - } + .stat-value-positive { + font-size: 20px; + font-weight: 600; + color: #52c41a; } } } @@ -449,6 +617,47 @@ } } + .friends-summary { + display: flex; + align-items: center; + justify-content: space-between; + background: #f5f9ff; + border: 1px solid #e0edff; + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 16px; + + .summary-item { + display: flex; + flex-direction: column; + gap: 4px; + } + + .summary-label { + font-size: 12px; + color: #666; + } + + .summary-value { + font-size: 20px; + font-weight: 600; + color: #111; + } + + .summary-value-highlight { + font-size: 20px; + font-weight: 600; + color: #fa541c; + } + + .summary-divider { + width: 1px; + height: 32px; + background: #e6e6e6; + margin: 0 12px; + } + } + .friends-list { .empty { text-align: center; @@ -467,83 +676,100 @@ } } - .friend-item { + .friend-card { display: flex; align-items: center; - padding: 12px; + padding: 14px; background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; - } + border: 1px solid #f0f0f0; + border-radius: 12px; + margin-bottom: 10px; + gap: 12px; + transition: box-shadow 0.2s, border-color 0.2s; - .friend-item-static { - display: flex; - align-items: center; - padding: 12px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - margin-bottom: 8px; + &:hover { + border-color: #cfe2ff; + box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15); + } } .friend-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - margin-right: 12px; + width: 48px; + height: 48px; + + .adm-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + } } - .friend-info { + .friend-main { flex: 1; min-width: 0; + } - .friend-header { - display: flex; - align-items: center; - justify-content: space-between; + .friend-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .friend-name { + font-size: 15px; + font-weight: 600; + color: #111; + flex-shrink: 0; + } + + .friend-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + .friend-tag { + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + background: #f5f5f5; + color: #666; + } + + .friend-id-row { + font-size: 12px; + color: #999; + margin-bottom: 6px; + } + + .friend-status-row { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .friend-status-chip { + background: #f0f7ff; + color: #1677ff; + font-size: 11px; + padding: 2px 8px; + border-radius: 8px; + } + + .friend-value { + text-align: right; + + .value-label { + font-size: 11px; + color: #999; margin-bottom: 4px; - - .friend-name { - font-size: 14px; - font-weight: 500; - color: #333; - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - .friend-remark { - color: #666; - margin-left: 4px; - } - } - - .friend-arrow { - font-size: 12px; - color: #ccc; - } } - .friend-wechat-id { - font-size: 12px; - color: #666; - margin-bottom: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .friend-tags { - display: flex; - flex-wrap: wrap; - gap: 4px; - - .friend-tag { - font-size: 10px; - padding: 2px 6px; - border-radius: 6px; - } + .value-amount { + font-size: 14px; + font-weight: 600; + color: #fa541c; } } } @@ -769,6 +995,416 @@ margin-right: 8px; } +.health-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + + .health-score-card { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-score-status { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + + .status-tag { + background: #ffebeb; + color: #ff4d4f; + font-size: 12px; + padding: 2px 8px; + border-radius: 4px; + } + + .status-time { + font-size: 12px; + color: #999; + } + } + + .health-score-display { + display: flex; + align-items: center; + + .score-circle-wrapper { + width: 100px; + height: 100px; + margin-right: 24px; + position: relative; + + .score-circle { + width: 100%; + height: 100%; + border-radius: 50%; + background: #fff; + border: 8px solid #ff4d4f; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .score-number { + font-size: 28px; + font-weight: 700; + color: #ff4d4f; + line-height: 1; + } + + .score-label { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + } + + .health-score-stats { + flex: 1; + + .stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + + .stats-label { + font-size: 14px; + color: #666; + } + + .stats-value { + font-size: 14px; + color: #333; + font-weight: 500; + } + } + } + } + } + + .health-section { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .health-section-title { + font-size: 16px; + font-weight: 600; + color: #ff8800; + margin-bottom: 12px; + position: relative; + padding-left: 12px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 16px; + background: #ff8800; + border-radius: 2px; + } + } + + .health-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + + .health-item-label { + font-size: 14px; + color: #333; + display: flex; + align-items: center; + + .health-item-icon-warning { + width: 16px; + height: 16px; + border-radius: 50%; + background: #ffebeb; + margin-right: 8px; + position: relative; + + &::before { + content: '!'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #ff4d4f; + font-size: 12px; + font-weight: bold; + } + } + + .health-item-tag { + background: #fff7e6; + color: #fa8c16; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 8px; + } + } + + .health-item-value-positive { + font-size: 14px; + font-weight: 600; + color: #52c41a; + } + + .health-item-value-negative { + font-size: 14px; + font-weight: 600; + color: #ff4d4f; + } + + .health-item-value-empty { + width: 20px; + } + } + + .health-empty { + text-align: center; + color: #999; + font-size: 14px; + padding: 20px 0; + } + } +} + +.moments-content { + padding: 16px 0; + height: 500px; + overflow-y: auto; + background: #f5f5f5; + + .moments-action-bar { + display: flex; + justify-content: space-between; + padding: 0 16px 16px; + + .action-button, .action-button-dark { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 70px; + height: 40px; + border-radius: 8px; + background: #1677ff; + + .action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export { + width: 20px; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + margin-bottom: 2px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 2px; + background: white; + } + } + + .action-icon-image::after { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 2px; + background: white; + } + + .action-icon-video::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + border-style: solid; + border-width: 5px 0 5px 8px; + border-color: transparent transparent transparent white; + } + + .action-text, .action-text-light { + font-size: 12px; + color: white; + } + } + + .action-button-dark { + background: #333; + } + } + + .moments-list { + padding: 0 16px; + + .moment-item { + display: flex; + margin-bottom: 16px; + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + .moment-date { + margin-right: 12px; + text-align: center; + + .date-day { + font-size: 20px; + font-weight: 600; + color: #333; + line-height: 1; + } + + .date-month { + font-size: 12px; + color: #999; + margin-top: 2px; + } + } + + .moment-content { + flex: 1; + + .moment-text { + font-size: 14px; + line-height: 1.5; + color: #333; + margin-bottom: 8px; + white-space: pre-wrap; // 保留换行和空格,确保文本完整显示 + word-wrap: break-word; // 长单词自动换行 + + .moment-emoji { + display: inline; + font-size: 16px; + vertical-align: middle; + } + } + + .moment-images { + margin-bottom: 8px; + + .image-grid { + display: grid; + gap: 8px; + width: 100%; + + // 1张图片:宽度拉伸,高度自适应 + &.single { + grid-template-columns: 1fr; + + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + } + } + + // 2张图片:左右并列 + &.double { + grid-template-columns: 1fr 1fr; + + img { + width: 100%; + height: 120px; + object-fit: cover; + border-radius: 8px; + } + } + + // 3张图片:三张并列 + &.triple { + grid-template-columns: 1fr 1fr 1fr; + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + } + + // 4张图片:2x2网格布局 + &.quad { + grid-template-columns: repeat(2, 1fr); + + img { + width: 100%; + height: 140px; + object-fit: cover; + border-radius: 8px; + } + } + + // 5张及以上:网格布局(9宫格) + &.grid { + grid-template-columns: repeat(3, 1fr); + + img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 8px; + } + + .image-more { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + border-radius: 8px; + color: white; + font-size: 12px; + font-weight: 500; + height: 100px; + } + } + } + } + + .moment-footer { + display: flex; + justify-content: flex-end; + + .moment-time { + font-size: 12px; + color: #999; + } + } + } + } + } +} + .risk-content { padding: 16px 0; height: 500px; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 3be617b5..50f2c599 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -21,11 +21,17 @@ import { } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import style from "./detail.module.scss"; -import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api"; +import { + getWechatAccountDetail, + getWechatFriends, + transferWechatFriends, + getWechatAccountOverview, + getWechatMoments, +} from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; -import { WechatAccountSummary, Friend } from "./data"; +import { WechatAccountSummary, Friend, MomentItem } from "./data"; const WechatAccountDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -34,6 +40,7 @@ const WechatAccountDetail: React.FC = () => { const [accountSummary, setAccountSummary] = useState(null); const [accountInfo, setAccountInfo] = useState(null); + const [overviewData, setOverviewData] = useState(null); const [showRestrictions, setShowRestrictions] = useState(false); const [showTransferConfirm, setShowTransferConfirm] = useState(false); const [selectedDevices, setSelectedDevices] = useState([]); @@ -50,6 +57,12 @@ const WechatAccountDetail: React.FC = () => { const [isFetchingFriends, setIsFetchingFriends] = useState(false); const [hasFriendLoadError, setHasFriendLoadError] = useState(false); const [isFriendsEmpty, setIsFriendsEmpty] = useState(false); + const [moments, setMoments] = useState([]); + const [momentsPage, setMomentsPage] = useState(1); + const [momentsTotal, setMomentsTotal] = useState(0); + const [isFetchingMoments, setIsFetchingMoments] = useState(false); + const [momentsError, setMomentsError] = useState(null); + const MOMENTS_LIMIT = 10; // 获取基础信息 const fetchAccountInfo = useCallback(async () => { @@ -80,6 +93,19 @@ const WechatAccountDetail: React.FC = () => { } }, [id]); + // 获取概览数据 + const fetchOverviewData = useCallback(async () => { + if (!id) return; + try { + const response = await getWechatAccountOverview(id); + if (response) { + setOverviewData(response); + } + } catch (e) { + console.error("获取概览数据失败:", e); + } + }, [id]); + // 获取好友列表 - 封装为独立函数 const fetchFriendsList = useCallback( async (page: number = 1, keyword: string = "") => { @@ -96,26 +122,44 @@ const WechatAccountDetail: React.FC = () => { keyword: keyword, }); - const newFriends = response.list.map((friend: any) => ({ - id: friend.id.toString(), - avatar: friend.avatar || "/placeholder.svg", - nickname: friend.nickname || "未知用户", - wechatId: friend.wechatId || "", - remark: friend.memo || "", - addTime: friend.createTime || new Date().toISOString().split("T")[0], - lastInteraction: - friend.lastInteraction || new Date().toISOString().split("T")[0], - tags: friend.tags - ? friend.tags.map((tag: string, index: number) => ({ - id: `tag-${index}`, - name: tag, - color: getRandomTagColor(), - })) - : [], - region: friend.region || "未知", - source: friend.source || "未知", - notes: friend.notes || "", - })); + const newFriends = response.list.map((friend: any) => { + const memoTags = Array.isArray(friend.memo) + ? friend.memo + : friend.memo + ? String(friend.memo) + .split(/[,\s,、]+/) + .filter(Boolean) + : []; + + const tagList = Array.isArray(friend.tags) + ? friend.tags + : friend.tags + ? [friend.tags] + : []; + + return { + id: friend.id.toString(), + avatar: friend.avatar || "/placeholder.svg", + nickname: friend.nickname || "未知用户", + wechatId: friend.wechatId || "", + remark: friend.notes || "", + addTime: + friend.createTime || new Date().toISOString().split("T")[0], + lastInteraction: + friend.lastInteraction || new Date().toISOString().split("T")[0], + tags: memoTags.map((tag: string, index: number) => ({ + id: `tag-${index}`, + name: tag, + color: getRandomTagColor(), + })), + statusTags: tagList, + region: friend.region || "未知", + source: friend.source || "未知", + notes: friend.notes || "", + value: friend.value, + valueFormatted: friend.valueFormatted, + }; + }); setFriends(newFriends); setFriendsTotal(response.total); @@ -137,6 +181,46 @@ const WechatAccountDetail: React.FC = () => { [id], ); + const fetchMomentsList = useCallback( + async (page: number = 1, append: boolean = false) => { + if (!id) return; + setIsFetchingMoments(true); + setMomentsError(null); + try { + const response = await getWechatMoments({ + wechatId: id, + page, + limit: MOMENTS_LIMIT, + }); + + const list: MomentItem[] = (response.list || []).map((moment: any) => ({ + id: moment.id?.toString() || Math.random().toString(), + snsId: moment.snsId, + type: moment.type, + content: moment.content || "", + resUrls: moment.resUrls || [], + commentList: moment.commentList || [], + likeList: moment.likeList || [], + createTime: moment.createTime || "", + momentEntity: moment.momentEntity || {}, + })); + + setMoments(prev => (append ? [...prev, ...list] : list)); + setMomentsTotal(response.total || list.length); + setMomentsPage(page); + } catch (error) { + console.error("获取朋友圈数据失败:", error); + setMomentsError("获取朋友圈数据失败"); + if (!append) { + setMoments([]); + } + } finally { + setIsFetchingMoments(false); + } + }, + [id], + ); + // 搜索好友 const handleSearch = useCallback(() => { setFriendsPage(1); @@ -161,8 +245,9 @@ const WechatAccountDetail: React.FC = () => { useEffect(() => { if (id) { fetchAccountInfo(); + fetchOverviewData(); } - }, [id, fetchAccountInfo]); + }, [id, fetchAccountInfo, fetchOverviewData]); // 监听标签切换 - 只在切换到好友列表时请求一次 useEffect(() => { @@ -173,6 +258,14 @@ const WechatAccountDetail: React.FC = () => { } }, [activeTab, id, fetchFriendsList, searchQuery]); + useEffect(() => { + if (activeTab === "moments" && id) { + if (moments.length === 0) { + fetchMomentsList(1, false); + } + } + }, [activeTab, id, fetchMomentsList, moments.length]); + // 工具函数 const getRandomTagColor = (): string => { const colors = [ @@ -271,6 +364,41 @@ const WechatAccountDetail: React.FC = () => { navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`); }; + const handleLoadMoreMoments = () => { + if (isFetchingMoments) return; + if (moments.length >= momentsTotal) return; + fetchMomentsList(momentsPage + 1, true); + }; + + const formatMomentDateParts = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return { day: "--", month: "--" }; + } + const day = date.getDate().toString().padStart(2, "0"); + const month = `${date.getMonth() + 1}月`; + return { day, month }; + }; + + const formatMomentTimeAgo = (dateString: string) => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return dateString || "--"; + } + const diff = Date.now() - date.getTime(); + const minutes = Math.floor(diff / (1000 * 60)); + if (minutes < 1) return "刚刚"; + if (minutes < 60) return `${minutes}分钟前`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}小时前`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}天前`; + return date.toLocaleDateString("zh-CN", { + month: "2-digit", + day: "2-digit", + }); + }; + return ( } loading={loadingInfo}>
@@ -313,73 +441,220 @@ const WechatAccountDetail: React.FC = () => { onChange={handleTabChange} className={style["tabs"]} > - +
-
-
-
- {accountInfo?.friendShip?.totalFriend ?? "-"} + {/* 健康分评估区域 */} +
+
健康分评估
+
+
+ {overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}
-
好友数量
-
-
-
- +{accountSummary?.statistics.todayAdded ?? "-"} +
+
+
+
+ {overviewData?.healthScoreAssessment?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{overviewData?.healthScoreAssessment?.dailyLimit || 0} 人
+
+
+
今日已加
+
{overviewData?.healthScoreAssessment?.todayAdded || 0} 人
+
+
-
今日新增
-
- 今日可添加: - - {accountSummary?.statistics.todayAdded ?? 0}/ - {accountSummary?.statistics.addLimit ?? 0} - -
-
-
-
-
-
-
-
-
- {accountInfo?.friendShip?.groupNumber ?? "-"} + + {/* 账号价值和好友数量区域 */} +
+ {/* 账号价值 */} +
+
+
账号价值
+
-
群聊数量
-
-
-
- {accountInfo?.activity?.yesterdayMsgCount ?? "-"} +
+ {overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`} +
+
+ + {/* 今日价值变化 */} +
+
+
今日价值变化
+
+
+
+ {overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
-
今日消息
-
-
设备信息
-
- 设备名称: - {accountInfo?.deviceName ?? "-"} + + {/* 好友数量和今日新增好友区域 */} +
+ {/* 好友总数 */} +
+
+
好友总数
+
+
+
+ {overviewData?.totalFriends || accountInfo?.friendShip?.totalFriend || "0"} +
-
- 系统类型: - {accountInfo?.deviceType ?? "-"} + + {/* 今日新增好友 */} +
+
+
今日新增好友
+
+
+
+ +{overviewData?.todayNewFriends || accountSummary?.statistics.todayAdded || "0"} +
-
- 系统版本: - {accountInfo?.deviceVersion ?? "-"} +
+ + {/* 高价群聊区域 */} +
+ {/* 高价群聊 */} +
+
+
高价群聊
+
+
+
+ {overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || "0"} +
+ + {/* 今日新增群聊 */} +
+
+
今日新增群聊
+
+
+
+ +{overviewData?.todayNewChatrooms || "0"} +
+
+
+ + +
+ + + +
+ {/* 健康分数圆环 */} +
+
+ 已添加加人 + 最后添加时间: {accountSummary?.healthScore?.lastAddTime || "18:36:06"} +
+
+
+
+
+ {accountSummary?.healthScore?.score || 67} +
+
SCORE
+
+
+
+
+
每日限额
+
{accountSummary?.statistics.addLimit || 0} 人
+
+
+
今日已加
+
{accountSummary?.statistics.todayAdded || 0} 人
+
+
+
+
+ + {/* 基础构成 */} +
+
基础构成
+ {(overviewData?.healthScoreAssessment?.baseComposition && + overviewData.healthScoreAssessment.baseComposition.length > 0 + ? overviewData.healthScoreAssessment.baseComposition + : [ + { name: "账号基础分", formatted: "+60" }, + { name: "已修改微信号", formatted: "+10" }, + { name: "好友数量加成", formatted: "+12", friendCount: 5595 }, + ] + ).map((item, index) => ( +
+
+ {item.name} + {item.friendCount ? ` (${item.friendCount})` : ""} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {item.formatted || `${item.score ?? 0}`} +
+
+ ))} +
+ + {/* 动态记录 */} +
+
动态记录
+ {overviewData?.healthScoreAssessment?.dynamicRecords && + overviewData.healthScoreAssessment.dynamicRecords.length > 0 ? ( + overviewData.healthScoreAssessment.dynamicRecords.map( + (record, index) => ( +
+
+ + {record.title || record.description || "记录"} + {record.statusTag && ( + + {record.statusTag} + + )} +
+
= 0 + ? style["health-item-value-positive"] + : style["health-item-value-negative"] + } + > + {record.formatted || + (record.score && record.score > 0 + ? `+${record.score}` + : record.score || "-")} +
+
+ ), + ) + ) : ( +
暂无动态记录
+ )}
+ 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} + title={`好友${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} key="friends" >
@@ -406,6 +681,23 @@ const WechatAccountDetail: React.FC = () => {
+ {/* 好友概要 */} +
+
+
好友总数
+
+ {friendsTotal || overviewData?.totalFriends || 0} +
+
+
+
+
好友总估值
+
+ {overviewData?.accountValue?.formatted || "¥1,500,000"} +
+
+
+ {/* 好友列表 */}
{isFetchingFriends && friends.length === 0 ? ( @@ -431,36 +723,53 @@ const WechatAccountDetail: React.FC = () => { {friends.map(friend => (
handleFriendClick(friend)} > - -
-
+
+ +
+
+
- {friend.nickname} - {friend.remark && ( - - ({friend.remark}) + {friend.nickname || "未知好友"} +
+
+ {friend.tags?.map((tag, index) => ( + + {typeof tag === "string" ? tag : tag.name} - )} + ))}
-
- {friend.wechatId} +
+ ID: {friend.wechatId || "-"}
-
- {friend.tags?.map((tag, index) => ( - + {friend.statusTags?.map((tag, idx) => ( + - {typeof tag === "string" ? tag : tag.name} - + {tag} + ))} + {friend.remark && ( + + {friend.remark} + + )} +
+
+
+
+ {friend.valueFormatted + || (typeof friend.value === "number" + ? `¥${friend.value.toLocaleString()}` + : "估值 -")}
@@ -484,45 +793,115 @@ const WechatAccountDetail: React.FC = () => {
- -
- {accountSummary?.restrictions && - accountSummary.restrictions.length > 0 ? ( -
- {accountSummary.restrictions.map(restriction => ( -
-
-
- {restriction.reason} -
-
- {restriction.date - ? formatDateTime(restriction.date) - : "暂无时间"} -
-
-
- - {restriction.level === 1 - ? "低风险" - : restriction.level === 2 - ? "中风险" - : "高风险"} - -
-
- ))} + + +
+ {/* 功能按钮栏 */} +
+
+ + 文本 +
+
+ + 图片 +
+
+ + 视频 +
+
+ + 导出 +
+
+ + {/* 朋友圈列表 */} +
+ {isFetchingMoments && moments.length === 0 ? ( +
+ +
+ ) : momentsError ? ( +
{momentsError}
+ ) : moments.length === 0 ? ( +
暂无朋友圈内容
+ ) : ( + moments.map(moment => { + const { day, month } = formatMomentDateParts( + moment.createTime, + ); + const timeAgo = formatMomentTimeAgo(moment.createTime); + const imageCount = moment.resUrls?.length || 0; + // 根据图片数量选择对应的grid类,参考素材管理的实现 + let gridClass = ""; + if (imageCount === 1) gridClass = style["single"]; + else if (imageCount === 2) gridClass = style["double"]; + else if (imageCount === 3) gridClass = style["triple"]; + else if (imageCount === 4) gridClass = style["quad"]; + else if (imageCount > 4) gridClass = style["grid"]; + + return ( +
+
+
{day}
+
{month}
+
+
+ {moment.content && ( +
+ {moment.content} +
+ )} + {imageCount > 0 && ( +
+
+ {moment.resUrls + .slice(0, 9) + .map((url, index) => ( + 朋友圈图片 + ))} + {imageCount > 9 && ( +
+ +{imageCount - 9} +
+ )} +
+
+ )} +
+ + {timeAgo} + +
+
+
+ ); + }) + )} +
+ + {moments.length < momentsTotal && ( +
+
- ) : ( -
暂无风险记录
)}
+
diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss index 4cfd3adc..8ddb6b92 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.module.scss @@ -2,6 +2,39 @@ padding: 0 12px; } +.filter-bar { + padding: 12px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + + .filter-buttons { + display: flex; + gap: 8px; + + .filter-button { + flex: 1; + height: 32px; + border-radius: 6px; + border: 1px solid #d9d9d9; + background: #fff; + color: #666; + font-size: 14px; + transition: all 0.2s; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.filter-button-active { + background: #1677ff; + border-color: #1677ff; + color: #fff; + } + } + } +} + .nav-title { font-size: 18px; font-weight: 600; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx index 4b761b60..dbd30443 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx @@ -33,11 +33,12 @@ const WechatAccounts: React.FC = () => { const [totalAccounts, setTotalAccounts] = useState(0); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all"); // 获取路由参数 wechatStatus const wechatStatus = searchParams.get("wechatStatus"); - const fetchAccounts = async (page = 1, keyword = "") => { + const fetchAccounts = async (page = 1, keyword = "", status?: "all" | "online" | "offline") => { setIsLoading(true); try { const params: any = { @@ -46,8 +47,12 @@ const WechatAccounts: React.FC = () => { keyword, }; - // 如果有 wechatStatus 参数,添加到请求参数中 - if (wechatStatus) { + // 优先使用传入的status参数,否则使用路由参数,最后使用状态中的筛选 + const filterStatus = status || wechatStatus || statusFilter; + + if (filterStatus && filterStatus !== "all") { + params.wechatStatus = filterStatus === "online" ? "1" : "0"; + } else if (wechatStatus) { params.wechatStatus = wechatStatus; } @@ -60,7 +65,7 @@ const WechatAccounts: React.FC = () => { setTotalAccounts(0); } } catch (e) { - Toast.show({ content: "获取微信号失败", position: "top" }); + setAccounts([]); setTotalAccounts(0); } finally { @@ -69,18 +74,24 @@ const WechatAccounts: React.FC = () => { }; useEffect(() => { - fetchAccounts(currentPage, searchTerm); + fetchAccounts(currentPage, searchTerm, statusFilter); // eslint-disable-next-line - }, [currentPage]); + }, [currentPage, statusFilter]); const handleSearch = () => { setCurrentPage(1); - fetchAccounts(1, searchTerm); + fetchAccounts(1, searchTerm, statusFilter); + }; + + const handleStatusFilterChange = (status: "all" | "online" | "offline") => { + setStatusFilter(status); + setCurrentPage(1); + fetchAccounts(1, searchTerm, status); }; const handleRefresh = async () => { setIsRefreshing(true); - await fetchAccounts(currentPage, searchTerm); + await fetchAccounts(currentPage, searchTerm, statusFilter); setIsRefreshing(false); Toast.show({ content: "刷新成功", position: "top" }); }; @@ -122,6 +133,31 @@ const WechatAccounts: React.FC = () => {
+
+
+ + + +
+
} > diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index d6246a26..66f31234 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -16,16 +16,22 @@ use think\facade\Cache; * 2. 各个评分维度独立存储 * 3. 使用独立的评分记录表 * 4. 好友数量评分特殊处理(避免同步问题) - * 5. 动态分仅统计近30天数据 + * 5. 动态分统计所有历史数据(不限制30天) * 6. 优化数据库查询,减少重复计算 * 7. 添加完善的日志记录,便于问题排查 + * 8. 每条频繁/封号记录只统计一次,避免重复扣分 + * 9. 使用is_counted字段标记已统计的记录 + * 10. 支持lastBanTime字段,记录最后一次封号时间 + * 11. 使用事务和锁避免并发问题 + * 12. 使用静态缓存避免重复检查字段 + * 13. 推荐数据库索引提高查询性能 * * 健康分 = 基础分 + 动态分 * 基础分:60-100分(默认60分 + 基础信息10分 + 好友数量30分) * 动态分:扣分和加分规则 * * @author Your Name - * @version 2.0.0 + * @version 2.3.0 */ class WechatAccountHealthScoreService { @@ -33,7 +39,8 @@ class WechatAccountHealthScoreService * 缓存相关配置 */ const CACHE_PREFIX = 'wechat_health_score:'; // 缓存前缀 - const CACHE_TTL = 3600; // 缓存有效期(秒) + const CACHE_TTL = 7200; // 缓存有效期(秒)- 提高到2小时 + const CACHE_TTL_SHORT = 300; // 短期缓存有效期(秒)- 5分钟,用于频繁变化的数据 /** * 默认基础分 @@ -70,9 +77,34 @@ class WechatAccountHealthScoreService */ const TABLE_WECHAT_ACCOUNT = 's2_wechat_account'; const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score'; + const TABLE_WECHAT_ACCOUNT_SCORE_LOG = 's2_wechat_account_score_log'; const TABLE_FRIEND_TASK = 's2_friend_task'; const TABLE_WECHAT_MESSAGE = 's2_wechat_message'; + /** + * 推荐数据库索引 + * 以下索引可以大幅提升查询性能 + * + * s2_wechat_account_score表: + * - PRIMARY KEY (`id`) + * - KEY `idx_account_id` (`accountId`) + * + * s2_friend_task表: + * - PRIMARY KEY (`id`) + * - KEY `idx_wechat_account_id` (`wechatAccountId`) + * - KEY `idx_wechat_id` (`wechatId`) + * - KEY `idx_create_time` (`createTime`) + * - KEY `idx_is_counted` (`is_counted`) + * + * s2_wechat_message表: + * - PRIMARY KEY (`id`) + * - KEY `idx_wechat_account_id` (`wechatAccountId`) + * - KEY `idx_msg_type` (`msgType`) + * - KEY `idx_create_time` (`createTime`) + * - KEY `idx_is_deleted` (`isDeleted`) + * - KEY `idx_is_counted` (`is_counted`) + */ + /** * 计算并更新账号健康分 * @@ -100,7 +132,7 @@ class WechatAccountHealthScoreService ->where('id', $accountId) ->find(); - Log::debug("查询账号数据: " . ($accountData ? "成功" : "失败")); + // 减少不必要的日志记录 } if (empty($accountData)) { @@ -116,11 +148,12 @@ class WechatAccountHealthScoreService throw new Exception($errorMsg); } - Log::debug("账号数据: accountId={$accountId}, wechatId={$wechatId}"); + // 减少不必要的日志记录 // 获取或创建评分记录 $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); - Log::debug("获取评分记录: " . ($scoreRecord ? "成功" : "失败")); + $scoreSnapshotBefore = $this->buildScoreSnapshotForLogging($scoreRecord); + // 减少不必要的日志记录 // 计算基础分(只计算一次,除非强制重新计算) if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) { @@ -131,7 +164,7 @@ class WechatAccountHealthScoreService $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord); $this->updateBaseScore($accountId, $baseScoreData); - Log::debug("基础分计算结果: " . json_encode($baseScoreData)); + // 减少不必要的日志记录 // 重新获取记录以获取最新数据 $scoreRecord = $this->getScoreRecord($accountId); @@ -166,6 +199,7 @@ class WechatAccountHealthScoreService 'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'], 'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'], 'isBanned' => $dynamicScoreData['isBanned'], + 'lastBanTime' => $dynamicScoreData['lastBanTime'], 'healthScore' => $healthScore, 'maxAddFriendPerDay' => $maxAddFriendPerDay, 'updateTime' => time() @@ -177,6 +211,19 @@ class WechatAccountHealthScoreService // 更新成功后,清除缓存 if ($updateResult !== false) { + $this->logScoreChangesIfNeeded( + $accountId, + $wechatId, + $scoreSnapshotBefore, + [ + 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], + 'banPenalty' => $dynamicScoreData['banPenalty'], + 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], + 'dynamicScore' => $dynamicScore, + 'healthScore' => $healthScore + ], + $dynamicScoreData + ); $this->clearScoreCache($accountId); } @@ -194,7 +241,7 @@ class WechatAccountHealthScoreService 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; - Log::debug("健康分计算完成,返回结果: " . json_encode($result)); + // 减少不必要的日志记录 return $result; } catch (\PDOException $e) { @@ -212,6 +259,7 @@ class WechatAccountHealthScoreService /** * 获取或创建评分记录 + * 优化:使用事务和锁避免并发问题,减少重复查询 * * @param int $accountId 账号ID * @param string $wechatId 微信ID @@ -224,35 +272,96 @@ class WechatAccountHealthScoreService // 如果记录不存在,创建新记录 if (empty($record)) { - Log::info("为账号 {$accountId} 创建新的评分记录"); - - // 创建新记录 - $data = [ - 'accountId' => $accountId, - 'wechatId' => $wechatId, - 'baseScore' => 0, - 'baseScoreCalculated' => 0, - 'baseInfoScore' => 0, - 'friendCountScore' => 0, - 'dynamicScore' => 0, - 'frequentCount' => 0, - 'consecutiveNoFrequentDays' => 0, - 'healthScore' => 0, - 'maxAddFriendPerDay' => 0, - 'createTime' => time(), - 'updateTime' => time() - ]; - - Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); - - return $data; + // 使用事务避免并发问题 + Db::startTrans(); + try { + // 再次检查记录是否存在(避免并发问题) + $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->lock(true) // 加锁防止并发插入 + ->find(); + + if (empty($record)) { + Log::info("为账号 {$accountId} 创建新的评分记录"); + + // 检查表中是否存在lastBanTime字段 + $this->ensureScoreTableFields(); + + // 创建新记录 + $data = [ + 'accountId' => $accountId, + 'wechatId' => $wechatId, + 'baseScore' => 0, + 'baseScoreCalculated' => 0, + 'baseInfoScore' => 0, + 'friendCountScore' => 0, + 'dynamicScore' => 0, + 'frequentCount' => 0, + 'consecutiveNoFrequentDays' => 0, + 'healthScore' => 0, + 'maxAddFriendPerDay' => 0, + 'lastBanTime' => null, + 'createTime' => time(), + 'updateTime' => time() + ]; + + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); + $record = $data; + } + + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + Log::error("创建评分记录失败: " . $e->getMessage()); + throw $e; + } } return $record; } + /** + * 确保评分表有所需字段 + * 优化:使用静态变量缓存结果,避免重复检查 + * + * @return void + */ + private function ensureScoreTableFields() + { + // 使用静态变量缓存检查结果,避免重复检查 + static $fieldsChecked = false; + + if ($fieldsChecked) { + return; + } + + try { + // 检查表中是否存在lastBanTime字段 + $hasLastBanTimeField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_ACCOUNT_SCORE); + foreach ($tableFields as $field) { + if ($field['Field'] == 'lastBanTime') { + $hasLastBanTimeField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasLastBanTimeField) { + Log::info("添加lastBanTime字段到" . self::TABLE_WECHAT_ACCOUNT_SCORE . "表"); + Db::execute("ALTER TABLE " . self::TABLE_WECHAT_ACCOUNT_SCORE . " ADD COLUMN lastBanTime INT(11) DEFAULT NULL COMMENT '最后一次封号时间'"); + } + + $fieldsChecked = true; + } catch (\Exception $e) { + Log::error("检查或添加字段失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + } + /** * 获取评分记录 + * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) @@ -266,7 +375,7 @@ class WechatAccountHealthScoreService // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); - Log::debug("从缓存获取评分记录,accountId: {$accountId}"); + // 减少日志记录,提高性能 return $cachedData ?: []; } @@ -277,8 +386,13 @@ class WechatAccountHealthScoreService // 如果记录存在且使用缓存,则缓存记录 if ($record && $useCache) { - Cache::set($cacheKey, $record, self::CACHE_TTL); - Log::debug("缓存评分记录,accountId: {$accountId}"); + // 根据数据更新频率设置不同的缓存时间 + // 如果记录最近更新过(1小时内),使用短期缓存 + $updateTime = $record['updateTime'] ?? 0; + $cacheTime = (time() - $updateTime < 3600) ? self::CACHE_TTL_SHORT : self::CACHE_TTL; + + Cache::set($cacheKey, $record, $cacheTime); + Log::debug("缓存评分记录,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $record ?: []; @@ -349,7 +463,7 @@ class WechatAccountHealthScoreService ->where('accountId', $accountId) ->update($baseScoreData); - Log::debug("更新基础分,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 // 更新成功后,清除缓存 if ($result !== false) { @@ -373,7 +487,7 @@ class WechatAccountHealthScoreService { $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; $result = Cache::rm($cacheKey); - Log::debug("清除评分记录缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 return $result; } @@ -556,7 +670,10 @@ class WechatAccountHealthScoreService 'frequentCount' => 0, 'lastNoFrequentTime' => null, 'consecutiveNoFrequentDays' => 0, - 'isBanned' => 0 + 'isBanned' => 0, + 'lastBanTime' => null, + 'frequentTaskIds' => [], + 'banMessageId' => null ]; if (empty($accountId) || empty($wechatId)) { @@ -564,8 +681,7 @@ class WechatAccountHealthScoreService return $result; } - // 计算30天前的时间戳(在多个方法中使用) - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 不再使用30天限制 // 检查添加好友记录表是否有记录,如果没有记录则动态分为0 // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数 @@ -595,23 +711,27 @@ class WechatAccountHealthScoreService $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0; $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0; $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0; + $result['lastBanTime'] = $scoreRecord['lastBanTime'] ?? null; } - // 1. 检查频繁记录(从s2_friend_task表查询,只统计近30天) - $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo); + // 1. 检查频繁记录(从s2_friend_task表查询,不限制时间) + $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; + $result['frequentTaskIds'] = $frequentData['taskIds'] ?? []; - // 2. 检查封号记录(从s2_wechat_message表查询) - $banData = $this->checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo); + // 2. 检查封号记录(从s2_wechat_message表查询,不限制时间) + $banData = $this->checkBannedFromMessage($accountId, $wechatId); if (!empty($banData)) { $result['isBanned'] = $banData['isBanned']; $result['banPenalty'] = $banData['banPenalty']; + $result['lastBanTime'] = $banData['lastBanTime']; + $result['banMessageId'] = $banData['messageId'] ?? null; } - // 3. 计算不频繁加分(基于近30天的频繁记录,反向参考频繁规则) - $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo); + // 3. 计算不频繁加分(基于频繁记录,反向参考频繁规则) + $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData); $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; @@ -629,27 +749,27 @@ class WechatAccountHealthScoreService /** * 从s2_friend_task表检查频繁记录 * extra字段包含"操作过于频繁"即需要扣分 - * 只统计近30天的数据 + * 统计所有时间的数据(不限制30天) + * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $scoreRecord 现有评分记录 - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null) { - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } + // 不再使用30天限制 - // 查询包含"操作过于频繁"的记录(只统计近30天) + // 减少不必要的日志记录 + + // 查询包含"操作过于频繁"的记录(统计所有时间且未被统计过的记录) // extra字段可能是文本或JSON格式,使用LIKE查询 // 优化查询:只查询必要的字段,减少数据传输量 + // 添加is_counted条件,只查询未被统计过的记录 $frequentTasks = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) - ->where('createTime', '>=', $thirtyDaysAgo) ->where(function($query) use ($wechatId) { if (!empty($wechatId)) { $query->where('wechatId', $wechatId); @@ -660,6 +780,12 @@ class WechatAccountHealthScoreService $query->where('extra', 'like', '%操作过于频繁%') ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); }) + ->where(function($query) { + // 只查询未被统计过的记录 + // 注意:需要兼容is_counted字段不存在的情况 + $query->where('is_counted', 0) + ->whereOr('is_counted', null); + }) ->order('createTime', 'desc') ->field('id, createTime, extra') ->select(); @@ -670,12 +796,47 @@ class WechatAccountHealthScoreService // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录) $frequentCount = count($frequentTasks); + Log::info("找到 {$frequentCount} 条未统计的频繁记录,accountId: {$accountId}, wechatId: {$wechatId}"); + + // 标记这些记录为已统计 + if (!empty($frequentTasks)) { + $taskIds = array_column($frequentTasks, 'id'); + try { + // 检查表中是否存在is_counted字段 + $hasIsCountedField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_FRIEND_TASK); + foreach ($tableFields as $field) { + if ($field['Field'] == 'is_counted') { + $hasIsCountedField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasIsCountedField) { + Log::info("添加is_counted字段到" . self::TABLE_FRIEND_TASK . "表"); + Db::execute("ALTER TABLE " . self::TABLE_FRIEND_TASK . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); + } + + // 更新记录为已统计 + Db::table(self::TABLE_FRIEND_TASK) + ->where('id', 'in', $taskIds) + ->update(['is_counted' => 1]); + + // 减少不必要的日志记录 + } catch (\Exception $e) { + Log::error("标记频繁记录失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + } + // 如果30天内没有频繁记录,清除扣分 if (empty($frequentTasks)) { return [ 'lastFrequentTime' => null, 'frequentCount' => 0, - 'frequentPenalty' => 0 + 'frequentPenalty' => 0, + 'taskIds' => [] ]; } @@ -683,69 +844,110 @@ class WechatAccountHealthScoreService $penalty = 0; if ($frequentCount == 1) { $penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 + Log::info("首次频繁,扣除 " . abs(self::PENALTY_FIRST_FREQUENT) . " 分,accountId: {$accountId}"); } elseif ($frequentCount >= 2) { $penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + Log::info("再次频繁,扣除 " . abs(self::PENALTY_SECOND_FREQUENT) . " 分,accountId: {$accountId}"); } return [ 'lastFrequentTime' => $latestFrequentTime, 'frequentCount' => $frequentCount, - 'frequentPenalty' => $penalty + 'frequentPenalty' => $penalty, + 'taskIds' => $taskIds ]; } /** * 从s2_wechat_message表检查封号记录 * content包含"你的账号被限制"且msgType为10000 - * 只统计近30天的数据 + * 统计所有时间的数据(不限制30天) + * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null) { - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } + // 不再使用30天限制 - // 查询封号消息(只统计近30天) + // 减少不必要的日志记录 + + // 查询封号消息(统计所有时间且未被统计过的记录) // 优化查询:只查询必要的字段,减少数据传输量 $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE) ->where('wechatAccountId', $accountId) ->where('msgType', 10000) ->where('content', 'like', '%你的账号被限制%') ->where('isDeleted', 0) - ->where('createTime', '>=', $thirtyDaysAgo) + ->where(function($query) { + // 只查询未被统计过的记录 + // 注意:需要兼容is_counted字段不存在的情况 + $query->where('is_counted', 0) + ->whereOr('is_counted', null); + }) ->field('id, createTime') // 只查询必要的字段 ->order('createTime', 'desc') ->find(); if (!empty($banMessage)) { + try { + // 检查表中是否存在is_counted字段 + $hasIsCountedField = false; + $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_MESSAGE); + foreach ($tableFields as $field) { + if ($field['Field'] == 'is_counted') { + $hasIsCountedField = true; + break; + } + } + + // 如果字段不存在,添加字段 + if (!$hasIsCountedField) { + Log::info("添加is_counted字段到" . self::TABLE_WECHAT_MESSAGE . "表"); + Db::execute("ALTER TABLE " . self::TABLE_WECHAT_MESSAGE . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); + } + + // 更新记录为已统计 + Db::table(self::TABLE_WECHAT_MESSAGE) + ->where('id', $banMessage['id']) + ->update(['is_counted' => 1]); + + // 减少不必要的日志记录 + Log::info("发现封号记录,扣除 " . abs(self::PENALTY_BANNED) . " 分,accountId: {$accountId}"); + } catch (\Exception $e) { + Log::error("标记封号记录失败: " . $e->getMessage()); + // 出错时不影响后续逻辑,继续执行 + } + return [ 'isBanned' => 1, - 'banPenalty' => self::PENALTY_BANNED // 封号-60分 + 'banPenalty' => self::PENALTY_BANNED, // 封号-60分 + 'lastBanTime' => $banMessage['createTime'], + 'messageId' => $banMessage['id'] ]; } return [ 'isBanned' => 0, - 'banPenalty' => 0 + 'banPenalty' => 0, + 'lastBanTime' => null, + 'messageId' => null ]; } /** * 计算不频繁加分 - * 反向参考频繁规则:查询近30天的频繁记录,计算连续不频繁天数 - * 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + * 反向参考频繁规则:计算连续不频繁天数 + * 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) * 如果连续3天没有频繁,则每天+5分 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount) - * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) + * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array 包含bonus、consecutiveDays、lastNoFrequentTime */ private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null) @@ -760,31 +962,22 @@ class WechatAccountHealthScoreService return $result; } - // 如果没有传入30天前的时间戳,则计算 - if ($thirtyDaysAgo === null) { - $thirtyDaysAgo = time() - (30 * 24 * 3600); - } $currentTime = time(); - // 获取最后一次频繁时间(30天内最后一次频繁的时间) + // 获取最后一次频繁时间 $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null; - // 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + // 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) if (empty($lastFrequentTime)) { - // 情况1:30天内没有频繁记录,说明30天内连续不频繁 - // 计算从30天前到现在的连续不频繁天数(最多30天) - $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); + // 情况1:没有频繁记录,说明一直连续不频繁 + // 默认给30天的连续不频繁天数(可以根据需要调整) + $consecutiveDays = 30; } else { - // 情况2:30天内有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 + // 情况2:有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算 // 计算从最后一次频繁时间到现在,连续多少天没有频繁 $timeDiff = $currentTime - $lastFrequentTime; $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数 - - // 边界情况:如果最后一次频繁时间在30天前(理论上不应该发生,因为查询已经限制了30天),则按30天处理 - if ($lastFrequentTime < $thirtyDaysAgo) { - $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); - } } // 如果连续3天或以上没有频繁,则每天+5分 @@ -800,6 +993,157 @@ class WechatAccountHealthScoreService return $result; } + /** + * 构建日志快照(用于对比前后分值) + * + * @param array $scoreRecord + * @return array + */ + private function buildScoreSnapshotForLogging($scoreRecord) + { + $baseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE; + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $scoreRecord['healthScore'] ?? ($baseScore + $dynamicScore); + + return [ + 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0, + 'banPenalty' => $scoreRecord['banPenalty'] ?? 0, + 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0, + 'dynamicScore' => $dynamicScore, + 'healthScore' => $healthScore + ]; + } + + /** + * 根据前后快照写加减分日志 + * + * @param int $accountId + * @param string $wechatId + * @param array $before + * @param array $after + * @param array $context + * @return void + */ + private function logScoreChangesIfNeeded($accountId, $wechatId, array $before, array $after, array $context = []) + { + $healthBefore = $before['healthScore'] ?? 0; + $healthAfter = $after['healthScore'] ?? 0; + + $this->recordScoreLog($accountId, $wechatId, 'frequentPenalty', $before['frequentPenalty'] ?? 0, $after['frequentPenalty'] ?? 0, [ + 'category' => 'penalty', + 'source' => 'friend_task', + 'sourceId' => !empty($context['frequentTaskIds']) ? $context['frequentTaskIds'][0] : null, + 'extra' => [ + 'taskIds' => $context['frequentTaskIds'] ?? [], + 'frequentCount' => $context['frequentCount'] ?? 0, + 'lastFrequentTime' => $context['lastFrequentTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'banPenalty', $before['banPenalty'] ?? 0, $after['banPenalty'] ?? 0, [ + 'category' => 'penalty', + 'source' => 'wechat_message', + 'sourceId' => $context['banMessageId'] ?? null, + 'extra' => [ + 'lastBanTime' => $context['lastBanTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'noFrequentBonus', $before['noFrequentBonus'] ?? 0, $after['noFrequentBonus'] ?? 0, [ + 'category' => 'bonus', + 'source' => 'system', + 'extra' => [ + 'consecutiveDays' => $context['consecutiveNoFrequentDays'] ?? 0, + 'lastNoFrequentTime' => $context['lastNoFrequentTime'] ?? null + ], + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'dynamicScore', $before['dynamicScore'] ?? 0, $after['dynamicScore'] ?? 0, [ + 'category' => 'dynamic_total', + 'source' => 'system', + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + + $this->recordScoreLog($accountId, $wechatId, 'healthScore', $before['healthScore'] ?? 0, $after['healthScore'] ?? 0, [ + 'category' => 'health_total', + 'source' => 'system', + 'totalScoreBefore' => $healthBefore, + 'totalScoreAfter' => $healthAfter + ]); + } + + /** + * 插入健康分加减分日志 + * + * @param int $accountId + * @param string $wechatId + * @param string $field + * @param int|null $beforeValue + * @param int|null $afterValue + * @param array $context + * @return void + */ + private function recordScoreLog($accountId, $wechatId, $field, $beforeValue, $afterValue, array $context = []) + { + $beforeValue = (int)($beforeValue ?? 0); + $afterValue = (int)($afterValue ?? 0); + + if ($beforeValue === $afterValue) { + return; + } + + $extraPayload = $context['extra'] ?? null; + if (is_array($extraPayload)) { + $extraPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } elseif (!is_string($extraPayload)) { + $extraPayload = null; + } + + $sourceId = null; + if (array_key_exists('sourceId', $context)) { + $sourceId = $context['sourceId']; + } + + $totalScoreBefore = null; + if (array_key_exists('totalScoreBefore', $context)) { + $totalScoreBefore = $context['totalScoreBefore']; + } + + $totalScoreAfter = null; + if (array_key_exists('totalScoreAfter', $context)) { + $totalScoreAfter = $context['totalScoreAfter']; + } + + $data = [ + 'accountId' => $accountId, + 'wechatId' => $wechatId, + 'field' => $field, + 'changeValue' => $afterValue - $beforeValue, + 'valueBefore' => $beforeValue, + 'valueAfter' => $afterValue, + 'category' => $context['category'] ?? null, + 'source' => $context['source'] ?? null, + 'sourceId' => $sourceId, + 'extra' => $extraPayload, + 'totalScoreBefore' => $totalScoreBefore, + 'totalScoreAfter' => $totalScoreAfter, + 'createTime' => time() + ]; + + try { + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE_LOG)->insert($data); + } catch (\Exception $e) { + Log::error("记录健康分加减分日志失败,accountId: {$accountId}, field: {$field}, 错误: " . $e->getMessage()); + } + } + /** * 根据健康分计算每日最大加人次数 * 公式:每日最大加人次数 = 健康分 * 0.2 @@ -814,14 +1158,16 @@ class WechatAccountHealthScoreService /** * 批量计算并更新多个账号的健康分 + * 优化:使用多线程处理、优化批处理逻辑、减少日志记录 * * @param array $accountIds 账号ID数组(为空则处理所有账号) * @param int $batchSize 每批处理数量 * @param bool $forceRecalculateBase 是否强制重新计算基础分 + * @param bool $useMultiThread 是否使用多线程处理(需要pcntl扩展支持) * @return array 处理结果统计 * @throws Exception 如果参数无效或批量处理过程中出现严重错误 */ - public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) + public function batchCalculateAndUpdate($accountIds = [], $batchSize = 50, $forceRecalculateBase = false, $useMultiThread = false) { // 参数验证 if (!is_array($accountIds)) { @@ -836,9 +1182,16 @@ class WechatAccountHealthScoreService throw new Exception($errorMsg); } + // 检查是否支持多线程 + if ($useMultiThread && !function_exists('pcntl_fork')) { + $useMultiThread = false; + Log::warning("系统不支持pcntl扩展,无法使用多线程处理,将使用单线程模式"); + } + try { $startTime = microtime(true); - Log::info("开始批量计算健康分,batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false')); + Log::info("开始批量计算健康分,batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false') . + ", useMultiThread: " . ($useMultiThread ? 'true' : 'false')); $stats = [ 'total' => 0, @@ -855,43 +1208,69 @@ class WechatAccountHealthScoreService ->column('id'); } - $stats['total'] = count($accountIds); - Log::info("需要处理的账号总数: {$stats['total']}"); - - // 分批处理 - $batches = array_chunk($accountIds, $batchSize); - $batchCount = count($batches); - Log::info("分批处理,共 {$batchCount} 批"); - - foreach ($batches as $batchIndex => $batch) { - $batchStartTime = microtime(true); - Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + $stats['total'] = count($accountIds); + Log::info("需要处理的账号总数: {$stats['total']}"); - foreach ($batch as $accountId) { - try { - $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); - $stats['success']++; - } catch (Exception $e) { - $stats['failed']++; - $stats['errors'][] = [ - 'accountId' => $accountId, - 'error' => $e->getMessage() - ]; - Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); + // 优化:减小批次大小,提高并行处理效率 + $batchSize = min($batchSize, 50); + + // 分批处理 + $batches = array_chunk($accountIds, $batchSize); + $batchCount = count($batches); + Log::info("分批处理,共 {$batchCount} 批"); + + // 多线程处理 + if ($useMultiThread && $batchCount > 1) { + $childPids = []; + $maxProcesses = 4; // 最大并行进程数 + $runningProcesses = 0; + + for ($i = 0; $i < $batchCount; $i++) { + // 如果达到最大进程数,等待某个子进程结束 + if ($runningProcesses >= $maxProcesses) { + $pid = pcntl_wait($status); + $runningProcesses--; + } + + // 创建子进程 + $pid = pcntl_fork(); + + if ($pid == -1) { + // 创建进程失败 + Log::error("创建子进程失败"); + continue; + } elseif ($pid == 0) { + // 子进程 + $this->processBatch($batches[$i], $i, $batchCount, $forceRecalculateBase); + exit(0); + } else { + // 父进程 + $childPids[] = $pid; + $runningProcesses++; + } + } + + // 等待所有子进程结束 + foreach ($childPids as $pid) { + pcntl_waitpid($pid, $status); + } + + Log::info("所有批次处理完成"); + } else { + // 单线程处理 + foreach ($batches as $batchIndex => $batch) { + $batchStats = $this->processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase); + $stats['success'] += $batchStats['success']; + $stats['failed'] += $batchStats['failed']; + $stats['errors'] = array_merge($stats['errors'], $batchStats['errors']); } } - $batchEndTime = microtime(true); - $batchDuration = round($batchEndTime - $batchStartTime, 2); - Log::info("第 " . ($batchIndex + 1) . " 批处理完成,耗时: {$batchDuration}秒," . - "成功: {$stats['success']},失败: {$stats['failed']}"); - } - - $endTime = microtime(true); - $totalDuration = round($endTime - $startTime, 2); - Log::info("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); - - return $stats; + $endTime = microtime(true); + $totalDuration = round($endTime - $startTime, 2); + Log::info("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); + + return $stats; } catch (\PDOException $e) { $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage(); Log::error($errorMsg); @@ -903,6 +1282,68 @@ class WechatAccountHealthScoreService } } + /** + * 处理单个批次的账号 + * + * @param array $batch 批次账号ID数组 + * @param int $batchIndex 批次索引 + * @param int $batchCount 总批次数 + * @param bool $forceRecalculateBase 是否强制重新计算基础分 + * @return array 处理结果统计 + */ + private function processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase) + { + $batchStartTime = microtime(true); + Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + + $stats = [ + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 优化:预先获取账号数据,减少重复查询 + $accountIds = implode(',', $batch); + $accountDataMap = []; + if (!empty($batch)) { + $accountDataList = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('id', 'in', $batch) + ->select(); + + foreach ($accountDataList as $accountData) { + $accountDataMap[$accountData['id']] = $accountData; + } + } + + // 批量处理账号 + foreach ($batch as $accountId) { + try { + $accountData = $accountDataMap[$accountId] ?? null; + $this->calculateAndUpdate($accountId, $accountData, $forceRecalculateBase); + $stats['success']++; + + // 减少日志记录,每10个账号记录一次进度 + if ($stats['success'] % 10 == 0) { + Log::debug("批次 " . ($batchIndex + 1) . " 已处理 {$stats['success']} 个账号"); + } + } catch (Exception $e) { + $stats['failed']++; + $stats['errors'][] = [ + 'accountId' => $accountId, + 'error' => $e->getMessage() + ]; + Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); + } + } + + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + Log::info("第 " . ($batchIndex + 1) . "/" . $batchCount . " 批处理完成,耗时: {$batchDuration}秒," . + "成功: {$stats['success']},失败: {$stats['failed']}"); + + return $stats; + } + /** * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测) * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成 @@ -985,6 +1426,7 @@ class WechatAccountHealthScoreService /** * 获取账号健康分信息 + * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) @@ -1005,7 +1447,7 @@ class WechatAccountHealthScoreService // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); - Log::debug("从缓存获取健康分信息,accountId: {$accountId}"); + // 减少日志记录,提高性能 return $cachedData; } @@ -1032,13 +1474,20 @@ class WechatAccountHealthScoreService 'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0, 'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null, 'frequentCount' => $scoreRecord['frequentCount'] ?? 0, - 'isBanned' => $scoreRecord['isBanned'] ?? 0 + 'isBanned' => $scoreRecord['isBanned'] ?? 0, + 'lastBanTime' => $scoreRecord['lastBanTime'] ?? null ]; // 如果使用缓存,则缓存健康分信息 if ($useCache) { - Cache::set($cacheKey, $healthScoreInfo, self::CACHE_TTL); - Log::debug("缓存健康分信息,accountId: {$accountId}"); + // 根据数据更新频率设置不同的缓存时间 + // 如果有频繁记录或封号记录,使用短期缓存 + $cacheTime = (!empty($scoreRecord['lastFrequentTime']) || !empty($scoreRecord['isBanned'])) + ? self::CACHE_TTL_SHORT + : self::CACHE_TTL; + + Cache::set($cacheKey, $healthScoreInfo, $cacheTime); + Log::debug("缓存健康分信息,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $healthScoreInfo; @@ -1058,7 +1507,7 @@ class WechatAccountHealthScoreService // 同时清除评分记录缓存 $this->clearScoreCache($accountId); - Log::debug("清除健康分信息缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + // 减少不必要的日志记录 return $result; } } diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index e6f7ee03..c34fcd07 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -36,13 +36,13 @@ Route::group('v1/', function () { Route::get(':id/summary', 'app\cunkebao\controller\wechat\GetWechatOnDeviceSummarizeV1Controller@index'); Route::get(':id/friends', 'app\cunkebao\controller\wechat\GetWechatOnDeviceFriendsV1Controller@index'); Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo'); - Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); - Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 - + Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据 + Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈 Route::get('count', 'app\cunkebao\controller\DeviceWechat@count'); Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量 Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态 - + Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 + Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); }); // 获客场景相关 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 69291123..bcf8e50b 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -1061,6 +1061,7 @@ class ContentLibraryController extends Controller $where = [ ['isDel', '=', 0], // 未删除 ['status', '=', 1], // 已开启 + ['id', '=', 99], // 已开启 ]; // 查询符合条件的内容库 @@ -1225,7 +1226,7 @@ class ContentLibraryController extends Controller foreach ($friends as $friend) { $processedFriends++; - + // 如果配置了API并且需要主动获取朋友圈 if ($needFetch) { try { @@ -1264,9 +1265,9 @@ class ContentLibraryController extends Controller } // 如果指定了采集类型,进行过滤 - if (!empty($catchTypes)) { + /*if (!empty($catchTypes)) { $query->whereIn('type', $catchTypes); - } + }*/ // 获取最近20条朋友圈 $moments = $query->page(1, 20)->select(); @@ -1289,7 +1290,7 @@ class ContentLibraryController extends Controller continue; } - // 如果启用了AI处理 + /* // 如果启用了AI处理 if (!empty($library['aiEnabled']) && !empty($content)) { try { $contentAi = $this->aiRewrite($library, $content); @@ -1300,7 +1301,7 @@ class ContentLibraryController extends Controller \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']'); $moment['contentAi'] = ''; } - } + }*/ // 保存到内容库的content_item表 if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) { diff --git a/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php new file mode 100644 index 00000000..b34ef7ff --- /dev/null +++ b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php @@ -0,0 +1,204 @@ +getUserInfo('companyId')) + ->column('id'); + } + + /** + * 非主操盘手仅可查看分配到的设备 + * + * @return array + */ + protected function getUserDevicesId(): array + { + return DeviceUserModel::where([ + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId'), + ])->column('deviceId'); + } + + /** + * 获取当前用户可访问的设备ID + * + * @return array + */ + protected function getDevicesId(): array + { + return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) + ? $this->getCompanyDevicesId() + : $this->getUserDevicesId(); + } + + /** + * 获取用户可访问的微信ID集合 + * + * @return array + * @throws \Exception + */ + protected function getAccessibleWechatIds(): array + { + $deviceIds = $this->getDevicesId(); + if (empty($deviceIds)) { + throw new \Exception('暂无可用设备', 200); + } + + return DeviceWechatLoginModel::distinct(true) + ->where('companyId', $this->getUserInfo('companyId')) + ->whereIn('deviceId', $deviceIds) + ->column('wechatId'); + } + + /** + * 查看朋友圈列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $wechatId = $this->request->param('wechatId/s', ''); + if (empty($wechatId)) { + return ResponseHelper::error('wechatId不能为空'); + } + + // 权限校验:只能查看当前账号可访问的微信 + $accessibleWechatIds = $this->getAccessibleWechatIds(); + if (!in_array($wechatId, $accessibleWechatIds, true)) { + return ResponseHelper::error('无权查看该微信的朋友圈', 403); + } + + // 获取对应的微信账号ID + $accountId = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->value('id'); + + if (empty($accountId)) { + return ResponseHelper::error('微信账号不存在或尚未同步', 404); + } + + $query = Db::table('s2_wechat_moments') + ->where('wechatAccountId', $accountId); + + // 关键词搜索 + if ($keyword = trim((string)$this->request->param('keyword', ''))) { + $query->whereLike('content', '%' . $keyword . '%'); + } + + // 类型筛选 + $type = $this->request->param('type', ''); + if ($type !== '' && $type !== null) { + $query->where('type', (int)$type); + } + + // 时间筛选 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + if ($startTime || $endTime) { + $start = $startTime ? strtotime($startTime) : 0; + $end = $endTime ? strtotime($endTime) : time(); + if ($start && $end && $end < $start) { + return ResponseHelper::error('结束时间不能早于开始时间'); + } + $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]); + } + + $page = (int)$this->request->param('page', 1); + $limit = (int)$this->request->param('limit', 10); + + $paginator = $query->order('createTime', 'desc') + ->paginate($limit, false, ['page' => $page]); + + $list = array_map(function ($item) { + return $this->formatMomentRow($item); + }, $paginator->items()); + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $paginator->total(), + 'page' => $page, + 'limit' => $limit, + ]); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 格式化朋友圈数据 + * + * @param array $row + * @return array + */ + protected function formatMomentRow(array $row): array + { + $formatTime = function ($timestamp) { + if (empty($timestamp)) { + return ''; + } + return is_numeric($timestamp) + ? date('Y-m-d H:i:s', $timestamp) + : date('Y-m-d H:i:s', strtotime($timestamp)); + }; + + return [ + 'id' => (int)$row['id'], + 'snsId' => $row['snsId'] ?? '', + 'type' => (int)($row['type'] ?? 0), + 'content' => $row['content'] ?? '', + 'commentList' => $this->decodeJson($row['commentList'] ?? null), + 'likeList' => $this->decodeJson($row['likeList'] ?? null), + 'resUrls' => $this->decodeJson($row['resUrls'] ?? null), + 'createTime' => $formatTime($row['createTime'] ?? null), + 'momentEntity' => [ + 'lat' => $row['lat'] ?? 0, + 'lng' => $row['lng'] ?? 0, + 'location' => $row['location'] ?? '', + 'picSize' => $row['picSize'] ?? 0, + 'userName' => $row['userName'] ?? '', + ], + ]; + } + + /** + * JSON字段解析 + * + * @param mixed $value + * @return array + */ + protected function decodeJson($value): array + { + if (empty($value)) { + return []; + } + + if (is_array($value)) { + return $value; + } + + $decoded = json_decode($value, true); + return $decoded ?: []; + } +} + diff --git a/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php new file mode 100644 index 00000000..a48156de --- /dev/null +++ b/Server/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php @@ -0,0 +1,411 @@ +request->param('wechatId', ''); + + if (empty($wechatId)) { + return ResponseHelper::error('微信ID不能为空'); + } + + $companyId = $this->getUserInfo('companyId'); + + // 获取微信账号ID(accountId) + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->find(); + + if (empty($account)) { + return ResponseHelper::error('微信账号不存在'); + } + + $accountId = $account['id']; + + // 1. 健康分评估 + $healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId); + + // 2. 账号价值(模拟数据) + $accountValue = $this->getAccountValue($accountId); + + // 3. 今日价值变化(模拟数据) + $todayValueChange = $this->getTodayValueChange($accountId); + + // 4. 好友总数 + $totalFriends = $this->getTotalFriends($wechatId, $companyId); + + // 5. 今日新增好友 + $todayNewFriends = $this->getTodayNewFriends($wechatId); + + // 6. 高价群聊 + $highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId); + + // 7. 今日新增群聊 + $todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId); + + $result = [ + 'healthScoreAssessment' => $healthScoreData, + 'accountValue' => $accountValue, + 'todayValueChange' => $todayValueChange, + 'totalFriends' => $totalFriends, + 'todayNewFriends' => $todayNewFriends, + 'highValueChatrooms' => $highValueChatrooms, + 'todayNewChatrooms' => $todayNewChatrooms, + ]; + + return ResponseHelper::success($result); + + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取健康分评估数据 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array + */ + protected function getHealthScoreAssessment($accountId, $wechatId) + { + // 获取健康分信息 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + $healthScore = $healthScoreInfo['healthScore'] ?? 0; + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0; + + // 获取今日已加好友数 + $todayAdded = $this->getTodayAddedCount($wechatId); + + // 获取最后添加时间 + $lastAddTime = $this->getLastAddTime($wechatId); + + // 判断状态标签 + $statusTag = $todayAdded > 0 ? '已添加加人' : ''; + + // 获取基础构成 + $baseComposition = $this->getBaseComposition($healthScoreInfo); + + // 获取动态记录 + $dynamicRecords = $this->getDynamicRecords($healthScoreInfo); + + return [ + 'score' => $healthScore, + 'dailyLimit' => $maxAddFriendPerDay, + 'todayAdded' => $todayAdded, + 'lastAddTime' => $lastAddTime, + 'statusTag' => $statusTag, + 'baseComposition' => $baseComposition, + 'dynamicRecords' => $dynamicRecords, + ]; + } + + /** + * 获取基础构成数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getBaseComposition($healthScoreInfo) + { + $baseScore = $healthScoreInfo['baseScore'] ?? 0; + $baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0; + $friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0; + $friendCount = $healthScoreInfo['friendCount'] ?? 0; + + // 账号基础分(默认60分) + $accountBaseScore = 60; + + // 已修改微信号(如果baseInfoScore > 0,说明已修改) + $isModifiedAlias = $baseInfoScore > 0; + + $composition = [ + [ + 'name' => '账号基础分', + 'score' => $accountBaseScore, + 'formatted' => '+' . $accountBaseScore, + ] + ]; + + // 如果已修改微信号,添加基础信息分 + if ($isModifiedAlias) { + $composition[] = [ + 'name' => '已修改微信号', + 'score' => $baseInfoScore, + 'formatted' => '+' . $baseInfoScore, + ]; + } + + // 好友数量加成 + if ($friendCountScore > 0) { + $composition[] = [ + 'name' => '好友数量加成', + 'score' => $friendCountScore, + 'formatted' => '+' . $friendCountScore, + 'friendCount' => $friendCount, // 显示好友总数 + ]; + } + + return $composition; + } + + /** + * 获取动态记录数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getDynamicRecords($healthScoreInfo) + { + $records = []; + + $frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + $banPenalty = $healthScoreInfo['banPenalty'] ?? 0; + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + $noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0; + $consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0; + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + + // 频繁扣分记录 + // 根据frequentCount判断是首次还是再次 + // frequentPenalty存储的是当前状态的扣分(-15或-25),不是累计值 + if ($frequentCount > 0 && $frequentPenalty < 0) { + if ($frequentCount == 1) { + // 首次频繁:-15分 + $records[] = [ + 'name' => '首次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } else { + // 再次频繁:-25分 + $records[] = [ + 'name' => '再次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } + } + + // 封号扣分记录 + if ($isBanned && $banPenalty < 0) { + $lastBanTime = $healthScoreInfo['lastBanTime'] ?? null; + $records[] = [ + 'name' => '封号', + 'score' => $banPenalty, + 'formatted' => (string)$banPenalty, + 'type' => 'penalty', + 'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null, + ]; + } + + // 不频繁加分记录 + if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) { + $lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null; + $records[] = [ + 'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁', + 'score' => $noFrequentBonus, + 'formatted' => '+' . $noFrequentBonus, + 'type' => 'bonus', + 'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null, + ]; + } + + return $records; + } + + /** + * 获取今日已加好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayAddedCount($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->count(); + } + + /** + * 获取最后添加时间 + * + * @param string $wechatId 微信ID + * @return string + */ + protected function getLastAddTime($wechatId) + { + $lastTask = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + + if (empty($lastTask) || empty($lastTask['createTime'])) { + return ''; + } + + return date('H:i:s', $lastTask['createTime']); + } + + /** + * 获取账号价值(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getAccountValue($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:¥29,800 + $value = 29800; + + return [ + 'value' => $value, + 'formatted' => '¥' . number_format($value, 0, '.', ','), + ]; + } + + /** + * 获取今日价值变化(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getTodayValueChange($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:+500 + $change = 500; + + return [ + 'change' => $change, + 'formatted' => $change > 0 ? '+' . $change : (string)$change, + 'isPositive' => $change > 0, + ]; + } + + /** + * 获取好友总数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTotalFriends($wechatId, $companyId) + { + // 优先从 s2_wechat_account 表获取 + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->field('totalFriend') + ->find(); + + if (!empty($account) && isset($account['totalFriend'])) { + return (int)$account['totalFriend']; + } + + // 如果 totalFriend 为空,则从 s2_wechat_friend 表统计 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取今日新增好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayNewFriends($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + // 从 s2_wechat_friend 表统计今日新增 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取高价群聊数量 + * 高价群聊定义:群成员数 >= 50 的群聊 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getHighValueChatrooms($wechatId, $companyId) + { + // 高价群聊定义:群成员数 >= 50 + $minMemberCount = 50; + + // 查询该微信账号下的高价群聊 + // 使用子查询统计每个群的成员数 + $result = Db::query(" + SELECT COUNT(DISTINCT c.chatroomId) as count + FROM s2_wechat_chatroom c + INNER JOIN ( + SELECT chatroomId, COUNT(*) as memberCount + FROM s2_wechat_chatroom_member + GROUP BY chatroomId + HAVING memberCount >= ? + ) m ON c.chatroomId = m.chatroomId + WHERE c.wechatAccountWechatId = ? + AND c.isDeleted = 0 + ", [$minMemberCount, $wechatId]); + + return !empty($result) ? (int)$result[0]['count'] : 0; + } + + /** + * 获取今日新增群聊数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTodayNewChatrooms($wechatId, $companyId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_wechat_chatroom') + ->where('wechatAccountWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } +} + diff --git a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php index 177d67a6..0b50aa99 100644 --- a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php +++ b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php @@ -8,14 +8,25 @@ use app\common\model\DeviceUser as DeviceUserModel; use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel; use app\common\model\User as UserModel; use app\common\model\WechatAccount as WechatAccountModel; -use app\common\model\WechatCustomer as WechatCustomerModel; -use app\common\model\WechatFriendShip as WechatFriendShipModel; +// 不再使用WechatFriendShipModel和WechatCustomerModel,改为直接查询s2_wechat_friend和s2_wechat_account_score表 use app\cunkebao\controller\BaseController; use library\ResponseHelper; use think\Db; /** * 微信控制器 + * + * 性能优化建议: + * 1. 为以下字段添加索引以提高查询性能: + * - device_wechat_login表: (companyId, wechatId), (deviceId) + * - wechat_account表: (wechatId) + * - wechat_customer表: (companyId, wechatId) + * - wechat_friend_ship表: (ownerWechatId), (createTime) + * - s2_wechat_message表: (wechatAccountId, wechatTime) + * + * 2. 考虑创建以下复合索引: + * - device_wechat_login表: (companyId, deviceId, wechatId) + * - wechat_friend_ship表: (ownerWechatId, createTime) */ class GetWechatsOnDevicesV1Controller extends BaseController { @@ -66,6 +77,7 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 获取有登录设备的微信id + * 优化:使用索引字段,减少数据查询量 * * @return array */ @@ -76,12 +88,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController throw new \Exception('暂无设备数据', 200); } - return DeviceWechatLoginModel::where( - [ + // 优化:直接使用DISTINCT减少数据传输量 + return DeviceWechatLoginModel::distinct(true) + ->where([ 'companyId' => $this->getUserInfo('companyId'), // 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE, - ] - ) + ]) ->where('deviceId', 'in', $deviceIds) ->column('wechatId'); } @@ -110,24 +122,50 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 获取在线微信账号列表 + * 优化:减少查询字段,使用索引,优化JOIN条件 * * @param array $where * @return \think\Paginator 分页对象 */ protected function getOnlineWechatList(array $where): \think\Paginator { + // 获取微信在线状态筛选参数(1=在线,0=离线,不传=全部) + $wechatStatus = $this->request->param('wechatStatus'); + + // 优化:只查询必要字段,使用FORCE INDEX提示数据库使用索引 $query = WechatAccountModel::alias('w') ->field( [ 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId', 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', - 'l.deviceId','l.alive' + 'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态 ] ) - ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId')) - ->order('w.id desc') - ->group('w.wechatId'); + // 优化:使用INNER JOIN代替LEFT JOIN,并添加索引提示 + ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER') + // 添加s2_wechat_account表的LEFT JOIN,用于筛选微信在线状态 + ->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT') + ->group('w.wechatId') + // 优化:在线状态优先排序(alive=1的排在前面),然后按wechatId排序 + // 注意:ORDER BY使用SELECT中定义的别名alive,而不是聚合函数 + ->order('alive desc, w.wechatId desc'); + + // 根据wechatStatus参数筛选(1=在线,0=离线,不传=全部) + if ($wechatStatus !== null && $wechatStatus !== '') { + $wechatStatus = (int)$wechatStatus; + if ($wechatStatus === 1) { + // 筛选在线:wechatAlive = 1 + $query->where('sa.wechatAlive', 1); + } elseif ($wechatStatus === 0) { + // 筛选离线:wechatAlive = 0 或 NULL + $query->where(function($query) { + $query->where('sa.wechatAlive', 0) + ->whereOr('sa.wechatAlive', 'exp', 'IS NULL'); + }); + } + } + // 应用查询条件 foreach ($where as $key => $value) { if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { $query->whereExp('', $value[1]); @@ -142,7 +180,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController $query->where($key, $value); } - return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + // 优化:使用简单计数查询 + return $query->paginate( + $this->request->param('limit/d', 10), + false, + ['page' => $this->request->param('page/d', 1)] + ); } /** @@ -167,9 +210,15 @@ class GetWechatsOnDevicesV1Controller extends BaseController $metrics = $this->collectWechatMetrics($wechatIds); foreach ($items as $item) { + $addLimit = $metrics['addLimit'][$item->wechatId] ?? 0; + $todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0; + // 计算今日可添加数量 = 可添加额度 - 今日已添加 + $todayCanAdd = max(0, $addLimit - $todayAdded); + $sections = $item->toArray() + [ - 'times' => $metrics['addLimit'][$item->wechatId] ?? 0, - 'addedCount' => $metrics['todayAdded'][$item->wechatId] ?? 0, + 'times' => $addLimit, + 'addedCount' => $todayAdded, + 'todayCanAdd' => $todayCanAdd, // 今日可添加数量 'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0, 'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0, 'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '', @@ -184,6 +233,8 @@ class GetWechatsOnDevicesV1Controller extends BaseController /** * 批量收集微信账号的统计信息 + * 优化:合并查询,减少数据库访问次数,使用缓存 + * * @param array $wechatIds * @return array */ @@ -203,106 +254,167 @@ class GetWechatsOnDevicesV1Controller extends BaseController } $companyId = $this->getUserInfo('companyId'); - - // 可添加好友额度 - $weightRows = WechatCustomerModel::where('companyId', $companyId) + + // 使用缓存键,避免短时间内重复查询 + $cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId); + + // 尝试从缓存获取数据(缓存5分钟) + $cachedMetrics = cache($cacheKey); + if ($cachedMetrics) { + return $cachedMetrics; + } + + // 优化1:可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay + $scoreRows = Db::table('s2_wechat_account_score') ->whereIn('wechatId', $wechatIds) - ->column('weight', 'wechatId'); - foreach ($weightRows as $wechatId => $weight) { - $decoded = json_decode($weight, true); - $metrics['addLimit'][$wechatId] = $decoded['addLimit'] ?? 0; + ->column('maxAddFriendPerDay', 'wechatId'); + foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) { + $metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0); } - // 今日新增好友 + // 优化2:今日新增好友 - 使用索引字段和预计算 $start = strtotime(date('Y-m-d 00:00:00')); $end = strtotime(date('Y-m-d 23:59:59')); - $todayRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds) - ->whereBetween('createTime', [$start, $end]) - ->field('ownerWechatId, COUNT(*) as total') - ->group('ownerWechatId') - ->select(); - foreach ($todayRows as $row) { - $wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? ''); + + // 使用单次查询获取所有wechatIds的今日新增和总好友数 + // 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship + $friendshipStats = Db::query(" + SELECT + ownerWechatId, + SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added, + COUNT(*) as total_friend + FROM + s2_wechat_friend + WHERE + ownerWechatId IN ('" . implode("','", $wechatIds) . "') + AND isDeleted = 0 + GROUP BY + ownerWechatId + "); + + // 处理结果 + foreach ($friendshipStats as $row) { + $wechatId = $row['ownerWechatId'] ?? ''; if ($wechatId) { - $metrics['todayAdded'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + $metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0); + $metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0); } } - // 总好友 - $friendRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds) - ->field('ownerWechatId, COUNT(*) as total') - ->group('ownerWechatId') + // 优化3:微信在线状态 - 从s2_wechat_account表获取wechatAlive + $wechatAccountRows = Db::table('s2_wechat_account') + ->whereIn('wechatId', $wechatIds) + ->field('wechatId, wechatAlive') ->select(); - foreach ($friendRows as $row) { - $wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? ''); - if ($wechatId) { - $metrics['totalFriend'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + + foreach ($wechatAccountRows as $row) { + $wechatId = $row['wechatId'] ?? ''; + if (!empty($wechatId)) { + $metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0); } } - // 设备状态与备注 + // 优化4:设备状态与备注 - 使用INNER JOIN和索引 $loginRows = Db::name('device_wechat_login') ->alias('l') - ->leftJoin('device d', 'd.id = l.deviceId') - ->field('l.wechatId,l.alive,d.memo') + ->join('device d', 'd.id = l.deviceId', 'LEFT') + ->field('l.wechatId, l.alive, d.memo') ->where('l.companyId', $companyId) ->whereIn('l.wechatId', $wechatIds) ->order('l.id', 'desc') ->select(); + + // 使用临时数组避免重复处理 + $processedWechatIds = []; foreach ($loginRows as $row) { - $wechatId = is_array($row) ? ($row['wechatId'] ?? '') : ($row->wechatId ?? ''); - if (empty($wechatId) || isset($metrics['wechatStatus'][$wechatId])) { - continue; + $wechatId = $row['wechatId'] ?? ''; + // 只处理每个wechatId的第一条记录(最新的) + if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) { + // 如果s2_wechat_account表中没有wechatAlive,则使用device_wechat_login的alive作为备用 + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0); + } + $metrics['deviceMemo'][$wechatId] = $row['memo'] ?? ''; + $processedWechatIds[] = $wechatId; } - $metrics['wechatStatus'][$wechatId] = (int)(is_array($row) ? ($row['alive'] ?? 0) : ($row->alive ?? 0)); - $metrics['deviceMemo'][$wechatId] = is_array($row) ? ($row['memo'] ?? '') : ($row->memo ?? ''); } - // 活跃时间 - $accountMap = Db::table('s2_wechat_account') - ->whereIn('wechatId', $wechatIds) - ->column('id', 'wechatId'); - if (!empty($accountMap)) { - $accountRows = Db::table('s2_wechat_message') - ->whereIn('wechatAccountId', array_values($accountMap)) - ->field('wechatAccountId, MAX(wechatTime) as lastTime') - ->group('wechatAccountId') - ->select(); - $accountLastTime = []; - foreach ($accountRows as $row) { - $accountId = is_array($row) ? ($row['wechatAccountId'] ?? 0) : ($row->wechatAccountId ?? 0); - if ($accountId) { - $accountLastTime[$accountId] = (int)(is_array($row) ? ($row['lastTime'] ?? 0) : ($row->lastTime ?? 0)); - } - } - foreach ($accountMap as $wechatId => $accountId) { - if (isset($accountLastTime[$accountId]) && $accountLastTime[$accountId] > 0) { - $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $accountLastTime[$accountId]); - } + // 优化5:活跃时间 - 使用JOIN减少查询次数 + $activeTimeResults = Db::query(" + SELECT + a.wechatId, + MAX(m.wechatTime) as lastTime + FROM + s2_wechat_account a + LEFT JOIN + s2_wechat_message m ON a.id = m.wechatAccountId + WHERE + a.wechatId IN ('" . implode("','", $wechatIds) . "') + GROUP BY + a.wechatId + "); + + foreach ($activeTimeResults as $row) { + $wechatId = $row['wechatId'] ?? ''; + $lastTime = (int)($row['lastTime'] ?? 0); + if (!empty($wechatId) && $lastTime > 0) { + $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime); + } else { + $metrics['activeTime'][$wechatId] = '-'; } } + + // 确保所有wechatId都有wechatStatus值(默认0) + foreach ($wechatIds as $wechatId) { + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = 0; + } + } + + // 存入缓存,有效期5分钟 + cache($cacheKey, $metrics, 300); return $metrics; } /** * 获取在线微信账号列表 + * 优化:添加缓存,优化分页逻辑 * * @return \think\response\Json */ public function index() { try { + // 获取分页参数 + $page = $this->request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + $keyword = $this->request->param('keyword'); + $wechatStatus = $this->request->param('wechatStatus'); + + // 创建缓存键(基于用户、分页、搜索条件和在线状态筛选) + $cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all'); + + // 尝试从缓存获取数据(缓存2分钟) + $cachedData = cache($cacheKey); + if ($cachedData) { + return ResponseHelper::success($cachedData); + } + + // 如果没有缓存,执行查询 $result = $this->getOnlineWechatList( $this->makeWhere() ); + + $responseData = [ + 'list' => $this->makeResultedSet($result), + 'total' => $result->total(), + ]; + + // 存入缓存,有效期2分钟 + cache($cacheKey, $responseData, 120); - return ResponseHelper::success( - [ - 'list' => $this->makeResultedSet($result), - 'total' => $result->total(), - ] - ); + return ResponseHelper::success($responseData); } catch (\Exception $e) { return ResponseHelper::error($e->getMessage(), $e->getCode()); } diff --git a/Server/sql.sql b/Server/sql.sql index b49716bc..9e6fe53d 100644 --- a/Server/sql.sql +++ b/Server/sql.sql @@ -11,7 +11,7 @@ Target Server Version : 50736 File Encoding : 65001 - Date: 12/11/2025 11:05:39 + Date: 24/11/2025 16:50:43 */ SET NAMES utf8mb4; @@ -123,7 +123,7 @@ CREATE TABLE `ck_app_version` ( `updateContent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `createTime` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_attachments @@ -145,7 +145,7 @@ CREATE TABLE `ck_attachments` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_hash_key`(`hash_key`) USING BTREE, INDEX `idx_server`(`server`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 481 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 505 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_call_recording @@ -222,7 +222,7 @@ CREATE TABLE `ck_content_item` ( INDEX `idx_wechatid`(`wechatId`) USING BTREE, INDEX `idx_friendid`(`friendId`) USING BTREE, INDEX `idx_create_time`(`createTime`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 5876 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 5993 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_content_library @@ -252,7 +252,7 @@ CREATE TABLE `ck_content_library` ( `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_coze_conversation @@ -272,7 +272,7 @@ CREATE TABLE `ck_coze_conversation` ( UNIQUE INDEX `idx_conversation_id`(`conversation_id`) USING BTREE, INDEX `idx_bot_id`(`bot_id`) USING BTREE, INDEX `idx_create_time`(`create_time`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_coze_message @@ -331,7 +331,7 @@ CREATE TABLE `ck_customer_acquisition_task` ( `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', `apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 162 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 168 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device @@ -372,7 +372,7 @@ CREATE TABLE `ck_device_handle_log` ( `companyId` int(11) NULL DEFAULT NULL COMMENT '租户id', `createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 304 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 339 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device_taskconf @@ -395,7 +395,7 @@ CREATE TABLE `ck_device_taskconf` ( `updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间', `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_device_user @@ -425,7 +425,7 @@ CREATE TABLE `ck_device_wechat_login` ( `isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移', PRIMARY KEY (`id`) USING BTREE, INDEX `wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 309 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 312 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_flow_package @@ -653,7 +653,7 @@ CREATE TABLE `ck_kf_follow_up` ( INDEX `idx_level`(`type`) USING BTREE, INDEX `idx_isRemind`(`isRemind`) USING BTREE, INDEX `idx_isProcess`(`isProcess`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_friend_settings @@ -675,7 +675,7 @@ CREATE TABLE `ck_kf_friend_settings` ( INDEX `idx_userId`(`userId`) USING BTREE, INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, INDEX `idx_friendId`(`friendId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_keywords @@ -769,7 +769,7 @@ CREATE TABLE `ck_kf_notice` ( `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', `readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 246 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 247 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_questions @@ -810,7 +810,7 @@ CREATE TABLE `ck_kf_reply` ( `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 130746 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 130751 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_kf_reply_group @@ -977,7 +977,7 @@ CREATE TABLE `ck_task_customer` ( INDEX `addTime`(`addTime`) USING BTREE, INDEX `passTime`(`passTime`) USING BTREE, INDEX `updateTime`(`updateTime`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 24192 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 28204 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_tokens_company @@ -990,7 +990,7 @@ CREATE TABLE `ck_tokens_company` ( `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_tokens_package @@ -1033,7 +1033,7 @@ CREATE TABLE `ck_tokens_record` ( `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 236 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_order @@ -1070,8 +1070,9 @@ CREATE TABLE `ck_traffic_pool` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, INDEX `idx_wechatId`(`wechatId`) USING BTREE, - INDEX `idx_mobile`(`mobile`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 959687 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; + INDEX `idx_mobile`(`mobile`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1063510 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_profile @@ -1114,8 +1115,9 @@ CREATE TABLE `ck_traffic_source` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_identifier_sourceId_sceneId`(`identifier`, `sourceId`, `sceneId`) USING BTREE, INDEX `idx_identifier`(`identifier`) USING BTREE, - INDEX `idx_companyId`(`companyId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 564508 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_company_status_time`(`companyId`, `status`, `updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 573831 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_traffic_source_package @@ -1242,7 +1244,7 @@ CREATE TABLE `ck_user_portrait` ( `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 17718 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 19014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_users @@ -1267,7 +1269,7 @@ CREATE TABLE `ck_users` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1652 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1658 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_vendor_order @@ -1360,7 +1362,7 @@ CREATE TABLE `ck_wechat_account` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3097959 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 3614968 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_customer @@ -1378,7 +1380,7 @@ CREATE TABLE `ck_wechat_customer` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 154 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_friendship @@ -1434,7 +1436,7 @@ CREATE TABLE `ck_wechat_group_member` ( `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 549847 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 554147 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_restricts @@ -1451,7 +1453,7 @@ CREATE TABLE `ck_wechat_restricts` ( `restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期', `recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1319 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_wechat_tag @@ -1489,7 +1491,7 @@ CREATE TABLE `ck_workbench` ( INDEX `idx_user_id`(`userId`) USING BTREE, INDEX `idx_type`(`type`) USING BTREE, INDEX `idx_status`(`status`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 275 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 282 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_auto_like @@ -1534,7 +1536,7 @@ CREATE TABLE `ck_workbench_auto_like_item` ( INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, INDEX `momentsId`(`momentsId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 4639 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 4653 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_group_create @@ -1604,9 +1606,14 @@ CREATE TABLE `ck_workbench_group_push` ( `promotionSiteId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东广告位', `trafficPools` json NULL COMMENT '流量池', `devices` json NULL, + `groupPushSubType` tinyint(2) NULL DEFAULT 1 COMMENT '群推送子类型 1=群群发,2=群公告', + `announcementContent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `enableAiRewrite` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `aiRewritePrompt` tinyint(2) NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, - INDEX `idx_workbench_id`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE, + INDEX `idx_status_targetType`(`status`, `targetType`, `workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_group_push_item @@ -1622,7 +1629,9 @@ CREATE TABLE `ck_workbench_group_push_item` ( `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', `isLoop` tinyint(2) NULL DEFAULT 0 COMMENT '是否循环完成', `createTime` int(11) NOT NULL COMMENT '创建时间', - PRIMARY KEY (`id`) USING BTREE + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_target_time`(`workbenchId`, `targetType`, `createTime`) USING BTREE, + INDEX `idx_workbench_target_friend`(`workbenchId`, `targetType`, `friendId`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- @@ -1677,7 +1686,7 @@ CREATE TABLE `ck_workbench_moments_sync` ( `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_workbench_id`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_moments_sync_item @@ -1691,8 +1700,10 @@ CREATE TABLE `ck_workbench_moments_sync_item` ( `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', `createTime` int(11) NOT NULL COMMENT '创建时间', `isLoop` tinyint(2) NULL DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1650 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_time`(`workbenchId`, `createTime`) USING BTREE, + INDEX `idx_workbench_content`(`workbenchId`, `contentId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_traffic_config @@ -1714,7 +1725,7 @@ CREATE TABLE `ck_workbench_traffic_config` ( `updateTime` int(11) NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uniq_workbench`(`workbenchId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for ck_workbench_traffic_config_item @@ -1736,7 +1747,7 @@ CREATE TABLE `ck_workbench_traffic_config_item` ( INDEX `deviceId`(`deviceId`) USING BTREE, INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 49898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 54241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s2_allot_rule @@ -1869,6 +1880,7 @@ CREATE TABLE `s2_device` ( `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称', `wechatAccounts` json NULL COMMENT '微信账号列表JSON', `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `aliveTime` int(11) NULL DEFAULT 0, `lastAliveTime` int(11) NULL DEFAULT NULL COMMENT '最后在线时间', `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', `groupId` int(11) NULL DEFAULT NULL COMMENT '分组ID', @@ -1938,6 +1950,7 @@ CREATE TABLE `s2_friend_task` ( `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', `accountUsername` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间戳', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', UNIQUE INDEX `uk_task_id`(`id`) USING BTREE, INDEX `idx_tenant_id`(`tenantId`) USING BTREE, INDEX `idx_operator_account_id`(`operatorAccountId`) USING BTREE, @@ -2064,6 +2077,7 @@ CREATE TABLE `s2_wechat_account` ( `keFuAlive` tinyint(1) NULL DEFAULT 0 COMMENT '客服是否在线', `deviceAlive` tinyint(1) NULL DEFAULT 0 COMMENT '设备是否在线', `wechatAlive` tinyint(1) NULL DEFAULT 0 COMMENT '微信是否在线', + `wechatAliveTime` int(11) NULL DEFAULT 0 COMMENT '在线时间', `yesterdayMsgCount` int(11) NULL DEFAULT 0 COMMENT '昨日消息数', `sevenDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '7天消息数', `thirtyDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '30天消息数', @@ -2092,9 +2106,82 @@ CREATE TABLE `s2_wechat_account` ( `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', `status` tinyint(3) NULL DEFAULT 1 COMMENT '状态值', - INDEX `idx_wechat_id`(`wechatId`) USING BTREE + `healthScore` int(11) NULL DEFAULT 60 COMMENT '健康分总分(基础分+动态分)', + `baseScore` int(11) NULL DEFAULT 60 COMMENT '基础分(60-100分)', + `dynamicScore` int(11) NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `isModifiedAlias` tinyint(1) NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NULL DEFAULT 0 COMMENT '连续不频繁天数(用于加分)', + `scoreUpdateTime` int(11) NULL DEFAULT NULL COMMENT '评分更新时间', + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_is_modified_alias`(`isModifiedAlias`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; +-- ---------------------------- +-- Table structure for s2_wechat_account_score +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score`; +CREATE TABLE `s2_wechat_account_score` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID(s2_wechat_account.id)', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `baseScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础分(60-100分)', + `baseScoreCalculated` tinyint(1) NOT NULL DEFAULT 0 COMMENT '基础分是否已计算(0=未计算,1=已计算)', + `baseScoreCalcTime` int(11) NULL DEFAULT NULL COMMENT '基础分计算时间', + `baseInfoScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础信息分(0-10分)', + `isModifiedAlias` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `friendCountScore` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量分(0-30分)', + `friendCount` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量(评分时的快照)', + `friendCountSource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友数量来源(manual=手动,sync=同步)', + `dynamicScore` int(11) NOT NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NOT NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `frequentPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '频繁扣分(累计)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NOT NULL DEFAULT 0 COMMENT '连续不频繁天数', + `noFrequentBonus` int(11) NOT NULL DEFAULT 0 COMMENT '不频繁加分(累计)', + `isBanned` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否封号(0=否,1=是)', + `banPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '封号扣分', + `healthScore` int(11) NOT NULL DEFAULT 0 COMMENT '健康分总分(基础分+动态分)', + `maxAddFriendPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大加人次数', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `lastBanTime` int(11) NULL DEFAULT NULL COMMENT '最后一次封号时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_account_id`(`accountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_base_score_calculated`(`baseScoreCalculated`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 363 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score_log +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score_log`; +CREATE TABLE `s2_wechat_account_score_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `field` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变动字段(如frequentPenalty)', + `changeValue` int(11) NOT NULL DEFAULT 0 COMMENT '变动值(正加负减)', + `valueBefore` int(11) NULL DEFAULT NULL COMMENT '变更前的字段值', + `valueAfter` int(11) NULL DEFAULT NULL COMMENT '变更后的字段值', + `category` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类:penalty/bonus/dynamic_total/health_total等', + `source` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '触发来源 friend_task/wechat_message/system', + `sourceId` bigint(20) NULL DEFAULT NULL COMMENT '关联记录ID(如任务/消息ID)', + `extra` json NULL COMMENT '附加信息(JSON)', + `totalScoreBefore` int(11) NULL DEFAULT NULL COMMENT '变更前健康总分', + `totalScoreAfter` int(11) NULL DEFAULT NULL COMMENT '变更后健康总分', + `createTime` int(11) NOT NULL COMMENT '记录时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_account_field`(`accountId`, `field`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic; + -- ---------------------------- -- Table structure for s2_wechat_chatroom -- ---------------------------- @@ -2150,7 +2237,7 @@ CREATE TABLE `s2_wechat_chatroom_member` ( UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE, INDEX `chatroomId`(`chatroomId`) USING BTREE, INDEX `wechatId`(`wechatId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 495043 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 495174 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s2_wechat_friend @@ -2198,6 +2285,9 @@ CREATE TABLE `s2_wechat_friend` ( `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职位', UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE, INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, INDEX `idx_wechat_id`(`wechatId`) USING BTREE, @@ -2257,6 +2347,7 @@ CREATE TABLE `s2_wechat_message` ( `msgId` bigint(20) NULL DEFAULT NULL COMMENT '消息ID', `recallId` tinyint(1) NULL DEFAULT 0 COMMENT '撤回ID', `isRead` tinyint(1) NULL DEFAULT 0 COMMENT '是否读取', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_wechatChatroomId`(`wechatChatroomId`) USING BTREE, INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, @@ -2296,6 +2387,6 @@ CREATE TABLE `s2_wechat_moments` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE, INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 39669 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 40130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; From 637dcddee2855f3b944ed35d9a00ca9b1dbbd79d Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 27 Nov 2025 17:09:53 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=8A=B6=E6=80=81=20+=20=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=BA=95=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/UserController.php | 42 +++-- .../chukebao/controller/DataProcessing.php | 40 +++++ .../controller/WechatFriendController.php | 94 +++------- .../common/controller/ExportController.php | 166 ++++++++++++++++++ .../cunkebao/controller/BaseController.php | 7 + 5 files changed, 256 insertions(+), 93 deletions(-) create mode 100644 Server/application/common/controller/ExportController.php diff --git a/Server/application/api/controller/UserController.php b/Server/application/api/controller/UserController.php index 209f1b0e..ab4ce133 100644 --- a/Server/application/api/controller/UserController.php +++ b/Server/application/api/controller/UserController.php @@ -201,40 +201,38 @@ class UserController extends BaseController * 修改密码 * @return \think\response\Json */ - public function modifyPwd() + public function modifyPwd($data = []) { - // 获取并验证参数 - $params = $this->validateModifyPwdParams(); - if (!is_array($params)) { - return $params; + + if (empty($data)) { + return json_encode(['code' => 400,'msg' => '参数缺失']); } - $authorization = trim($this->request->header('authorization', $this->authorization)); + if (!isset($data['id']) || !isset($data['pwd'])) { + return json_encode(['code' => 401,'msg' => '参数缺失']); + } + $authorization = $this->authorization; + if (empty($authorization)) { - return errorJson('缺少授权信息'); + return json_encode(['code' => 400,'msg' => '缺少授权信息']); } - $headerData = ['client:' . self::CLIENT_TYPE]; - $header = setHeader($headerData, $authorization, 'plain'); + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + $params = [ + 'id' => $data['id'], + 'newPw' => $data['pwd'], + ]; try { - $result = requestCurl($this->baseUrl . 'api/Account/self', $params, 'PUT', $header); + $result = requestCurl($this->baseUrl . 'api/Account/modifypw', $params, 'PUT', $header,'json'); $response = handleApiResponse($result); - if (empty($response)) { - // 获取当前用户信息 - $currentUser = CompanyAccountModel::where('token', $authorization)->find(); - if ($currentUser) { - recordUserLog($currentUser['id'], $currentUser['userName'], 'MODIFY_PASSWORD', '修改密码成功', [], 200, '修改成功'); - } - return successJson(['message' => '修改成功']); + return json_encode(['code' => 200,'msg' => '修改成功']); } - - recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码失败', $params, 500, $response); - return errorJson($response); + return json_encode(['code' => 400,'msg' => $response]); } catch (\Exception $e) { - recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码异常', $params, 500, $e->getMessage()); - return errorJson('修改密码失败:' . $e->getMessage()); + return json_encode(['code' => 400,'msg' => '修改密码失败:' . $e->getMessage()]); } } diff --git a/Server/application/chukebao/controller/DataProcessing.php b/Server/application/chukebao/controller/DataProcessing.php index 72e617ea..863fee48 100644 --- a/Server/application/chukebao/controller/DataProcessing.php +++ b/Server/application/chukebao/controller/DataProcessing.php @@ -4,6 +4,7 @@ namespace app\chukebao\controller; use library\ResponseHelper; use app\api\model\WechatFriendModel; +use app\api\model\WechatMessageModel; use app\api\controller\MessageController; @@ -33,6 +34,7 @@ class DataProcessing extends BaseController 'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId} 'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId} 'CmdNewMessage', //接收消息 + 'CmdSendMessageResult', //更新消息状态 ]; if (empty($type) || empty($wechatAccountId)) { @@ -107,6 +109,44 @@ class DataProcessing extends BaseController $msg = '消息记录失败'; $codee = 400; } + break; + case 'CmdSendMessageResult': + $friendMessageId = $this->request->param('friendMessageId', 0); + $chatroomMessageId = $this->request->param('chatroomMessageId', 0); + $sendStatus = $this->request->param('sendStatus', null); + $wechatTime = $this->request->param('wechatTime', 0); + + if ($sendStatus === null) { + return ResponseHelper::error('sendStatus不能为空'); + } + + if (empty($friendMessageId) && empty($chatroomMessageId)) { + return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个'); + } + + $messageId = $friendMessageId ?: $chatroomMessageId; + $update = [ + 'sendStatus' => (int)$sendStatus, + ]; + + if (!empty($wechatTime)) { + $update['wechatTime'] = strlen((string)$wechatTime) > 10 + ? intval($wechatTime / 1000) + : (int)$wechatTime; + } + + $affected = WechatMessageModel::where('id', $messageId)->update($update); + + if ($affected === false) { + return ResponseHelper::error('更新消息状态失败'); + } + + if ($affected === 0) { + return ResponseHelper::error('消息不存在'); + } + + $msg = '更新消息状态成功'; + break; } return ResponseHelper::success('',$msg,$codee); } diff --git a/Server/application/chukebao/controller/WechatFriendController.php b/Server/application/chukebao/controller/WechatFriendController.php index 931d8ce6..a37e31e6 100644 --- a/Server/application/chukebao/controller/WechatFriendController.php +++ b/Server/application/chukebao/controller/WechatFriendController.php @@ -23,7 +23,7 @@ class WechatFriendController extends BaseController $total = $query->count(); $list = $query->page($page, $limit)->select(); - // 提取所有好友ID + // 提取所有好友ID $friendIds = array_column($list, 'id'); $aiTypeData = []; @@ -67,7 +67,7 @@ class WechatFriendController extends BaseController $friend = Db::table('s2_wechat_friend') ->where(['id' => $friendId, 'isDeleted' => 0]) ->find(); - + if (empty($friend)) { return ResponseHelper::error('好友不存在'); } @@ -78,11 +78,11 @@ class WechatFriendController extends BaseController $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; $friend['passTime'] = !empty($friend['passTime']) ? date('Y-m-d H:i:s', $friend['passTime']) : ''; - + // 获取AI类型设置 $aiTypeSetting = FriendSettings::where('friendId', $friendId)->find(); $friend['aiType'] = $aiTypeSetting ? $aiTypeSetting['type'] : 0; - + return ResponseHelper::success(['detail' => $friend]); } @@ -183,87 +183,38 @@ class WechatFriendController extends BaseController if (empty($accountId)) { return ResponseHelper::error('请先登录'); } - + // 直接使用operatorAccountId查询添加好友任务记录 $query = Db::table('s2_friend_task') ->where('operatorAccountId', $accountId) ->order('createTime desc'); - + // 如果指定了状态筛选 if ($status !== '' && $status !== null) { $query->where('status', $status); } - + $total = $query->count(); $tasks = $query->page($page, $limit)->select(); - - // 提取所有任务的phone和wechatId,用于查询好友信息(获取通过时间) - $taskPhones = []; - $taskWechatIds = []; - foreach ($tasks as $task) { - if (!empty($task['phone'])) { - $taskPhones[] = $task['phone']; - } - if (!empty($task['wechatId'])) { - $taskWechatIds[] = $task['wechatId']; - } - } - - // 查询好友信息,获取通过时间 - $friendPassTimeMap = []; - if (!empty($taskPhones) || !empty($taskWechatIds)) { - // 分别通过phone和wechatId查询,确保都能匹配到 - $friendsByPhone = []; - $friendsByWechatId = []; - - if (!empty($taskPhones)) { - $friendsByPhone = Db::table('s2_wechat_friend') - ->where('accountId', $accountId) - ->where('isDeleted', 0) - ->where('phone', 'in', $taskPhones) - ->field('phone,wechatId,passTime,nickname') - ->select(); - } - - if (!empty($taskWechatIds)) { - $friendsByWechatId = Db::table('s2_wechat_friend') - ->where('accountId', $accountId) - ->where('isDeleted', 0) - ->where('wechatId', 'in', $taskWechatIds) - ->field('phone,wechatId,passTime,nickname') - ->select(); - } - - // 合并结果并构建映射表(优先使用phone作为key) - $allFriends = array_merge($friendsByPhone, $friendsByWechatId); - foreach ($allFriends as $friend) { - // 使用phone作为key(如果存在) - if (!empty($friend['phone'])) { - $friendPassTimeMap[$friend['phone']] = [ - 'passTime' => $friend['passTime'] ?? 0, - 'nickname' => $friend['nickname'] ?? '', - ]; - } - // 同时使用wechatId作为key(如果存在且phone为空) - if (!empty($friend['wechatId']) && empty($friend['phone'])) { - $friendPassTimeMap[$friend['wechatId']] = [ - 'passTime' => $friend['passTime'] ?? 0, - 'nickname' => $friend['nickname'] ?? '', - ]; - } - } - } - + + // 处理任务数据 $list = []; foreach ($tasks as $task) { - $taskKey = !empty($task['phone']) ? $task['phone'] : ($task['wechatId'] ?? ''); - $friendInfo = isset($friendPassTimeMap[$taskKey]) ? $friendPassTimeMap[$taskKey] : null; - + // 提取所有任务的phone、wechatId,用于查询好友信息(获取通过时间) + $friendInfo = Db::table('s2_wechat_friend') + ->where(['isDeleted' => 0, 'ownerWechatId' => $task['wechatId']]) + ->where(function ($query) use ($task) { + $query->whereLike('phone', '%'.$task['phone'].'%')->whereOr('alias', $task['phone'])->whereOr('wechatId', $task['phone']); + })->field('phone,wechatId,alias,passTime,nickname')->find(); + + + $item = [ 'taskId' => $task['id'] ?? 0, 'phone' => $task['phone'] ?? '', 'wechatId' => $task['wechatId'] ?? '', + 'alias' => $task['alias'] ?? '', // 添加者信息 'adder' => [ 'avatar' => $task['wechatAvatar'] ?? '', // 添加者头像 @@ -276,6 +227,7 @@ class WechatFriendController extends BaseController 'status' => [ 'code' => $task['status'] ?? 0, // 状态码:0执行中,1执行成功,2执行失败 'text' => $this->getTaskStatusText($task['status'] ?? 0), // 状态文本 + 'extra' => '' ], // 时间信息 'time' => [ @@ -299,10 +251,10 @@ class WechatFriendController extends BaseController 'labels' => !empty($task['labels']) ? explode(',', $task['labels']) : [], // 标签 ] ]; - + $list[] = $item; } - + return ResponseHelper::success(['list' => $list, 'total' => $total]); } @@ -319,7 +271,7 @@ class WechatFriendController extends BaseController 1 => '执行成功', 2 => '执行失败', ]; - + return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态'; } } \ No newline at end of file diff --git a/Server/application/common/controller/ExportController.php b/Server/application/common/controller/ExportController.php new file mode 100644 index 00000000..aea58f15 --- /dev/null +++ b/Server/application/common/controller/ExportController.php @@ -0,0 +1,166 @@ + 需要在请求结束时清理的临时文件 + */ + protected static $tempFiles = []; + + /** + * 导出 Excel(支持指定列插入图片) + * + * @param string $fileName 输出文件名(可不带扩展名) + * @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话'] + * @param array $rows 数据行,需与 $headers 的 key 对应 + * @param array $imageColumns 需要渲染为图片的列 key 列表 + * @param string $sheetName 工作表名称 + * + * @throws Exception + */ + public static function exportExcelWithImages( + $fileName, + array $headers, + array $rows, + array $imageColumns = [], + $sheetName = 'Sheet1' + ) { + if (empty($headers)) { + throw new Exception('导出列定义不能为空'); + } + if (empty($rows)) { + throw new Exception('导出数据不能为空'); + } + + $excel = new PHPExcel(); + $sheet = $excel->getActiveSheet(); + $sheet->setTitle($sheetName); + + $columnKeys = array_keys($headers); + + // 写入表头 + foreach ($columnKeys as $index => $key) { + $columnLetter = self::columnLetter($index); + $sheet->setCellValue($columnLetter . '1', $headers[$key]); + $sheet->getColumnDimension($columnLetter)->setAutoSize(true); + } + + // 写入数据与图片 + foreach ($rows as $rowIndex => $rowData) { + $excelRow = $rowIndex + 2; // 数据从第 2 行开始 + foreach ($columnKeys as $colIndex => $key) { + $columnLetter = self::columnLetter($colIndex); + $cell = $columnLetter . $excelRow; + $value = isset($rowData[$key]) ? $rowData[$key] : ''; + + if (in_array($key, $imageColumns, true) && !empty($value)) { + $imagePath = self::resolveImagePath($value); + if ($imagePath) { + $drawing = new PHPExcel_Worksheet_Drawing(); + $drawing->setPath($imagePath); + $drawing->setCoordinates($cell); + $drawing->setOffsetX(5); + $drawing->setOffsetY(5); + $drawing->setHeight(60); + $drawing->setWorksheet($sheet); + $sheet->getRowDimension($excelRow)->setRowHeight(60); + } else { + $sheet->setCellValue($cell, $value); + } + } else { + $sheet->setCellValue($cell, $value); + } + } + } + + $safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His')); + if (stripos($safeName, '.xlsx') === false) { + $safeName .= '.xlsx'; + } + + if (ob_get_length()) { + ob_end_clean(); + } + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Cache-Control: max-age=0'); + header('Content-Disposition: attachment;filename="' . $safeName . '"'); + + $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007'); + $writer->save('php://output'); + + self::cleanupTempFiles(); + exit; + } + + /** + * 根据列序号生成 Excel 列字母 + * + * @param int $index + * @return string + */ + protected static function columnLetter($index) + { + $letters = ''; + do { + $letters = chr($index % 26 + 65) . $letters; + $index = intval($index / 26) - 1; + } while ($index >= 0); + + return $letters; + } + + /** + * 将远程或本地图片路径转换为可用的本地文件路径 + * + * @param string $path + * @return string|null + */ + protected static function resolveImagePath($path) + { + if (empty($path)) { + return null; + } + + if (preg_match('/^https?:\/\//i', $path)) { + $tempFile = tempnam(sys_get_temp_dir(), 'export_img_'); + $stream = @file_get_contents($path); + if ($stream === false) { + return null; + } + file_put_contents($tempFile, $stream); + self::$tempFiles[] = $tempFile; + return $tempFile; + } + + if (file_exists($path)) { + return $path; + } + + return null; + } + + /** + * 清理所有临时文件 + */ + protected static function cleanupTempFiles() + { + foreach (self::$tempFiles as $file) { + if (file_exists($file)) { + @unlink($file); + } + } + self::$tempFiles = []; + } +} \ No newline at end of file diff --git a/Server/application/cunkebao/controller/BaseController.php b/Server/application/cunkebao/controller/BaseController.php index 26bd79db..f3875d54 100644 --- a/Server/application/cunkebao/controller/BaseController.php +++ b/Server/application/cunkebao/controller/BaseController.php @@ -3,6 +3,7 @@ namespace app\cunkebao\controller; use app\api\controller\AccountController; +use app\api\controller\UserController; use app\common\service\ClassTableService; use library\ResponseHelper; use think\Controller; @@ -148,6 +149,12 @@ class BaseController extends Controller $res = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data); if (!empty($res)) { + if ($user['typeId'] == 1 && !empty($user['s2_accountId'])) { + $UserController = new UserController(); + $UserController->modifyPwd(['id' => $user['s2_accountId'],'pwd' => $passWord]); + } + + return ResponseHelper::success('密码修改成功'); } else { return ResponseHelper::error('密码修改失败'); From e658a32244fab2a0988e2f4d27e9d70187cef0ce Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 27 Nov 2025 18:10:23 +0800 Subject: [PATCH 05/11] 1 --- Server/application/chukebao/controller/DataProcessing.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/application/chukebao/controller/DataProcessing.php b/Server/application/chukebao/controller/DataProcessing.php index 863fee48..af0d0d1b 100644 --- a/Server/application/chukebao/controller/DataProcessing.php +++ b/Server/application/chukebao/controller/DataProcessing.php @@ -138,11 +138,11 @@ class DataProcessing extends BaseController $affected = WechatMessageModel::where('id', $messageId)->update($update); if ($affected === false) { - return ResponseHelper::error('更新消息状态失败'); + return ResponseHelper::success('','更新消息状态失败'); } if ($affected === 0) { - return ResponseHelper::error('消息不存在'); + return ResponseHelper::success('','消息不存在'); } $msg = '更新消息状态成功'; From a03c162962d534c39c60f3919ad5f6b75dc2e7e8 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 28 Nov 2025 15:32:02 +0800 Subject: [PATCH 06/11] 111 --- .../chukebao/controller/DataProcessing.php | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/Server/application/chukebao/controller/DataProcessing.php b/Server/application/chukebao/controller/DataProcessing.php index af0d0d1b..887fb2ef 100644 --- a/Server/application/chukebao/controller/DataProcessing.php +++ b/Server/application/chukebao/controller/DataProcessing.php @@ -2,6 +2,7 @@ namespace app\chukebao\controller; +use app\api\model\WechatChatroomModel; use library\ResponseHelper; use app\api\model\WechatFriendModel; use app\api\model\WechatMessageModel; @@ -77,15 +78,31 @@ class DataProcessing extends BaseController if(empty($toAccountId)){ return ResponseHelper::error('参数缺失'); } - - $friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); - if(empty($friend)){ - return ResponseHelper::error('好友不存在'); + if(empty($wechatFriendId) && empty($wechatChatroomId)){ + return ResponseHelper::error('参数缺失'); } - $friend->accountId = $toAccountId; - $friend->updateTime = time(); - $friend->save(); - $msg = '好友转移成功'; + + + if (!empty($wechatFriendId)){ + $dsta = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = '好友转移成功'; + if(empty($ddta)){ + return ResponseHelper::error('好友不存在'); + } + } + + + if (!empty($wechatChatroomId)){ + $dsta = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = '群聊转移成功'; + if(empty($ddta)){ + return ResponseHelper::error('群聊不存在'); + } + } + + $dsta->accountId = $toAccountId; + $dsta->updateTime = time(); + $dsta->save(); break; case 'CmdNewMessage': if(empty($friendMessage) && empty($chatroomMessage)){ @@ -107,7 +124,7 @@ class DataProcessing extends BaseController $msg = '消息记录成功'; }else{ $msg = '消息记录失败'; - $codee = 400; + $codee = 200; } break; case 'CmdSendMessageResult': From 5087190816d39a31b07685fd11b670088a719f8d Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 28 Nov 2025 15:46:21 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/common/TaskServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/application/common/TaskServer.php b/Server/application/common/TaskServer.php index 2e205fdf..50d2d634 100644 --- a/Server/application/common/TaskServer.php +++ b/Server/application/common/TaskServer.php @@ -82,7 +82,7 @@ class TaskServer extends Server if ($current_worker_id == 1) { // 每60秒检查一次自动问候规则 Timer::add(60, function () use ($adapter) { - $adapter->handleAutoGreetings(); + //$adapter->handleAutoGreetings(); }); } From 5691d78004a4b07b2f2b0859fa5006ba6b4c3ed2 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 28 Nov 2025 16:03:56 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/wechat-accounts/detail/api.ts | 67 ++++++ .../wechat-accounts/detail/detail.module.scss | 50 +++++ .../mine/wechat-accounts/detail/index.tsx | 208 +++++++++++++++++- .../common/controller/ExportController.php | 184 ++++++++++++++-- Server/application/cunkebao/config/route.php | 1 + .../wechat/GetWechatMomentsV1Controller.php | 159 +++++++++++++ 6 files changed, 654 insertions(+), 15 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 11998fe6..7a9a1ce3 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -1,4 +1,6 @@ import request from "@/api/request"; +import axios from "axios"; +import { useUserStore } from "@/store/module/user"; // 获取微信号详情 export function getWechatAccountDetail(id: string) { @@ -50,3 +52,68 @@ export function transferWechatFriends(params: { }) { return request("/v1/wechats/transfer-friends", params, "POST"); } + +// 导出朋友圈接口(直接下载文件) +export async function exportWechatMoments(params: { + wechatId: string; + keyword?: string; + type?: number; + startTime?: string; + endTime?: string; +}): Promise { + const { token } = useUserStore.getState(); + const baseURL = + (import.meta as any).env?.VITE_API_BASE_URL || "/api"; + + // 构建查询参数 + const queryParams = new URLSearchParams(); + queryParams.append("wechatId", params.wechatId); + if (params.keyword) { + queryParams.append("keyword", params.keyword); + } + if (params.type !== undefined) { + queryParams.append("type", params.type.toString()); + } + if (params.startTime) { + queryParams.append("startTime", params.startTime); + } + if (params.endTime) { + queryParams.append("endTime", params.endTime); + } + + try { + const response = await axios.get( + `${baseURL}/v1/wechats/moments/export?${queryParams.toString()}`, + { + responseType: "blob", + headers: { + Authorization: token ? `Bearer ${token}` : undefined, + }, + } + ); + + // 创建下载链接 + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + + // 从响应头获取文件名,如果没有则使用默认文件名 + const contentDisposition = response.headers["content-disposition"]; + let fileName = "朋友圈导出.xlsx"; + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (fileNameMatch && fileNameMatch[1]) { + fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, "")); + } + } + + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error: any) { + throw new Error(error.response?.data?.message || error.message || "导出失败"); + } +} diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index 08f297a2..354503b6 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -845,6 +845,56 @@ margin-top: 20px; } + .popup-footer { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } + + .export-form { + margin-top: 20px; + + .form-item { + margin-bottom: 20px; + + label { + display: block; + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + } + + .type-selector { + display: flex; + gap: 8px; + flex-wrap: wrap; + + .type-option { + padding: 8px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s; + background: white; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.active { + background: #1677ff; + border-color: #1677ff; + color: white; + } + } + } + } + } + .restrictions-detail { .restriction-detail-item { display: flex; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 50f2c599..aeb10125 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -11,6 +11,7 @@ import { Avatar, Tag, Switch, + DatePicker, } from "antd-mobile"; import { Input, Pagination } from "antd"; import NavCommon from "@/components/NavCommon"; @@ -27,6 +28,7 @@ import { transferWechatFriends, getWechatAccountOverview, getWechatMoments, + exportWechatMoments, } from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -64,6 +66,16 @@ const WechatAccountDetail: React.FC = () => { const [momentsError, setMomentsError] = useState(null); const MOMENTS_LIMIT = 10; + // 导出相关状态 + const [showExportPopup, setShowExportPopup] = useState(false); + const [exportKeyword, setExportKeyword] = useState(""); + const [exportType, setExportType] = useState(undefined); + const [exportStartTime, setExportStartTime] = useState(null); + const [exportEndTime, setExportEndTime] = useState(null); + const [showStartTimePicker, setShowStartTimePicker] = useState(false); + const [showEndTimePicker, setShowEndTimePicker] = useState(false); + const [exportLoading, setExportLoading] = useState(false); + // 获取基础信息 const fetchAccountInfo = useCallback(async () => { if (!id) return; @@ -370,6 +382,50 @@ const WechatAccountDetail: React.FC = () => { fetchMomentsList(momentsPage + 1, true); }; + // 处理朋友圈导出 + const handleExportMoments = useCallback(async () => { + if (!id) { + Toast.show({ content: "微信ID不存在", position: "top" }); + return; + } + + setExportLoading(true); + try { + // 格式化时间 + const formatDate = (date: Date | null): string | undefined => { + if (!date) return undefined; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + await exportWechatMoments({ + wechatId: id, + keyword: exportKeyword || undefined, + type: exportType, + startTime: formatDate(exportStartTime), + endTime: formatDate(exportEndTime), + }); + + Toast.show({ content: "导出成功", position: "top" }); + setShowExportPopup(false); + // 重置筛选条件 + setExportKeyword(""); + setExportType(undefined); + setExportStartTime(null); + setExportEndTime(null); + } catch (error: any) { + console.error("导出失败:", error); + Toast.show({ + content: error.message || "导出失败,请重试", + position: "top", + }); + } finally { + setExportLoading(false); + } + }, [id, exportKeyword, exportType, exportStartTime, exportEndTime]); + const formatMomentDateParts = (dateString: string) => { const date = new Date(dateString); if (Number.isNaN(date.getTime())) { @@ -810,7 +866,10 @@ const WechatAccountDetail: React.FC = () => { 视频
-
+
setShowExportPopup(true)} + > 导出
@@ -1023,6 +1082,153 @@ const WechatAccountDetail: React.FC = () => {
+ {/* 朋友圈导出弹窗 */} + setShowExportPopup(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

导出朋友圈

+ +
+ +
+ {/* 关键词搜索 */} +
+ + setExportKeyword(e.target.value)} + allowClear + /> +
+ + {/* 类型筛选 */} +
+ +
+
setExportType(undefined)} + > + 全部 +
+
setExportType(4)} + > + 文本 +
+
setExportType(1)} + > + 图片 +
+
setExportType(3)} + > + 视频 +
+
+
+ + {/* 开始时间 */} +
+ + setShowStartTimePicker(true)} + /> + setShowStartTimePicker(false)} + onConfirm={val => { + setExportStartTime(val); + setShowStartTimePicker(false); + }} + /> +
+ + {/* 结束时间 */} +
+ + setShowEndTimePicker(true)} + /> + setShowEndTimePicker(false)} + onConfirm={val => { + setExportEndTime(val); + setShowEndTimePicker(false); + }} + /> +
+
+ +
+ + +
+
+
+ {/* 好友详情弹窗 */} {/* Removed */} diff --git a/Server/application/common/controller/ExportController.php b/Server/application/common/controller/ExportController.php index aea58f15..8d2a8079 100644 --- a/Server/application/common/controller/ExportController.php +++ b/Server/application/common/controller/ExportController.php @@ -26,6 +26,11 @@ class ExportController extends Controller * @param array $rows 数据行,需与 $headers 的 key 对应 * @param array $imageColumns 需要渲染为图片的列 key 列表 * @param string $sheetName 工作表名称 + * @param array $options 额外选项: + * - imageWidth(图片宽度,默认100) + * - imageHeight(图片高度,默认100) + * - imageColumnWidth(图片列宽,默认15) + * - titleRow(标题行内容,支持多行文本数组) * * @throws Exception */ @@ -34,7 +39,8 @@ class ExportController extends Controller array $headers, array $rows, array $imageColumns = [], - $sheetName = 'Sheet1' + $sheetName = 'Sheet1', + array $options = [] ) { if (empty($headers)) { throw new Exception('导出列定义不能为空'); @@ -43,22 +49,114 @@ class ExportController extends Controller throw new Exception('导出数据不能为空'); } + // 默认选项 + $imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100; + $imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100; + $imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15; + $rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10); + $excel = new PHPExcel(); $sheet = $excel->getActiveSheet(); $sheet->setTitle($sheetName); $columnKeys = array_keys($headers); + $totalColumns = count($columnKeys); + $lastColumnLetter = self::columnLetter($totalColumns - 1); - // 写入表头 + // 定义特定列的固定宽度(如果未指定则使用默认值) + $columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : []; + + // 检查是否有标题行 + $titleRow = isset($options['titleRow']) ? $options['titleRow'] : null; + $dataStartRow = 1; // 数据开始行(表头行) + + // 如果有标题行,先写入标题行 + if ($titleRow && is_array($titleRow) && !empty($titleRow)) { + $dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头) + + // 合并标题行单元格(从第一列到最后一列) + $titleRange = 'A1:' . $lastColumnLetter . '1'; + $sheet->mergeCells($titleRange); + + // 构建标题内容(支持多行) + $titleContent = ''; + if (is_array($titleRow)) { + $titleContent = implode("\n", $titleRow); + } else { + $titleContent = (string)$titleRow; + } + + // 写入标题 + $sheet->setCellValue('A1', $titleContent); + + // 设置标题行样式 + $sheet->getStyle('A1')->applyFromArray([ + 'font' => ['bold' => true, 'size' => 16], + 'alignment' => [ + 'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER, + 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER, + 'wrap' => true + ], + 'fill' => [ + 'type' => \PHPExcel_Style_Fill::FILL_SOLID, + 'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景 + ], + 'borders' => [ + 'allborders' => [ + 'style' => \PHPExcel_Style_Border::BORDER_THIN, + 'color' => ['rgb' => '000000'] + ] + ] + ]); + $sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度 + } + + // 写入表头并设置列宽 + $headerRow = $dataStartRow; foreach ($columnKeys as $index => $key) { $columnLetter = self::columnLetter($index); - $sheet->setCellValue($columnLetter . '1', $headers[$key]); - $sheet->getColumnDimension($columnLetter)->setAutoSize(true); + $sheet->setCellValue($columnLetter . $headerRow, $headers[$key]); + + // 如果是图片列,设置固定列宽 + if (in_array($key, $imageColumns, true)) { + $sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth); + } elseif (isset($columnWidths[$key])) { + // 如果指定了该列的宽度,使用指定宽度 + $sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]); + } else { + // 否则自动调整 + $sheet->getColumnDimension($columnLetter)->setAutoSize(true); + } } + // 设置表头样式 + $headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow; + $sheet->getStyle($headerRange)->applyFromArray([ + 'font' => ['bold' => true, 'size' => 11], + 'alignment' => [ + 'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER, + 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER, + 'wrap' => true + ], + 'fill' => [ + 'type' => \PHPExcel_Style_Fill::FILL_SOLID, + 'color' => ['rgb' => 'FFF8DC'] + ], + 'borders' => [ + 'allborders' => [ + 'style' => \PHPExcel_Style_Border::BORDER_THIN, + 'color' => ['rgb' => '000000'] + ] + ] + ]); + $sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示 + // 写入数据与图片 + $dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始 foreach ($rows as $rowIndex => $rowData) { - $excelRow = $rowIndex + 2; // 数据从第 2 行开始 + $excelRow = $dataRowStart + $rowIndex; // 数据行 + $maxRowHeight = $rowHeight; // 记录当前行的最大高度 + foreach ($columnKeys as $colIndex => $key) { $columnLetter = self::columnLetter($colIndex); $cell = $columnLetter . $excelRow; @@ -67,21 +165,79 @@ class ExportController extends Controller if (in_array($key, $imageColumns, true) && !empty($value)) { $imagePath = self::resolveImagePath($value); if ($imagePath) { - $drawing = new PHPExcel_Worksheet_Drawing(); - $drawing->setPath($imagePath); - $drawing->setCoordinates($cell); - $drawing->setOffsetX(5); - $drawing->setOffsetY(5); - $drawing->setHeight(60); - $drawing->setWorksheet($sheet); - $sheet->getRowDimension($excelRow)->setRowHeight(60); + // 获取图片实际尺寸并等比例缩放 + $imageSize = @getimagesize($imagePath); + if ($imageSize) { + $originalWidth = $imageSize[0]; + $originalHeight = $imageSize[1]; + + // 计算等比例缩放后的尺寸 + $ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight); + $scaledWidth = $originalWidth * $ratio; + $scaledHeight = $originalHeight * $ratio; + + // 确保不超过最大尺寸 + if ($scaledWidth > $imageWidth) { + $scaledWidth = $imageWidth; + $scaledHeight = $originalHeight * ($imageWidth / $originalWidth); + } + if ($scaledHeight > $imageHeight) { + $scaledHeight = $imageHeight; + $scaledWidth = $originalWidth * ($imageHeight / $originalHeight); + } + + $drawing = new PHPExcel_Worksheet_Drawing(); + $drawing->setPath($imagePath); + $drawing->setCoordinates($cell); + + // 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素) + $cellWidthPx = $imageColumnWidth * 7; + $cellHeightPx = $maxRowHeight * 0.75; + $offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2); + $offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2); + + $drawing->setOffsetX((int)$offsetX); + $drawing->setOffsetY((int)$offsetY); + $drawing->setWidth((int)$scaledWidth); + $drawing->setHeight((int)$scaledHeight); + $drawing->setWorksheet($sheet); + + // 更新行高以适应图片(留出一些边距) + $neededHeight = (int)($scaledHeight / 0.75) + 10; + if ($neededHeight > $maxRowHeight) { + $maxRowHeight = $neededHeight; + } + } else { + // 如果无法获取图片尺寸,使用默认尺寸 + $drawing = new PHPExcel_Worksheet_Drawing(); + $drawing->setPath($imagePath); + $drawing->setCoordinates($cell); + $drawing->setOffsetX(5); + $drawing->setOffsetY(5); + $drawing->setWidth($imageWidth); + $drawing->setHeight($imageHeight); + $drawing->setWorksheet($sheet); + } } else { - $sheet->setCellValue($cell, $value); + $sheet->setCellValue($cell, ''); } } else { $sheet->setCellValue($cell, $value); + // 设置文本对齐和换行 + $style = $sheet->getStyle($cell); + $style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER); + $style->getAlignment()->setWrapText(true); + // 根据列类型设置水平对齐 + if (in_array($key, ['date', 'postTime'])) { + $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER); + } else { + $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT); + } } } + + // 设置行高 + $sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight); } $safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His')); diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index c34fcd07..056667da 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -38,6 +38,7 @@ Route::group('v1/', function () { Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo'); Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据 Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈 + Route::get('moments/export', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@export'); // 导出微信朋友圈 Route::get('count', 'app\cunkebao\controller\DeviceWechat@count'); Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量 Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态 diff --git a/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php index b34ef7ff..2057a44d 100644 --- a/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php +++ b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php @@ -2,6 +2,7 @@ namespace app\cunkebao\controller\wechat; +use app\common\controller\ExportController; use app\common\model\Device as DeviceModel; use app\common\model\DeviceUser as DeviceUserModel; use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel; @@ -145,6 +146,164 @@ class GetWechatMomentsV1Controller extends BaseController } } + /** + * 导出朋友圈数据到Excel + * + * @return void + */ + public function export() + { + try { + $wechatId = $this->request->param('wechatId/s', ''); + if (empty($wechatId)) { + return ResponseHelper::error('wechatId不能为空'); + } + + // 权限校验:只能查看当前账号可访问的微信 + $accessibleWechatIds = $this->getAccessibleWechatIds(); + if (!in_array($wechatId, $accessibleWechatIds, true)) { + return ResponseHelper::error('无权查看该微信的朋友圈', 403); + } + + // 获取对应的微信账号ID + $accountId = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->value('id'); + + if (empty($accountId)) { + return ResponseHelper::error('微信账号不存在或尚未同步', 404); + } + + $query = Db::table('s2_wechat_moments') + ->where('wechatAccountId', $accountId); + + // 关键词搜索 + if ($keyword = trim((string)$this->request->param('keyword', ''))) { + $query->whereLike('content', '%' . $keyword . '%'); + } + + // 类型筛选 + $type = $this->request->param('type', ''); + if ($type !== '' && $type !== null) { + $query->where('type', (int)$type); + } + + // 时间筛选 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + if ($startTime || $endTime) { + $start = $startTime ? strtotime($startTime) : 0; + $end = $endTime ? strtotime($endTime) : time(); + if ($start && $end && $end < $start) { + return ResponseHelper::error('结束时间不能早于开始时间'); + } + $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]); + } + + // 获取所有数据(不分页) + $moments = $query->order('createTime', 'desc')->select(); + + if (empty($moments)) { + return ResponseHelper::error('暂无数据可导出'); + } + + // 定义表头 + $headers = [ + 'date' => '日期', + 'postTime' => '投放时间', + 'functionCategory' => '作用分类', + 'content' => '朋友圈文案', + 'selfReply' => '自回评内容', + 'displayForm' => '朋友圈展示形式', + 'image1' => '配图1', + 'image2' => '配图2', + 'image3' => '配图3', + 'image4' => '配图4', + 'image5' => '配图5', + 'image6' => '配图6', + 'image7' => '配图7', + 'image8' => '配图8', + 'image9' => '配图9', + ]; + + // 格式化数据 + $rows = []; + foreach ($moments as $moment) { + $resUrls = $this->decodeJson($moment['resUrls'] ?? null); + $imageUrls = is_array($resUrls) ? $resUrls : []; + + // 格式化日期和时间 + $createTime = !empty($moment['createTime']) + ? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime'])) + : 0; + $date = $createTime ? date('Y年m月d日', $createTime) : ''; + $postTime = $createTime ? date('H:i', $createTime) : ''; + + // 判断展示形式 + $displayForm = ''; + if (!empty($moment['content']) && !empty($imageUrls)) { + $displayForm = '文字+图片'; + } elseif (!empty($moment['content'])) { + $displayForm = '文字'; + } elseif (!empty($imageUrls)) { + $displayForm = '图片'; + } + + $row = [ + 'date' => $date, + 'postTime' => $postTime, + 'functionCategory' => '', // 暂时放空 + 'content' => $moment['content'] ?? '', + 'selfReply' => '', // 暂时放空 + 'displayForm' => $displayForm, + ]; + + // 分配图片到配图1-9列 + for ($i = 1; $i <= 9; $i++) { + $imageKey = 'image' . $i; + $row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : ''; + } + + $rows[] = $row; + } + + // 定义图片列(配图1-9) + $imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9']; + + // 生成文件名 + $fileName = '朋友圈投放_' . date('Ymd_His'); + + // 调用导出方法,优化图片显示效果 + ExportController::exportExcelWithImages( + $fileName, + $headers, + $rows, + $imageColumns, + '朋友圈投放', + [ + 'imageWidth' => 120, // 图片宽度(像素) + 'imageHeight' => 120, // 图片高度(像素) + 'imageColumnWidth' => 18, // 图片列宽(Excel单位) + 'rowHeight' => 130, // 行高(像素) + 'columnWidths' => [ // 特定列的固定宽度 + 'date' => 15, // 日期列宽 + 'postTime' => 12, // 投放时间列宽 + 'functionCategory' => 15, // 作用分类列宽 + 'content' => 40, // 朋友圈文案列宽(自动调整可能不够) + 'selfReply' => 30, // 自回评内容列宽 + 'displayForm' => 18, // 朋友圈展示形式列宽 + ], + 'titleRow' => [ // 标题行内容(第一行) + '朋友圈投放', + '我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)' + ] + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error('导出失败:' . $e->getMessage(), 500); + } + } + /** * 格式化朋友圈数据 * From 0244871fc1fa672ed9c9ca0bca110f6474223f08 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 28 Nov 2025 16:04:29 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E5=A5=BD=E5=8F=8B/=E7=BE=A4=E8=81=8A?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chukebao/controller/DataProcessing.php | 14 ++++---- .../Adapters/ChuKeBao/Adapter.php | 33 ++++++++++--------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Server/application/chukebao/controller/DataProcessing.php b/Server/application/chukebao/controller/DataProcessing.php index 887fb2ef..fdde86b7 100644 --- a/Server/application/chukebao/controller/DataProcessing.php +++ b/Server/application/chukebao/controller/DataProcessing.php @@ -84,25 +84,25 @@ class DataProcessing extends BaseController if (!empty($wechatFriendId)){ - $dsta = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + $data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); $msg = '好友转移成功'; - if(empty($ddta)){ + if(empty($data)){ return ResponseHelper::error('好友不存在'); } } if (!empty($wechatChatroomId)){ - $dsta = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find(); + $data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find(); $msg = '群聊转移成功'; - if(empty($ddta)){ + if(empty($data)){ return ResponseHelper::error('群聊不存在'); } } - $dsta->accountId = $toAccountId; - $dsta->updateTime = time(); - $dsta->save(); + $data->accountId = $toAccountId; + $data->updateTime = time(); + $data->save(); break; case 'CmdNewMessage': if(empty($friendMessage) && empty($chatroomMessage)){ diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 247684bc..d89bf8ea 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -307,7 +307,7 @@ class Adapter implements WeChatServiceInterface $conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'], 'tags' => $tags]); - $this->createFriendAddTask($accountId, $task['phone'], $conf); + $this->createFriendAddTask($accountId, $task['phone'], $conf, $task['remark']); $friendAddTaskCreated = true; $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 break; @@ -951,28 +951,29 @@ class Adapter implements WeChatServiceInterface } // 创建添加好友任务/执行添加 - public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf) + public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf, $remark = '') { if (empty($wechatAccountId) || empty($phone) || empty($conf)) { return; } - switch ($conf['remarkType']) { - case 'phone': - $remark = $phone . '-' . $conf['task_name']; - break; - case 'nickname': - $remark = ''; - break; - case 'source': - $remark = $conf['task_name']; - break; - default: - $remark = ''; - break; + if (empty($remark)){ + switch ($conf['remarkType']) { + case 'phone': + $remark = $phone . '-' . $conf['task_name']; + break; + case 'nickname': + $remark = ''; + break; + case 'source': + $remark = $conf['task_name']; + break; + default: + $remark = ''; + break; + } } - $tags = []; if (!empty($conf['tags'])) { if (is_array($conf['tags'])) { From e4a8975baa2bc8ab2d5a125c0a568f1561b217db Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 28 Nov 2025 16:44:08 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/scenarios/plan/list/index.tsx | 32 +++++++++++-------- .../plan/new/steps/BasicSettings.tsx | 4 ++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index d6da5a65..59613c07 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -369,13 +369,15 @@ const ScenarioList: React.FC = () => { backFn={() => navigate("/scenarios")} title={scenarioName || ""} right={ - + scenarioId !== "10" ? ( + + ) : null } /> @@ -424,13 +426,15 @@ const ScenarioList: React.FC = () => {
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
- + {scenarioId !== "10" && ( + + )}
) : ( <> diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 76d460c8..6c61b6da 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -242,7 +242,9 @@ const BasicSettings: React.FC = ({
) : (
- {sceneList.map(scene => { + {sceneList + .filter(scene => scene.id !== 10) + .map(scene => { const selected = formData.scenario === scene.id; return (