跳到正文
☰
UA
评论中间件现状
重构前事实基线
审计日期 2026-08-12
◐
正在渲染文档...
此文档需要启用 JavaScript 才能渲染 Markdown 和 Mermaid 图。
# 四类事实补证 > 调查日期:2026-08-12 > 调查范围:评论中间件、`ua-pim`、`ua-pim-worker-management`、`ua-pim-site`、`UA`,以及本机只读运行状态。 > 本文补充生产数据、任务运行、跨系统上下游和渠道协议四类事实。它仍是重构前现状基线,不是 Node.js 目标方案。 ## 1. 补证结论总览 | 类别 | 本轮得到的事实 | 仍未知 | 状态 | | --- | --- | --- | --- | | 数据库 | 八张业务表没有完整建表迁移;只确认回复表四个增量列;应用代码定义了五组逻辑去重键 | 生产 DDL、真实索引/FK、行数、重复、状态和 JSON 质量 | Partial | | Cron / Worker / Redis | 历史环境至少经历 File Queue 到 Redis Queue 切换;曾有 Worker 执行和短时双 Worker 重叠 | 生产调度周期、稳定并发、守护方式、当前积压 | Partial | | 上游 / 下游 | 当前中间件只消费回复表并写平台;指定仓库内未发现回复记录生产者和评论业务消费者;`ua-pim` 只确证读取相邻的账户表 | 真正回复生产者、评论/回复结果消费者、数据所有权和保留规则 | Partial | | 渠道协议 | 已确认三渠道请求、分页、成功判定、超时及历史失败样本;回复仅 JD/Tmall 有固定重试 | 官方配额、真实成功响应、所有错误码、授权范围、服务端幂等保证 | Partial | 结论不是“四类事实已经齐全”,而是已经明确划开三条边界:**仓库可证事实、历史运行事实、必须从生产环境继续采集的事实**。当前材料足以约束重构设计,但不足以直接决定生产表迁移、调度容量和切流策略。 ## 2. 证据范围与限制 ### 2.1 已检查范围 - 评论中间件的业务源码、迁移、配置、三渠道 SDK 中实际调用到的类,以及 `console/runtime/logs/app.log`。 - `ua-pim`、`ua-pim-worker-management`、`ua-pim-site` 和 `UA` 的源码、配置声明、测试及相关历史参考目录。 - 本机进程、Redis keyspace、Docker 容器和 Kubernetes context,全部为只读检查。 ### 2.2 没有执行的动作 - 未连接生产/UAT MySQL,未调用任何真实渠道 API。 - 未启动 Yii 命令或 Worker,未消费、反序列化、删除或重排 Redis 消息。 - 未读取或记录数据库密码、Token、密钥、评论正文、回复正文、店铺编码或完整请求/响应。 因此,本文中的“未找到”只表示在上述代码和环境边界内没有证据,不能外推为整个组织或生产环境中不存在。 ## 3. 数据库 DDL、索引与数据剖面 ### 3.1 当前能确认的 DDL 仓库只有三条迁移:两条属于 Yii 模板 `user` 表,评论业务只有一条回复表增量迁移。八张业务表的完整 `CREATE TABLE` 均缺失。 | 表 | 可确认的数据库变更 | 证据 | | --- | --- | --- | | `jd_comments_reply` | 增加 `submit_status CHAR(32) DEFAULT 'new'` | `console/migrations/m251114_094210_add_column_comment_reply_table.php:15` | | `jd_comments_reply` | 增加 `retry_count SMALLINT DEFAULT 0` | `console/migrations/m251114_094210_add_column_comment_reply_table.php:16` | | `tmall_comments_reply` | 增加 `submit_status CHAR(32) DEFAULT 'new'` | `console/migrations/m251114_094210_add_column_comment_reply_table.php:17` | | `tmall_comments_reply` | 增加 `retry_count SMALLINT DEFAULT 0` | `console/migrations/m251114_094210_add_column_comment_reply_table.php:18` | 该迁移只能证明两张回复表在执行迁移前已经存在,不能证明其主键、其余列、索引或字符集。八个 ActiveRecord 的 `rules()` 只代表应用输入预期,也不能替代数据库 DDL。特别是 `tmall_comments.oid` 的 PHPDoc 与验证规则类型不一致,见 `console/modules/tmall/models/Comments.php:13`、`:35`。 ### 3.2 逻辑键不等于唯一索引 源码在写入前用以下字段查重,但检查到的业务迁移没有创建对应唯一索引、普通索引或外键: | 表 | 源码使用的逻辑键 | 证据 | | --- | --- | --- | | `app_info` | `(channel_type, shop_code)` | `console/models/AppInfo.php:80-97` | | `jd_comments` | `(shop_code, comment_id)` | `console/modules/jd/CommentJob.php:45-51` | | `tmall_comments` | `(shop_code, oid)` | `console/modules/tmall/CommentJob.php:46-53` | | `douyin_products` | `(shop_code, product_id)` | `console/modules/douyin/CommentJob.php:41-49` | | `douyin_comments` | `(shop_code, product_id, comment_id)` | `console/modules/douyin/ProductCommentJob.php:34-45` | 这是“先查后写”的应用级去重。没有生产索引证据时,不能认为它能防止并发重复,也不能据此确定 Node.js 迁移的唯一约束。 ### 3.3 数据量、重复与状态分布 本轮没有取得可信的真实统计: - 本机没有监听 3306 的 MySQL,也没有评论 MySQL 容器或有效业务 dump。 - 兄弟项目测试中的 1 条或 2 条账户记录均为 mock,只证明接口契约,不代表任何环境的数据量。 - 历史日志包含零散 SQL 和错误上下文,既不完整也可能包含业务数据,不能用来估算行数、重复率或状态分布。 因此,下列项目全部保持 **Unknown**:八表精确行数与磁盘量、五组逻辑键重复、`comments_limitation.status` 分布、两张回复表的 `submit_status/retry_count` 分布、孤儿记录、时间范围及原始 JSON 质量。 ### 3.4 生产只读采集清单 应先由 DBA 在只读副本或低峰期执行元数据采集,再决定是否执行大表精确统计。结果只能保留 schema 和聚合值。 ```sql -- 1. 完整 DDL:八条需要逐条执行 SHOW CREATE TABLE app_info; SHOW CREATE TABLE comments_limitation; SHOW CREATE TABLE jd_comments; SHOW CREATE TABLE jd_comments_reply; SHOW CREATE TABLE tmall_comments; SHOW CREATE TABLE tmall_comments_reply; SHOW CREATE TABLE douyin_products; SHOW CREATE TABLE douyin_comments; -- 2. 索引元数据 SELECT TABLE_NAME, INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, SUB_PART, INDEX_TYPE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ( 'app_info', 'comments_limitation', 'jd_comments', 'jd_comments_reply', 'tmall_comments', 'tmall_comments_reply', 'douyin_products', 'douyin_comments' ) ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; -- 3. 低成本近似规模;InnoDB TABLE_ROWS 必须标记 approximate SELECT TABLE_NAME, TABLE_ROWS AS approximate_rows, DATA_LENGTH, INDEX_LENGTH, DATA_FREE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ( 'app_info', 'comments_limitation', 'jd_comments', 'jd_comments_reply', 'tmall_comments', 'tmall_comments_reply', 'douyin_products', 'douyin_comments' ) ORDER BY TABLE_NAME; ``` 重复统计只输出重复组数和多余行数,不输出逻辑键值。其余四组按同样方式分别替换为 `jd_comments(shop_code, comment_id)`、`tmall_comments(shop_code, oid)`、`douyin_products(shop_code, product_id)`、`douyin_comments(shop_code, product_id, comment_id)`。 ```sql SELECT COUNT(*) AS duplicate_groups, COALESCE(SUM(group_count - 1), 0) AS extra_rows FROM ( SELECT channel_type, shop_code, COUNT(*) AS group_count FROM app_info GROUP BY channel_type, shop_code HAVING COUNT(*) > 1 ) d; ``` 状态分布只输出低基数状态和聚合结果: ```sql SELECT CASE WHEN limit_type LIKE 'replay\_%' ESCAPE '\\' THEN 'reply_cursor' ELSE 'comment_window' END AS limitation_kind, status, COUNT(*) AS row_count, MIN(created_at) AS oldest_created_at, MAX(created_at) AS newest_created_at FROM comments_limitation GROUP BY limitation_kind, status ORDER BY limitation_kind, status; SELECT submit_status, COUNT(*) AS row_count, MIN(retry_count) AS min_retry, MAX(retry_count) AS max_retry, AVG(retry_count) AS avg_retry FROM jd_comments_reply GROUP BY submit_status ORDER BY submit_status; SELECT submit_status, COUNT(*) AS row_count, MIN(retry_count) AS min_retry, MAX(retry_count) AS max_retry, AVG(retry_count) AS avg_retry FROM tmall_comments_reply GROUP BY submit_status ORDER BY submit_status; ``` 采集前置条件:只读账号、优先只读副本、显式查询超时、先 DDL 后统计;不得交付数据 dump、`shop_code`、评论/回复/result 原文、DSN 或账户信息。 ## 4. Cron、Worker 与 Redis 历史任务 ### 4.1 队列后端发生过切换 ```mermaid timeline title 可证的队列时间线 2025-10-22 : 历史 queue/run 堆栈进入 File Queue 驱动 : 随仓库保留的文件队列索引 lastId 为 10,waiting/reserved 为空 2025-11-28 : 历史日志记录 Redis 127.0.0.1、DB 1 连接失败 : 数秒内消费一批遗留 Tmall Job 并失败 2026-08-12 : 当前源码配置 Redis Queue,TTR 为 4 小时 : 本机 Redis DB 1 为空,无 Yii Worker ``` - 2025-10-22 的 `queue/run` 错误堆栈进入 yii2-queue File Queue 驱动,见 `console/runtime/logs/app.log:2321`。 - 当前代码配置 `yii\queue\redis\Queue`,TTR 为 4 小时;本地配置指向 Redis DB 1,见 `console/config/main.php:25-37`、`console/config/main-local.php:6-16`。 - 2025-11-28 历史日志记录 Redis DB 1 连接失败,见 `console/runtime/logs/app.log:17965-17966`。 - `console/runtime/queue/index.data` 是 2025-10-22 的 File Queue 快照,只有 `lastId=10`,`waiting/reserved` 均为空且没有 Job 数据文件。它不能代表后来的 Redis 或生产状态。 所以可以确认后端至少发生过一次 File Queue 到 Redis Queue 的切换,但切换时间、消息迁移和旧消息清理方式未知。 ### 4.2 Worker 与并发的证据边界 历史日志覆盖 2025-10-22 至 2025-11-28。错误上下文中可解析到 25 次 `queue/run` 和 122 次内部 `queue/exec`;它们都是错误路径留下的下限,不是完整执行量。 `queue/exec` 是 Yii isolate 模式由父 Worker 启动的子进程,不能把每个 PID 都算成独立 Worker。2025-10-27 06:58:57 同秒出现两个不同父 Worker PID,可以确认当时至少有两个 Worker 短时重叠,但不能推出固定并发为 2。隔离进程参数含义见 `vendor/yiisoft/yii2-queue/src/cli/Command.php:121-185`。 历史命令只证明这些入口曾被执行: | 历史入口 | 可确认事实 | 不能确认 | | --- | --- | --- | | `queue/run` | Worker 曾运行 | 是人工、Cron 还是守护进程 | | `tmall/comments/tmall` | 天猫采集入口曾执行 | 周期和持续运行状态 | | `jd/reply`、`tmall/reply` | 回复扫描入口曾执行 | 是否定时、多久一次 | | 多个拼错/旧路由 | 存在明显人工排障操作 | 不能当调度配置样本 | 日志没有稳定、连续的周期样本,仓库也没有 crontab、Supervisor、systemd、Kubernetes、docker-compose 或 PM2 部署声明。因此 Cron 频率、稳定 Worker 数、重启策略和日志轮转仍是 **Unknown**。 ### 4.3 当前本机状态 - 本机没有 PHP/Yii Queue 进程、相关用户 crontab 或 launchd 项。 - Docker 仅有无关 MongoDB 容器;kubectl 没有 current-context;本机不能代表生产编排。 - 本机 Redis 可连接,但 DB 1 的 `DBSIZE=0`,DB 0 未发现默认 `queue.*` 键。这只说明当前本机没有旧队列任务。 - `/Users/linjianbing/Documents/projects/ua-pim/ecosystem.config.js:23-52` 配置了新系统的一个 Worker 和一个 Cron 进程,但同文件说明 PHP DB 队列维持原状;它不能用于回填旧评论中间件并发。 ### 4.4 生产只读采集清单 Redis 默认 channel 为 `queue`,源码键定义见 `vendor/yiisoft/yii2-queue/src/drivers/redis/Queue.php:27-37`。只允许执行计数/元数据命令,禁止 `queue/run/listen/exec` 以及任何弹出、移动或删除操作。 ```bash # 调度与进程;输出交付前遮蔽主机、用户和业务参数 ps -eo pid,ppid,lstart,cmd | rg 'yii .*queue/(listen|run)|yii .*(comments|reply)' crontab -l systemctl list-units --type=service --all | rg -i 'yii|queue|comment|supervisor' systemctl list-timers --all | rg -i 'yii|queue|comment' supervisorctl status 2>/dev/null # Redis:显式使用运维确认的 host、port、db redis-cli -h <host> -p <port> -n <db> LLEN queue.waiting redis-cli -h <host> -p <port> -n <db> ZCOUNT queue.delayed -inf +inf redis-cli -h <host> -p <port> -n <db> ZCOUNT queue.reserved -inf +inf redis-cli -h <host> -p <port> -n <db> HLEN queue.messages redis-cli -h <host> -p <port> -n <db> HLEN queue.attempts redis-cli -h <host> -p <port> -n <db> INFO persistence ``` ## 5. 回复上游与评论下游 ### 5.1 回复表生产者仍未知 ```mermaid flowchart LR UP["回复记录生产者:Unknown"] -.->|"INSERT new 记录;已审计代码中未找到"| R[("jd_comments_reply / tmall_comments_reply")] RC["ReplyController"] -->|"SELECT new;UPDATE processing"| R RC --> Q["Redis CommentReplyJob"] Q --> API["京东 / 淘宝回复接口"] Q -->|"UPDATE result、retry_count、submit_status"| R ``` - 京东和天猫 ReplyController 都只查询已有 `new` 记录、改为 `processing` 并投递 Job,不创建回复记录。证据:`console/modules/jd/controllers/ReplyController.php:21-58`、`console/modules/tmall/controllers/ReplyController.php:21-60`。 - 两个回复 Job 只读取现有记录,调用平台并更新 `result/retry_count/submit_status`。证据:`console/modules/jd/CommentReplyJob.php:23-65`、`console/modules/tmall/CommentReplyJob.php:26-66`。 - 在五个指定项目中,没有找到对两张回复表的 INSERT、ORM create/upsert、写 API、同步脚本或消息消费者。 生产者可能是未提供的旧 PIM、人工 SQL、集成脚本、其他服务或数据库触发器。在拿到数据库审计和真实操作链前,不能假设当前 `ua-pim` 是生产者。 ### 5.2 评论数据消费者仍未知 ```mermaid flowchart LR JDAPI["京东评论 API"] --> JDJOB["JD CommentJob"] --> JDC[("jd_comments")] TMAPI["淘宝评论 / 详情 API"] --> TMJOB["Tmall Jobs"] --> TMC[("tmall_comments")] DYAPI["抖店商品评论 API"] --> DYJOB["Douyin Jobs"] --> DYC[("douyin_comments")] JDC -->|"仅为 upsert 回读"| JDJOB TMC -->|"仅为 upsert / 详情更新回读"| TMJOB DYC -->|"仅为 upsert 回读"| DYJOB JDC -.-> DOWN["业务下游:Unknown"] TMC -.-> DOWN DYC -.-> DOWN ``` 当前中间件对三张评论表的读取仅用于查重、比较和详情更新,仓库中没有评论查询 HTTP API、导出或下游推送。指定兄弟项目中也没有这些表名或对应字段组合的读取链路。 `ua-pim` 中名称相近的“评论/回复”写入 `pim_comment_comment`,属于产品协作评论,不是渠道评价。证据:`/Users/linjianbing/Documents/projects/ua-pim/src/pages/api/products/[productCode]/comments.ts:63-99`、`:138-181`。旧京东中间件的 `/comments` 和 `/comments/reply` 只是京东 POP API 代理,也不写回复表。 ### 5.3 已确认的相邻跨库读取 `ua-pim` 确实具备读取评论中间件相邻账户数据的能力:它从运行时配置的 MySQL 表执行 `SELECT * ... WHERE channel_type = 4`,结果用于生成抖音兼容 Token 缓存,并由 API 返回。证据: - `/Users/linjianbing/Documents/projects/ua-pim/src/server/services/channels/douyin/lib/douyin-middleware-token-store.ts:17-50` - `/Users/linjianbing/Documents/projects/ua-pim/src/server/services/channels/douyin/lib/douyin-middleware-token.ts:35-72` - `/Users/linjianbing/Documents/projects/ua-pim/src/pages/api/rest/v1/channel/get-middleware-token.ts:17-41` 实际表名由环境配置决定,本轮没有读取配置值。因此只能确认这条跨库读取能力,不能宣称某个环境当前一定读取 `app_info`。它同时说明:表名和 JSON 契约不能在重构时仅按 PHP 仓库内部引用随意修改。 ## 6. 渠道响应、分页、限流、幂等与错误码 ### 6.1 渠道协议矩阵 | 项目 | 京东 | 天猫 / 淘宝 | 抖音 | | --- | --- | --- | --- | | 列表分页 | `page/pageSize`,固定 50;空 `comments` 结束 | `page_no/page_size`,固定 100;空列表或 `has_next` 为空结束 | 商品 `page/size`;评论 `offset/count`,固定 100,以 `has_more` 结束 | | 成功判定 | 评论响应 `resultCode == 200`;回复同样检查 200 | 列表读取 `trade_rates.trade_rate`;回复检查 `is_success` | 业务 `code == 10000` 且目标 `data` 非空 | | HTTP 超时 | 30 秒 | connect/read 各 30 秒 | SDK 声明 1000/5000,但直接传入 PHP cURL 秒级选项,存在单位错配 | | 业务重试 | 评论无;回复失败 120 秒后重试,总尝试上限 5 | 评论/详情无;回复失败 120 秒后重试,总尝试上限 5 | 无应用级重试 | | 限流处理 | 未发现 429、`Retry-After` 或退避 | 未发现配额感知或退避 | 未发现配额感知或退避 | | 幂等 | 评论通过先查后写近似幂等;回复无业务幂等键 | 同左 | 评论通过先查后写近似幂等;无回复流程 | | 真实成功样本 | 未保存 | 未保存 | 未保存 | 主要源码证据: - 京东请求、响应包装和 30 秒超时:`console/modules/jd/HuFuClient.php:61-88`、`:91-146`。 - 京东分页:`console/modules/jd/CommentJob.php:27-42`;回复重试:`console/modules/jd/CommentReplyJob.php:47-65`。 - 天猫参数和 API 方法:`console/modules/tmall/sdk/top/request/TaobaoTraderatesGet.php:5-62`、`TaobaoTraderateExplainAdd.php:3-27`。 - 天猫分页和回复重试:`console/modules/tmall/CommentJob.php:30-89`、`console/modules/tmall/CommentReplyJob.php:49-66`。 - 抖音响应判断:`console/modules/douyin/Client.php:26-55`、`:96-129`;评论 offset 分页:`console/modules/douyin/ProductCommentJob.php:19-76`。 - 抖音超时声明和 cURL 传递:`console/modules/douyin/sdk/open/core/DoudianOpConfig.php:7-9`、`console/modules/douyin/sdk/open/core/http/HttpClient.php:88-100`。 ### 6.2 历史真实失败样本 历史日志没有可安全复用的完整成功响应,但确实留下了下列真实失败类别。数字只代表当前日志文件内样本,不代表生产总错误率。 | 渠道 | 时间范围 | 脱敏后的响应/错误事实 | 样本数 | | --- | --- | --- | ---: | | 京东回复 | 2025-11-17 | HTTP 404 | 15 | | 京东回复 | 2025-11-17 | HTTP 500 | 2 | | 京东回复 | 2025-11-17 | HTTP 200,但 body 为空且无法解析业务结果 | 3 | | 抖音 | 2025-11-18 | 业务码 `40003`,子码表示 Token 不存在 | 1 | | 抖音 | 2025-11-20 | 业务码 `40003`,子码表示 Token 过期 | 2 | | 抖音 | 2025-11-18 | TLS 连接错误 | 1 | 日志证据位于 `console/runtime/logs/app.log:13187-16479`、`:16799-17117`。另有抖音响应结构与代码假设不一致引发 `stdClass as array`、缺失 `data` 键,见 `:17222-17327`。这证明渠道响应不仅会“返回失败码”,还会发生 HTTP 成功但业务体不可用、网络失败和结构漂移。 天猫日志中没有保存可确认为渠道真实响应的脱敏样本;能确认的是回复 Job 曾因数组写入字符串列、空记录解引用而失败,见 `console/runtime/logs/app.log:15017-15749`、`:18077`。这些属于本地数据/代码错误,不能归类为天猫错误码。 ### 6.3 当前协议风险事实 1. **回复结果不确定时可能重复提交。** 京东曾出现 HTTP 200 空 body;Job 将其视为失败并重新投递,但请求没有业务幂等键。网络超时或无法解析响应时,也无法判断平台是否已经写入回复。 2. **回复状态不是原子状态机。** Controller 先更新 DB 为 `processing`,再 push 队列;两步没有事务或 outbox。push 失败会留下无消息的 `processing` 记录。证据:`console/modules/jd/controllers/ReplyController.php:45-51`、`console/modules/tmall/controllers/ReplyController.php:47-54`。 3. **人工重启不重置状态和重试数。** `actionRestart` 直接 push Job;Job 会继续增加 `retry_count`,没有去重锁。证据:`console/modules/jd/controllers/ReplyController.php:63-88`、`console/modules/tmall/controllers/ReplyController.php:65-90`。 4. **没有统一错误分类。** HTTP、渠道业务码、解析错误、本地数据错误都最终压入 `result` 或日志;没有可重试/不可重试分类,也没有死信语义。 5. **没有仓库级配额事实。** 未发现 429、`Retry-After`、剩余额度响应头、官方 QPS 配置或统一限流器;这只能证明当前实现没有处理,不能证明渠道无限额。 6. **抖音商品查询带硬编码名称过滤。** `getProducts()` 固定设置 `name`,因此现有“全店商品分页”实际上可能只覆盖匹配商品。证据:`console/modules/douyin/Client.php:96-113`。 ## 7. 对 Node.js 重构的事实约束 以下不是目标技术设计,而是下一阶段不能绕过的输入条件: - 在生产 DDL、索引和重复统计到位前,不得直接生成最终 ORM schema 或启用唯一约束;要先制定重复数据处置规则。 - 新旧系统并行时必须兼容现有表名、原始 JSON 和 `app_info` 的跨库读取,直到真实消费者完成确认或迁移。 - 切换 Worker 前必须盘点 File Queue 与 Redis 的 waiting/delayed/reserved;不能只看当前本机空 Redis。 - 回复迁移必须把“不确定结果下的重复提交”、DB 与队列双写、人工重启、旧序列化 Job 类名漂移纳入验证场景。 - 渠道适配器必须基于真实成功/失败样本建立契约测试;当前仓库只提供部分失败样本,不能据 SDK 字段一次性定型。 ## 8. 关闭 Unknown 的最小交付物 | 责任方 | 需要交付的脱敏证据 | 用途 | | --- | --- | --- | | DBA | 八表 DDL、列/索引、近似与获批后的精确行数、重复/状态/孤儿/JSON 聚合 | 确定迁移 schema、索引和清洗范围 | | 运维 | Cron/服务/容器配置,Worker 参数,Redis 六类键计数,队列切换记录 | 确定调度、容量和切流方案 | | 业务/PIM | 一条脱敏回复的页面到 DB、队列、渠道、回显全链路 | 确认回复生产者与业务结果消费者 | | 渠道负责人 | 每渠道脱敏成功/失败响应、分页边界、官方 QPS/错误码、授权范围 | 建立 Node.js 适配器契约与重试策略 | 四项交付完成后,现状基线才可从 `Partial` 升级为生产可验证的 `Done`,并进入 Node.js 目标架构和迁移波次设计。