docs(changelog): #8752 团期管理员一键催办未提交房 / 车需求的定制师(新增接口·管理后台)
changelog-filename-gate / validate (push) Failing after 2s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
这个提交包含在:
jw
2026-10-03 23:18:14 +08:00
共同撰写人 Claude Opus 5.5
父节点 93027c27eb
当前提交 cc8292ea1e
@@ -0,0 +1,394 @@
---
schema: "hl-changelog/v2"
ticket: "8752"
title: "团期管理员一键催办未提交房 / 车需求的定制师:新增 POST requirement/nudge,按定制师每人一条站内信 + 企微,无定制师的户列为跳过"
consumer: "admin"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/nudge(权限码 group-batch:manage:团期管理员 / ADMIN / SUPER_ADMIN,定制师 589507)。圈本团「该定制师动手」的户(与定制师待办同源:资源准备中、房 / 车需求未提交或被打回定制师),按定制师聚合每人一条通知(管理端站内信 + 企微,站内信点开到「定制师待办」/order/todos),只投员工不触达客户;无定制师的户在 skipped 里列出。同团 300 秒冷却(589852 带剩余秒数),一条都没发出时不占冷却。已合并 dev-v3(5b204b9d2)并部署 TEST(user-service、order-v3),经网关真实鉴权验收 AC-1~AC-10 通过,工单 #8752 已关。前端待做:团期详情需求 Tab「企微催需求」按钮、看板铃铛接本接口;成功后按 recipients / skipped 提示,wecomBound=false 的定制师提示「未绑企微,只收到站内信」,按钮按 cooldownSeconds 置灰倒计时。"
updated_at: "2026-10-03"
base: "dev-v3"
---
# 团期催办:新增一键催办定制师提交房 / 车需求(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)+ hl-user-service(通知配置迁移)
> **PR**: #8777
> **Issue**: #8752
> **日期**: 2026-10-03
> **影响范围**: 管理后台团期详情「需求」Tab 的「企微催需求」按钮、团期看板铃铛(原型 `batchDetail.jsx:889` / `batchBoard.jsx:495`);既有接口零变化
---
## ⚠️ 关键变化
1. **新增写端点** `POST .../{groupBatchId}/requirement/nudge`:团期管理员一键催本团所有「房型 / 用车需求未提交或被打回」户的定制师。此前前端写明「企微催需求无接口契约,不做」,底栏「去催需求」只跳需求 Tab;现在有接口了。
2. **一位定制师一条**:同一定制师名下多户合成一条通知,正文分列房型、用车待提交户数并列出订单号(最多 10 个,多的写「等 N 户」)。
3. **只发员工**:管理端站内信 + 企微,不发短信、不进客户收件箱。站内信链接是 `/order/todos`(定制师待办),**不是团期详情**——定制师角色没有「出团详情」菜单。
4. **同一团 300 秒冷却**:冷却期内再点返回 589852,`message` 里带剩余秒数;前端拿 `cooldownSeconds` 做倒计时置灰即可。
---
## 一、背景
成团后系统按户给定制师开了「房型需求 · 待提交」「用车需求 · 待提交」待办,但待办只在后台列表里,**不推送**;团期管理员发现有户没提交,只能线下去找定制师。原型团期详情有「企微催需求」按钮、看板有铃铛,本单给它补后端。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 一键催办定制师提交房 / 车需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/nudge` | 新增接口 | 无请求体;按定制师每人一条站内信 + 企微;同团 300 秒冷却 |
网关无改动(在既有 `/v3/admin/**` → order-service-v3 通配下)。
---
## 三、接口详情
### 1. 一键催办定制师提交房 / 车需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/nudge`
**VO**: `GroupBatchRequirementNudgeRespVO`
#### 使用场景
团期详情「需求」Tab 的「企微催需求」按钮、团期看板铃铛。团期已成团(资源准备中)、还有户没提交房 / 车需求时,团期管理员点一下,系统按定制师聚合、每人发一条站内信 + 企微。成功后用 `recipients` 提示「已催 N 位定制师(M 户)」,`skipped` 非空时提示「K 户未指派定制师,未催」,`wecomBound=false` 的定制师提示「未绑企微,只收到站内信」;按 `cooldownSeconds` 倒计时置灰按钮。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 团期主键 | 不存在返 589500 |
无请求体、无查询参数。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| groupBatchId | String | 团期主键(雪花 id,按字符串返回) |
| groupBatchNo | String | 团号,如 `T26-0938` |
| departDate | String | 出发日期 `yyyy-MM-dd` |
| eventCode | String | 固定 `GROUP_BATCH_REQUIREMENT_NUDGE` |
| pendingHouseholdCount | Integer | 待定制师提交的户数(含 skipped 里无定制师的户) |
| consultantCount | Integer | 本次催到的定制师人数(= recipients 条数) |
| enqueuedCount | Integer | 通知成功投进通知中心的定制师人数(≤ consultantCount) |
| skippedHouseholdCount | Integer | 因无定制师跳过的户数(= skipped 条数) |
| recipients | Array | 逐定制师结果,按首个待提交户的顺序 |
| recipients[].consultantId | String | 定制师(员工)id |
| recipients[].consultantName | String | 定制师姓名(企微名,缺省登录名);员工信息查不到时为 null |
| recipients[].wecomBound | Boolean | 是否绑企微:true 同时收到企微;false 只收站内信;查不到时为 null(未知) |
| recipients[].householdCount | Integer | 该定制师名下待提交户数 |
| recipients[].hotelPendingCount | Integer | 其中房型需求待提交 / 被打回的户数 |
| recipients[].vehiclePendingCount | Integer | 其中用车需求待提交 / 被打回的户数 |
| recipients[].households | Array | 该定制师名下待提交户明细(结构同下 `skipped[]`,skipReason 为 null) |
| recipients[].enqueued | Boolean | 通知是否已投进通知中心 |
| recipients[].message | String | 未投进的原因(如「通知中心投递失败」);成功为 null |
| skipped | Array | 无定制师、没人可催的户 |
| skipped[].orderId | String | 子订单 id |
| skipped[].orderNo | String | 订单号 |
| skipped[].teamNo | String | 团号(子订单级) |
| skipped[].customerName | String | 客户姓名 |
| skipped[].hotelPending | Boolean | 房型需求是否待提交 / 被打回 |
| skipped[].vehiclePending | Boolean | 用车需求是否待提交 / 被打回 |
| skipped[].skipReason | String | 跳过原因,现为「订单未指派定制师」 |
| notifiedAt | String | 本次催办时间 `yyyy-MM-dd HH:mm:ss`;一条都没发时为 null |
| cooldownSeconds | Integer | 下次可催前的冷却秒数:有通知发出时 300;一条都没发(全部户无定制师)时 0 |
#### 请求示例
```http
POST /v3/admin/order/group-batch/2106397492469985281/requirement/nudge
Authorization: Bearer <团期管理员 token>
```
#### 响应示例
TEST 实测(团期 T26-0938,两位定制师 + 一户无定制师):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106397492469985281",
"groupBatchNo": "T26-0938",
"departDate": "2026-11-18",
"eventCode": "GROUP_BATCH_REQUIREMENT_NUDGE",
"pendingHouseholdCount": 4,
"consultantCount": 2,
"enqueuedCount": 2,
"skippedHouseholdCount": 1,
"recipients": [
{
"consultantId": "1002",
"consultantName": "test_admin",
"wecomBound": true,
"householdCount": 2,
"hotelPendingCount": 2,
"vehiclePendingCount": 1,
"households": [
{
"orderId": "2106397492268658689",
"orderNo": "HL20261003225431608",
"teamNo": "26-2132",
"customerName": "乌云毕力格",
"hotelPending": true,
"vehiclePending": true,
"skipReason": null
},
{
"orderId": "2106397493581516801",
"orderNo": "HL20261003225431974",
"teamNo": "26-9255",
"customerName": "王淑芬",
"hotelPending": true,
"vehiclePending": false,
"skipReason": null
}
],
"enqueued": true,
"message": null
},
{
"consultantId": "2073973862738944001",
"consultantName": "designer_4760",
"wecomBound": false,
"householdCount": 1,
"hotelPendingCount": 1,
"vehiclePendingCount": 1,
"households": [
{
"orderId": "2106397494344839169",
"orderNo": "HL20261003225432223",
"teamNo": "26-6378",
"customerName": "刘志强",
"hotelPending": true,
"vehiclePending": true,
"skipReason": null
}
],
"enqueued": true,
"message": null
}
],
"skipped": [
{
"orderId": "2106397494781087745",
"orderNo": "HL20261003225432341",
"teamNo": "26-3501",
"customerName": "高玉梅",
"hotelPending": true,
"vehiclePending": true,
"skipReason": "订单未指派定制师"
}
],
"notifiedAt": "2026-10-03 22:57:38",
"cooldownSeconds": 300
}
}
```
#### 空数据 / 降级响应
待提交的户全部没有定制师(TEST 实测团期 T26-7327):返回 200,不发任何通知、不写时间线、不占冷却,`cooldownSeconds=0`、`notifiedAt=null`,可立即再点:
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106397514322350082",
"groupBatchNo": "T26-7327",
"departDate": "2026-11-24",
"eventCode": "GROUP_BATCH_REQUIREMENT_NUDGE",
"pendingHouseholdCount": 1,
"consultantCount": 0,
"enqueuedCount": 0,
"skippedHouseholdCount": 1,
"recipients": [],
"skipped": [
{
"orderId": "2106397514263629826",
"orderNo": "HL20261003225436973",
"teamNo": "26-0624",
"customerName": "孙德胜",
"hotelPending": true,
"vehiclePending": true,
"skipReason": "订单未指派定制师"
}
],
"notifiedAt": null,
"cooldownSeconds": 0
}
}
```
员工信息查询降级(user-service 不可用)时照常催办,只是 `consultantName`、`wecomBound` 为 null(未知),不要显示成「未绑企微」。
#### 错误响应
冷却期内重复催办(`{0}` 为剩余秒数,TEST 实测):
```json
{ "code": 589852, "message": "催办过于频繁,请 298 秒后再试", "success": false, "data": null }
```
没有可催的户(在团户的房 / 车需求都已提交):
```json
{ "code": 589850, "message": "本团期在团户的房型 / 用车需求均已提交,无需催办", "success": false, "data": null }
```
团期还在招募中:
```json
{ "code": 589552, "message": "团期尚未成团,请先完成成团后再操作", "success": false, "data": null }
```
团期已过资源准备中(物料准备中及以后、已取消),定制师已不能提交需求:
```json
{ "code": 589851, "message": "团期当前状态为「已取消」,定制师已不能提交房型 / 用车需求,无法催办", "success": false, "data": null }
```
无权限(如定制师):
```json
{ "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "success": false, "data": null }
```
| code | 含义 | 前端处理 |
|---|---|---|
| 589500 | 团期不存在 | 提示后返回列表 |
| 589507 | 当前角色无 `group-batch:manage`(定制师、财务等) | 不展示按钮 |
| 589552 | 团期还在招募中 | 按钮置灰「成团后可催」 |
| 589850 | 没有可催的户 | 提示「需求已全部提交」 |
| 589851 | 团期已过资源准备中,需求已冻结 | 不展示按钮 |
| 589852 | 冷却中,`message` 带剩余秒数 | 按剩余秒数倒计时置灰 |
| 589853 | 通知中心暂不可用,一条都没发出 | 提示稍后重试(冷却已释放,可立即重试) |
#### 业务边界
- 权限码 `group-batch:manage`(授 `GROUP_BATCH_MANAGER` / `ADMIN` / `SUPER_ADMIN`),定制师不能点。
- 「待提交」的口径与定制师待办同源:在团户中订单处于资源准备中,且需要房(或车)、对应需求**从未提交**或**被打回定制师**;车侧整团已声明免车的户不算。
- 被房务 / 车队**退回团期管理员重审**(`REJECTED_TO_ADMIN`)的户**不催**——球在管理员手里,不在定制师。需求 Tab 车侧逐户页把这类户显示为「待重提」,与催办户数可能差这一类。
- 一次催全团,不支持按户 / 按定制师勾选;同一定制师名下多户合成一条。
- 只发员工:管理端站内信 + 企微,不发短信 / 小程序 / 公众号,客户收件箱不会出现。
- 冷却按团计 300 秒;一条都没发出(无人可催、通知中心不可用)不占冷却。部分定制师投递失败时冷却照常生效,失败的那几位也要等冷却结束才能再催(看 `recipients[].enqueued`)。
- 「已投进通知中心」不等于「已送达」:站内信 / 企微的实际结果由通知中心异步记录;未绑企微的定制师只收到站内信。
- 停用 / 离职的定制师照样会被催;订单需要改派定制师的请先改派。
- 每次点击都会写一条团期时间线(GB-ADM-096 事件类型 `BATCH_REQUIREMENT_NUDGE`「催办定制师提交需求」),不要轮询调用。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用顺序
- ✅ 进入需求 Tab 时按团期状态决定按钮:资源准备中才展示可点;招募中置灰;之后的阶段不展示。
- ✅ 点击后用响应里的 `cooldownSeconds` 做倒计时;收到 589852 时从 `message` 解析剩余秒数或直接展示 `message`。
- ✅ 用 `recipients` / `skipped` 组提示文案,`wecomBound` 为 null 时不要提示「未绑企微」。
- ❌ 不要把 `enqueued=true` 说成「已送达」。
- ❌ 不要拿响应里的户数去和需求 Tab 车侧「待重提」户数强行对齐(见业务边界第 3 条)。
---
## 五、数据库行为
- **order-v3**:不建表、不改表;每次成功催办在 `group_batch_status_log` 追加一条 DATA 流水(`event_type=BATCH_REQUIREMENT_NUDGE`,content「催办定制师提交需求(N 位定制师 · M 户)」,extra 记定制师 / 订单 / 跳过户 id 与房车户数)。冷却只在 Redis(键 `order:gb:requirement-nudge:{groupBatchId}`,TTL 300 秒),不落库。
- **user-service**:Flyway `V20261003_8752__group_batch_requirement_nudge_notification.sql` 往 `notification_event_config` 插(或按 `uk_event_code` 收敛)一行 `GROUP_BATCH_REQUIREMENT_NUDGE`:站内信 + 企微开、短信 / 小程序 / 公众号关、接收人 `ORDER_CONSULTANT`、站内信链接 `/order/todos`。通知中心消费后写 `admin_message` 与 `notification_send_log`,不写 `user_message`。
---
## 六、边界行为
| 场景 | 行为 |
|---|---|
| 团期不存在 | 589500 |
| 招募中 | 589552 |
| 物料准备中 / 待出发 / 出行中 / 核单 / 已结算 / 已取消 | 589851 |
| 资源准备中但全部户已提交 | 589850 |
| 全部待提交户都没有定制师 | 200,`consultantCount=0`、`cooldownSeconds=0`,不发不留痕 |
| 部分户无定制师 | 有定制师的照发,无定制师的列入 `skipped` |
| 300 秒内再点 | 589852,带剩余秒数 |
| 通知中心不可用(一条都没投进) | 589853,冷却释放可立即重试 |
| 部分定制师投递失败 | 200,失败者 `enqueued=false`、`message` 有原因;冷却生效 |
| 员工信息查不到 | 照常催办,`consultantName` / `wecomBound` 为 null |
---
## 六.5、枚举 / 数据字典
- `skipped[].skipReason`:目前只有「订单未指派定制师」。
- 时间线事件类型 `BATCH_REQUIREMENT_NUDGE`,中文名「催办定制师提交需求」,变更类型 DATA。
- 新错误码段 589850-589899:589850 无可催的户、589851 阶段已冻结、589852 冷却中、589853 通知中心不可用。
---
## 七、不影响范围
- **仅影响**: 新增一个写端点 + 一条通知事件配置。
- **零影响**:
- 需求 Tab 既有接口(requirement-summary、hotel-households、vehicle-households、confirm、reject 等)的入参、出参与错误码
- 定制师待办的开单 / 关单逻辑(判定抽成单源后行为不变)
- 补发成团通知 `notify-formed` 及其冷却
- 小程序端、客户通知
- 零表结构变更、零网关变更、零权限种子变更(复用 `group-batch:manage`)。
---
## 八、测试环境已验证
部署:hl-user-service 与 hl-order-service-v3 = dev-v3 @ 5b204b9d2(2026-10-03 22:31 / 22:34,user-service 先上);TEST `flyway_schema_history` 20261003.8752 success=1;四批取证前各打一次构建身份探针(不存在的团期 → 589500),经网关 `https://api.test.1814.love` 真实鉴权实测(2026-10-03 22:40–23:05);工单 #8752 已验收关单。
| # | 场景 | 结果 |
|---|---|---|
| 1 | 团期管理员(gbm8154test,单角色)催两位定制师 + 一户无定制师的团 | 200;每位定制师恰好一条;无定制师户进 `skipped`;户数与定制师待办逐户一致 |
| 2 | 落库:`admin_message` / `notification_send_log` / `user_message` | 站内信 2 行(link `/order/todos`)、ADMIN_INAPP 两行 status=0;`user_message` 前后 0 行 |
| 3 | 企微 | 未绑定的定制师 `wecomBound=false`、WEWORK status=2「无企微接收人」;已绑定的测试号调到真实企微接口、因测试假 userid 被拒(errcode 81013),**未真实送达** |
| 4 | 冷却 | 1 秒后再点 589852「298 秒」、13 秒后「286 秒」;300 秒后再点 200 |
| 5 | ADMIN / 定制师 | ADMIN 200;定制师 589507 |
| 6 | 招募中 / 全部已提交 / 已取消 / 核单中 / 不存在 | 589552 / 589850 / 589851 / 589851 / 589500 |
| 7 | 全部户无定制师的团 | 200、`cooldownSeconds=0`,不写站内信、不写发送记录、不写时间线 |
| 8 | 时间线 GB-ADM-096 | 读到 `BATCH_REQUIREMENT_NUDGE`「催办定制师提交需求(2 位定制师 · 3 户)」 |
| 9 | 投递失败释放冷却 | 本机真组件注入(真 Redis + 不可达的真 RocketMQ):589853、冷却键已释放、立即重试仍 589853 |
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #7534 | 补发成团通知(300 秒冷却先例) | ✅ |
| — | #7105 | 团期批量催签合同(批量触达 + 逐户结果先例) | ✅ |
| — | #8562 | 用车逐户区分从未提交 / 被打回 | ✅ |
| — | #8228 | 站内信 link 留空成死链(本单 link 不留空的原因) | ✅ |
| **本 PR #8777** | **#8752** | 一键催办定制师提交房 / 车需求 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8752](https://git.1814.love/wx/HL/issues/8752)
- 关联 PR: [wx/HL#8777](https://git.1814.love/wx/HL/pulls/8777)
## 关联 / 联系人
### 链接
- **Issue**: [#8752](https://git.1814.love/wx/HL/issues/8752)
- **PR**: [#8777](https://git.1814.love/wx/HL/pulls/8777)
- **Merge commit**: [5b204b9d2](https://git.1814.love/wx/HL/commit/5b204b9d2cb2459238283646c65eff41fd07a3f0)
### 联系人
- **后端负责人**: @jw