From 41ffb8a220a1a933d8df3e426d1f69844552d3cb Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 12 May 2026 16:58:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=82=B9=E8=B5=9E=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/自动点赞-自动评论功能开发说明.md | 196 ++++++++++++ .../mobile/workspace/auto-like/list/data.ts | 4 + .../mobile/workspace/auto-like/new/api.ts | 2 +- .../mobile/workspace/auto-like/new/data.ts | 131 +------- .../mobile/workspace/auto-like/new/index.tsx | 117 ++++++- .../mobile/workspace/auto-like/record/api.ts | 2 +- .../mobile/workspace/auto-like/record/data.ts | 33 +- .../workspace/moments-sync/record/api.ts | 2 +- Server/.gitignore | 1 + .../api/controller/FriendTaskController.php | 6 + .../api/controller/WebSocketController.php | 17 +- .../command/WorkbenchAutoLikeCommand.php | 2 +- .../WechatAccountHealthScoreService.php | 3 +- .../service/WorkbenchAutoCommentAiService.php | 147 +++++++++ .../workbench/WorkbenchController.php | 101 +++++- .../cunkebao/validate/Workbench.php | 63 +++- .../application/job/WorkbenchAutoLikeJob.php | 153 ++++++++- .../20260512_workbench_auto_comment.sql | 12 + ...rkbench_auto_like_max_comments_per_day.sql | 2 + Server/docs/付款码获客.md | 291 ++++++++++++++++++ .../Adapters/ChuKeBao/Adapter.php | 112 ++++++- Server/sql.sql | 6 + 22 files changed, 1247 insertions(+), 156 deletions(-) create mode 100644 Cunkebao/docs/自动点赞-自动评论功能开发说明.md create mode 100644 Server/application/common/service/WorkbenchAutoCommentAiService.php create mode 100644 Server/database/migrations/20260512_workbench_auto_comment.sql create mode 100644 Server/database/migrations/20260513_drop_workbench_auto_like_max_comments_per_day.sql create mode 100644 Server/docs/付款码获客.md diff --git a/Cunkebao/docs/自动点赞-自动评论功能开发说明.md b/Cunkebao/docs/自动点赞-自动评论功能开发说明.md new file mode 100644 index 000000000..f78d178b7 --- /dev/null +++ b/Cunkebao/docs/自动点赞-自动评论功能开发说明.md @@ -0,0 +1,196 @@ +# 存客宝 · 自动点赞扩展「自动评论」开发说明 + +## 1. 文档目的 + +在现有「工作台 · 自动点赞」能力上,增加**自动评论**子能力。评论方式分为两种: + +| 模式 | 说明 | +|------|------| +| **固定评论** | 用户预先配置**多条**文案;执行时**随机选取一条**发出。 | +| **AI 评论** | 根据朋友圈正文 / 可见描述等上下文,由大模型生成短评论后再发出。 | + +本文档供前后端与设备协议联调使用,并约定配置字段、接口与任务执行顺序。 + +--- + +## 2. 需求摘要 + +### 2.1 产品硬性约定(本次补充) + +1. **自动评论总开关(必选)** + - 每个自动点赞任务必须提供**独立的「自动评论」开关**(开 / 关)。 + - **关**:不发起任何自动评论逻辑,行为与当前线上「仅自动点赞」一致。 + - **开**:再根据下方「评论模式」走固定评论或 AI 评论。 + - 对应配置字段:`enableAutoComment`(0=关,1=开);前后端表单、列表展示均需体现该开关状态。 + +2. **固定评论:多条配置、随机一条** + - 固定评论模式下,用户可配置**多条**文案(数组,至少 1 条)。 + - 每次对某条朋友圈执行自动评论时,从 `fixedComments` 中**随机抽取一条**作为本次评论内容(均匀随机即可;不要求去重轮播,除非后续产品另定)。 + - 仅 1 条配置时,行为等价于「每次都用该条」。 + +### 2.2 其他需求说明 + +- 自动评论依附于**现有自动点赞工作台**(`type = 1`),在同一任务配置中通过上述**总开关** + **评论模式**管理。 +- **固定评论**:除「多条 + 随机一条」外,仍需校验每条长度(建议与微信单条评论上限对齐,具体以客户端协议为准)。 +- **AI 评论**:用户可编辑「补充提示词」;**默认提示词**(新建任务时表单默认值、后端缺省回填)为: + +``` +1、模仿好友自然语气,简洁走心,贴合朋友圈内容,口语化不生硬,适配日常互动 +2、轻松接地气,简短真诚,不浮夸,适配风景 / 日常 / 自拍等多类朋友圈 +3、可适当在句末加一个微信内置表情(客户端识别的方括号写法),例如:[微笑]、[666]、[强]、[玫瑰]、[愉快],与语气相符即可,最多一个,勿堆砌;不要用一串 Unicode 小黄脸代替 +``` + +- 业务上需明确:**仅评论**、**先赞后评**、**仅赞不评** 的组合策略(见第 5 节建议默认)。 + +--- + +## 3. 现状与代码入口(便于对接) + +### 3.1 存客宝(`F:\karuo\yi-shi\Cunkebao`) + +- 路由:`/workspace/auto-like`、`/workspace/auto-like/new`、`/workspace/auto-like/edit/:id`、`/workspace/auto-like/record/:id` +- 列表 / 详情 API 封装:`src/pages/mobile/workspace/auto-like/list/api.ts`(`/v1/workbench/list`、`/v1/workbench/detail`、`/v1/workbench/create`、`/v1/workbench/update` 等,`type: 1`) +- 任务与表单类型:`src/pages/mobile/workspace/auto-like/new/data.ts` 等处的 `LikeTask`、`CreateLikeTaskData` + +### 3.2 服务端(`F:\karuo\yi-shi\Server`) + +- 配置表:`ck_workbench_auto_like`(模型 `app\cunkebao\model\WorkbenchAutoLike`) +- 执行记录:`ck_workbench_auto_like_item` +- 创建 / 更新配置:`application\cunkebao\controller\workbench\WorkbenchController.php` 中 `TYPE_AUTO_LIKE` 分支 +- 校验:`application\cunkebao\validate\Workbench.php` 中 `type = 1` 相关规则 +- 定时 / 队列任务:`application\job\WorkbenchAutoLikeJob.php`,点赞调用 `WebSocketController::momentInteract()` +- 当前 `momentInteract` 仅下发 `momentInteractType => 1`(点赞),**评论能力需在设备指令层扩展**(见第 6 节) + +--- + +## 4. 配置项设计(建议字段) + +在 `workbench_auto_like` 上扩展存储(**新增列**或 **`json` 扩展字段**二选一,推荐新增列便于查询与默认值): + +| 字段(示例命名) | 类型 | 说明 | +|------------------|------|------| +| `enableAutoComment` | tinyint(1) | **自动评论总开关**:0 关 / 1 开(关则完全不评论) | +| `commentMode` | tinyint(1) | 仅当 `enableAutoComment=1` 时有效:1=固定评论,2=AI 评论 | +| `fixedComments` | json / text | 固定评论文案**数组**(≥1 条);执行时**随机取一条**评论,如 `["真好","赞"]` | +| `aiCommentPrompt` | varchar(2000) 或 text | 用户自定义 AI 提示词;**空则使用产品默认拼接** | +| `commentAfterLike` | tinyint(1) | 可选;1=先赞后评,0=仅评论不强制点赞(与 `enableAutoComment`、原点赞开关组合) | + +**默认行为建议:** + +- `enableAutoComment = 0`:**自动评论总开关关闭**,与线上一致,只点赞、不评论。 +- `enableAutoComment = 1`:必须带有效 `commentMode`;固定模式下 `fixedComments` 至少 1 条,执行时随机一条。 +- `commentMode = 2` 且 `aiCommentPrompt` 为空:服务端保存/返回时回填为第 2 节中的两段默认文案(可存库,也可仅前端默认 + 后端生成时用常量)。 + +--- + +## 5. 任务执行逻辑(`WorkbenchAutoLikeJob`) + +建议流程(可在 PRD 确认后微调): + +1. 沿用现有:时间窗、`maxLikes`、内容类型、好友分页、点赞间隔等。 +2. 对「目标朋友圈」在点赞成功后: + - **仅当 `enableAutoComment = 1`(总开关开)** 且当日已成功评论次数(按 `commentTime` 统计、与点赞同一时段窗口)**未达到 `maxLikes` 所允许的条数**时,才进入评论逻辑(评论与点赞**各自计数**,**上限数值均取配置中的 `maxLikes`**);总开关为关则跳过整段评论。 + - `commentMode = 1`(固定评论):从 `fixedComments` 数组中 **随机选取一条** 作为本次评论文案并下发。 + - `commentMode = 2`(AI 评论):拉取朋友圈文本上下文 → 调用 AI 服务 → 得到短句 → 截断与敏感词过滤(如有)→ 下发评论指令。 +3. 评论与点赞的间隔:建议在 `interval` 之外增加**独立评论间隔**字段 `commentInterval`(秒),避免操作过快触发风控(可选,产品定)。 + +**去重:** 若同一条朋友圈既要赞又要评,`workbench_auto_like_item` 需能区分「已赞」「已评」或增加评论记录表,避免重复评论(见第 7 节)。 + +--- + +## 6. 设备 / WebSocket 协议(必排期) + +当前 `application\api\controller\WebSocketController.php` 中 `momentInteract` 固定为点赞类型。需与**终端 / 协议文档**确认: + +- 朋友圈评论是否仍使用 `CmdMomentInteract`,通过不同 `momentInteractType` 或附加 `commentContent` 等字段区分; +- 或独立命令如 `CmdMomentComment`(以实际协议为准)。 + +**后端交付物:** 新增或扩展方法(例如 `momentComment($snsId, $wechatAccountId, $wechatFriendId, $content)`),由 `WorkbenchAutoLikeJob` 在生成文案后调用。 + +--- + +## 7. 数据与统计 + +### 7.1 记录表扩展 + +任选一种: + +- **方案 A**:在 `ck_workbench_auto_like_item` 增加 `commentContent`、`commentTime`、`commentStatus` 等;同一条动态赞+评可同一行更新或两行(需唯一约束说明)。 +- **方案 B**:新建 `ck_workbench_auto_comment_item`,与 `momentsId` / `snsId` / `workbenchId` 关联,结构更清晰。 + +列表页若需展示「评论内容 / 是否 AI」,需同步扩展 `WorkbenchAutoLikeController` / `WorkbenchController` 的 records 接口字段。 + +### 7.2 统计 + +与现有 `todayLikeCount`、`totalLikeCount` 并列,可增加 `todayCommentCount`、`totalCommentCount`(在 `WorkbenchController` 组装 `config` 时查询)。 + +--- + +## 8. 接口与校验(`WorkbenchController` + `Workbench` 校验器) + +- `create` / `update`(`type = 1`)增加对上述字段的读写。 +- 校验规则示例: + - **`enableAutoComment`**:必填布尔或 0/1;为 1 时下列规则生效。 + - `enableAutoComment = 1` 时:`commentMode` 必填(1 或 2)。 + - `commentMode = 1`:`fixedComments` 为**非空数组**(至少一条文案),每条长度 ≤ 限制;服务端/Job 执行时**随机取一条**。 + - `commentMode = 2`:`aiCommentPrompt` 可为空(空则用默认常量);若限制长度建议 ≥ 500 字以容纳用户扩展。 + +存客宝侧:`CreateLikeTaskData` / `LikeTask` 增加对应 TS 字段;新建/编辑页增加表单项(**自动评论总开关**、模式单选、**固定评论多条录入 + 说明「随机一条」**、AI 提示词多行文本默认值)。 + +--- + +## 9. AI 评论实现建议(服务端) + +- 复用项目内已有 AI 调用模式(例如内容库 `ContentLibraryController::aiRewrite` 或 `application\ai\controller\OpenAi.php` 所走通道),封装 **`generateMomentComment(context, userPrompt)`**: + - **System / 前置说明**:强调输出**一条**短评论、无引号包裹、无多余解释。 + - **User 内容**:拼接「朋友圈正文/摘要」+ 默认两段提示 + 用户自定义 `aiCommentPrompt`。 +- 失败策略:记录日志;可选降级为「不发评论」或「随机一条固定兜底文案」(产品定)。 + +--- + +## 10. 存客宝前端交互要点 + +- 在「新建/编辑自动点赞」页增加区块:**自动评论**,顶部为**总开关**(关则隐藏或禁用下方模式与文案配置,提交时 `enableAutoComment=0`)。 +- 总开关**开**时展示子选项: + - **固定评论**:支持录入**多条**文案(列表、多标签或换行拆分等交互任选);文案旁或区块说明:**「每次评论随机抽取其中一条」**。 + - **AI 评论**:多行 `TextArea`,占位符或默认值展示第 2 节两段文案。 +- 列表 / 记录页:如需区分展示,可增加「评论数」或记录列(依赖接口)。 + +--- + +## 11. 测试清单(提测用) + +- [ ] 仅点赞:老数据无新字段或**自动评论总开关关**时,行为与线上一致。 +- [ ] **总开关**:关不评论;开才评论。 +- [ ] 固定评论:**多条配置**、多次执行观察为**随机一条**、单条边界长度、仅文字朋友圈 / 带图朋友圈。 +- [ ] AI 评论:默认提示词、自定义提示词、AI 超时 / 失败。 +- [ ] 当日评论次数达到 `maxLikes` 后不再发评论(与点赞各自计数、上限数值均为 `maxLikes`);点赞是否仍执行(按产品设计)。 +- [ ] 先赞后评顺序与间隔;重复调度不产生重复评论。 +- [ ] 多端设备协议联调通过。 + +--- + +## 12. 交付物清单 + +| 层级 | 交付物 | +|------|--------| +| DB | `workbench_auto_like`(及可选新表)迁移 SQL | +| Server | `WorkbenchController`、`WorkbenchValidate`、`WorkbenchAutoLikeJob`、`WebSocketController`(或新评论方法)、AI 封装 | +| Cunkebao | `auto-like` 表单/类型/API 字段、文案与校验提示 | +| 联调 | 设备侧评论指令说明与抓包用例 | + +--- + +## 13. 附录:默认 AI 提示词(常量) + +以下为产品约定默认值,建议定义为后端常量 + 前端表单初始值,保持完全一致: + +``` +1、模仿好友自然语气,简洁走心,贴合朋友圈内容,口语化不生硬,适配日常互动 +2、轻松接地气,简短真诚,不浮夸,适配风景 / 日常 / 自拍等多类朋友圈 +3、可适当在句末加一个微信内置表情(客户端识别的方括号写法),例如:[微笑]、[666]、[强]、[玫瑰]、[愉快],与语气相符即可,最多一个,勿堆砌;不要用一串 Unicode 小黄脸代替 +``` + +--- + +*文档版本:v1.2 · 与仓库路径 `Cunkebao`、`Server` 当前结构对应,实施时以当时代码为准微调字段命名与表结构。* diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/list/data.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/list/data.ts index de39bd280..89386ea9a 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/list/data.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/list/data.ts @@ -65,7 +65,11 @@ export interface LikeTask { enableFriendTags: boolean; todayLikeCount: number; totalLikeCount: number; + todayCommentCount?: number; + totalCommentCount?: number; updateTime: string; + planType?: number; + config?: Record; } // 创建任务数据 diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/api.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/new/api.ts index 785a631a4..891503411 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/api.ts @@ -3,7 +3,7 @@ import { CreateLikeTaskData, UpdateLikeTaskData, LikeTask, -} from "@/pages/workspace/auto-like/record/data"; +} from "@/pages/mobile/workspace/auto-like/record/data"; // 获取自动点赞任务详情 export function fetchAutoLikeTaskDetail(id: string): Promise { diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts index f8cea3c40..df0b62539 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/data.ts @@ -1,130 +1 @@ -import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; -import { FriendSelectionItem } from "@/components/FriendSelection/data"; - -// 自动点赞任务状态 -export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 - -// 内容类型 -export type ContentType = "text" | "image" | "video" | "link"; - -// 设备信息 -export interface Device { - id: string; - name: string; - status: "online" | "offline"; - lastActive: string; -} - -// 好友信息 -export interface Friend { - id: string; - nickname: string; - wechatId: string; - avatar: string; - tags: string[]; - region: string; - source: string; -} - -// 点赞记录 -export interface LikeRecord { - id: string; - workbenchId: string; - momentsId: string; - snsId: string; - wechatAccountId: string; - wechatFriendId: string; - likeTime: string; - content: string; - resUrls: string[]; - momentTime: string; - userName: string; - operatorName: string; - operatorAvatar: string; - friendName: string; - friendAvatar: string; -} - -// 自动点赞任务 -export interface LikeTask { - id: string; - name: string; - status: LikeTaskStatus; - deviceCount: number; - targetGroup: string; - likeCount: number; - lastLikeTime: string; - createTime: string; - creator: string; - likeInterval: number; - maxLikesPerDay: number; - timeRange: { start: string; end: string }; - contentTypes: ContentType[]; - targetTags: string[]; - devices: string[]; - friends: string[]; - friendMaxLikes: number; - friendTags: string; - enableFriendTags: boolean; - todayLikeCount: number; - totalLikeCount: number; - updateTime: string; - // 计划类型:0-全局计划,1-独立计划 - planType?: number; -} - -// 创建任务数据 -export interface CreateLikeTaskData { - name: string; - interval: number; - maxLikes: number; - startTime: string; - endTime: string; - contentTypes: ContentType[]; - deviceGroups: number[]; - deviceGroupsOptions: DeviceSelectionItem[]; - // 实际使用的好友字段(兼容旧字段) - wechatFriends?: number[]; - wechatFriendsOptions?: FriendSelectionItem[]; - friendMaxLikes: number; - friendTags?: string; - enableFriendTags: boolean; - targetTags: string[]; - // 计划类型:0-全局计划,1-独立计划 - planType?: number; - [key: string]: any; -} - -// 更新任务数据 -export interface UpdateLikeTaskData extends CreateLikeTaskData { - id: string; -} - -// 任务配置 -export interface TaskConfig { - interval: number; - maxLikes: number; - startTime: string; - endTime: string; - contentTypes: ContentType[]; - devices: string[]; - friends: string[]; - friendMaxLikes: number; - friendTags: string; - enableFriendTags: boolean; -} - -// API响应类型 -export interface ApiResponse { - code: number; - msg: string; - data: T; -} - -// 分页响应类型 -export interface PaginatedResponse { - list: T[]; - total: number; - page: number; - limit: number; -} +export * from "../record/data"; diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx index 0a94706c9..9e516fe45 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx @@ -12,7 +12,12 @@ import { updateAutoLikeTask, fetchAutoLikeTaskDetail, } from "./api"; -import { CreateLikeTaskData, ContentType } from "./data"; +import { + CreateLikeTaskData, + ContentType, + AUTO_COMMENT_DEFAULT_AI_PROMPT, + CommentMode, +} from "./data"; import { useUserStore } from "@/store/module/user"; import style from "./new.module.scss"; @@ -56,6 +61,10 @@ const NewAutoLike: React.FC = () => { friendMaxLikes: 10, enableFriendTags: false, friendTags: "", + enableAutoComment: false, + commentMode: 1, + fixedCommentsText: "", + aiCommentPrompt: AUTO_COMMENT_DEFAULT_AI_PROMPT, }); useEffect(() => { @@ -86,6 +95,15 @@ const NewAutoLike: React.FC = () => { friendMaxLikes: config.friendMaxLikes || 10, enableFriendTags: config.enableFriendTags || false, friendTags: config.friendTags || "", + enableAutoComment: !!config.enableAutoComment, + commentMode: (config.commentMode === 2 ? 2 : 1) as CommentMode, + fixedCommentsText: Array.isArray(config.fixedComments) + ? config.fixedComments.filter(Boolean).join("\n") + : "", + aiCommentPrompt: + config.aiCommentPrompt && String(config.aiCommentPrompt).trim() + ? String(config.aiCommentPrompt) + : AUTO_COMMENT_DEFAULT_AI_PROMPT, }); setAutoEnabled( (taskDetail as any).status === 1 || @@ -135,14 +153,33 @@ const NewAutoLike: React.FC = () => { message.warning("请选择执行设备"); return; } + if (formData.enableAutoComment && formData.commentMode === 1) { + const lines = (formData.fixedCommentsText || "") + .split(/\n/) + .map(s => s.trim()) + .filter(Boolean); + if (lines.length < 1) { + message.warning("固定评论模式请至少填写一行文案(一行一条,随机抽取)"); + return; + } + } setIsSubmitting(true); try { - // 如果选择了全部好友,提交时传空数组或特殊标识 + const fixedLines = (formData.fixedCommentsText || "") + .split(/\n/) + .map(s => s.trim()) + .filter(Boolean); + const { fixedCommentsText: _omitText, ...formRest } = formData; const submitData = { - ...formData, + ...formRest, wechatFriends: selectAllFriends ? [] : formData.wechatFriends, wechatFriendsOptions: selectAllFriends ? [] : formData.wechatFriendsOptions, - selectAllFriends: selectAllFriends, // 添加标识字段 + selectAllFriends: selectAllFriends, + status: autoEnabled ? 1 : 0, + enableAutoComment: formData.enableAutoComment ? 1 : 0, + commentMode: formData.commentMode ?? 1, + fixedComments: fixedLines, + aiCommentPrompt: formData.aiCommentPrompt || "", }; if (isEditMode) { @@ -349,6 +386,78 @@ const NewAutoLike: React.FC = () => { + {/* 自动评论 */} +
+
+
+ 自动评论 + + handleUpdateFormData({ enableAutoComment: checked }) + } + className={style.switch} + /> +
+
+ 关闭则仅点赞;开启后点赞成功再发评论(受每日上限约束) +
+
+ {formData.enableAutoComment && ( + <> +
+
评论方式
+ + handleUpdateFormData({ + commentMode: e.target.value as CommentMode, + }) + } + className={style.radioGroup} + > + 固定评论(多条随机一条) + AI 评论 + +
+ {formData.commentMode === 1 && ( +
+
固定评论文案
+ + handleUpdateFormData({ + fixedCommentsText: e.target.value, + }) + } + className={style.input} + /> +
+ )} + {formData.commentMode === 2 && ( +
+
AI 补充提示词
+ + handleUpdateFormData({ + aiCommentPrompt: e.target.value, + }) + } + className={style.input} + /> +
+ 已内置默认语气与风格说明,可按需补充 +
+
+ )} + + )} +
+ {/* 好友标签和自动开启 */}
diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/record/api.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/record/api.ts index 6142fde86..c7856ba6b 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/record/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/record/api.ts @@ -5,7 +5,7 @@ import { UpdateLikeTaskData, LikeRecord, PaginatedResponse, -} from "@/pages/workspace/auto-like/record/data"; +} from "@/pages/mobile/workspace/auto-like/record/data"; // 获取自动点赞任务列表 export function fetchAutoLikeTasks( diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/record/data.ts b/Cunkebao/src/pages/mobile/workspace/auto-like/record/data.ts index de39bd280..ec47d9d50 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/record/data.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/record/data.ts @@ -1,9 +1,21 @@ +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; + // 自动点赞任务状态 export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 // 内容类型 export type ContentType = "text" | "image" | "video" | "link"; +/** 自动评论:1 固定多条随机一条,2 AI */ +export type CommentMode = 1 | 2; + +/** AI 评论默认提示词(与服务端 WorkbenchAutoCommentAiService::defaultUserPrompt 一致) */ +export const AUTO_COMMENT_DEFAULT_AI_PROMPT = + "1、模仿好友自然语气,简洁走心,贴合朋友圈内容,口语化不生硬,适配日常互动\n" + + "2、轻松接地气,简短真诚,不浮夸,适配风景 / 日常 / 自拍等多类朋友圈\n" + + "3、可适当在句末加一个微信内置表情(客户端识别的方括号写法),例如:[微笑]、[666]、[强]、[玫瑰]、[愉快],与语气相符即可,最多一个,勿堆砌;不要用一串 Unicode 小黄脸代替"; + // 设备信息 export interface Device { id: string; @@ -65,7 +77,11 @@ export interface LikeTask { enableFriendTags: boolean; todayLikeCount: number; totalLikeCount: number; + todayCommentCount?: number; + totalCommentCount?: number; updateTime: string; + planType?: number; + config?: Record; } // 创建任务数据 @@ -76,12 +92,25 @@ export interface CreateLikeTaskData { startTime: string; endTime: string; contentTypes: ContentType[]; - devices: string[]; - friends?: string[]; + deviceGroups: number[]; + deviceGroupsOptions: DeviceSelectionItem[]; + wechatFriends?: number[]; + wechatFriendsOptions?: FriendSelectionItem[]; friendMaxLikes: number; friendTags?: string; enableFriendTags: boolean; targetTags: string[]; + planType?: number; + /** 自动评论总开关(表单 boolean,提交可为 0/1) */ + enableAutoComment?: boolean | number; + /** 1 固定随机 2 AI */ + commentMode?: CommentMode; + /** 提交给后端的固定文案数组(可由 fixedCommentsText 转换) */ + fixedComments?: string[]; + /** 表单:多行固定评论,一行一条 */ + fixedCommentsText?: string; + aiCommentPrompt?: string; + [key: string]: any; } // 更新任务数据 diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts index 1a131cbcf..cd4fb3a8c 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts @@ -5,7 +5,7 @@ import { UpdateLikeTaskData, LikeRecord, PaginatedResponse, -} from "@/pages/workspace/auto-like/record/data"; +} from "@/pages/mobile/workspace/auto-like/record/data"; // 获取自动点赞任务列表 export function fetchAutoLikeTasks( diff --git a/Server/.gitignore b/Server/.gitignore index 1047f5bc5..67bbdc2a5 100644 --- a/Server/.gitignore +++ b/Server/.gitignore @@ -17,3 +17,4 @@ nginx.htaccess thinkphp/ public/static/ *.log +Server.code-workspace diff --git a/Server/application/api/controller/FriendTaskController.php b/Server/application/api/controller/FriendTaskController.php index 8844ec71b..7400e22fb 100644 --- a/Server/application/api/controller/FriendTaskController.php +++ b/Server/application/api/controller/FriendTaskController.php @@ -188,6 +188,12 @@ class FriendTaskController extends BaseController $data['memo'] = '操作过于频繁,请稍后再试'; } + if (strpos('登录过期,请重新登录', $item['extra']) !== false){ + $data['reason'] = '登录过期'; + $data['memo'] = '登录过期,请重新登录'; + } + + if (strpos('当前账号存在安全风险', $item['extra']) !== false){ $data['reason'] = '账号风险'; $data['memo'] = '当前账号存在安全风险,需先到「微信团队」进行安全验证后才能继续使用当前功能'; diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index bbc954263..df3f380c0 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -349,7 +349,11 @@ class WebSocketController extends BaseController $snsId = !empty($data['snsId']) ? $data['snsId'] : ''; $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0; - + $momentInteractType = isset($data['momentInteractType']) ? intval($data['momentInteractType']) : 1; + if (!in_array($momentInteractType, [1, 2], true)) { + $momentInteractType = 1; + } + $sendWord = isset($data['sendWord']) ? (string)$data['sendWord'] : ''; //过滤消息 if (empty($snsId)) { @@ -358,19 +362,26 @@ class WebSocketController extends BaseController if (empty($wechatAccountId)) { return json_encode(['code' => 400, 'msg' => '微信id不能为空']); } + if ($momentInteractType === 2 && $sendWord === '') { + return json_encode(['code' => 400, 'msg' => '评论内容不能为空']); + } try { $result = [ "cmdType" => "CmdMomentInteract", - "momentInteractType" => 1, + "momentInteractType" => $momentInteractType, "seq" => time(), "snsId" => $snsId, "wechatAccountId" => $wechatAccountId, "wechatFriendId" => $wechatFriendId, ]; + if ($momentInteractType === 2) { + $result['sendWord'] = $sendWord; + } $message = $this->sendMessage($result); - return json_encode(['code' => 200, 'msg' => '点赞成功', 'data' => $message]); + $okMsg = $momentInteractType === 2 ? '评论成功' : '点赞成功'; + return json_encode(['code' => 200, 'msg' => $okMsg, 'data' => $message]); } catch (\Exception $e) { return json_encode(['code' => 500, 'msg' => $e->getMessage()]); } diff --git a/Server/application/command/WorkbenchAutoLikeCommand.php b/Server/application/command/WorkbenchAutoLikeCommand.php index 0d6e78518..878ee8646 100644 --- a/Server/application/command/WorkbenchAutoLikeCommand.php +++ b/Server/application/command/WorkbenchAutoLikeCommand.php @@ -35,7 +35,7 @@ class WorkbenchAutoLikeCommand extends Command // 检查队列是否已经在运行 $queueLockKey = "queue_lock:{$this->queueName}"; - //Cache::rm($queueLockKey); + Cache::rm($queueLockKey); if (Cache::get($queueLockKey)) { $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index 22f82e4e2..ffe9e239f 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -770,7 +770,8 @@ class WechatAccountHealthScoreService ->where(function($query) { // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON) $query->where('extra', 'like', '%操作过于频繁%') - ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); + ->whereOr('extra', 'like', '%"当前账号存在安全风险"%') + ->whereOr('extra', 'like', '%"登录过期,请重新登录"%'); }) ->where(function($query) { // 只查询未被统计过的记录 diff --git a/Server/application/common/service/WorkbenchAutoCommentAiService.php b/Server/application/common/service/WorkbenchAutoCommentAiService.php new file mode 100644 index 000000000..7af2935a7 --- /dev/null +++ b/Server/application/common/service/WorkbenchAutoCommentAiService.php @@ -0,0 +1,147 @@ + $model, + 'messages' => [ + ['role' => 'system', 'content' => '你是微信好友,用中文写朋友圈评论;可适当在句尾加一个微信方括号表情(如[微笑][强]),勿冗长。'], + ['role' => 'user', 'content' => $userContent], + ], + ]; + } + + /** + * @return array|null 解码后的 JSON,请求失败为 null + */ + protected static function requestChatCompletion(string $url, string $apiKey, array $body): ?array + { + $headerData = ['Authorization:Bearer ' . $apiKey]; + $header = setHeader($headerData); + $raw = requestCurl($url, $body, 'POST', $header, 'json'); + if ($raw === '' || $raw === false) { + return null; + } + $decoded = json_decode($raw, true); + return is_array($decoded) ? $decoded : null; + } + + /** + * @param array|null $result + * @return string|false + */ + protected static function contentFromChatResponse(?array $result) + { + if ($result === null) { + return false; + } + if (!empty($result['error'])) { + return false; + } + if (!empty($result['choices'][0]['message']['content'])) { + $text = trim((string) $result['choices'][0]['message']['content']); + return self::truncateComment($text); + } + return false; + } + + protected static function truncateComment(string $text) + { + $text = preg_replace('/\s+/u', ' ', $text); + if (function_exists('mb_substr')) { + $text = mb_substr($text, 0, 200); + } else { + $text = substr($text, 0, 200); + } + return $text !== '' ? $text : false; + } + + /** + * 豆包(与 ContentLibraryController::aiRewrite 相同通道) + */ + protected static function generateDoubao(string $userContent) + { + $url = Env::get('doubaoAi.api_url', ''); + $apiKey = Env::get('doubaoAi.api_key', ''); + $model = Env::get('doubaoAi.model', 'doubao-1-5-pro-32k-250115'); + if (empty($apiKey) || empty($url)) { + return false; + } + + $body = self::chatCompletionBody($model, $userContent); + $result = self::requestChatCompletion($url, $apiKey, $body); + return self::contentFromChatResponse($result); + } +} diff --git a/Server/application/cunkebao/controller/workbench/WorkbenchController.php b/Server/application/cunkebao/controller/workbench/WorkbenchController.php index 2be3a14bc..e03a44654 100644 --- a/Server/application/cunkebao/controller/workbench/WorkbenchController.php +++ b/Server/application/cunkebao/controller/workbench/WorkbenchController.php @@ -104,6 +104,9 @@ class WorkbenchController extends Controller $config->friendMaxLikes = $param['friendMaxLikes']; $config->friendTags = $param['friendTags']; $config->enableFriendTags = $param['enableFriendTags']; + foreach ($this->normalizeWorkbenchAutoLikeCommentPayload($param) as $k => $v) { + $config->$k = $v; + } $config->createTime = time(); $config->updateTime = time(); $config->save(); @@ -272,7 +275,7 @@ class WorkbenchController extends Controller // 定义关联关系 $with = [ 'autoLike' => function ($query) { - $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags,enableAutoComment,commentMode,fixedComments,aiCommentPrompt'); }, 'momentsSync' => function ($query) { $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); @@ -309,7 +312,13 @@ class WorkbenchController extends Controller $item->config->devices = json_decode($item->config->devices, true); $item->config->contentTypes = json_decode($item->config->contentTypes, true); $item->config->friends = json_decode($item->config->friends, true); - + if (!empty($item->config->fixedComments)) { + $item->config->fixedComments = json_decode($item->config->fixedComments, true) ?: []; + } else { + $item->config->fixedComments = []; + } + $item->config->enableAutoComment = isset($item->config->enableAutoComment) ? intval($item->config->enableAutoComment) : 0; + $item->config->commentMode = isset($item->config->commentMode) ? intval($item->config->commentMode) : 1; // 添加今日点赞数 $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); @@ -323,8 +332,20 @@ class WorkbenchController extends Controller ->where('workbenchId', $item->id) ->count(); + $todayCommentCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->where('commentTime', '>', 0) + ->whereTime('commentTime', 'between', [$startTime, $endTime]) + ->count(); + $totalCommentCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->where('commentTime', '>', 0) + ->count(); + $item->config->todayLikeCount = $todayLikeCount; $item->config->totalLikeCount = $totalLikeCount; + $item->config->todayCommentCount = $todayCommentCount; + $item->config->totalCommentCount = $totalCommentCount; } unset($item->autoLike, $item->auto_like); break; @@ -536,7 +557,7 @@ class WorkbenchController extends Controller // 定义关联关系 $with = [ 'autoLike' => function ($query) { - $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags'); + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags,enableAutoComment,commentMode,fixedComments,aiCommentPrompt'); }, 'momentsSync' => function ($query) { $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); @@ -588,7 +609,13 @@ class WorkbenchController extends Controller $workbench->config->targetType = 2; //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true); $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true); - + if (!empty($workbench->config->fixedComments)) { + $workbench->config->fixedComments = json_decode($workbench->config->fixedComments, true) ?: []; + } else { + $workbench->config->fixedComments = []; + } + $workbench->config->enableAutoComment = isset($workbench->config->enableAutoComment) ? intval($workbench->config->enableAutoComment) : 0; + $workbench->config->commentMode = isset($workbench->config->commentMode) ? intval($workbench->config->commentMode) : 1; // 添加今日点赞数 $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); @@ -605,6 +632,18 @@ class WorkbenchController extends Controller $workbench->config->todayLikeCount = $todayLikeCount; $workbench->config->totalLikeCount = $totalLikeCount; + $todayCommentCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->where('commentTime', '>', 0) + ->whereTime('commentTime', 'between', [$startTime, $endTime]) + ->count(); + $totalCommentCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->where('commentTime', '>', 0) + ->count(); + $workbench->config->todayCommentCount = $todayCommentCount; + $workbench->config->totalCommentCount = $totalCommentCount; + unset($workbench->autoLike, $workbench->auto_like); } break; @@ -1214,6 +1253,9 @@ class WorkbenchController extends Controller $config->friendMaxLikes = $param['friendMaxLikes']; $config->friendTags = $param['friendTags']; $config->enableFriendTags = $param['enableFriendTags']; + foreach ($this->normalizeWorkbenchAutoLikeCommentPayload($param) as $k => $v) { + $config->$k = $v; + } $config->updateTime = time(); $config->save(); } @@ -1488,6 +1530,13 @@ class WorkbenchController extends Controller $newConfig->contentTypes = $config->contentTypes; $newConfig->devices = $config->devices; $newConfig->friends = $config->friends; + $newConfig->friendMaxLikes = $config->friendMaxLikes ?? null; + $newConfig->friendTags = $config->friendTags ?? null; + $newConfig->enableFriendTags = isset($config->enableFriendTags) ? intval($config->enableFriendTags) : 0; + $newConfig->enableAutoComment = isset($config->enableAutoComment) ? intval($config->enableAutoComment) : 0; + $newConfig->commentMode = isset($config->commentMode) ? intval($config->commentMode) : 1; + $newConfig->fixedComments = $config->fixedComments ?? '[]'; + $newConfig->aiCommentPrompt = $config->aiCommentPrompt ?? ''; $newConfig->createTime = time(); $newConfig->updateTime = time(); $newConfig->save(); @@ -4198,4 +4247,48 @@ class WorkbenchController extends Controller } + /** + * 自动点赞:自动评论相关字段写入前归一化 + * @return array + */ + protected function normalizeWorkbenchAutoLikeCommentPayload(array $param) + { + $enable = !empty($param['enableAutoComment']) ? 1 : 0; + $commentMode = isset($param['commentMode']) ? intval($param['commentMode']) : 1; + if (!in_array($commentMode, [1, 2], true)) { + $commentMode = 1; + } + $fixed = []; + if (!empty($param['fixedComments'])) { + if (is_array($param['fixedComments'])) { + foreach ($param['fixedComments'] as $line) { + $line = is_string($line) ? trim($line) : ''; + if ($line !== '') { + $fixed[] = function_exists('mb_substr') ? mb_substr($line, 0, 500) : substr($line, 0, 500); + } + } + } elseif (is_string($param['fixedComments'])) { + foreach (preg_split('/\r\n|\r|\n/', $param['fixedComments']) as $line) { + $line = trim($line); + if ($line !== '') { + $fixed[] = function_exists('mb_substr') ? mb_substr($line, 0, 500) : substr($line, 0, 500); + } + } + } + } + $ai = isset($param['aiCommentPrompt']) ? trim((string)$param['aiCommentPrompt']) : ''; + if (function_exists('mb_strlen') && mb_strlen($ai) > 2000) { + $ai = mb_substr($ai, 0, 2000); + } elseif (strlen($ai) > 2000) { + $ai = substr($ai, 0, 2000); + } + + return [ + 'enableAutoComment' => $enable, + 'commentMode' => $commentMode, + 'fixedComments' => json_encode($fixed, JSON_UNESCAPED_UNICODE), + 'aiCommentPrompt' => $ai, + ]; + } + } \ No newline at end of file diff --git a/Server/application/cunkebao/validate/Workbench.php b/Server/application/cunkebao/validate/Workbench.php index 0ba19d786..862c632a8 100644 --- a/Server/application/cunkebao/validate/Workbench.php +++ b/Server/application/cunkebao/validate/Workbench.php @@ -28,6 +28,9 @@ class Workbench extends Validate 'startTime' => 'requireIf:type,1|dateFormat:H:i', 'endTime' => 'requireIf:type,1|dateFormat:H:i', 'contentTypes' => 'requireIf:type,1|array|contentTypeEnum:text,image,video', + 'enableAutoComment' => 'checkAutoLikeEnableComment', + 'commentMode' => 'checkAutoLikeCommentMode', + 'aiCommentPrompt' => 'max:2000', //'targetGroups' => 'requireIf:type,1|array', // 朋友圈同步特有参数 //'syncInterval' => 'requireIf:type,2|number|min:1', @@ -98,6 +101,9 @@ class Workbench extends Validate 'contentTypes.requireIf' => '请选择点赞内容类型', 'contentTypes.array' => '点赞内容类型必须是数组', 'contentTypes.contentTypeEnum' => '点赞内容类型只能是text、image、video', + 'enableAutoComment.checkAutoLikeEnableComment' => '自动评论开关参数错误', + 'commentMode.checkAutoLikeCommentMode' => '开启自动评论时请选择评论方式,固定模式需至少一条文案', + 'aiCommentPrompt.max' => 'AI补充提示词最多2000个字符', // 朋友圈同步相关提示 /* 'syncInterval.requireIf' => '请设置同步间隔', 'syncInterval.number' => '同步间隔必须为数字', @@ -187,6 +193,7 @@ class Workbench extends Validate protected $scene = [ 'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', + 'enableAutoComment', 'commentMode', 'fixedComments', 'aiCommentPrompt', 'syncCount', 'syncType', 'accountGroups', 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', @@ -197,6 +204,7 @@ class Workbench extends Validate 'update_status' => ['id', 'status'], 'update' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', + 'enableAutoComment', 'commentMode', 'fixedComments', 'aiCommentPrompt', 'syncCount', 'syncType', 'accountGroups', 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', @@ -419,4 +427,57 @@ class Workbench extends Validate } return true; } -} \ No newline at end of file + + /** + * 自动点赞:enableAutoComment 合法值(未传视为 0) + */ + protected function checkAutoLikeEnableComment($value, $rule, $data) + { + if (!isset($data['type']) || intval($data['type']) !== self::TYPE_AUTO_LIKE) { + return true; + } + if (!array_key_exists('enableAutoComment', $data)) { + return true; + } + return in_array(intval($value), [0, 1], true); + } + + /** + * 自动点赞:开启评论时校验模式与固定文案 + */ + protected function checkAutoLikeCommentMode($value, $rule, $data) + { + if (!isset($data['type']) || intval($data['type']) !== self::TYPE_AUTO_LIKE) { + return true; + } + $en = isset($data['enableAutoComment']) ? intval($data['enableAutoComment']) : 0; + if (!$en) { + return true; + } + $mode = isset($data['commentMode']) ? intval($data['commentMode']) : (isset($value) ? intval($value) : 0); + if (!in_array($mode, [1, 2], true)) { + return false; + } + if ($mode === 1) { + $fixed = $data['fixedComments'] ?? null; + $count = 0; + if (is_array($fixed)) { + foreach ($fixed as $s) { + if (is_string($s) && trim($s) !== '') { + $count++; + } + } + } elseif (is_string($fixed) && trim($fixed) !== '') { + foreach (preg_split('/\r\n|\r|\n/', $fixed) as $s) { + if (trim($s) !== '') { + $count++; + } + } + } + if ($count < 1) { + return false; + } + } + return true; + } +} diff --git a/Server/application/job/WorkbenchAutoLikeJob.php b/Server/application/job/WorkbenchAutoLikeJob.php index 11c8d84c6..fba463bcd 100644 --- a/Server/application/job/WorkbenchAutoLikeJob.php +++ b/Server/application/job/WorkbenchAutoLikeJob.php @@ -14,6 +14,7 @@ use think\Db; use app\api\controller\WebSocketController; use app\api\controller\AutomaticAssign; use app\api\controller\WechatFriendController; +use app\common\service\WorkbenchAutoCommentAiService; class WorkbenchAutoLikeJob { @@ -32,6 +33,7 @@ class WorkbenchAutoLikeJob { $jobId = $data['jobId'] ?? ''; $queueLockKey = $data['queueLockKey'] ?? ''; + try { $this->logJobStart($jobId, $queueLockKey); $workbenches = $this->getActiveWorkbenches(); @@ -55,7 +57,8 @@ class WorkbenchAutoLikeJob protected function getActiveWorkbenches() { return Workbench::where([ - ['status', '=', 1], + ['id', '=', 386], + //['status', '=', 1], ['isDel', '=', 0], ['type', '=', 1] // 只获取自动点赞类型的工作台 ])->order('id DESC')->select(); @@ -121,7 +124,6 @@ class WorkbenchAutoLikeJob protected function processAllFriends($workbench, $config, $page = 1, $pageSize = 100) { $friendList = $this->getFriendList($config, $page, $pageSize); - if (empty($friendList)) { return; } @@ -283,7 +285,7 @@ class WorkbenchAutoLikeJob ['wali.id', 'null', null] ]) ->where('wm.update_time', '>=', time() - 86400) - ->field('wm.id, wm.snsId') + ->field('wm.id, wm.snsId, wm.content') ->group('wali.wechatFriendId') ->order('wm.createTime DESC') ->page(1,1) @@ -304,17 +306,21 @@ class WorkbenchAutoLikeJob $result = $webSocket->momentInteract([ 'snsId' => $moment['snsId'], 'wechatAccountId' => $friend['wechatAccountId'], + 'wechatFriendId' => $friend['friendId'], + 'momentInteractType' => 1, ]); $result = json_decode($result, true); if ($result['code'] == 200) { - $this->recordLike($workbench, $moment, $friend); + $itemId = $this->recordLike($workbench, $moment, $friend); // 添加间隔时间 if (!empty($config['interval'])) { sleep($config['interval']); } + + $this->tryAutoCommentAfterLike($workbench, $config, $friend, $moment, $webSocket, $itemId); } else { Log::error("工作台 {$workbench->id} 点赞失败: " . ($result['msg'] ?? '未知错误')); } @@ -323,15 +329,135 @@ class WorkbenchAutoLikeJob } } + /** + * 点赞成功后尝试自动评论(受总开关、每日上限、模式约束) + * @param int|false $itemId workbench_auto_like_item 主键 + * @return bool 是否已成功发出评论 + */ + protected function tryAutoCommentAfterLike($workbench, $config, $friend, $moment, $webSocket, $itemId) + { + if (empty($config['enableAutoComment'])) { + return false; + } + if (empty($itemId)) { + return false; + } + + $cap = $this->getCommentDailyCap($config); + if ($cap <= 0) { + return false; + } + $todayComments = $this->getTodayCommentCount($workbench, $config, $friend['deviceId']); + if ($todayComments >= $cap) { + Log::info("工作台 {$workbench->id} 今日自动评论已达上限 {$cap}"); + return false; + } + + $mode = intval($config['commentMode'] ?? 1); + $text = ''; + if ($mode === 1) { + $text = $this->pickFixedCommentText($config); + } elseif ($mode === 2) { + $momentContent = isset($moment['content']) ? (string)$moment['content'] : ''; + $extra = isset($config['aiCommentPrompt']) ? trim((string)$config['aiCommentPrompt']) : ''; + $generated = WorkbenchAutoCommentAiService::generate($momentContent, $extra); + if ($generated === false || $generated === '') { + Log::error("工作台 {$workbench->id} AI评论生成失败或为空"); + return false; + } + $text = $generated; + } else { + return false; + } + + if ($text === '') { + return false; + } + + try { + $raw = $webSocket->momentInteract([ + 'snsId' => $moment['snsId'], + 'wechatAccountId' => $friend['wechatAccountId'], + 'wechatFriendId' => $friend['friendId'], + 'momentInteractType' => 2, + 'sendWord' => $text, + ]); + $decoded = json_decode($raw, true); + if (!empty($decoded['code']) && (int)$decoded['code'] === 200) { + $this->markItemCommented($itemId, $text); + Log::info("工作台 {$workbench->id} 评论成功: {$moment['snsId']}"); + if (!empty($config['interval'])) { + sleep($config['interval']); + } + return true; + } + Log::error("工作台 {$workbench->id} 评论失败: " . ($decoded['msg'] ?? '未知错误')); + } catch (\Exception $e) { + Log::error("工作台 {$workbench->id} 评论异常: " . $e->getMessage()); + } + return false; + } + + /** + * 每日评论次数上限:与「每日最大点赞数」maxLikes 相同(分别计数) + */ + protected function getCommentDailyCap($config) + { + return intval($config['maxLikes'] ?? 0); + } + + /** + * 从固定文案列表随机取一条 + */ + protected function pickFixedCommentText($config) + { + $raw = $config['fixedComments'] ?? '[]'; + if (is_array($raw)) { + $list = $raw; + } else { + $list = json_decode((string)$raw, true) ?: []; + } + $list = array_values(array_filter($list, function ($s) { + return is_string($s) && trim($s) !== ''; + })); + if (count($list) === 0) { + return ''; + } + $idx = random_int(0, count($list) - 1); + $line = trim($list[$idx]); + if (function_exists('mb_substr')) { + return mb_substr($line, 0, 500); + } + return substr($line, 0, 500); + } + + /** + * @param int $itemId + */ + protected function markItemCommented($itemId, $text) + { + $content = $text; + if (function_exists('mb_substr')) { + $content = mb_substr($content, 0, 500); + } else { + $content = substr($content, 0, 500); + } + Db::name('workbench_auto_like_item')->where('id', $itemId)->update([ + 'commentContent' => $content, + 'commentTime' => time(), + ]); + } + /** * 记录点赞 * @param Workbench $workbench * @param array $moment * @param array $friend + * @return int|false 新增记录主键 */ protected function recordLike($workbench, $moment, $friend) { - Db::name('workbench_auto_like_item')->insert([ + $id = Db::name('workbench_auto_like_item')->insertGetId([ 'workbenchId' => $workbench->id, 'deviceId' => $friend['deviceId'], 'momentsId' => $moment['id'], @@ -341,6 +467,7 @@ class WorkbenchAutoLikeJob 'createTime' => time() ]); Log::info("工作台 {$workbench->id} 点赞成功: {$moment['snsId']}"); + return $id; } /** @@ -404,6 +531,22 @@ class WorkbenchAutoLikeJob ->count(); } + /** + * 当日已成功评论次数(按配置时段与 commentTime) + */ + protected function getTodayCommentCount($workbench, $config, $deviceId) + { + return Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->where('deviceId', $deviceId) + ->where('commentTime', '>', 0) + ->whereTime('commentTime', 'between', [ + strtotime(date('Y-m-d') . ' ' . $config['startTime'] . ':00'), + strtotime(date('Y-m-d') . ' ' . $config['endTime'] . ':00') + ]) + ->count(); + } + /** * 检查是否在点赞时间范围内 * @param WorkbenchAutoLike $config diff --git a/Server/database/migrations/20260512_workbench_auto_comment.sql b/Server/database/migrations/20260512_workbench_auto_comment.sql new file mode 100644 index 000000000..4811314c2 --- /dev/null +++ b/Server/database/migrations/20260512_workbench_auto_comment.sql @@ -0,0 +1,12 @@ +-- 自动点赞工作台:自动评论(执行前请在测试库验证;若列已存在请跳过对应语句) +-- 表名不含 ck_ 前缀时与项目 database.prefix 一致 + +ALTER TABLE `workbench_auto_like` + ADD COLUMN `enableAutoComment` tinyint(1) NOT NULL DEFAULT 0 COMMENT '自动评论总开关 0关1开' AFTER `enableFriendTags`, + ADD COLUMN `commentMode` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1固定多条随机一条 2AI' AFTER `enableAutoComment`, + ADD COLUMN `fixedComments` json NULL COMMENT '固定评论文案JSON数组' AFTER `commentMode`, + ADD COLUMN `aiCommentPrompt` varchar(2000) NOT NULL DEFAULT '' COMMENT 'AI评论用户补充提示词' AFTER `fixedComments`; + +ALTER TABLE `workbench_auto_like_item` + ADD COLUMN `commentContent` varchar(500) NULL DEFAULT NULL COMMENT '已发送评论内容' AFTER `createTime`, + ADD COLUMN `commentTime` int(11) NULL DEFAULT NULL COMMENT '评论成功时间戳' AFTER `commentContent`; diff --git a/Server/database/migrations/20260513_drop_workbench_auto_like_max_comments_per_day.sql b/Server/database/migrations/20260513_drop_workbench_auto_like_max_comments_per_day.sql new file mode 100644 index 000000000..2a731d194 --- /dev/null +++ b/Server/database/migrations/20260513_drop_workbench_auto_like_max_comments_per_day.sql @@ -0,0 +1,2 @@ +-- 若曾执行过含 maxCommentsPerDay 的旧版迁移,可执行本脚本删除该列(列不存在时会报错,请先确认表结构) +ALTER TABLE `workbench_auto_like` DROP COLUMN `maxCommentsPerDay`; diff --git a/Server/docs/付款码获客.md b/Server/docs/付款码获客.md new file mode 100644 index 000000000..7a643bbb5 --- /dev/null +++ b/Server/docs/付款码获客.md @@ -0,0 +1,291 @@ +# 付款码获客功能设计(含支付记录 + 手机授权入池) + +## 1. 目标 + +实现一种新获客方式:用户通过“付款码”完成支付后,再引导其授权手机号;系统需要记录: + +- 付款时间(支付成功时间) +- 付款金额(实付金额) +- 商品名称 +- 并把该用户作为“订单获客”来源进入现有流量池/标签/分配体系(复用你们当前 V2 入池、分销奖励、支付行为统计逻辑)。 + +--- + +## 2. 现状复用点(来自项目代码/设计) + +### 2.1 V2 流量入池(已存在) + +- 入池核心:`application/cunkebao/service/TrafficPoolService.php` 的 `enterPool(...)` + - 内部会写入: + - 流量总表 `TrafficPoolV2` + - 公司流量表 `TrafficPoolCompany` + - 来源表 `TrafficPoolSource`(来源类型 + extra/remark) +- 来源类型枚举:`application/common/model/TrafficPoolSource.php` + - `SOURCE_TYPE_ORDER = 5`(订单获客) +- 订单支付行为统计: + - `application/common/model/TrafficPoolBehavior.php` + - `BEHAVIOR_TYPE_PAY = 7` + - `recordOrderBehavior(...)` 中:当 `behaviorType === BEHAVIOR_TYPE_PAY` 会调用 `TrafficPoolCompany::incrementOrderStats(...)` 更新 `totalOrderCount / totalOrderAmount / rfmM / lastOrderTime` + +### 2.2 手机号授权流程(已存在,可复用) + +- 小程序“授权手机号”接口:`application/cunkebao/controller/plan/PosterWeChatMiniProgram.php` + - `getPhoneNumber()`:拿到 `code` -> 调用 `phone_number->getUserPhoneNumber($code)` -> 写入 `ck_task_customer` + - 再调用 `TrafficPoolService->enterPool(...)` + - 再调用 `DistributionRewardService::recordCustomerReward(...)` 走分销奖励 + +> 付款码获客会新建/复用一个类似接口,但来源类型应是“订单获客(5)”,并且要把支付信息挂到来源 extra / 行为记录里。 + +### 2.3 分销奖励(已存在,可复用) + +- `application/cunkebao/service/DistributionRewardService.php` + - `recordCustomerReward($taskId, $customerId, $phone, $channelId)` + +### 2.4 表结构参考(设计文档已写过来源类型) + +- `docs/traffic_pool_design.md` + - 来源类型:`5 = 订单获客` + - 来源来源详情字段:`sourceId/sourceName/sourceChannelId/sourceSceneId/taskCustomerId/extra/remark` + +--- + +## 3. 业务流程(端到端) + +> 下列流程拆成两段:支付回调落库、手机号授权触发入池/关联订单。 + +### 3.1 生成付款码(前置) + +1. 运营创建“付款码获客任务”(建议复用 `ck_customer_acquisition_task` 体系) +2. 系统生成付款码(二维码/支付链接) +3. 付款码承载必要参数(至少需要能回推): + - `taskId`(获客任务ID) + - `cid`(分销渠道ID,可选) + - 用于支付成功回调时关联订单记录 + +### 3.2 用户支付成功(回调触发) + +1. 支付平台回调(webhook)到你们服务端:**付款码获客回调接口** +2. 服务端完成: + - 校验签名、验签 + - 幂等写入支付记录: + - 保存 `orderno / payTime / actualPay / goddsName / companyId` + - 标记该支付记录处于“等待手机号授权/入池”状态(或等价字段) + +### 3.3 支付成功后引导授权手机号 + +1. 支付完成后,前端打开/引导用户进入小程序“手机号授权页面” +2. 页面会调用 `wx.getPhoneNumber` 拿到 `code` +3. 页面把以下参数传给服务端: + - `taskId` + - `orderNo`(用于查到刚才支付回调落库的订单记录) + - `code`(加密后的手机号 code) + - `cid`(如果你希望走分销奖励;也可以从 payment 落库记录里反查) + +### 3.4 服务端解密手机号并入池(关键步骤) + +1. `decryptPhone` 接口拿到 `code` 后解密手机号 +2. 服务端查支付记录(通过 `taskId + orderNo` 或 `orderno`) +3. 服务端写入 `ck_task_customer`(与 `PosterWeChatMiniProgram@getPhoneNumber` 同款逻辑) +4. 服务端调用 `TrafficPoolService->enterPool(...)`: + - `sourceType = TrafficPoolSource::SOURCE_TYPE_ORDER (5)` + - `identifier` 用手机号 + - 在 `sourceData.extra` 中写入订单信息(付款时间、金额、商品名、orderNo 等) +5. 同步支付行为统计: + - 调用 `TrafficPoolBehavior::recordOrderBehavior(... BEHAVIOR_TYPE_PAY=7 ...)` +6. 分销奖励(若开启): + - 调用 `DistributionRewardService::recordCustomerReward(...)` +7. 返回手机号(或仅返回成功即可) + +--- + +### 3.5 端到端流程图 + +```mermaid +flowchart TD + A[运营创建“付款码获客任务”] --> B[系统生成付款码/支付链接\n(携带 taskId / cid 回推参数)] + B --> C[用户扫码并完成支付] + C --> D[支付成功:支付平台回调\npayment notify(Webhook)] + + D --> E[服务端:校验签名 + 幂等写入支付记录\norderno / payTime / actualPay / goddsName / companyId] + E --> F[标记状态:等待手机号授权/入池] + F --> G[前端引导用户进入小程序“手机号授权页面”] + + G --> H[小程序:wx.getPhoneNumber 得到 code] + H --> I[服务端 decryptphone 接口\n传入 taskId + orderNo + code +(可选)cid] + I --> J[服务端:解密手机号] + + J --> K[查支付记录:taskId+orderNo 或 orderno] + K -->|成功| L[幂等写入 ck_task_customer\n按 task_id + phone 或 task_id + orderNo 判重] + L --> M[调用入池:TrafficPoolService->enterPool\nsourceType=订单获客(5)\nidentifier=手机号\nextra写订单信息] + M --> N[记录支付行为:TrafficPoolBehavior->recordOrderBehavior\nBEHAVIOR_TYPE_PAY=7\n更新 totalOrderCount/totalOrderAmount/rfmM/lastOrderTime] + N --> O[可选:分销奖励 DistributionRewardService->recordCustomerReward] + O --> P[返回成功/手机号] + + K -->|失败:订单不存在/未支付| X1[返回 400:订单不存在/未支付] + I -->|失败:解密失败| X2[返回 400/500:手机号授权失败] + M -->|失败:入池失败| X3[返回 500 并保持等待状态(可重试)] +``` + +--- + +## 4. 数据设计与字段映射 + +### 4.1 支付记录表 + +你们项目里已有订单表概念(用于 RFM/订单统计): + +- `application/store/controller/StatisticsController.php`、`application/cunkebao/controller/RFMController.php` + - 通过 `TrafficOrderModel` 聚合 `identifier/actualPay/createTime` +- `application/store/model/TrafficOrderModel.php` + - 指向 `traffic_order_v1` + +> 注意:当前 repo 的 `sql.sql` 里展示的是 `ck_traffic_order`,而模型指向 `traffic_order_v1`,落地时请确认真实表名与字段完全一致。文档按“订单表有以下字段”来设计。 + +**建议订单表需要包含(与现有字段对齐/扩展):** + +- `identifier`:后续手机号(授权后再写) +- `orderno`:订单编号(支付平台 `out_trade_no`) +- `createTime`:支付成功时间(付款时间) +- `actualPay`:实际支付金额(用于统计;建议保存为可 CAST 的数值字符串或 decimal) +- `goddsName`:商品名称(注意字段拼写 `goddsName`) +- `ownerWechatId`:如果你们希望按“客服”统计成交(可在后续分配阶段再补) +- `companyId`:租户公司ID + +**并新增/关联:** + +- `taskId`:付款码获客任务ID(建议在 payment 回调落库时直接写入) +- `cid`(可选):分销渠道ID + +如果现有表结构暂时不方便新增列,则可用“orderNo 规则编码 taskId/cid”,或者用 `remark/extra`(如果表支持)存 JSON。 + +--- + +### 4.2 流量来源表写入(关键) + +在 `enterPool` 写入的 `TrafficPoolSource` 上,建议 `extra` 至少包含: + +- `taskId` +- `taskName`(如果可取) +- `orderNo` +- `payTime`(支付成功时间) +- `actualPay` +- `goddsName` +- `channelId`(即 `cid`,用于归因/奖励) + +`TrafficPoolSource` 结构参考: + +- 来源类型:`sourceType=5` +- 备注:`remark` +- 额外信息:`extra`(json) + +--- + +## 5. API 设计(建议新增/扩展) + +> 当前路由文件显示已存在“小程序 poster 的 getPhoneNumber/decryptphone”。付款码获客建议新建同风格的业务分组与控制器方法。 + +### 5.1 支付回调(Webhook) + +**接口:** `POST /v1/frontend/payment/notify`(建议不走 JWT,走支付签名鉴权) + +- 请求体:由支付平台回传(示例字段需你们按实际支付方对齐) + - `out_trade_no`:orderNo + - `transaction_time`:支付完成时间 + - `total_amount`:实付金额 + - `attach`:建议放 `{taskId, cid}`(或至少能关联到任务) + - `body`:商品名称(建议写入 goddsName) +- 响应: + - `200 {code:200,msg:"ok"}` + +**后端逻辑:** + +1. 幂等:同一 `orderNo` 多次回调只更新/不重复插入 +2. 写入订单表:`actualPay / goddsName / createTime / companyId / taskId / cid` +3. 标记订单状态:`paid_wait_phone`(或用等价字段) + +--- + +### 5.2 手机授权解密 + 关联入池 + +**接口:** `POST /v1/frontend/business/payment/decryptphone` + +- 请求参数: + - `id`:`taskId`(获客任务ID) + - `orderNo`:支付订单编号(用于查支付记录) + - `code`:`wx.getPhoneNumber` 返回的 code + - `cid`:分销渠道ID(可选;也可从支付落库记录里读) +- 响应: + - 成功:`{code:200, message:"ok", data:"手机号"}` + - 失败:`{code:400或500, message:"xxx"}` + +**后端逻辑:** + +1. 解密手机号(复用 EasyWeChat 的 `phone_number->getUserPhoneNumber($code)`) +2. 查订单支付记录(按 `orderNo + taskId`) +3. 幂等入库: + - `ck_task_customer`:按 `task_id + phone` 或按 `task_id + orderNo` 去重(沿用 `PosterWeChatMiniProgram@getPhoneNumber` 的 `where(task_id, phone)` 判重思想) +4. 写 `ck_task_customer` +5. 调用入池: + - `TrafficPoolService->enterPool($identifier=$phone, $companyId, TrafficPoolSource::SOURCE_TYPE_ORDER, ...)` + - `sourceData.extra` 写入支付字段 +6. 记录支付行为: + - `TrafficPoolBehavior::recordOrderBehavior($poolCompanyId, identifier=$phone, companyId=$companyId, behaviorType=PAY, orderId=$orderNo, amount=$actualPay)` +7. 分销奖励(如果任务分销开启): + - `DistributionRewardService::recordCustomerReward($taskId,$customerId,$phone,$channelId)` +8. 返回成功 + +--- + +## 6. 幂等策略与异常处理 + +### 6.1 支付回调幂等 + +- 以 `orderno` 为幂等键(建议再加 `companyId`) +- 重复回调只允许: + - 更新状态/金额(如果之前是空或异常状态) + - 不重复创建订单记录 + +### 6.2 手机授权幂等 + +- 幂等键建议: + - 首选:`taskId + phone` + - 或:`taskId + orderNo` +- 如果用户重复授权/重复打开页面: + - 不重复插入 `ck_task_customer` + - 只补齐缺失字段(如把订单的 identifier 写上) + +### 6.3 失败兜底 + +- 若支付记录不存在:返回 `400 订单不存在/未支付` +- 若手机号解密失败:返回 `400 手机号授权失败` +- 若入池失败: + - 记录日志(`Log::error`) + - 返回 `500`,并保持订单为“等待状态”(可人工重试) + +--- + +## 7. 需要你确认的关键信息(我建议你回复我) + +1. 你们“付款码”使用的是哪个支付平台?回调参数字段名是什么(`out_trade_no/transaction_time/total_amount/body/attach`)? +2. 支付成功后,前端要跳转到哪个小程序页面?该页面能不能拿到 `taskId/orderNo/cid`? +3. 订单表落地时你们到底用的是: + - `traffic_order_v1`(按模型 `TrafficOrderModel`) + - 还是 `ck_traffic_order`(按 `sql.sql`) + - 或两者都有?(需要确认,以免字段对不齐) +4. `actualPay` 你们希望保存“分”为整数,还是“元”为 decimal 字符串? + +--- + +## 8. 交付物清单(开发阶段建议) + +- 新增/扩展: + - 支付回调控制器方法 + 路由 + - 手机授权解密控制器方法 + 路由 +- 数据: + - 订单表字段补齐/确认(至少要能存:orderno/createTime/actualPay/goddsName/companyId,并能关联 taskId/cid) +- 流量侧: + - `sourceType=5` 写入 `extra` 支付信息 + - `recordOrderBehavior(... PAY ...)` 更新 rfmM/成交统计 + + + diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 61f46bac5..0275d0907 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -26,6 +26,7 @@ use app\cunkebao\service\DistributionRewardService; class Adapter implements WeChatServiceInterface { protected $config; + protected $hasRetryCountColumn = null; // protected $apiClient; // 如果使用 VendorAApiClient @@ -157,6 +158,9 @@ class Adapter implements WeChatServiceInterface public function handleCustomerTaskWithStatusIsNew(int $current_worker_id, int $process_count_for_status_0) { + $retryIntervalSeconds = 2 * 3600; // 失败后至少间隔2小时再重试 + $maxRetryTimes = 5; // 执行失败后最多重试5次 + $task = Db::name('customer_acquisition_task') ->where(['status' => 1, 'deleteTime' => 0]) /* ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")*/ @@ -175,11 +179,37 @@ class Adapter implements WeChatServiceInterface if ($deviceCount <= 0) { continue; } + // 分两段取数,避免“先limit再过滤”导致可执行任务被截断而空转 $tasks = Db::name('task_customer') - ->where(['status' => 0, 'task_id' => $item['id']]) + ->where('task_id', $item['id']) + ->where('status', 0) ->order('id DESC') ->limit($deviceCount) ->select(); + + $missingCount = $deviceCount - count($tasks); + if ($missingCount > 0) { + $failedTasks = Db::name('task_customer') + ->where('task_id', $item['id']) + ->where('status', 3) + ->order('updateTime ASC,id DESC') + ->limit($deviceCount * 10) + ->select(); + + if (!empty($failedTasks)) { + foreach ($failedTasks as $candidateTask) { + if ($this->canRetryFailedTask($candidateTask, $retryIntervalSeconds, $maxRetryTimes)) { + $tasks[] = $candidateTask; + $missingCount--; + } + + if ($missingCount <= 0) { + break; + } + } + } + } + $taskData = array_merge($taskData, $tasks); } if ($taskData) { @@ -331,6 +361,75 @@ class Adapter implements WeChatServiceInterface } } + /** + * 是否允许对失败任务发起重试 + * + * 规则: + * 1. 最多重试5次(不含首次尝试) + * 2. 每次重试至少间隔2小时 + */ + private function canRetryFailedTask(array $task, int $retryIntervalSeconds, int $maxRetryTimes): bool + { + $retryUsed = $this->getTaskRetryCount($task); + + if ($retryUsed >= $maxRetryTimes) { + return false; + } + + $lastFailedTime = (int)($task['updateTime'] ?? 0); + if ($lastFailedTime <= 0) { + return true; + } + + return (time() - $lastFailedTime) >= $retryIntervalSeconds; + } + + private function getTaskRetryCount(array $task): int + { + // 新逻辑:优先使用独立字段 retry_count 统计重试次数 + if ($this->hasRetryCountColumn() && isset($task['retry_count'])) { + return max(0, (int)$task['retry_count']); + } + + // 兼容旧数据:退化到根据 processed_wechat_ids 估算 + $attemptCount = $this->getTaskAttemptCountByProcessedWechatIds($task); + return max(0, $attemptCount - 1); + } + + /** + * 根据 processed_wechat_ids 估算当前任务已尝试次数(兼容旧逻辑) + */ + private function getTaskAttemptCountByProcessedWechatIds(array $task): int + { + $processedWechatIds = trim((string)($task['processed_wechat_ids'] ?? ''), ','); + if ($processedWechatIds === '') { + return 1; + } + + $ids = array_filter(explode(',', $processedWechatIds), function ($id) { + return $id !== ''; + }); + + return max(1, count($ids)); + } + + private function hasRetryCountColumn(): bool + { + if ($this->hasRetryCountColumn !== null) { + return $this->hasRetryCountColumn; + } + + try { + $tableName = config('database.prefix') . 'task_customer'; + $column = Db::query("SHOW COLUMNS FROM `{$tableName}` LIKE 'retry_count'"); + $this->hasRetryCountColumn = !empty($column); + } catch (\Exception $e) { + $this->hasRetryCountColumn = false; + } + + return $this->hasRetryCountColumn; + } + // 处理添加中的获客任务, only run in workerman process! public function handleCustomerTaskWithStatusIsCreated() { @@ -449,9 +548,18 @@ class Adapter implements WeChatServiceInterface // todo 判断处理执行失败的情况 status=2,根据 extra 的描述去处理;-- 可以先直接更新为失败,然后 extra =》fail_reason -- 因为有专门的任务会处理失败的 if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 2) { + $updateData = [ + 'status' => 3, + 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', + 'updateTime' => time() + ]; + if ($this->hasRetryCountColumn()) { + $updateData['retry_count'] = Db::raw('IFNULL(retry_count,0)+1'); + } + Db::name('task_customer') ->where('id', $task['id']) - ->update(['status' => 3, 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', 'updateTime' => time()]); + ->update($updateData); break; } } diff --git a/Server/sql.sql b/Server/sql.sql index 633756e7b..9dbcedeca 100644 --- a/Server/sql.sql +++ b/Server/sql.sql @@ -1630,6 +1630,10 @@ CREATE TABLE `ck_workbench_auto_like` ( `friendMaxLikes` int(10) NULL DEFAULT NULL COMMENT '好友最大点赞数', `friendTags` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友标签', `enableFriendTags` tinyint(1) NULL DEFAULT 0 COMMENT '启用好友标签', + `enableAutoComment` tinyint(1) NOT NULL DEFAULT 0 COMMENT '自动评论总开关', + `commentMode` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1固定随机 2AI', + `fixedComments` json NULL COMMENT '固定评论文案JSON数组', + `aiCommentPrompt` varchar(2000) NOT NULL DEFAULT '' COMMENT 'AI补充提示词', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_workbench_id`(`workbenchId`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '自动点赞配置表' ROW_FORMAT = Dynamic; @@ -1647,6 +1651,8 @@ CREATE TABLE `ck_workbench_auto_like_item` ( `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', `momentsId` int(11) NULL DEFAULT NULL COMMENT '朋友圈id', `createTime` int(11) NOT NULL COMMENT '创建时间', + `commentContent` varchar(500) NULL DEFAULT NULL COMMENT '已发送评论内容', + `commentTime` int(11) NULL DEFAULT NULL COMMENT '评论成功时间戳', PRIMARY KEY (`id`) USING BTREE, INDEX `workbenchId`(`workbenchId`) USING BTREE, INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE,