3d3caddc7d
崔琪2.xlsx 1427 条询盘按 Excel 行序轮询分配给 6 名业务员, 更新 crm_customer.user_id 并发每人一封汇总邮件。 Co-Authored-By: Claude <noreply@anthropic.com>
6.1 KiB
6.1 KiB
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(新),复用CustomerHismodel。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
--dry-run:核对匹配率、6 人分配条数均衡、未匹配清单、抽样分配结果。--only-user=34:单点试发,确认模板与送达。- 全量执行,检查运行清单 + 终端汇总报告。
Constraints / Out of Scope
- 仅处理
deleted_at IS NULL的活跃客户。 - 未匹配 code 跳过 + 记清单,不中止整个流程。
- 轮询顺序 = Excel 行顺序(用户已确认)。
- 邮件粒度 = 每人一封汇总(用户已确认),不做逐条邮件。
- 不做永久 API / Web 界面 / 定时任务;不引入新依赖。