Files
fdesk/docs/superpowers/specs/2026-07-17-inquiry-bulk-assign-design.md
T
mzl92 3d3caddc7d docs: 询盘批量分配+汇总邮件方案设计
崔琪2.xlsx 1427 条询盘按 Excel 行序轮询分配给 6 名业务员,
更新 crm_customer.user_id 并发每人一封汇总邮件。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 14:46:04 +08:00

6.1 KiB
Raw Blame History

Inquiry Bulk Assign + Digest Email Design

Goal

一次性将 docs/崔琪2.xlsx 中的 1427 条询盘,按 Excel 行顺序轮询分配给 6 个业务员(user_id: 34、73、72、155、64、211),更新 crm_customer.user_id,并为每人发送一封汇总邮件,列出分给其名下的全部询盘。

这是一次性数据操作,以 webman console 命令形式交付,不做成永久 API / 控制器 / 定时任务。

Data Model(已核实)

  • 匹配键:Excel 第一列 编号 = crm_customer.code(如 CM815EE43E7D4C1C)。
  • crm_customer.code —— varchar(32),无唯一索引(仅主键 + USER_ID/SN/TYPE 普通索引),匹配需防御撞多条。
  • crm_customer.user_id —— 待更新字段(所属用户,bigint unsigned nullable)。
  • crm_customer.deleted_at —— int 时间戳软删除标记,匹配需 IS NULL。
  • crm_customer_his —— 历史表,记录 customer_id / user_his(原 user_id) / user_id(新),复用 CustomerHis model。
  • user 表:id / name / email / corp_id / status,6 人收件邮箱取自 user.email。
  • Excel 行数:1428(含表头)→ 1427 条询盘,13 列(编号/录入人/接单人/渠道/邮箱/电话/WhatsApp/联系人/备注/国家(中)/国家(英)/备注_原始HTML/x)。

复用的现成组件(不引入新依赖)

  • app/command/CrmRotateUserCommand.php —— 轮转 user_id + 事务 + 写 CustomerHis 的模板。
  • app/common/utils/RedisUidRotator.php —— 轮询分配器(本方案改用内存确定性轮询,见下)。
  • app/common/utils/email/MailSend.php —— MailSend::send($to, $subject, $html, $attachments),SMTP 配置在 config/imap.smtp_xyh。
  • app/admin/controller/InquiryTransmitController.php::emailHtml() —— 现成询盘 HTML 邮件模板,改写为列表版复用。
  • phpoffice/phpspreadsheet(已在依赖)—— 读 Excel。

Command Interface

php webman inquiry:bulk-assign \
  --file=docs/崔琪2.xlsx \
  --users=34,73,72,155,64,211 \
  [--dry-run]        # 只解析+匹配+分配,打印报告,不写库不发邮件(强制先跑)
  [--only-user=34]   # 仅给指定 user_id 发邮件(单点试发)
  [--notify-only]    # 跳过写库,按 runtime 清单补发未成功的邮件

webman console 自动发现 app/command/ 下命令,无需改 config/command.php。

Execution Flow

① 解析 Excel(PhpSpreadsheet)

读 Sheet1(跳过表头),按行顺序提取 code 及邮件所需字段:联系人、邮箱、电话、WhatsApp、国家(中)、渠道、备注_原始HTML。得到有序列表,长度 N≈1427。

② 匹配 crm_customer(关键防错)

批量查询:

SELECT id, code, user_id FROM crm_customer
WHERE code IN (...) AND deleted_at IS NULL
  • 撞多条 code → 取 MIN(id),记 warning;
  • Excel 有、库查不到(或仅软删除)→ 记入「未匹配清单」,跳过不分配(不中止);
  • 输出匹配率;保留 Excel 行顺序,记下每个客户原 user_id。

③ 内存轮询分配(确定性 i % 6)

targetUserIds = [34, 73, 72, 155, 64, 211],第 i 条匹配客户 → targetUserIds[i % 6]。 不依赖 Redis(避免跨 run 状态残留、保证可重跑确定性),按目标用户分组得到每人清单。 --dry-run 到此为止:打印匹配率、6 人条数(应 ≈238 均衡)、未匹配清单、抽样 3 条后退出。

④ 写库(单事务,可回滚)

事务内对每条匹配客户:

UPDATE crm_customer SET user_id = ? WHERE id = ?;
INSERT INTO crm_customer_his (customer_id, user_his, user_id) VALUES (?, ?, ?);

提交后才进入发邮件。CustomerHis 保留原归属以便回滚。

⑤ 发汇总邮件(每人 1 封,共 6 封)

  • 从 user 表取 6 人 email/name,空邮箱跳过并告警;
  • 复用 emailHtml() 改成列表版,把该用户分到的询盘做成 HTML 表格;
  • 每条展示字段:编号 / 联系人 / 邮箱 / 电话 / 国家 / 渠道 / 意向车型或备注;
  • 主题:询盘分配汇总(共 N 条)- {name} - 2026-07-17;
  • MailSend::send($user_email, $subject, $html)。

Engineering Points

(a) DB 与邮件解耦 + 防重复发送

  • 顺序:先事务提交 → 再发邮件(邮件不可回滚)。
  • 写运行清单 runtime/inquiry_bulk_assign_2026-07-17.json:分配结果 + 每人发送状态。
  • 重跑时:写库幂等(覆盖 user_id,会追加 CustomerHis 历史);邮件步骤只给清单中未标记成功的人发——杜绝跑两次双倍发。

(b) 邮件失败不连坐

MailSend 失败返回 false 并记日志。命令收集每人结果、失败重试 1 次,最终汇总报告,单封失败不中断(库已提交)。

(c) 回滚

  • DB 事务失败 → 回滚,零影响。
  • 已提交但分错 → crm_customer_his.user_his 保留原归属,可反查/还原。
  • 邮件发出无法撤回 → 故强制 dry-run,并建议先 --only-user 单点试发。

File Changes

Action File Description
New app/command/InquiryBulkAssignCommand.php 主命令:解析→匹配→分配→写库→发邮件,含 dry-run/only-user/notify-only
New app/common/utils/email/InquiryDigestMail.php 汇总邮件 HTML 模板构建(基于 emailHtml 改写)
Write runtime/inquiry_bulk_assign_2026-07-17.json 运行清单(分配结果 + 发送状态,防重复发,.gitignore 已忽略 runtime)

不修改任何现有文件、不改路由、不改表结构。

Verification

  1. --dry-run:核对匹配率、6 人分配条数均衡、未匹配清单、抽样分配结果。
  2. --only-user=34:单点试发,确认模板与送达。
  3. 全量执行,检查运行清单 + 终端汇总报告。

Constraints / Out of Scope

  • 仅处理 deleted_at IS NULL 的活跃客户。
  • 未匹配 code 跳过 + 记清单,不中止整个流程。
  • 轮询顺序 = Excel 行顺序(用户已确认)。
  • 邮件粒度 = 每人一封汇总(用户已确认),不做逐条邮件。
  • 不做永久 API / Web 界面 / 定时任务;不引入新依赖。