3d3caddc7d
崔琪2.xlsx 1427 条询盘按 Excel 行序轮询分配给 6 名业务员, 更新 crm_customer.user_id 并发每人一封汇总邮件。 Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
6.1 KiB
Markdown
113 lines
6.1 KiB
Markdown
# 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 界面 / 定时任务;不引入新依赖。
|