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

113 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(关键防错)
批量查询:
```sql
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 条后退出。
### ④ 写库(单事务,可回滚)
事务内对每条匹配客户:
```sql
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 界面 / 定时任务;不引入新依赖。