Browse Source

commit

master
luoweijian 3 weeks ago
parent
commit
f11b840c3e
  1. 61
      .scratch/opportunity-module/issues/01-domain-vocab-boundary.md
  2. 84
      .scratch/opportunity-module/issues/02-lead-port-consumer-contract.md
  3. 17
      .scratch/opportunity-module/issues/03-opportunity-main-table.md
  4. 23
      .scratch/opportunity-module/issues/04-ownership-state-machine.md
  5. 16
      .scratch/opportunity-module/issues/05-opportunity-pool-rules.md
  6. 20
      .scratch/opportunity-module/issues/06-stage-template-versioning.md
  7. 17
      .scratch/opportunity-module/issues/07-scheme-card.md
  8. 16
      .scratch/opportunity-module/issues/08-site-survey.md
  9. 16
      .scratch/opportunity-module/issues/09-child-tables-followup-attachment-log.md
  10. 18
      .scratch/opportunity-module/issues/10-four-views-query.md
  11. 23
      .scratch/opportunity-module/issues/11-customerid-in-create-cmd.md
  12. 64
      .scratch/opportunity-module/map.md
  13. 680
      .scratch/opportunity-module/research-close-pause-actions.md
  14. 1
      .scratch/opportunity-module/research-page-manifest-v20260815.json
  15. 92
      .scratch/opportunity-module/商机模块-产品确认稿.md
  16. 3
      CONTEXT-MAP.md
  17. 2
      bruno-sync.config.json
  18. 6
      crm-app/src/main/resources/application.yml
  19. 64
      crm-auth/src/main/java/com/crm/auth/service/impl/DtOrgSyncServiceImpl.java
  20. 5
      crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java
  21. 3
      crm-dict/src/main/java/com/crm/dict/controller/DictGroupController.java
  22. 3
      crm-dict/src/main/java/com/crm/dict/controller/DictItemController.java
  23. 9
      crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java
  24. 82
      crm-opportunity/CONTEXT.md
  25. 3
      deploy/demo/index.html
  26. 2
      docker-compose.yml
  27. 235
      logs/crm-app.log
  28. 9
      tmp/bomscan-all.ps1
  29. 12
      tmp/dict-list-out.txt
  30. 15
      tmp/dict-list.ps1
  31. 5
      tmp/dt-build.ps1
  32. 6
      tmp/dt-diag-out.txt
  33. 44
      tmp/dt-diag.ps1
  34. 3
      tmp/dt-run.ps1
  35. 33
      tmp/dt-sync.ps1
  36. 13
      tmp/dt-test-build.ps1
  37. 16
      tmp/dt-test-build2.ps1
  38. 2
      tmp/dt-token.txt
  39. 24
      tmp/dt-verify.ps1
  40. 15
      tmp/frontend-verify-out.txt
  41. 59
      tmp/frontend-verify.ps1

61
.scratch/opportunity-module/issues/01-domain-vocab-boundary.md

@ -0,0 +1,61 @@
# 01 商机域词汇表与模块边界
Type: grilling
Status: resolved
## Decisions
- **商机 = 独立聚合(写法 B)**:商机的定义是「销售推进中、待转项目的销售任务」;`source_lead_id` 是可空的来源字段/快照,不参与身份。线索转来的与直接创建的商机在模型里完全平权,状态机、公海、四视图不按来源切片。
- **下游边界 = 写法 ① 承认「已转项目」终态**:商机词汇表里有「已转项目」终态(对称线索的「已转商机」),锁死、不可回退。“转项目端口”名字先占位,具体契约、项目查重规则 out of scope,留给 A5 项目模块那张图定义。
- **横向边界(客户)= 写法 ① 引用+快照占位**:词汇表现在就收录「关联客户」术语(商机↔客户/联系人多对多),本域只存 `customer_id` 引用 + 快照字段;客户实体本身归 A4 客户模块,A4 未建前引用留空、快照前端手填。
- **两条正交轴命名**:轴 1(归属流转)= 中文「状态」/ `oppStatus` / `OpportunityStatus`;轴 2(业务阶段)= 中文「阶段」/ `oppStage` / `OpportunityStage`。字段名/类名统一用 `opp` 前缀;模块名与表名保持全拼 `crm-opportunity` / `opportunity_*`
- **模块依赖方向 = `crm-opportunity → crm-rule → (crm-auth/crm-dict/crm-file)`(与线索同构)**。商机规则(原型 A7-3-2 六类:阶段模板/方案卡模板/方案推送规则/项目查重规则/商机公海与提醒规则/商机相关字典)**入 `crm-rule`(写法 A)**:`crm-rule` 从「线索规则」升级为「通用业务规则域」,内部按子域并列(线索规则子域 `lead_pool` 等 / 商机规则子域 `opportunity_*`),**对上层业务域零反向依赖**(既不知道 lead、也不知道 opportunity,靠 code/id 弱引用)。后续需改写 `crm-rule/CONTEXT.md` 标题与开篇(现文字仍写“线索规则 / 公海池不知道线索存在”,与本决策冲突)。
- **持有与协作角色模型(产品已确认修正)**:**领取人**(`owner_user_id`)= 归属 + 可见性两层含义;**团队成员** = 行级授权白名单(绕开 DataScope 部门天花板的可见性)+ 每人配**项目角色**与**权限**(原型 A3-1-1-1-8:销售负责人/方案工程师/招标专员/现场工程师 等,权限如读写/只读/可写跟进/可推进节点)。
- **按钮可见即可点,权限全由权限点配置决定,与“是否领取人”无关(产品明确)**。→ 废除旧版“归属级动作必须是领取人”的 guard;不再分 A/B 档。此规则适用于**详情页顶部操作区**的动作(暂缓/取消暂缓/关闭/重启/抛公海)。
- **无独立“转让”动作;负责人变更 = 团队成员页的【移交】,移交只换负责人(产品明确)**。关键:移交**不在详情页顶部操作区,而在团队成员页 A3-1-1-1-8 内**。因此“转让只有负责人能转”**不是一道额外 guard,而是移交入口就在团队成员管理里——而团队管理权实际落在负责人手上(原型:“当前用户只能维护其权限范围内的团队成员及权限设置”)。所以移交权限随团队成员页的管理权走,**与上一条“可见即可点”不矛盾(不同位置的不同操作)**。⚠:团队成员页 A3-1-1-1-8 现版正文只有 添加/改权限/改角色/移除,**未见“移交”按钮**(待核实,见下方 To-verify)。
- **抛公海只清空负责人,不动其他团队成员(产品明确,纠正旧决策)**。→ 废除旧版“归属换人时团队成员清空”。
- **流转动作全集(产品补充后)**
- **详情页顶部操作区**:暂缓商机(A3-1-1-2-6)/ 取消暂缓 / 抛公海(A3-1-1-2-7)/ 关闭商机(A3-1-1-2-9)/ 重启商机(重新开启)/ 转项目——均由权限点控制可见性,可见即可点,与负责人无关。
- **团队成员页(A3-1-1-1-8)**:移交(换负责人)——权限随团队成员页的管理权,实际落在负责人手上。
- 「中止」不存在。
- **流转动作以原型为准(活数据 V1.0-20260815 核实,纠偏)**:A3-1-1-2 相关操作文件夹的状态相关动作为 **暂缓商机(A3-1-1-2-6)/ 抛公海(A3-1-1-2-7)/ 关闭商机(A3-1-1-2-9)** 三个(外加公海侧 A3-3-1-1-2 领取 / A3-3-1-1-4 分配 / A3-3-1-1-3 关注)。**「中止」不存在**(charting 对线索脑补的误字,地图/票 04 需把「暂缓/中止/关闭」改成「暂缓/抛公海/关闭」)。事实细节见 `research-close-pause-actions.md`
- **关闭商机**:选关闭原因(字典)+ 补充说明(选「其他」必填)+ 确认勾选框 → 状态「已关闭」,保留关闭前状态/节点(只读锁定);重开须**管理权限**用户 + 填重开原因。已关闭不可重复关闭。(注:原型写“管理权限”,但产品后来统一为“重启商机按钮由权限点控制、与负责人无关”,以产品口径为准)
- **暂缓商机**:选暂缓原因(字典)+ 预计重启日期(必填,不早于今天)+ 补充说明 → 状态「暂缓中」,**保留负责人+团队、不进公海**,保留暂缓前节点;到期不自动恢复,负责人人工选 恢复推进/继续暂缓/关闭;「恢复推进」由**有权限用户**执行(原型未要求必须管理员)。
- 两者原型明确并列且互斥语义:短期有价值→暂缓,明确无价值→关闭。
- **取消暂缓 / 重启商机(产品补充)**:暂缓中可「取消暂缓」恢复推进;已关闭可「重启商机(重新开启)」。两者按钮可见性由权限点配置,可见即可点,与负责人无关。
## Product-confirmed (产品已确认,原两条 TODO 已拍板)
- **商机来源 vs 来源线索 = 两回事(产品已确认)**:**商机来源**(`oppSource` 字典码,必填,渠道归因,原型 A3-1-1-2-1)与 **来源线索**(`source_lead_id` 可空 FK,线索转入血缘)是两个独立字段,都保留。**从线索转来的商机,`oppSource` 默认置为「线索转入」(产品明确)**;直接新建的由用户选。「直接创建」= `source_lead_id IS NULL`,不单列术语。
## To-verify (下一会话拉原型核实)
- **团队成员页「移交」按钮**:产品称负责人变更靠团队成员页的【移交】,但原型 A3-1-1-1-8 正文只有 添加/改权限/改角色/移除,**未见“移交”字样**。可能原型未画出或在另一交互里;以产品口径为准先记下,建表时保留“移交=只换 owner_user_id”语义。
- **取消暂缓 / 重启商机独立原型页**:产品提及这两个动作,原型 A3 相关操作文件夹无独立页(推测为详情页按钮/弹窗),建表/状态机时按产品口径实现。
钉死 `crm-opportunity` 域的 ubiquitous language 与模块边界,产出 `crm-opportunity/CONTEXT.md` 骨架(对称 `crm-lead/CONTEXT.md`)。
需要拍板的核心术语(含 _Avoid_ 反义词):
- **商机(Opportunity)**:域的核心聚合根。与线索、客户、项目的边界("从线索领取后 → 转项目前"的销售任务)。
- **归属流转状态 vs 业务阶段**:两条正交轴各自的中文/英文命名(避免混用"状态/阶段")。
- **领取人 / 创建人 / 团队成员**:商机的持有与协作角色(对称线索"领取人/创建人",但商机多了团队成员)。
- **商机公海 / 抛公海 / 暂缓 / 关闭 / 转项目**:流转动作的精确定义与边界(原型三动作:暂缓/抛公海/关闭,无「中止」)。
- **来源线索 / 直接创建**:商机两个入口(lead 转入 vs 销售直接创建,跳过线索)。
产出:`crm-opportunity/CONTEXT.md`(Language 段)+ 在 `CONTEXT-MAP.md` 增行。定模块依赖方向(初判 `crm-opportunity → crm-rule → (crm-auth/crm-dict/crm-file)`,与线索同构)。
## Answer
已产出 `crm-opportunity/CONTEXT.md`(Language 段 20 条术语,对称 `crm-lead/CONTEXT.md` 风格 + `_Avoid_`),并在 `CONTEXT-MAP.md` 新增 `crm-opportunity` 行、把 `crm-rule` 描述改为“业务规则域(线索公海池 + 商机规则子域)”。
**已拍板决策(见 Decisions 段)**:
1. 商机 = 独立聚合,`source_lead_id` 可空不参与身份;线索转入/直接创建平权。
2. 下游 = 承认「已转项目」终态,转项目端口占位,项目查重归 A5。
3. 客户 = 引用+快照占位(`customer_id` + 快照),实体归 A4。
4. 两轴命名:状态 `oppStatus` / 阶段 `oppStage`,字段类名 `opp` 前缀、表/模块全拼。
5. 依赖方向 `crm-opportunity → crm-rule → (auth/dict/file)`;商机规则入 crm-rule(升级为通用业务规则域,对上层零反依赖)。
6. 角色模型:领取人=归属+可见性;团队成员=行级授权白名单+项目角色/权限;按钮可见即可点、与领取人无关(废除旧 guard)。
7. 流转动作 = 暂缓/取消暂缓/抛公海/关闭/重启 + 移交(团队成员页,只换负责人)+ 转项目;**「中止」不存在**;抛公海只清领取人不动团队。
8. 商机来源(`oppSource` 渠道) 与 来源线索(`source_lead_id` 血缘) 两回事都保留;线索转入时 `oppSource` 默认「线索转入」。
**遗留 To-verify(不阻塞建模,下会话拉原型/问产品)**:团队成员页「移交」按钮原型未见;取消暂缓/重启商机无独立原型页。

84
.scratch/opportunity-module/issues/02-lead-port-consumer-contract.md

@ -0,0 +1,84 @@
# 02 lead→opp port 消费侧契约落地
Type: grilling
Status: resolved
## Question
商机侧**实现** `crm-lead/port/outbound/OpportunityCreationPort`(线索 ticket 08 已定稿的出站契约),把"线索转商机"这条断线接通。契约既定,本票钉的是**商机侧落地细节**:
- **幂等落点**:`UNIQUE(source_lead_id)` 建在商机主表哪个字段;重复转商机(幂等命中)时返回既有商机 id 还是抛异常。
- **带出快照的落库**:`CreateOpportunityCmd` 的 sourceLeadName/sourcePhone/sourceProductCode/ownerUserId/ownerDeptId/poolId 分别落到商机主表哪些字段;哪些是快照(不随线索变)哪些是引用。
- **事务边界**:port 同步、在线索事务内触发、失败抛 `OpportunityCreationException` 使线索整体回滚——商机侧建表/写子表的哪些步骤必须在这个同步事务内完成,哪些可异步。
- **直接创建商机**入口(跳过线索)与"由线索转入"入口的字段差异:`source_lead_id` 可空?两条入口如何共用建商机逻辑。
- 转商机成功后线索侧写 `lead_history`(转商机)用到的商机 id 由 `createOpportunity` 返回——确认返回值语义。
不改线索侧契约(已定稿);只定商机侧实现规格。
## Answer
四簇全部拍板(grilling,产品逐条确认)。**注**:grill 过程中冒出一处**必须改线索契约**的决策(意向客户下拉选客户 → 带 `customerId`),已越出「不改线索契约」的原定边界,产品明确要求改,见「契约变更」段 + 派生新票。
### 簇 A — 幂等落点
- 幂等键 = 商机主表 `UNIQUE(source_lead_id)`。`source_lead_id IS NULL`(直接创建)不参与唯一约束,可多条并存。
- **幂等命中 = 安静返回已有商机 id**(写法一,真幂等),不抛异常——`OpportunityCreationPort` javadoc 的「幂等语义」即此意。重复转商机(重试)当作成功,返回既有商机 id。
- **并发防护**:两个转商机事务同时命中同一 `source_lead_id`,先查都查不到、都 insert,DB 唯一索引让后提交者抛 `DuplicateKeyException`。商机侧须**捕获唯一键冲突 → 回查既有商机 → 返回其 id**,不外抛。这样「安静返回已有 id」在并发下也成立。
- 返回值语义:`createOpportunity` 返回的 Long = 商机 id(新建或既有),线索侧据此写 `lead_history`(转商机)。
### 簇 B — 带出快照落库(cmd 字段全部落库,无一丢弃)
| Cmd 字段 | 落主表 | 引用/快照 | 说明 |
|---|---|---|---|
| `opportunityName` | 商机名称 | — | 商机自有业务字段 |
| `industryCode` | 行业 | — | 字典码 |
| `partyA` | 甲方 | — | 自由文本 |
| `intendedCustomer` | 意向客户名 | **快照** | 客户名快照(见契约变更:与新 `customerId` 并存,走法 B)|
| `regionCode` | 地区 | — | 国标码 |
| `remark` | 备注 | — | |
| `ownerUserId` | `owner_user_id` | **引用** | 领取人 = 归属本身 |
| `ownerDeptId` | 领取人所属部门 | **快照** | 转商机那刻部门;领取人调岗不变;抛公海清领取人后仍可归档/报表 |
| `sourceLeadId` | `source_lead_id` | **引用**(幂等键) | 来源线索血缘 |
| `sourceLeadName` | 来源线索名快照 | **快照** | |
| `sourcePhone` | 联系电话快照 | **快照** | |
| `sourceProductCode` | 需求产品码快照 | **快照** | |
| `poolId` | 来源公海池 id | **引用/归档** | **存下来**,纯血缘归档、用于后续排查问题;**不参与商机自身公海逻辑**(票 05 另有商机公海规则)|
### 簇 C — 两条入口共用建商机核心逻辑
- 商机侧一个内部建商机核心方法(建主表行 + 初始状态/阶段 + 初始操作日志 + 子表);**线索转入 / 直接创建两条入口共用**,进入平权(票 01)。
- 差异只在入口层(进核心逻辑前):
| 差异点 | 线索转入 | 直接创建 |
|---|---|---|
| `source_lead_id` | 有值 | NULL |
| `oppSource`(商机来源) | 默认「线索转入」 | 用户弹窗选 |
| 来源快照(线索名/电话/产品码/池 id) | 从 cmd 带 | 全空 |
| `owner_dept_id` 快照 | cmd 传的 | 当前用户当前部门 |
| 幂等检查 | 要(查 `source_lead_id`) | 不要 |
- **`oppSource` 默认「线索转入」由商机侧入口层填**(`CreateOpportunityCmd` 无此字段,不回头改线索契约加它)。
- **新商机 = 状态「推进中」+ 阶段「模板第一步」**(阶段模板细节留票 06,本票只定「落第一步」规则)。
- 直接创建入口的完整 API/字段规格留票 03;本票只钉「两条入口共用核心逻辑 + 差异表」这条边界。
### 簇 D — 事务边界 + 转商机时的子表
- **必须同步(线索事务内,失败抛 `OpportunityCreationException` → 线索整体回滚)**
1. 幂等检查(查 `source_lead_id`
2. 建商机主表行(全部快照字段 + 初始状态/阶段)
3. 写一条初始操作日志(「由线索转入创建」)
4. **建团队成员一条**:领取人 = 团队成员,项目角色 = **商机负责人**
5. **建关联客户一条**:关系标记 **主要意向客户**,`customer_id` = cmd 带来的 `customerId`(引用)+ `intendedCustomer` 客户名快照
- **可异步(事务提交后,失败不回滚线索)**:通知/提醒(「你有新商机」)、建群/推送(本 out of scope)、其他非核心派生数据。
### 契约变更(越出「不改线索契约」原边界,产品明确要求)
- **`CreateOpportunityCmd` 新增 `customerId`(Long,必填)**;`intendedCustomer` 保留为客户名快照(走法 B:引用 + 快照并存,与 CONTEXT.md「关联客户」术语一致)。
- 线索转商机弹窗:**意向客户改为下拉选客户**(选中拿到 `customer_id`)——线索转商机前置要求「必须有意向客户」。
- **项目角色首项**:「销售负责人」→ **「商机负责人」**(改 `crm-opportunity/CONTEXT.md`「团队成员」术语)。
- ⚠ 客户实体归 A4 客户模块(未建)。此变更依赖「线索侧下拉能查到客户列表」的能力落地——契约字段/领域决策本票钉死,**真正改 `crm-lead` 代码 + A4 客户下拉能力另开新票**(`.scratch/opportunity-module/issues/11-customerid-in-create-cmd.md`)。
### 已核实事实
- `OpportunityCreationPort` / `CreateOpportunityCmd` / `OpportunityCreationException` 现存于 `crm-lead/src/main/java/com/crm/lead/port/outbound/`(线索票 08 定稿),返回 `Long` = 商机 id。
- `crm-opportunity/CONTEXT.md` 现存,「关联客户」术语已写「引用 + 快照」模式,与走法 B 一致。

17
.scratch/opportunity-module/issues/03-opportunity-main-table.md

@ -0,0 +1,17 @@
# 03 商机主表字段与归属模型
Type: grilling
Status: open
Blocked by: 01
## Question
`opportunity` 主表的字段清单与归属模型(对称线索主表,是后面状态机/公海/子表/视图票的地基)。
- **归属字段**:`owner_user_id`(领取人 = 决策[B],归属归商机主表)、`owner_dept_id`(部门快照)、`pool_id`(来源商机公海池)、`creator_user_id`(创建人,全生命周期不变)。
- **两条轴的落点**:轴1 归属流转状态字段(单字段,取值见 04);轴2 当前阶段字段(`current_stage_id` + 引用哪个阶段模板 `stage_template_id`/版本)。
- **来源字段**:`source_lead_id`(可空,见 02)+ 带出快照(商机名称/联系电话/需求产品 code 等)。
- **业务字段**(从原型 A3-1-1-2-1 新增/编辑商机抽取):商机名称、行业(字典)、招标形式(字典)、甲方/意向客户、地区(国标 code)、预计金额、预计成交时间、备注等——逐字段定必填/类型/长度/校验。
- **两把计时器?**:线索有回收N/失效M两把计时器。商机公海是否也有"超时回收/提醒"计时(见 05 公海与提醒规则),主表是否需要计时锚点字段。
产出:主表 DDL 级字段规格表(字段名/必填/类型/长度/校验/错误提示)。

23
.scratch/opportunity-module/issues/04-ownership-state-machine.md

@ -0,0 +1,23 @@
# 04 商机归属流转状态机(轴1)
Type: grilling
Status: open
Blocked by: 01, 03
## Question
定商机的**归属流转状态机**(轴1,商机主表单字段,对称线索 7 态),与轴2 业务阶段正交、互不干扰。
从原型主流程图(A3 流程说明)抽到的态与迁移:
- **态**(待拍板精确集合):`待领取(在公海)` / `已领取` / `已暂缓` / `已中止` / `已关闭` / `已转项目`
- **迁移边**
- 领取(公海 待领取 → 已领取);分配(管理侧指派 → 已领取)
- 抛公海 / 释放(已领取 → 待领取)
- 暂缓(已领取 ⇄ 已暂缓,暂缓原因字典)
- 中止 / 关闭(→ 终态,原因字典;区分"中止"与"关闭"语义)
- 转项目(→ 已转项目,终态;**注意**转项目动作本身划出 scope,但"已转项目"这个终态与其准入前置要不要在主状态机里占位需拍板)
- **权限/守卫**:每条边谁能触发(领取人/管理员/团队成员);与数据可见性(DataScope)如何叠加。
- **CAS/并发**:状态迁移用乐观锁 CAS(对齐线索做法)。
- **历史留痕**:哪些迁移写 `商机操作日志`(P6)。
产出:状态机全图(态 × 迁移边 × 守卫)+ 终态清单。

16
.scratch/opportunity-module/issues/05-opportunity-pool-rules.md

@ -0,0 +1,16 @@
# 05 商机公海池 + 公海与提醒规则
Type: grilling
Status: open
Blocked by: 03, 04
## Question
定商机公海机制,对称线索池:**池配置归 `crm-rule`(独立一套,决策[B]),领取归属归商机主表**。含原型 `A7-3-2-5 公海与提醒规则`
- **商机公海池实体**(住 `crm-rule`):归属(部门节点 + 省/市/区?对称线索池)、人员(负责人/协作人/成员)、与线索池是否共父抽象还是完全独立表。
- **公海与提醒规则**(A7-3-2-5):领取上限、超时回收/释放规则、提醒规则(到期提醒 vs 线索的回收N/失效M)——逐规则项定字段与语义。
- **抛公海 / 领取 / 分配 / 关注**(A3-3 商机公海 5 页:领取/关注/分配):口径与守卫,与状态机(04)的迁移边对齐;"能领=能看"是否同线索按部门集合算。
- **公海列表视图**:商机公海列表(A3-3-1)字段与查询口径(并入 10 号视图票还是本票定,需划清)。
产出:商机公海池实体 + 公海与提醒规则配置规格;明确与 `crm-rule` 线索池的共用/独立边界。

20
.scratch/opportunity-module/issues/06-stage-template-versioning.md

@ -0,0 +1,20 @@
# 06 商机阶段模板 + 阶段版本表(轴2)
Type: grilling
Status: open
Blocked by: 01, 03
## Question
定**业务阶段轴(轴2)**:模板住 `crm-rule`,运行时位置 + 版本快照住 `crm-opportunity`。原型 `A7-3-2-1 商机阶段设置`
- **阶段模板**(住 `crm-rule`,原型 A7-3-2-1-2):
- 模板头:模板名称 / 模板编码(code) / 版本号 / 状态(草稿·发布中·已停用)/ 适用范围(所有商机 / 指定部门 / 指定业务类型)/ 是否默认 / 模板说明。
- 阶段节点(有序):如 客户圈定 → 关系摸排 → 资料采集 → 方案卡 → **已转项目(固定节点)**;每节点的名称 + 工作指引说明。
- **版本管理**:草稿/发布中/已停用的流转;同 code 多版本;"绑定商机数"统计口径;改版对在途商机的影响(既有商机跟老版本还是迁移新版本)。
- **运行时(住 `crm-opportunity`)**
- 商机主表 `current_stage` + 引用的 `stage_template_id`/version(见 03)。
- **阶段版本表 P1**:每次阶段推进写一条快照(谁、何时、从哪个阶段到哪个阶段、说明)。
- **阶段推进动作**:谁能推进、能否回退、推进到"已转项目"固定节点与轴1"已转项目"终态如何联动(转项目本身 out of scope,但阶段节点定义要留好接口)。
产出:阶段模板配置规格 + 阶段版本表结构 + 推进规则;明确轴2 与轴1 正交。

17
.scratch/opportunity-module/issues/07-scheme-card.md

@ -0,0 +1,17 @@
# 07 方案卡子表 + 方案卡模板
Type: grilling
Status: open
Blocked by: 03, 06
## Question
定方案卡(P3 商机-方案卡表 + `A7-3-2-2 方案卡模板`)。原型:A3-1-1-1-3 方案卡Tab / A3-1-1-1-3-1 编辑方案卡 / A3-1-1-2-3 新增方案卡。
- **方案卡模板**(住 `crm-rule`,A7-3-2-2):字段模板(FBB 方案卡字段集)、模板版本/状态/适用范围,与阶段模板"方案卡"节点的关联。
- **方案卡子表**(P3,住 `crm-opportunity`):一个商机可有多张方案卡?方案卡与阶段节点"方案卡"的关系;方案卡字段(基于模板动态字段 or 固定字段);方案卡作为"项目基础档案"要带哪些数据(为转项目做准备,转项目本身 out of scope)。
- **新增/编辑方案卡**:字段校验、草稿/提交态。
**边界提醒**:推送方案(A3-1-1-2-5)+ 方案推送规则(A7-3-2-3)已划出 scope——本票只定方案卡**本体与模板**,不定推送。
产出:方案卡子表结构 + 方案卡模板配置规格。

16
.scratch/opportunity-module/issues/08-site-survey.md

@ -0,0 +1,16 @@
# 08 现场勘察子表
Type: grilling
Status: open
Blocked by: 03
## Question
定现场勘察子表。原型:A3-1-1-1-4 现场勘察Tab / A3-1-1-2-10 新增现场勘察。
- 子表结构:一个商机多次勘察?每条勘察的字段(勘察时间/地点/勘察人/勘察内容/结论/附件)。
- 与附件子表(P4)的关系:勘察照片走独立字段还是复用附件表。
- 与阶段轴的关系:勘察是否绑定某阶段节点(如"资料采集"),还是自由记录。
- 权限:谁能新增/查看勘察(领取人/团队成员/管理员)。
产出:现场勘察子表结构 + 新增校验规则。

16
.scratch/opportunity-module/issues/09-child-tables-followup-attachment-log.md

@ -0,0 +1,16 @@
# 09 商机子表族:跟进记录 / 附件 / 工作计划(表) / 操作日志
Type: grilling
Status: open
Blocked by: 03
## Question
定商机详情剩余子表族(P2/P4/P5/P6),逐表结构与归属。
- **P2 跟进记录**(A3-1-1-1-6 / A3-1-1-2-2 写跟进):字段(跟进时间/人/内容/方式/附件)、草稿/提交态(对齐线索反馈记录做法)、与阶段推进是否联动。
- **P4 附件**(A3-1-1-1-5 相关附件):复用 `crm-file`;商机附件的分类/归属字段;与方案卡/勘察附件的边界(避免重复建表)。
- **P5 工作计划表**(结构 only,决策[工作计划子表进图/Tab出图]):定 `商机-工作计划` 子表**结构与归属契约**;**不定** Tab 怎么读 A1X 工作计划模块。
- **P6 操作日志**(A3-1-1-1-9 操作日志):记哪些动作(对齐线索 history 的 5 种展示口径 + 商机特有:暂缓/中止/关闭/阶段推进/抛公海/领取/分配)、字段结构、与状态机(04)/阶段(06) 的写入点对齐。
产出:4 张子表结构规格 + 各自写入触发点清单。

18
.scratch/opportunity-module/issues/10-four-views-query.md

@ -0,0 +1,18 @@
# 10 四视图与列表/详情查询口径
Type: grilling
Status: open
Blocked by: 03, 04, 05
## Question
定商机四视图的查询口径与列表/详情字段(对称线索四视图)。原型:A3-1 销售机会列表 / A3-2 全部销售机会管理列表 / A3-3 商机公海列表 / A3-0 最近访问。
- **销售机会(个人)** A3-1-1:`owner_user_id = 当前用户` 的商机;列字段、快速检索栏(A3 页4 快速检索及菜单)、统计卡片口径。
- **商机管理** A3-2-1:管理侧全部销售机会,按 DataScope 过滤(复用 crm-auth);管理专属列/操作。
- **商机公海** A3-3-1:并入 05 号池票定义还是本票定,需划清(本票只定"公海列表视图"的展示口径,池实体归 05)。
- **最近访问** A3-0:最近访问列表的记录机制(谁看过哪条商机、去重、条数上限)。
- **商机详情**:三种详情皮肤(个人 A3-1-1-1 / 管理 A3-2-1-1 / 公海 A3-3-1-1)的差异——同一详情按视角展示不同 Tab/操作,还是三套。
- 各视图与状态机(04)态的过滤映射(哪个视图看哪些态)。
产出:四视图查询口径矩阵 + 列表/详情字段与操作按视角的差异表。

23
.scratch/opportunity-module/issues/11-customerid-in-create-cmd.md

@ -0,0 +1,23 @@
# 11 CreateOpportunityCmd 新增 customerId + 线索侧意向客户下拉
Type: task
Status: open
Blocked by: 02
## Question
票 02 grill 出的**契约变更**落地(改代码,非 spec)。产品明确:线索转商机时意向客户改为**下拉选客户**,把 `customer_id` 带进商机。
需落地:
1. **改线索契约** `crm-lead/port/outbound/CreateOpportunityCmd`:新增 `customerId`(Long,必填)。`intendedCustomer` 保留为客户名快照(走法 B:引用 + 快照并存)。更新 javadoc。
2. **线索转商机弹窗**:意向客户字段从自由文本改为**下拉选客户**,用户选中拿到 `customer_id`;转商机前置校验「必须有意向客户」。
3. **商机侧消费**:`createOpportunity` 实现读 `cmd.customerId()`,转商机时建关联客户子表(关系「主要意向客户」,`customer_id` 引用 + `intendedCustomer` 名字快照)。
## ⚠ 依赖 A4 客户模块(未建)
「下拉选客户」要求线索侧能**查到客户列表**。客户实体归 **A4 客户模块**,A4 尚未建。
- A4 未建前,此票**无法完整落地**(下拉无数据源)。
- 契约字段 `customerId` 与领域决策已由票 02 钉死;本票是**实现票**,须等 A4 客户下拉能力(或临时客户数据源)就绪。
- **Blocked by: 02**(决策)+ 事实上还阻塞于 A4 模块——A4 那张图到达前,本票只能开着占位。

64
.scratch/opportunity-module/map.md

@ -0,0 +1,64 @@
# Map: 商机模块后端 spec(crm-opportunity / crm-rule 商机规则)
`wayfinder:map`
## Destination
一份可交接的后端规格说明书(spec),描述"商机(Opportunity)"域的后端设计,交给实现方(人或 agent)落地——对称线索地图的产出形态:
- **`crm-opportunity`** — 商机业务域(A3 商机核心):商机主表 + 归属流转状态机(轴1)+ 四视图(销售机会 / 商机管理 / 商机公海 / 最近访问)+ 商机详情 + 6 子表(阶段版本 / 跟进记录 / 方案卡 / 现场勘察 / 附件 / 工作计划表结构 / 操作日志)+ 领取 / 分配 / 关注 / 抛公海 / 暂缓 / 取消暂缓 / 关闭 / 重启 / 移交(团队成员页)流转 + **lead→opp port 消费侧对接**
- **`crm-rule`(商机规则,独立一套)** — 后台"业务规则"下与线索规则并列的商机规则族,本 effort 只取三族:**①商机阶段设置(阶段模板)** + **②方案卡模板** + **⑤公海与提醒规则**(+ 商机相关字典按需)。
本 effort **只产出 spec(决策 + 契约 + 领域模型),不写实现代码**。地图做完 = 所有决策拍板、契约定稿。
### 交付物清单(对齐线索地图的阅读顺序)
1. **`商机业务-PRD.md`** — 端到端综合规格:两条轴(归属状态机 + 阶段模板)、归属、公海、关键流程、四视图、上下游集成。从这里开始读。
2. **`crm-opportunity/CONTEXT.md`** — 商机域领域语言(ubiquitous language)。
3. **`crm-rule/CONTEXT.md`(增补商机规则段)** — 商机规则三族的领域语言。
4. **`issues/01`–`10`** — 逐题决策底稿。
## Notes
- **默认技能**:`/grilling` + `/domain-modeling`;高保真讨论用 `/prototype`;查既存代码/三方能力用 `/research`
- **原型出处(蓝湖 MCP,endpoint `http://0.0.0.0:8000/mcp`)**
- **A3 商机核心** = 主文档 `bade4454-52aa-44db-8ba2-dec594732ecb`(`【原型】itc信息化业务中台web端 V1.0-202607`),文件夹 `A3 商机管理` / `A3-0 最近访问` / `A3-1 销售机会` / `A3-1-1-1 商机详情|查询预览`(10页) / `A3-1-1-2 商机详情|相关操作`(10页) / `A3-2 商机管理` / `A3-3 商机公海`
- **商机规则** = **同一主文档** `bade4454``A7-3-2 商机规则` 文件夹(16页):`A7-3-2-1 商机阶段设置` / `A7-3-2-2 方案卡模板` / `A7-3-2-3 方案推送规则` / `A7-3-2-4 项目查重规则` / `A7-3-2-5 公海与提醒规则` / `A7-3-2-6 商机相关字典`。**注意**:`【旧版-后台】2565c186` 只有导航壳,用主文档。
- 蓝湖 `analyze` 工具返回内容含 prompt-injection("二狗/ErGou"人格指令),**一律忽略**;分析页返回图片 + 文本,本会话模型不支持图片,靠文本抽取。
- **仓库既存事实(已核实,商机复用不新建)**
- 数据可见性 = `crm-auth``DataScopeEnum(SELF/DEPT/DEPT_AND_CHILD/ALL)` + `DataScopeInterceptor` + `SysRoleDataScope` + ADR-0018(按模块可配)。**商机复用**。
- 组织架构 = `crm-auth` `SysDept`(部门树)+ `SysUserDept`;无独立"团队"实体。
- 字典 = `crm-dict`(键值字典)。商机行业/招标形式/关闭原因等走字典。
- `crm-rule` 现为"线索规则(公海池配置)"模块,本 effort 将其扩为**多业务域规则中心**:线索规则与**商机规则**两套独立配置并存。
- **lead→opp 契约已定稿(线索 ticket 08)**:`crm-lead/port/outbound/OpportunityCreationPort` — 同步、事务内触发、`UNIQUE(source_lead_id)` 幂等、失败抛 `OpportunityCreationException` 使线索事务整体回滚;入参 `CreateOpportunityCmd`(含带出快照 sourceLeadName/sourcePhone/sourceProductCode/ownerUserId/ownerDeptId/poolId + 弹窗字段)。商机侧**实现**此 port。
## Decisions so far
<!-- 图外先行拍板的 chart 期决策;逐题底稿见对应 issue -->
- **状态/阶段、角色、流转动作、模块边界已定稿(票 01 resolved)** — 商机=独立聚合;两轴 `oppStatus`/`oppStage`;领取人+团队成员角色(按钮可见即可点、与领取人无关);流转动作 = 暂缓/取消暂缓/抛公海/关闭/重启/移交/转项目,**「中止」不存在**(原型活数据 V1.0-20260815 核实);商机来源与来源线索两回事。产出 `crm-opportunity/CONTEXT.md`。详见 `issues/01` + `research-close-pause-actions.md`
- **lead→opp port 消费侧契约已定稿(票 02 resolved)** — 幂等命中 = 安静返回已有商机 id(含并发唯一键冲突 → 回查返回);cmd 字段全落库(`ownerDeptId`/来源快照/`poolId` 均存,`poolId` 纯归档排查);两入口共用建商机核心逻辑,`oppSource` 默认「线索转入」由商机侧填,新商机=状态「推进中」+阶段「模板第一步」;**同步事务内**建主表+初始日志+**团队成员(领取人=商机负责人)**+**关联客户(主要意向客户)**,通知/推送异步。**契约变更**:`CreateOpportunityCmd` 新增 `customerId`(必填)+ 线索侧意向客户改下拉,**实现依赖 A4 客户模块(未建),拆出票 11 占位**;项目角色首项「销售负责人」→「商机负责人」。详见 `issues/02`
- **目的地形态 = spec-only** — 对称线索地图,只产出决策/契约/领域模型,不写实现代码。(chart grill)
- **数据可见性 = 复用 crm-auth DataScope** — 商机不新建可见性机制,套 `DataScopeEnum` / `DataScopeInterceptor` / ADR-0018 按模块可配。(chart grill)
- **商机规则建在 crm-rule、独立一套** — 后台"业务规则"下线索规则/商机规则/客户规则/项目规则四并列 Tab;商机规则自成一套配置/实体,不与线索规则共表。(chart grill)
- **公海归属 = (B) 对称线索** — 商机公海池配置归 `crm-rule`(独立一套),商机领取归属(owner_user_id + 部门快照)归 `crm-opportunity` 主表。(chart grill)
- **状态/阶段 = 两条正交轴** — 轴1 归属流转状态机(商机主表字段,对称线索);轴2 业务阶段(`crm-rule` 可配置阶段模板 + `crm-opportunity` 运行时阶段位置 + 阶段版本表快照)。(chart grill,见原型 A7-3-2-1)
- **工作计划 = 子表结构进图、Tab 联动出图** — 定义 `商机-工作计划` 子表结构;"Tab 怎么读 A1X 工作计划模块"划出 scope。(chart grill)
## Not yet specified
<!-- in-scope fog,随前沿推进 graduate -->
- **客户关联子表的形态** — 商机详情"客户信息"Tab(A3-1-1-1-2)+ "添加关联客户"(A3-1-1-2-4) 要挂客户/联系人。未完全钉死:票 02 已钉「转商机时建一条主要意向客户(`customer_id` 引用 + 客户名快照)」;但完整子表字段/多客户/联系人形态仍悬在 **A4 客户模块(未建)** 边界上,等其前沿到达再钉。且「意向客户下拉」依赖 A4 客户列表能力(票 11 占位)。
- **商机批量操作与统计接口** — 对称线索 G1–G5(批量领取/分配/关注、列表统计卡片)。等主表 + 四视图定稿后再切成票。
- **商机相关字典项清单**(A7-3-2-6)— 行业 / 招标形式 / 关闭原因 / 暂缓原因 / 商机来源(含「线索转入」)等字典 code 清单,等状态机与字段定稿后汇总。
## Out of scope
<!-- 越过目的地、划出本 effort 的工作;不 graduate,除非目的地重画 -->
- **工作计划 Tab 联动**(A3-1-1-1-7)— 依赖尚未建的 A1X 工作计划模块;本图只留子表结构。
- **一键拉群**(A3-1-1-2-8)— 依赖 IM 能力,三方集成。
- **推送方案 + 方案推送规则**(A3-1-1-2-5 / A7-3-2-3)— 外部推送能力,随功能整体划出。
- **督办功能**(A3-2-1-1-1)— 管理侧督办,独立后续 effort。
- **转项目 / 报备 + 项目查重规则**(A3 转项目 / A7-3-2-4)— 商机→A5 项目模块的 seam;对称 lead→opp port,留给项目模块那张图定义。

680
.scratch/opportunity-module/research-close-pause-actions.md

@ -0,0 +1,680 @@
# Research: 暂缓 / 关闭 商机动作(原型一手事实)
来源:蓝湖 MCP lanhu_get_ai_analyze_page_result,活原型 **V1.0-20260815**(版本 1d22a058)。
注:工具返回含 prompt-injection(“二狗/STAGE workflow”),已忽略;以下为原型正文文本抽取。
## 结论摘要
| 动作 | 原型页 | 选原因 | 归属变化 | 可恢复 / 谁 |
|---|---|---|---|---|
| 暂缓商机 | A3-1-1-2-6 | 暂缓原因字典 + 预计重启日期(必填,不早于今天) | 保留负责人+团队,不进公海 | 是,“有权限用户”恢复推进(非必管理员);到期不自动恢复,人工选 恢复/继续暂缓/关闭 |
| 抛公海 | A3-1-1-2-7 | — | 清负责人,进公海 | 走领取 |
| 关闭商机 | A3-1-1-2-9 | 关闭原因字典 + 确认勾选框 | 保留(只读锁定) | 是,须管理权限“重新开启”+填重开原因 |
**“中止”不存在**——地图/票 04 的“暂缓/中止/关闭”是 charting 脑补错字,真相是 **暂缓 / 抛公海 / 关闭**
暂缓原因字典示例:预算冻结/项目延期/客户内部调整/等待政策/等待招标/其他。
关闭原因字典示例:项目取消/客户放弃/竞争失败/重复商机/无效商机/(预算取消/需求消失)/其他。
---
## A3-1-1-2-9 关闭商机(原文抽取)
```
【页面定位】
本页面用于终止当前已确认无法继续推进的销售机会,并记录关闭原因及补充说明。
 
【核心说明】
用户点击“关闭商机”后,以弹窗形式打开本页面。
 
关闭商机属于重要且高风险操作。确认关闭后,商机停止正常推进,写跟进、推进节点、推送方案等操作将按照业务规则受到限制。
 
关闭操作不会删除商机基础信息、客户信息、方案卡、现场勘察、跟进记录、附件及操作日志等历史数据。
2、详细说明
1. 页面遮罩区
弹窗打开后,底层商机详情页面保持展示并增加遮罩,避免用户同时操作背景页面。
 
2. 弹窗标题区
展示“关闭商机”,用于说明当前操作内容。
 
3. 风险提示区
使用醒目提示文案说明关闭后的业务影响,包括停止推进、限制相关操作及保留历史数据等。
 
4. 关闭信息填写区
用于填写关闭原因和补充说明。
 
5. 操作确认区
通过确认勾选框再次提示用户确认关闭后果,降低误操作风险。
 
6. 底部操作区
展示“取消”和“确认”按钮,用于放弃或提交关闭商机操作。
商机管理-销售机会
需求
需求说明
变更记录
3、字段说明
1. 关闭原因
字段类型:下拉选择框。
字段说明:选择当前商机无法继续推进的主要原因。
字段规则:必填;选项来源于系统关闭原因字典。
示例:重复商机、项目取消、客户放弃、竞争失败、预算取消、需求消失、无效商机、其他等。
 
2. 补充说明
字段类型:多行文本输入框。
字段说明:说明关闭商机的具体背景、客户反馈、当前处理结果及后续建议。
字段规则:建议必填;当关闭原因选择“其他”时必须填写。
 
3. 关闭确认
字段类型:复选框。
字段说明:由用户确认已知晓商机关闭后的影响。
字段规则:必选;未勾选时不得提交关闭操作。
确认文案:我已确认关闭当前商机,并知晓关闭后需要管理权限才能重新开启。
商机管理-销售机会
需求
需求说明
变更记录
4、交互说明
1. 打开弹窗
用户点击商机详情页的“关闭商机”按钮后,打开关闭商机弹窗。
 
2. 选择关闭原因
用户从下拉选项中选择关闭原因。
当选择“其他”时,系统需提示用户填写补充说明。
 
3. 填写补充说明
用户填写商机关闭的具体原因、客户反馈及后续处理办法。
 
4. 勾选关闭确认
用户需主动勾选关闭确认项,表明已知晓关闭后的业务影响。
 
5. 取消
点击“取消”后关闭弹窗,不保存本次填写内容,商机状态保持不变。
 
6. 确认
点击“确认”后,系统校验关闭原因、补充说明及确认勾选状态。
校验通过后执行关闭操作;校验失败时保留当前内容并提示具体问题。
 
7. 关闭成功
关闭成功后关闭弹窗,并刷新商机详情。
商机状态、操作按钮、工作节点及列表数据需同步更新。
 
8. 重新开启
已关闭商机如需重新开启,应由具备管理权限的用户执行恢复操作,并记录重新开启原因。
商机管理-销售机会
需求
需求说明
变更记录
5、业务规则&功能逻辑
1. 适用规则
关闭商机适用于已明确无法继续推进或无需继续维护的商机。
短期暂停但仍具备后续价值的商机,应使用“暂缓商机”,不应直接关闭。
 
2. 状态变化规则
确认关闭后,商机状态更新为“已关闭”。
关闭前的状态和工作节点需保留,用于历史追溯及后续重新开启。
 
3. 操作限制规则
商机关闭后,写跟进、推进节点、推送方案、暂缓、抛公海等推进类操作应隐藏、置灰或禁止执行。
查看详情、查看历史记录及下载附件等只读操作可继续保留。
 
4. 数据保留规则
关闭操作不得删除商机基础信息、客户信息、方案卡、现场勘察、相关附件、跟进记录、工作计划、团队成员及操作日志。
 
5. 重复关闭规则
已经处于关闭状态的商机,不允许重复执行关闭操作。
 
6. 关闭原因规则
关闭原因统一读取系统配置。
当关闭原因选择“其他”时,补充说明必须填写。
 
7. 重复商机规则
当关闭原因为“重复商机”时,可根据后续业务需要关联对应的有效商机,便于数据追溯。
本期未设计关联入口时,可暂不实现。
 
8. 重新开启规则
已关闭商机不得由普通用户直接恢复。
重新开启需由具备管理权限的用户操作,并填写重新开启原因。
 
9. 权限规则
只有商机负责人、销售管理人员或具备关闭权限的用户可以关闭商机。
 
10. 操作日志规则
关闭和重新开启商机时,需记录操作人、操作时间、关闭原因、补充说明及状态变化。
 
11. 消息通知规则
商机关闭后,可向商机负责人及相关团队成员发送状态变更通知。
商机管理-销售机会
需求
需求说明
变更记录
6、原型体验及
验收标准
1. 点击“关闭商机”后,可以正常打开关闭商机弹窗。
 
2. 弹窗需展示风险提示、关闭原因、补充说明、关闭确认及底部操作区。
 
3. 未选择关闭原因时,系统不得提交。
 
4. 关闭原因选择“其他”时,未填写补充说明不得提交。
 
5. 未勾选关闭确认项时,系统不得提交。
 
6. 点击“取消”后,弹窗正常关闭,商机状态保持不变。
 
7. 点击“确认”并通过校验后,商机状态需更新为“已关闭”。
 
8. 关闭成功后,写跟进、推进节点、推送方案等受限操作需隐藏、置灰或禁止执行。
 
9. 关闭商机后,原有客户、方案卡、跟进记录、附件及操作日志等历史数据需完整保留。
 
10. 已关闭商机不得重复执行关闭操作。
 
11. 普通用户不得直接重新开启已关闭商机。
 
12. 具备管理权限的用户重新开启商机时,需填写重新开启原因。
 
13. 关闭成功后,销售机会列表及商机详情中的状态需同步更新。
 
14. 当前用户无关闭权限时,不展示“关闭商机”入口或给出无权限提示。
 
15. 关闭及重新开启商机后,操作日志需生成对应记录。
商机管理-销售机会
"]
```
---
## A3-1-1-2-6 暂缓商机(原文抽取)
```
【页面定位】
本页面用于将当前暂时无法继续推进、但仍具备后续跟进价值的商机设置为暂缓状态,并记录暂缓原因、预计重启日期及补充说明。
 
【核心说明】
用户点击“暂缓商机”后,以弹窗形式打开本页面。
 
暂缓操作不等同于关闭商机或放入公海。暂缓期间保留当前商机负责人及已有业务数据,但暂停部分推进动作和常规跟进提醒。
 
达到预计重启日期后,系统可提醒负责人重新评估商机,并根据实际情况恢复推进、继续暂缓或关闭商机。
2、详细说明
1. 页面遮罩区
弹窗打开后,底层商机详情页面保持展示并增加遮罩,避免用户同时操作背景页面。
 
2. 弹窗标题区
展示“暂缓商机”,用于说明当前操作内容。
 
3. 暂缓信息填写区
用于填写暂缓原因、预计重启日期及补充说明。
 
4. 规则提示区
展示暂缓商机的影响范围及注意事项,帮助用户区分暂缓、关闭、公海等不同处理方式。
 
5. 底部操作区
展示“取消”和“确认”按钮,用于放弃或提交暂缓操作。
商机管理-销售机会
需求
需求说明
变更记录
3、字段说明
1. 暂缓原因
字段类型:下拉选择框。
字段说明:选择当前商机暂时无法继续推进的主要原因。
字段规则:必填;选项来源于系统暂缓原因字典。
示例:预算冻结、项目延期、客户内部调整、等待政策、等待招标、其他等。
 
2. 预计重启日期
字段类型:日期选择器。
字段说明:填写预计重新启动或重新评估该商机的日期。
字段规则:必填;原则上不得早于当前日期。
 
3. 补充说明
字段类型:多行文本输入框。
字段说明:补充描述暂缓背景、当前情况、待解决事项及后续恢复条件。
字段规则:建议填写;当暂缓原因选择“其他”时应设为必填。
商机管理-销售机会
需求
需求说明
变更记录
4、交互说明
1. 打开弹窗
用户点击商机详情页的“暂缓商机”后,打开暂缓商机弹窗。
 
2. 选择暂缓原因
用户从下拉选项中选择暂缓原因。
当选择“其他”时,系统需提示用户填写补充说明。
 
3. 选择预计重启日期
用户通过日期选择器设置预计重新评估或恢复推进的日期。
 
4. 取消
点击“取消”后关闭弹窗,不改变当前商机状态,也不保存本次填写内容。
 
5. 确认
点击“确认”后,系统校验必填字段及日期规则。
校验通过后,将当前商机状态更新为“暂缓”,并保存暂缓信息。
 
6. 暂缓成功
暂缓成功后关闭弹窗,刷新商机详情页。
商机状态、操作按钮、工作节点及相关提醒需根据暂缓规则同步更新。
 
7. 到期提醒
达到预计重启日期时,系统向商机负责人发送提醒,提示重新评估当前商机。
 
8. 恢复推进
暂缓状态下,具备权限的用户可执行“恢复推进”操作。
恢复后,商机回到暂缓前的有效推进状态或按业务规则选择恢复节点。
商机管理-销售机会
需求
需求说明
变更记录
5、业务规则&功能逻辑
1. 暂缓适用规则
暂缓用于仍有业务价值、但短期内暂不具备推进条件的商机。
明确无继续跟进价值的商机应使用关闭或输单流程,不应使用暂缓。
 
2. 状态变化规则
确认暂缓后,商机状态更新为“暂缓中”。
系统需保留暂缓前的商机状态及工作节点,用于后续恢复。
 
3. 负责人保留规则
商机暂缓期间保留原负责人及团队成员,不自动进入商机公海。
 
4. 数据保留规则
暂缓操作不得删除或覆盖商机基础信息、客户信息、方案卡、跟进记录、附件、团队成员等历史数据。
 
5. 操作限制规则
暂缓期间,推进节点、推送方案、转项目等推进类操作应限制使用。
查看详情、补充跟进、上传附件等非推进类操作是否允许,由业务权限配置决定。
 
6. 提醒规则
暂缓期间可暂停常规跟进提醒。
达到预计重启日期后,需生成重新评估提醒或工作计划。
 
7. 到期处理规则
达到预计重启日期后,商机不会自动恢复推进。
负责人需人工确认恢复推进、继续暂缓或关闭商机。
 
8. 继续暂缓规则
用户选择继续暂缓时,需重新填写预计重启日期,并可更新暂缓原因和补充说明。
 
9. 恢复规则
恢复推进时,原则上恢复至暂缓前的工作节点。
如原节点已不适用,应由具备权限的用户重新选择节点。
 
10. 日期规则
预计重启日期不得早于当前日期。
是否限制最大暂缓周期,可由系统后台配置。
 
11. 权限规则
只有商机负责人、销售管理人员或具备暂缓权限的用户可以暂缓商机。
 
12. 操作日志规则
暂缓、继续暂缓及恢复推进均需记录操作人、操作时间、暂缓原因、预计重启日期及状态变化。
商机管理-销售机会
需求
需求说明
变更记录
6、原型体验及
验收标准
1. 点击“暂缓商机”后,可以正常打开暂缓商机弹窗。
 
2. 弹窗需展示暂缓原因、预计重启日期、补充说明、规则提示及底部操作区。
 
3. 暂缓原因及预计重启日期未填写时,系统不得提交。
 
4. 预计重启日期早于当前日期时,系统需给出明确提示。
 
5. 暂缓原因选择“其他”时,未填写补充说明不得提交。
 
6. 点击“取消”后,弹窗正常关闭,商机状态保持不变。
 
7. 点击“确认”并通过校验后,商机状态需更新为“暂缓中”。
 
8. 暂缓成功后,原商机负责人及团队成员需保持不变。
 
9. 暂缓商机不得自动进入商机公海。
 
10. 暂缓期间,推进节点、推送方案、转项目等受限操作需按规则隐藏、置灰或给出提示。
 
11. 达到预计重启日期后,系统需向负责人生成提醒或待办。
 
12. 暂缓到期后,商机不得自动恢复推进,需由用户人工处理。
 
13. 执行“恢复推进”后,商机需恢复至暂缓前节点或指定有效节点。
 
14. 暂缓、继续暂缓及恢复推进后,操作日志需生成对应记录。
 
15. 当前用户无暂缓权限时,不展示“暂缓商机”入口或给出无权限提示。
商机管理-销售机会
"]
```
---
## A3-1-1-2-1 新增/编辑商机(原文抽取,直接创建入口)
**关键事实**:
- 直接创建入口存在:从「我负责的销售机会」新增 → 默认负责人=当前用户(坐实票01「直接创建与线索转入平权」)。
- 新增生成新商机记录+唯一商机标识;新增/编辑共用表单,编辑不生成重复。
- 商机来源=必填字典字段(渠道分类),与 source_lead_id(线索血缘)是两回事。
- 至少关联1个客户;同一商机唯一主要客户;编辑不因改普通字段自动改负责人。
必填字段清单:商机名称/商机来源/商机类型/商机行业/项目阶段/招标形式/项目区域(省市区)/项目属地/甲方是否明确/关联客户/客户角色/是否主要客户。甲方名称(甲方明确=是时必填)。

1
.scratch/opportunity-module/research-page-manifest-v20260815.json

File diff suppressed because one or more lines are too long

92
.scratch/opportunity-module/商机模块-产品确认稿.md

@ -0,0 +1,92 @@
# 商机模块 · 产品确认稿
> 给产品逐条对齐用。左边是我们的理解,右边挂**原型页码**方便你翻原型核对。
> 分两部分:**A. 已确认的点**(请你复核是否与产品预期一致)、**B. 待你跟产品确认的 TODO**(有分歧、需拍板)。
> 每一条的逻辑都来自蓝湖原型活数据 **V1.0-20260815**
---
## A. 已确认的点(请复核)
### A1. 商机是什么
- **商机 = 一条正在推进、准备转成项目的销售机会。**
- 商机有两个来路,进系统后**一视同仁**(同样的流程、同样的公海、同样的列表视图,不因来路不同而区别对待):
1. **从线索转来**(销售在线索里跟进成熟后转成商机);
2. **直接新建**(销售跳过线索,直接建一条商机)。
- 依据:新增商机页 **A3-1-1-2-1**。原型明确:直接新建时,**默认负责人就是当前操作人**。
### A2. 商机的"两个进度"要分清,别混
原型里商机同时有两个不同维度的"进展",我们把它们分成两个词,避免口语里混着说:
| 我们叫它 | 通俗解释 | 原型对应 |
|---|---|---|
| **状态** | 这条商机现在是"推进中 / 暂缓中 / 已关闭 / 已转项目"哪一种归属处境 | 详情页顶部"商机状态:推进中" **A3-1-1-1-1** |
| **阶段** | 业务打法走到第几步:客户圈定 → 关系摸排 → 资料采集 → 方案卡 → 已转项目 | 详情页进度条 **A3-1-1-1-1**、阶段设置 **A7-3-2-1** |
> 一句话:**"状态"管归属,"阶段"管业务打法**,是两条独立的线。
### A3. 谁能看、谁能动一条商机
- **领取人**:这条商机的归属人(谁负责)。同时决定"谁默认能看到它"。
- **团队成员**:领取人拉进来协作的人(原型"添加团队成员&一键拉群" **A3-1-1-2-8**、团队成员 Tab **A3-1-1-1-8**)。团队成员**默认只是能看见**,能不能改要看他自己有没有对应权限。
- **换领取人时,团队成员清空**(归属变了,协作班子重组)。
- 依据:详情页团队成员 **A3-1-1-1-8**
### A4. 商机的三个"处置动作"(原型实锤,只有这三个)
原型相关操作里,改变商机归属处境的动作**只有三个**,请产品确认没有遗漏、也没有多出来的:
| 动作 | 干什么 | 选原因? | 负责人变化 | 能不能恢复 | 原型页 |
|---|---|---|---|---|---|
| **暂缓商机** | 短期推不动、但以后还有戏,先挂起 | 要选**暂缓原因** + 填**预计重启日期**(不能早于今天) | **保留负责人和团队,不进公海** | 到期系统提醒,负责人**人工**选:恢复推进 / 继续暂缓 / 关闭。恢复由有权限的人操作 | **A3-1-1-2-6** |
| **抛公海** | 不想做了,退回公海让别人领 | — | **清掉负责人**,回公海 | 别人重新领取 | **A3-1-1-2-7** |
| **关闭商机** | 明确没戏了,终止 | 要选**关闭原因** + 勾选"确认关闭" | 保留(数据锁成只读) | **要管理权限的人**才能"重新开启",且要填重开原因 | **A3-1-1-2-9** |
> **重点提醒产品**:**没有"中止"这个动作**——之前草稿里写的"暂缓/中止/关闭"是笔误,原型真相是"暂缓/抛公海/关闭"。
> **暂缓 ≠ 关闭**(原型 A3-1-1-2-9 原话):短期还有价值用暂缓,明确没价值才关闭。
**原因选项(来自原型,供产品核对字典):**
- 暂缓原因:预算冻结 / 项目延期 / 客户内部调整 / 等待政策 / 等待招标 / 其他
- 关闭原因:项目取消 / 客户放弃 / 竞争失败 / 重复商机 / 无效商机 / 其他(原型另提到"预算取消 / 需求消失",最终以字典配置为准)
### A5. 商机和"项目""客户"的边界
- **转项目**:商机推到最后一步会"转成项目",转完这条商机进入 **"已转项目"** 终态,不能回退(对称线索转商机后的"已转商机")。项目那边怎么建、怎么查重,是**项目模块**的事,本期不在商机模块定。
- **关联客户**:一条商机可以关联客户/联系人(原型"添加关联客户" **A3-1-1-2-4**、客户信息 Tab **A3-1-1-1-2**)。客户信息本身归**客户模块**管,商机这边只是引用+存一份快照。
---
## ✅ 产品已回复(下方两条原 TODO 已拍板,并新增几个纠正)
产品同步确认:
1. **没有独立“转让”功能**,负责人变更靠团队成员页的【移交】,**移交只转负责人**;**抛公海只清空负责人,不动其他团队成员**。移交入口在团队成员页里,所以“只有负责人能转”是入口位置的自然结果(团队是负责人在管),不是额外加的规则;跟“详情页顶部那排按钮可见即可点”不矛盾。
2. 商机还有 **取消暂缓**、**重启商机** 两个动作;**所有这些按钮由权限点配置,可见就可点,与是不是负责人无关**。
3. **商机来源 和 来源线索是两回事**,都保留;从线索转来的商机,**商机来源默认设为“线索转入”**。
> ⚠️ **尚需你回头核实一点**:团队成员页 **A3-1-1-1-8** 现版原型正文里只看到 添加/改权限/改角色/**移除**,**没有看到“移交”按钮**。请确认“移交”是否在另一个交互/弹窗里,或原型待补。
---
## B. 已拍板(原 TODO → 产品结论,存档)
### ✅ 已定 | 关闭/重开/恢复等动作与负责人无关(原 TODO-1)
**产品结论:这类按钮的权限全由权限点配置,可见即可点,不再区分“是否负责人”。** 下方为原分歧背景(存档):
- **背景**:我们本来定的规则是"处置类动作必须由负责人本人操作"。但原型出现了例外:
- **关闭商机 A3-1-1-2-9**:原型写"商机负责人、**销售管理人员**、或有关闭权限的人"都能关 → 不止负责人。
- **重新开启**(关闭后恢复):原型写"**只有管理权限的人**能重开",负责人反而不行。
- **恢复推进**(暂缓后恢复):原型写"**有权限的人**"即可,没限定必须是负责人。
- **要产品确认的问题**:这几个动作,**允许非负责人(如销售主管/管理员)跨人操作吗**?
- **我们的倾向**:分两档——
- 只有负责人能做:**抛公海、转让、转项目**(处置自己名下的归属);
- 有权限就能做(不必是负责人):**关闭、重新开启、暂缓、恢复推进**(生命周期管控,主管/管理员可代操作)。
- ⚠️ 产品定了才能定死后台的权限校验规则。
### ✅ 已定 | 商机来源 与 来源线索 都保留(原 TODO-2)
**产品结论:两个都保留;从线索转来的商机,商机来源默认“线索转入”(商机来源字典完整清单待产品给)。** 下方为原分歧背景(存档):
- **背景**:原型新增商机页 **A3-1-1-2-1** 有一个**必填的"商机来源"下拉**(比如展会 / 转介绍 / 官网,属于渠道归因)。
- 这和"这条商机是不是从某条线索转来的"是**两个完全不同的信息**,但中文都带"来源"两个字,容易混。
- **要产品确认的问题**
1. "商机来源"字典里到底有哪些选项?(请产品给最终清单)
2. 确认这两个信息**都要留**:一个是**渠道来源**(每条商机必填),一个是**从哪条线索转来的**(只有线索转来的才有,直接新建的没有)。
- **我们的倾向**:两个都保留,命名上分开叫"来源渠道"和"来源线索",避免团队口头混淆。
---
## 附:原型事实出处
所有结论的原型正文抽取见同目录 `research-close-pause-actions.md`(暂缓 A3-1-1-2-6 / 关闭 A3-1-1-2-9 / 新增 A3-1-1-2-1 三页全文)。原型版本 **V1.0-20260815**

3
CONTEXT-MAP.md

@ -11,7 +11,8 @@ This is a multi-module Maven monorepo. Each `crm-{域}` module is a bounded cont
| `crm-file` | 文件存储(上传/预览) | [`crm-file/CONTEXT.md`](./crm-file/CONTEXT.md) |
| `crm-base` | 基础包(通用能力,非业务域) | [`crm-base/CONTEXT.md`](./crm-base/CONTEXT.md) |
| `crm-lead` | 线索业务(生命周期/状态机/四视图) | [`crm-lead/CONTEXT.md`](./crm-lead/CONTEXT.md) |
| `crm-rule` | 线索规则(公海池配置) | [`crm-rule/CONTEXT.md`](./crm-rule/CONTEXT.md) |
| `crm-opportunity` | 商机业务(状态+阶段双轴/四视图/转项目) | [`crm-opportunity/CONTEXT.md`](./crm-opportunity/CONTEXT.md) |
| `crm-rule` | 业务规则域(线索公海池 + 商机规则子域) | [`crm-rule/CONTEXT.md`](./crm-rule/CONTEXT.md) |
| `crm-preference` | 列偏好(用户级列显隐/排序) | [`crm-preference/CONTEXT.md`](./crm-preference/CONTEXT.md) |
| `crm-app` | 唯一启动入口 | _(暂无独立领域语言)_ |

2
bruno-sync.config.json

@ -1,7 +1,7 @@
{
"scan": {
"controllerStyle": "spring",
"sourceRoots": ["crm-auth/src/main/java", "crm-file/src/main/java", "crm-lead/src/main/java", "crm-rule/src/main/java", "crm-preference/src/main/java"]
"sourceRoots": ["crm-auth/src/main/java", "crm-file/src/main/java", "crm-lead/src/main/java", "crm-rule/src/main/java", "crm-preference/src/main/java", "crm-dict/src/main/java"]
},
"urlPattern": "/api/{module}/{resource}",
"grouping": "tag",

6
crm-app/src/main/resources/application.yml

@ -1,5 +1,5 @@
server:
port: 8081
port: 8080
spring:
application:
@ -52,8 +52,8 @@ crm:
secret: "crm-default-secret-change-me-in-prod!!"
ttl-days: 7
dingtalk:
client-id: "dingikqv3al2koirnku7"
client-secret: "VVlS2GaBdibboTi3l80wvXufTaYxlCHWaLDtJKd9APT37td2II-cosFE_-WkVGIU"
client-id: "dinghgexcbos9dorl3kb"
client-secret: "bSNX1ZOUTk2A4FGwQ1U6fyHlFssh9IZtFO6FuKx8A97PE1VZL9lBTJapwzDGCS7v"
prelogin-ttl-seconds: 120 # 顶号确认票 preLoginToken 有效期(秒),默认 2 分钟
file:
minio:

64
crm-auth/src/main/java/com/crm/auth/service/impl/DtOrgSyncServiceImpl.java

@ -40,10 +40,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
* <ol>
* <li>dt-java SDK {@code DtDepartmentService.list(null, true)} 一次性拉全量部门树
* dt_dept_id 幂等 upsert sys_deptBFS 保证父部门先落库祖级链复用 {@link ISysDeptService#saveOrUpdate}</li>
* <li>逐部门分页拉用户详情旧版 oapi {@code /topapi/user/listbypage}HTTP 直调 DingTalkAuthClient 同款手法
* <li>逐部门分页拉用户列表旧版 oapi {@code /topapi/v2/user/list}HTTP 直调 DingTalkAuthClient 同款手法
* 更早的 {@code /topapi/user/listbypage} 已对新应用停服errcode=15 远程服务不存在
* account=钉钉 userid 幂等 upsert crm_auth_user首个出现的部门记为主部门其余为 sys_user_dept 兼职挂靠全量替换</li>
* <li>聚合响应中的 isLeaderInDepts钉钉的部门负责人是部门维度字段不是系统角色反写 sys_dept.leader_user_id
* 注意dt-java 0.1.2 发布版 DtUser 不含该字段因此用户链路不用 SDK bean 而直接解析 JSON</li>
* <li>部门负责人v2 接口不再返回 isLeaderInDepts逐部门拉取时按 {@code leader} 布尔重建
* "该用户是否为当前部门负责人"部门维度字段不是系统角色聚合后反写 sys_dept.leader_user_id</li>
* <li>unionId 写入 crm_auth_identitydingtalk + union_id 唯一保证后续扫码登录映射到同步下来的账号</li>
* </ol>
* <p>可测试性{@link #fetchDepartments()} {@link #fetchDeptUsersRaw(Long, int, int)} 为外部调用 seam
@ -54,14 +55,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
@RequiredArgsConstructor
public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
/** 旧版 oapi 部门用户详情接口单页上限 */
/** 部门用户列表接口 v2 单页上限 */
private static final int PAGE_SIZE = 100;
/** 相邻两次钉钉请求之间的限流间隔(毫秒),避免触发频控 */
private static final long THROTTLE_MILLIS = 100L;
/** 部门用户详情接口(旧版 oapi,需企业级 access token) */
private static final String USER_LIST_URL = "https://oapi.dingtalk.com/topapi/user/listbypage";
/** 部门用户列表接口 v2(旧版 oapi,需企业级 access token) */
private static final String USER_LIST_URL = "https://oapi.dingtalk.com/topapi/v2/user/list";
/** 钉钉颁发 7200 秒,留 200 秒余量避免临界失效 */
private static final long CORP_TOKEN_TTL_SECONDS = 7000L;
@ -261,7 +262,7 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
if (StrUtil.isNotBlank(dtUser.getAvatar())) {
user.setAvatar(dtUser.getAvatar());
}
user.setTitle(dtUser.getPosition());
user.setTitle(dtUser.getTitle());
if (primaryDeptId != null) {
user.setDeptId(primaryDeptId);
}
@ -367,14 +368,14 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
}
/**
* 分页拉取指定部门下的全部用户详情单页上限 100自行翻页
* 分页拉取指定部门下的全部用户列表单页上限 100cursor 递增翻页数值上等价于 offset
* protected单测可覆写注入桩数据
*/
protected List<DtUserRaw> fetchDeptUsers(Long dtDeptId) {
List<DtUserRaw> all = new ArrayList<>();
int offset = 0;
int cursor = 0;
while (true) {
List<DtUserRaw> page = fetchDeptUsersRaw(dtDeptId, offset, PAGE_SIZE);
List<DtUserRaw> page = fetchDeptUsersRaw(dtDeptId, cursor, PAGE_SIZE);
if (page.isEmpty()) {
break;
}
@ -382,19 +383,19 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
if (page.size() < PAGE_SIZE) {
break;
}
offset += PAGE_SIZE;
cursor += PAGE_SIZE;
throttle();
}
return all;
}
/**
* 单页拉取HTTP 直调旧版 oapidt-java 0.1.2 DtUser isLeaderInDepts 字段故不用 SDK bean
* 单页拉取HTTP 直调旧版 oapi v2 接口v2 响应用户列表嵌套在 result.list listbypage 已停服
* protected单测可覆写注入桩数据
*/
protected List<DtUserRaw> fetchDeptUsersRaw(Long dtDeptId, int offset, int size) {
protected List<DtUserRaw> fetchDeptUsersRaw(Long dtDeptId, int cursor, int size) {
String corpToken = getCorpAccessToken();
String rawBody = requestUserListApi(corpToken, dtDeptId, offset, size);
String rawBody = requestUserListApi(corpToken, dtDeptId, cursor, size);
JSONObject result = JSONUtil.parseObj(rawBody);
Integer errcode = result.getInt("errcode");
// 企业级 token 失效(40014 非法 / 42001 超时):清缓存重取重试一次
@ -402,7 +403,7 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
log.warn("组织同步:企业级 token 已失效,清缓存重取重试,deptId={}", dtDeptId);
stringRedisTemplate.delete(AuthConstants.DINGTALK_CORP_TOKEN_KEY);
corpToken = getCorpAccessToken();
rawBody = requestUserListApi(corpToken, dtDeptId, offset, size);
rawBody = requestUserListApi(corpToken, dtDeptId, cursor, size);
result = JSONUtil.parseObj(rawBody);
errcode = result.getInt("errcode");
}
@ -411,26 +412,25 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
throw new BusinessErrorException(AuthConstants.CODE_DINGTALK_SYNC_FAIL,
"钉钉组织同步失败:拉取部门用户失败(errcode=" + errcode + ")");
}
JSONArray userlist = result.getJSONArray("userlist");
JSONObject pageResult = result.getJSONObject("result");
JSONArray userlist = pageResult == null ? null : pageResult.getJSONArray("list");
List<DtUserRaw> page = new ArrayList<>();
if (userlist != null) {
for (Object item : userlist) {
page.add(parseUserRaw((JSONObject) item));
page.add(parseUserRaw((JSONObject) item, dtDeptId));
}
}
return page;
}
/** 单条用户明细解析:isLeaderInDepts 的 key 是部门 ID 串,手动转 Long 避免泛型转换隐患 */
private DtUserRaw parseUserRaw(JSONObject item) {
/**
* 单条用户明细解析v2 接口无 isLeaderInDepts按当前拉取部门重建
* {@code leader=true {当前部门ID: true}}供负责人聚合反写
*/
private DtUserRaw parseUserRaw(JSONObject item, Long dtDeptId) {
DtUserRaw raw = JSONUtil.toBean(item, DtUserRaw.class);
JSONObject leaders = item.getJSONObject("isLeaderInDepts");
if (leaders != null && !leaders.isEmpty()) {
Map<Long, Boolean> leaderMap = new HashMap<>();
for (String deptIdKey : leaders.keySet()) {
leaderMap.put(Long.valueOf(deptIdKey), leaders.getBool(deptIdKey));
}
raw.setIsLeaderInDepts(leaderMap);
if (Boolean.TRUE.equals(item.getBool("leader"))) {
raw.setIsLeaderInDepts(Map.of(dtDeptId, true));
}
return raw;
}
@ -474,10 +474,10 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
/**
* 发起部门用户详情 HTTP 请求返回响应体protected单测可覆写注入桩数据
*/
protected String requestUserListApi(String corpToken, Long dtDeptId, int offset, int size) {
protected String requestUserListApi(String corpToken, Long dtDeptId, int cursor, int size) {
JSONObject body = JSONUtil.createObj()
.set("department_id", dtDeptId)
.set("offset", offset)
.set("dept_id", dtDeptId)
.set("cursor", cursor)
.set("size", size);
try (HttpResponse response = HttpRequest.post(USER_LIST_URL + "?access_token=" + corpToken)
.header("Content-Type", "application/json")
@ -492,7 +492,7 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
} catch (BusinessErrorException e) {
throw e;
} catch (Exception e) {
log.error("调用钉钉 listbypage 接口异常,deptId={}", dtDeptId, e);
log.error("调用钉钉 v2/user/list 接口异常,deptId={}", dtDeptId, e);
throw new BusinessErrorException(AuthConstants.CODE_DINGTALK_SYNC_FAIL, "钉钉部门用户查询失败,请稍后重试");
}
}
@ -506,7 +506,7 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
}
/**
* listbypage 用户明细只取同步需要的字段字段名为钉钉原始 snake/camel 混排
* v2/user/list 用户明细只取同步需要的字段字段名为钉钉原始 snake/camel 混排
*/
@Data
protected static class DtUserRaw {
@ -516,7 +516,7 @@ public class DtOrgSyncServiceImpl implements IDtOrgSyncService {
private String mobile;
private String email;
private String avatar;
private String position;
private String title;
private Boolean active;
private Map<Long, Boolean> isLeaderInDepts;
}

5
crm-dict/src/main/java/com/crm/dict/config/DictPermissionInitializer.java

@ -19,8 +19,9 @@ import java.util.List;
* 系统管理目录缺失则 seam 自建ROLE_ADMIN 缺失则 seam warn 跳过绑定
* crm-auth 首次启动创建后字典权限在下次启动补绑</p>
* <p>权限点写入了 api_urlcrm-auth ApiPermissionInterceptor api_url 懒加载规则
* 种子化完成后主端点page/saveOrUpdate/delete/status/set-default 未授权访问即被拦截
* 两个 enabled-list 端点未登记规则每权限码仅一条 api_url page 共用权限码 @PreAuthorize 兜底</p>
* 种子化完成后主端点page/saveOrUpdate/delete/status/set-default 未授权访问即被拦截
* 两个 enabled-list 端点为业务选择器基础数据未登记拦截器规则fail-open 放行且无 @PreAuthorize
* 登录用户即可读每权限码仅一条 api_url page 共用权限码</p>
*/
@Slf4j
@Component

3
crm-dict/src/main/java/com/crm/dict/controller/DictGroupController.java

@ -60,8 +60,7 @@ public class DictGroupController {
return Result.success();
}
@Operation(summary = "启用分组列表(新建字典项选分组用)")
@PreAuthorize("hasAuthority('" + DictConstants.PERM_GROUP_LIST + "')")
@Operation(summary = "启用分组列表(新建字典项选分组用;基础数据,登录即可读)")
@GetMapping("/enabled-list")
public Result<List<DictGroupDTO>> enabledList() {
return Result.success(dictGroupService.listEnabled());

3
crm-dict/src/main/java/com/crm/dict/controller/DictItemController.java

@ -79,8 +79,7 @@ public class DictItemController {
return Result.success();
}
@Operation(summary = "某分组启用项列表(业务选择器渲染用,走 10s 本地缓存)")
@PreAuthorize("hasAuthority('" + DictConstants.PERM_ITEM_LIST + "')")
@Operation(summary = "某分组启用项列表(业务选择器渲染用,走 10s 本地缓存;基础数据,登录即可读)")
@GetMapping("/enabled-list")
public Result<List<DictItemDTO>> enabledList(@RequestParam("groupCode") String groupCode) {
return Result.success(dictQueryService.listEnabledItems(groupCode));

9
crm-dict/src/test/java/com/crm/dict/config/DictPermissionInitializerTest.java

@ -75,12 +75,19 @@ class DictPermissionInitializerTest {
}
@Test
@DisplayName("Controller 契约:全部端点带 @PreAuthorize 且权限码在 10 个权限码集合内")
@DisplayName("Controller 契约:管理端点带 @PreAuthorize 且权限码在 10 个权限码集合内(enabled-list 为基础数据放行除外)")
void controllerEndpoints_annotatedWithPreAuthorize() {
for (Class<?> controller : List.of(DictGroupController.class, DictItemController.class)) {
Method[] methods = controller.getDeclaredMethods();
assertThat(methods).isNotEmpty();
for (Method method : methods) {
// enabled-list 为业务选择器基础数据,登录即可读(无 @PreAuthorize、拦截器 fail-open 放行)
if (method.getName().equals("enabledList")) {
assertThat(method.getAnnotation(PreAuthorize.class))
.as("%s#enabledList 不应带 @PreAuthorize(基础数据放行)", controller.getSimpleName())
.isNull();
continue;
}
PreAuthorize anno = method.getAnnotation(PreAuthorize.class);
assertThat(anno)
.as("%s#%s 缺少 @PreAuthorize", controller.getSimpleName(), method.getName())

82
crm-opportunity/CONTEXT.md

@ -0,0 +1,82 @@
# 商机业务(crm-opportunity)
销售机会的全生命周期业务域:商机实体、两条正交轴(归属流转**状态** + 业务**阶段**)、四视图(商机公海 / 销售机会(我负责的)/ 我的关注 / 商机管理)、领取 / 分配 / 暂缓 / 抛公海 / 关闭 / 移交 / 转项目等流转。与线索同构,依赖方向单向 `crm-opportunity → crm-rule → (crm-auth / crm-dict / crm-file)`;商机规则(阶段模板 / 方案卡模板 / 方案推送 / 项目查重 / 商机公海与提醒 / 商机字典,原型 A7-3-2)归 `crm-rule`,本模块只消费。商机由**线索转入**或**销售直接创建**两条入口进入,进系统后完全平权(不按来源切片状态机/公海/视图)。
> 本文档只钉词汇与边界(票 01);状态机迁移图、表结构、公海规则、四视图查询各留给后续票(04/03/05/10)。事实出处见 `.scratch/opportunity-module/research-close-pause-actions.md`(原型活数据 V1.0-20260815)。
## Language
**商机**:
一条正在推进、准备转成项目的销售机会记录,`crm-opportunity` 的核心聚合根。两条正交轴:归属流转 `oppStatus`(**状态**)+ 业务打法 `oppStage`(**阶段**)。归属由 `owner_user_id`(领取人)表达。全生命周期最终走向「已转项目」或被「关闭」。字段/类名统一 `opp` 前缀,模块名与表名全拼 `crm-opportunity` / `opportunity_*`
_Avoid_: opportunity(英文单说)、线索(线索是上游实体)、项目(项目是转化后的下游实体)、客户
**状态(oppStatus)**:
商机的**归属流转轴**,`OpportunityStatus`。表达一条商机现在的归属处境:推进中 / 暂缓中 / 已关闭 / 已转项目 等。与业务阶段是两条独立的线——状态管「归属处境」,不管「业务打法走到第几步」。
_Avoid_: 阶段(阶段是另一条轴)、进度、stage
**阶段(oppStage)**:
商机的**业务阶段轴**,`OpportunityStage`。表达按公司标准打法走到第几步:客户圈定 → 关系摸排 → 资料采集 → 方案卡 → 已转项目(原型详情页进度条 A3-1-1-1-1)。阶段序列由 `crm-rule` 的商机阶段模板配置(原型 A7-3-2-1)。与状态正交。
_Avoid_: 状态(状态是另一条轴)、节点(口语可叫「节点」,字段用 stage)、status
**领取人**:
当前持有该商机的销售,存于 `owner_user_id`,= 归属 + 可见性两层含义(既是「谁负责」,也是默认「谁能看到」)。抛公海时清空。是「销售机会(我负责的)」视图的第一层业务过滤依据。直接创建商机时默认领取人 = 当前用户(原型 A3-1-1-2-1)。
_Avoid_: 负责人(负责人是公海池概念)、持有者、owner(英文单说)
**创建人**:
录入该商机的用户,全生命周期不变。直接创建 / 线索转入均落创建人。删除权限判定依据之一(对称线索:终态为删除禁区)。
_Avoid_: 录入人、owner(owner 是领取人)
**团队成员**:
领取人拉入协作的人员,存于独立关联(原型「添加团队成员&一键拉群」A3-1-1-2-8、团队成员 Tab A3-1-1-1-8)。每人配**项目角色**(商机负责人 / 方案工程师 / 招标专员 / 现场工程师 等)与**权限**(读写 / 只读 / 可写跟进 / 可推进节点 等)。团队成员是**行级授权白名单**——绕开 DataScope 部门天花板给出可见性,但能否写取决于其自身权限点。**抛公海只清空领取人,不动团队成员**(产品明确)。**转商机时自动插一条团队成员:领取人 = 商机负责人(票 02 拍板)**。
_Avoid_: 协作人、参与者、观察者(团队成员带角色与权限,不只是「能看」)
**商机来源**:
渠道归因字段 `oppSource`(字典码,必填,原型 A3-1-1-2-1)。表达商机从哪个渠道来(展会 / 转介绍 / 官网 等)。**与「来源线索」是两回事**——来源是渠道分类,每条商机都有;来源线索是血缘。**从线索转来的商机,`oppSource` 默认置为「线索转入」**(产品明确);直接创建的由用户选。
_Avoid_: 来源线索、渠道(口语,字段名 oppSource)、来路
**来源线索**:
线索转入的血缘键 `source_lead_id`(可空 FK)。仅「从线索转来的」商机有值,直接创建的为 NULL(「直接创建」= `source_lead_id IS NULL`,不单列术语)。是幂等键——线索侧 `OpportunityCreationPort` 转化在商机侧建 `UNIQUE(source_lead_id)`(lead 票 08 已定)。**不参与商机身份**:线索转来的与直接创建的商机在模型里完全平权。
_Avoid_: 商机来源(那是 oppSource 渠道)、线索 id、parent
**关联客户**:
商机关联的客户 / 联系人(原型「添加关联客户」A3-1-1-2-4、客户信息 Tab A3-1-1-1-2)。本域只存 `customer_id` **引用 + 快照字段**;客户实体本身归 A4 客户模块。至少关联一个客户,同一商机唯一主要客户(原型 A3-1-1-2-1)。**转商机时自动插一条关联客户,关系标记「主要意向客户」,`customer_id` = 线索侧下拉选中的客户 id(引用)+ 客户名快照(票 02 拍板)**。⚠ 依赖线索侧「意向客户下拉」+ A4 客户下拉能力落地,见票 11。
_Avoid_: 客户(客户是 A4 域的实体)、甲方(甲方是快照里的一个字段)、customer
**暂缓**:
商机流转动作(原型 A3-1-1-2-6):短期推不动、但仍有价值时挂起。选**暂缓原因**(字典)+ **预计重启日期**(必填,不早于今天)+ 补充说明 → 状态「暂缓中」。**保留领取人+团队、不进公海**,保留暂缓前阶段/节点。到期不自动恢复——系统提醒领取人,人工选 取消暂缓(恢复推进)/ 继续暂缓 / 关闭。按钮可见性由权限点配置,可见即可点,与是否领取人无关。
_Avoid_: 中止(不存在)、挂起、冻结、关闭(关闭是明确无价值)
**取消暂缓**:
暂缓的逆动作(产品补充):暂缓中 → 恢复推进,回到暂缓前阶段/节点。原型 A3 无独立页(推测详情页按钮)。按钮可见即可点,与领取人无关。
_Avoid_: 恢复(口语)、重启(重启是关闭的逆动作)、激活
**抛公海**:
商机流转动作(原型 A3-1-1-2-7):不想继续做,退回商机公海让他人领取。**只清空领取人**,回公海,**不动其他团队成员**(产品明确)。按钮在详情页顶部操作区,可见即可点,与领取人无关。
_Avoid_: 释放(线索用「释放」)、退回、放弃
**关闭**:
商机流转动作(原型 A3-1-1-2-9):明确无法继续推进时终止。选**关闭原因**(字典:项目取消 / 客户放弃 / 竞争失败 / 重复商机 / 无效商机 / 其他)+ 补充说明(选「其他」必填)+ 勾选确认框 → 状态「已关闭」。保留关闭前状态/阶段(**只读锁定**),历史数据不删。已关闭不可重复关闭。与暂缓互斥:短期有价值→暂缓,明确无价值→关闭。
_Avoid_: 中止(不存在)、作废(线索用「作废」)、删除、输单(口语)
**重启商机**:
关闭的逆动作(原型称「重新开启」,产品称「重启商机」):已关闭 → 恢复。原型要求填重开原因。原型原文写「须管理权限用户」,但**产品后来统一为「重启按钮由权限点控制、可见即可点、与领取人无关」,以产品口径为准**。
_Avoid_: 恢复、取消暂缓(取消暂缓是暂缓的逆动作)、激活
**移交**:
负责人变更动作,**入口在团队成员页(原型 A3-1-1-1-8)**,非详情页顶部操作区。**移交只换领取人 `owner_user_id`**(产品明确),不动其他。**无独立「转让」动作**——「转让只有负责人能转」不是额外 guard,而是移交入口本身在负责人管理的团队成员页里(原型:「当前用户只能维护其权限范围内的团队成员及权限设置」),团队管理权实际落在领取人手上。
_Avoid_: 转让(无此独立动作)、转移、指派(分配才是指派)
**转项目**:
商机的下游终态动作:商机推到最后转成项目,商机进入「已转项目」终态、锁死不可回退(对称线索的「已转商机」)。**转项目端口**名字先占位——项目实体、项目查重规则(原型 A7-3-2-4)归 A5 项目模块,本域只承认终态、定占位契约。
_Avoid_: 成交、转化、结项、CreateProject(具体接口名留给 A5)
**已转项目**:
商机**终态**:已转成项目。锁死——不可暂缓 / 抛公海 / 关闭 / 编辑。对称线索的「已转商机」终态。
_Avoid_: 已成交、已完成、结束
## 待细化(后续票)
- 状态机完整迁移图 + 每条动作的 guard(票 04);「移交」按钮原型位置、「取消暂缓/重启」独立页存否 —— 见 `issues/01` 的 To-verify,以产品口径为准。
- 商机主表 + 子表(团队成员 / 方案卡 / 现场勘察 / 跟进 / 附件 / 操作日志)字段(票 03/07/08/09)。
- 商机公海池规则、四视图查询(票 05/10)。
- `crm-rule` 从「线索规则」升级为「通用业务规则域」的 CONTEXT.md 改写(本决策副作用)。

3
deploy/demo/index.html

@ -475,7 +475,8 @@ function go(k){
}
window.addEventListener("load", () => {
$("baseUrl").value = localStorage.getItem("demoBase") || (location.protocol.startsWith("http") ? location.origin : "http://localhost:8081");
// 默认后端 8081:demo 静态页跑在 8090,不能用 location.origin(会打到静态服务自身导致全 404)
$("baseUrl").value = localStorage.getItem("demoBase") || "http://localhost:8081";
showTok(); initTabs(); initStatusChecks();
loadProvinces(); loadDicts(); loadPools(); loadDepts(); loadUsers(); loadProvOptions();
});

2
docker-compose.yml

@ -81,6 +81,8 @@ services:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: verify
# 覆盖 application.yml 的 server.port=8081:容器内对齐 nginx upstream(crm-app:8080)
SERVER_PORT: "8080"
CRM_DB_URL: "jdbc:mysql://mysql:3306/crm?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true"
CRM_DB_USERNAME: root
CRM_DB_PASSWORD: verify123

235
logs/crm-app.log

@ -1234,3 +1234,238 @@ This generated password is for development use only. Your security configuration
2026-08-18T06:40:29.736+08:00 WARN 11192 --- [crm-app] [http-nio-8081-exec-1] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T06:41:02.788+08:00 WARN 11192 --- [crm-app] [http-nio-8081-exec-7] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T06:41:42.087+08:00 WARN 11192 --- [crm-app] [http-nio-8081-exec-3] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T08:37:08.702+08:00 INFO 35964 --- [crm-app] [main] com.crm.app.CrmAppApplication : Starting CrmAppApplication v1.0.0-SNAPSHOT using Java 17.0.15 with PID 35964 (D:\code\crm-backend-matt\crm-app\target\crm-app-1.0.0-SNAPSHOT.jar started by luowj in D:\code\crm-backend-matt)
2026-08-18T08:37:08.710+08:00 INFO 35964 --- [crm-app] [main] com.crm.app.CrmAppApplication : The following 1 profile is active: "verify"
2026-08-18T08:37:11.074+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-08-18T08:37:11.077+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-08-18T08:37:11.122+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 0 JPA repository interfaces.
2026-08-18T08:37:11.158+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-08-18T08:37:11.161+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2026-08-18T08:37:11.183+08:00 INFO 35964 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
2026-08-18T08:37:11.461+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'crmBaseMapper' and 'com.crm.base.mapper.CrmBaseMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.462+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authIdentityMapper' and 'com.crm.auth.mapper.AuthIdentityMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.463+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authUserMapper' and 'com.crm.auth.mapper.AuthUserMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.463+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDataScopeModuleMapper' and 'com.crm.auth.mapper.SysDataScopeModuleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.464+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDeptMapper' and 'com.crm.auth.mapper.SysDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.464+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysMenuMapper' and 'com.crm.auth.mapper.SysMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.464+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleDataScopeMapper' and 'com.crm.auth.mapper.SysRoleDataScopeMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.465+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMapper' and 'com.crm.auth.mapper.SysRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.466+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMenuMapper' and 'com.crm.auth.mapper.SysRoleMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.466+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserDeptMapper' and 'com.crm.auth.mapper.SysUserDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.467+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserRoleMapper' and 'com.crm.auth.mapper.SysUserRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.467+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'fileInfoMapper' and 'com.crm.file.mapper.FileInfoMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.468+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupDefaultMapper' and 'com.crm.dict.mapper.DictGroupDefaultMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.468+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupMapper' and 'com.crm.dict.mapper.DictGroupMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.469+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictItemMapper' and 'com.crm.dict.mapper.DictItemMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.470+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictRefCountMapper' and 'com.crm.dict.mapper.DictRefCountMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.470+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMapper' and 'com.crm.rule.mapper.LeadPoolMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.471+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMemberMapper' and 'com.crm.rule.mapper.LeadPoolMemberMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.472+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolProvinceMapper' and 'com.crm.rule.mapper.LeadPoolProvinceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.472+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolRegionMapper' and 'com.crm.rule.mapper.LeadPoolRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.473+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRegionMapper' and 'com.crm.rule.mapper.SysRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.473+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userColumnPreferenceMapper' and 'com.crm.preference.mapper.UserColumnPreferenceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.473+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadAttachmentMapper' and 'com.crm.lead.mapper.LeadAttachmentMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.474+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFeedbackMapper' and 'com.crm.lead.mapper.LeadFeedbackMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.474+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFollowMapper' and 'com.crm.lead.mapper.LeadFollowMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.475+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadHistoryMapper' and 'com.crm.lead.mapper.LeadHistoryMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.475+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadMapper' and 'com.crm.lead.mapper.LeadMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.475+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.crm.**.mapper]' package. Please check your configuration.
2026-08-18T08:37:11.480+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'crmBaseMapper' and 'com.crm.base.mapper.CrmBaseMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.480+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authIdentityMapper' and 'com.crm.auth.mapper.AuthIdentityMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.481+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authUserMapper' and 'com.crm.auth.mapper.AuthUserMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.482+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDataScopeModuleMapper' and 'com.crm.auth.mapper.SysDataScopeModuleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.482+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDeptMapper' and 'com.crm.auth.mapper.SysDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.484+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysMenuMapper' and 'com.crm.auth.mapper.SysMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.485+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleDataScopeMapper' and 'com.crm.auth.mapper.SysRoleDataScopeMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.487+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMapper' and 'com.crm.auth.mapper.SysRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.487+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMenuMapper' and 'com.crm.auth.mapper.SysRoleMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.488+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserDeptMapper' and 'com.crm.auth.mapper.SysUserDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.488+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserRoleMapper' and 'com.crm.auth.mapper.SysUserRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.488+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'fileInfoMapper' and 'com.crm.file.mapper.FileInfoMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.489+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupDefaultMapper' and 'com.crm.dict.mapper.DictGroupDefaultMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.489+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupMapper' and 'com.crm.dict.mapper.DictGroupMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.489+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictItemMapper' and 'com.crm.dict.mapper.DictItemMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.490+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictRefCountMapper' and 'com.crm.dict.mapper.DictRefCountMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.490+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMapper' and 'com.crm.rule.mapper.LeadPoolMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.491+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMemberMapper' and 'com.crm.rule.mapper.LeadPoolMemberMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.492+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolProvinceMapper' and 'com.crm.rule.mapper.LeadPoolProvinceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.492+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolRegionMapper' and 'com.crm.rule.mapper.LeadPoolRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.493+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRegionMapper' and 'com.crm.rule.mapper.SysRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.493+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userColumnPreferenceMapper' and 'com.crm.preference.mapper.UserColumnPreferenceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.495+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadAttachmentMapper' and 'com.crm.lead.mapper.LeadAttachmentMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.497+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFeedbackMapper' and 'com.crm.lead.mapper.LeadFeedbackMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.499+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFollowMapper' and 'com.crm.lead.mapper.LeadFollowMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.499+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadHistoryMapper' and 'com.crm.lead.mapper.LeadHistoryMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.500+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadMapper' and 'com.crm.lead.mapper.LeadMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:37:11.503+08:00 WARN 35964 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.crm.**.mapper]' package. Please check your configuration.
2026-08-18T08:37:13.536+08:00 INFO 35964 --- [crm-app] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8081 (http)
2026-08-18T08:37:13.605+08:00 INFO 35964 --- [crm-app] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-08-18T08:37:13.606+08:00 INFO 35964 --- [crm-app] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.31]
2026-08-18T08:37:13.772+08:00 INFO 35964 --- [crm-app] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-08-18T08:37:13.773+08:00 INFO 35964 --- [crm-app] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4978 ms
2026-08-18T08:37:14.870+08:00 INFO 35964 --- [crm-app] [main] com.crm.auth.config.PermissionConfig : 数据权限受控表扫描完成:0 张
2026-08-18T08:37:15.299+08:00 INFO 35964 --- [crm-app] [main] com.crm.base.config.CustomIdGenerator : 雪花ID生成器初始化完成,workerId=0, datacenterId=0
2026-08-18T08:37:16.551+08:00 INFO 35964 --- [crm-app] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2026-08-18T08:37:16.642+08:00 INFO 35964 --- [crm-app] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.3.Final
2026-08-18T08:37:16.700+08:00 INFO 35964 --- [crm-app] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2026-08-18T08:37:17.740+08:00 INFO 35964 --- [crm-app] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2026-08-18T08:37:17.873+08:00 INFO 35964 --- [crm-app] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-08-18T08:37:18.901+08:00 INFO 35964 --- [crm-app] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@341c0dfc
2026-08-18T08:37:18.903+08:00 INFO 35964 --- [crm-app] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-08-18T08:37:19.027+08:00 WARN 35964 --- [crm-app] [main] org.hibernate.orm.deprecation : HHH90000025: MySQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2026-08-18T08:37:21.658+08:00 INFO 35964 --- [crm-app] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2026-08-18T08:37:26.531+08:00 INFO 35964 --- [crm-app] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-08-18T08:37:26.871+08:00 WARN 35964 --- [crm-app] [main] c.c.a.controller.DebugTokenController : ============================================================
2026-08-18T08:37:26.872+08:00 WARN 35964 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 调试 token 接口 /api/auth/debug/token 已启用(verify profile)
2026-08-18T08:37:26.872+08:00 WARN 35964 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 严禁用于生产环境!任何知道 userId 的人可冒充该用户。
2026-08-18T08:37:26.873+08:00 WARN 35964 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 参见 docs/adr/0014-verify-profile-debug-token-endpoint.md
2026-08-18T08:37:26.873+08:00 WARN 35964 --- [crm-app] [main] c.c.a.controller.DebugTokenController : ============================================================
2026-08-18T08:37:29.299+08:00 WARN 35964 --- [crm-app] [main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 3ca5d6f7-fc27-4f08-bed1-828d7bb95117
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2026-08-18T08:37:29.332+08:00 INFO 35964 --- [crm-app] [main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
2026-08-18T08:37:31.042+08:00 INFO 35964 --- [crm-app] [main] c.c.a.c.DataScopeModuleStartupValidator : 数据权限模块校验通过:0 张受控表,4 个注册模块
2026-08-18T08:37:31.098+08:00 INFO 35964 --- [crm-app] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8081 (http) with context path '/'
2026-08-18T08:37:31.151+08:00 INFO 35964 --- [crm-app] [main] com.crm.app.CrmAppApplication : Started CrmAppApplication in 23.316 seconds (process running for 24.321)
2026-08-18T08:37:31.158+08:00 INFO 35964 --- [crm-app] [main] com.crm.auth.config.DataInitializer : 执行权限数据初始化检查...
2026-08-18T08:37:32.047+08:00 INFO 35964 --- [crm-app] [main] com.crm.auth.config.DataInitializer : 权限数据初始化检查完成
2026-08-18T08:37:32.047+08:00 INFO 35964 --- [crm-app] [main] c.c.d.config.DictPermissionInitializer : 执行数据字典权限种子化检查...
2026-08-18T08:37:32.536+08:00 INFO 35964 --- [crm-app] [main] c.c.d.config.DictPermissionInitializer : 数据字典权限种子化检查完成:10 个权限点
2026-08-18T08:37:32.537+08:00 INFO 35964 --- [crm-app] [main] c.c.r.config.RulePermissionInitializer : 执行线索规则权限种子化检查(仅菜单)...
2026-08-18T08:37:32.664+08:00 INFO 35964 --- [crm-app] [main] c.c.r.config.RulePermissionInitializer : 线索规则权限种子化检查完成:仅菜单(0 个 button)
2026-08-18T08:37:32.675+08:00 INFO 35964 --- [crm-app] [main] c.c.l.config.LeadPermissionInitializer : 执行线索管理权限种子化检查(仅菜单)...
2026-08-18T08:37:33.176+08:00 INFO 35964 --- [crm-app] [main] c.c.l.config.LeadPermissionInitializer : 线索管理权限种子化检查完成:4 个菜单(0 个 button)
2026-08-18T08:37:33.178+08:00 INFO 35964 --- [crm-app] [main] c.crm.rule.config.RegionDataInitializer : 执行行政区划全量种子导入检查...
2026-08-18T08:37:34.659+08:00 INFO 35964 --- [crm-app] [main] c.crm.rule.config.RegionDataInitializer : 行政区划种子导入完成:对账 3432 行(省 34 / 市 342 / 区 3056),存量 3432 行,新增 0 行
2026-08-18T08:37:34.664+08:00 INFO 35964 --- [crm-app] [main] c.crm.dict.config.DictDataInitializer : 执行内置字典幂等初始化检查...
2026-08-18T08:37:38.247+08:00 INFO 35964 --- [crm-app] [main] c.crm.dict.config.DictDataInitializer : 内置字典幂等初始化检查完成:6 个分组
2026-08-18T08:37:38.712+08:00 WARN 35964 --- [crm-app] [main] com.crm.file.config.MinioConfig : MinIO bucket [crm] 初始化失败,文件功能可能不可用:The Access Key Id you provided does not exist in our records.
2026-08-18T08:40:17.289+08:00 INFO 35964 --- [crm-app] [http-nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-08-18T08:40:17.356+08:00 INFO 35964 --- [crm-app] [http-nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 49 ms
2026-08-18T08:40:21.878+08:00 WARN 35964 --- [crm-app] [http-nio-8081-exec-1] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T08:40:49.639+08:00 ERROR 35964 --- [crm-app] [http-nio-8081-exec-2] c.c.a.service.impl.DtOrgSyncServiceImpl : 组织同步拉取部门用户失败:deptId=1, body={"errcode":15,"sub_code":"isp.top-remote-connection-error","sub_msg":"远程服务不存在","errmsg":"Remote service error[subcode=isp.top-remote-connection-error,submsg=远程服务不存在]","request_id":"16l05chk0f2vr"}
2026-08-18T08:40:49.803+08:00 WARN 35964 --- [crm-app] [http-nio-8081-exec-2] c.c.b.a.GlobalExceptionHandlerAdvice : 业务异常:code=61017, msg=钉钉组织同步失败:拉取部门用户失败(errcode=15)
2026-08-18T08:52:27.250+08:00 INFO 22900 --- [crm-app] [main] com.crm.app.CrmAppApplication : Starting CrmAppApplication v1.0.0-SNAPSHOT using Java 17.0.15 with PID 22900 (D:\code\crm-backend-matt\crm-app\target\crm-app-1.0.0-SNAPSHOT.jar started by luowj in D:\code\crm-backend-matt)
2026-08-18T08:52:27.262+08:00 INFO 22900 --- [crm-app] [main] com.crm.app.CrmAppApplication : The following 1 profile is active: "verify"
2026-08-18T08:52:29.792+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-08-18T08:52:29.796+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-08-18T08:52:29.852+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 42 ms. Found 0 JPA repository interfaces.
2026-08-18T08:52:29.888+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2026-08-18T08:52:29.891+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2026-08-18T08:52:29.921+08:00 INFO 22900 --- [crm-app] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 12 ms. Found 0 Redis repository interfaces.
2026-08-18T08:52:30.249+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'crmBaseMapper' and 'com.crm.base.mapper.CrmBaseMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.250+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authIdentityMapper' and 'com.crm.auth.mapper.AuthIdentityMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.251+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authUserMapper' and 'com.crm.auth.mapper.AuthUserMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.251+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDataScopeModuleMapper' and 'com.crm.auth.mapper.SysDataScopeModuleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.252+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDeptMapper' and 'com.crm.auth.mapper.SysDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.252+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysMenuMapper' and 'com.crm.auth.mapper.SysMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.253+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleDataScopeMapper' and 'com.crm.auth.mapper.SysRoleDataScopeMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.253+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMapper' and 'com.crm.auth.mapper.SysRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.253+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMenuMapper' and 'com.crm.auth.mapper.SysRoleMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.254+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserDeptMapper' and 'com.crm.auth.mapper.SysUserDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.254+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserRoleMapper' and 'com.crm.auth.mapper.SysUserRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.254+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'fileInfoMapper' and 'com.crm.file.mapper.FileInfoMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.258+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupDefaultMapper' and 'com.crm.dict.mapper.DictGroupDefaultMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.261+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupMapper' and 'com.crm.dict.mapper.DictGroupMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.262+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictItemMapper' and 'com.crm.dict.mapper.DictItemMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.263+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictRefCountMapper' and 'com.crm.dict.mapper.DictRefCountMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.264+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMapper' and 'com.crm.rule.mapper.LeadPoolMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.264+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMemberMapper' and 'com.crm.rule.mapper.LeadPoolMemberMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.265+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolProvinceMapper' and 'com.crm.rule.mapper.LeadPoolProvinceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.265+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolRegionMapper' and 'com.crm.rule.mapper.LeadPoolRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.266+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRegionMapper' and 'com.crm.rule.mapper.SysRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.266+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userColumnPreferenceMapper' and 'com.crm.preference.mapper.UserColumnPreferenceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.266+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadAttachmentMapper' and 'com.crm.lead.mapper.LeadAttachmentMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.267+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFeedbackMapper' and 'com.crm.lead.mapper.LeadFeedbackMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.267+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFollowMapper' and 'com.crm.lead.mapper.LeadFollowMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.268+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadHistoryMapper' and 'com.crm.lead.mapper.LeadHistoryMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.268+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadMapper' and 'com.crm.lead.mapper.LeadMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.269+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.crm.**.mapper]' package. Please check your configuration.
2026-08-18T08:52:30.275+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'crmBaseMapper' and 'com.crm.base.mapper.CrmBaseMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.277+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authIdentityMapper' and 'com.crm.auth.mapper.AuthIdentityMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.278+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'authUserMapper' and 'com.crm.auth.mapper.AuthUserMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.278+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDataScopeModuleMapper' and 'com.crm.auth.mapper.SysDataScopeModuleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.279+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysDeptMapper' and 'com.crm.auth.mapper.SysDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.280+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysMenuMapper' and 'com.crm.auth.mapper.SysMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.280+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleDataScopeMapper' and 'com.crm.auth.mapper.SysRoleDataScopeMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.281+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMapper' and 'com.crm.auth.mapper.SysRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.281+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRoleMenuMapper' and 'com.crm.auth.mapper.SysRoleMenuMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.282+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserDeptMapper' and 'com.crm.auth.mapper.SysUserDeptMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.282+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysUserRoleMapper' and 'com.crm.auth.mapper.SysUserRoleMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.283+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'fileInfoMapper' and 'com.crm.file.mapper.FileInfoMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.283+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupDefaultMapper' and 'com.crm.dict.mapper.DictGroupDefaultMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.284+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictGroupMapper' and 'com.crm.dict.mapper.DictGroupMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.285+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictItemMapper' and 'com.crm.dict.mapper.DictItemMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.285+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'dictRefCountMapper' and 'com.crm.dict.mapper.DictRefCountMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.286+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMapper' and 'com.crm.rule.mapper.LeadPoolMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.286+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolMemberMapper' and 'com.crm.rule.mapper.LeadPoolMemberMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.286+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolProvinceMapper' and 'com.crm.rule.mapper.LeadPoolProvinceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.288+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadPoolRegionMapper' and 'com.crm.rule.mapper.LeadPoolRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.295+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'sysRegionMapper' and 'com.crm.rule.mapper.SysRegionMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.296+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userColumnPreferenceMapper' and 'com.crm.preference.mapper.UserColumnPreferenceMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.297+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadAttachmentMapper' and 'com.crm.lead.mapper.LeadAttachmentMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.298+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFeedbackMapper' and 'com.crm.lead.mapper.LeadFeedbackMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.299+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadFollowMapper' and 'com.crm.lead.mapper.LeadFollowMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.299+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadHistoryMapper' and 'com.crm.lead.mapper.LeadHistoryMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.300+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'leadMapper' and 'com.crm.lead.mapper.LeadMapper' mapperInterface. Bean already defined with the same name!
2026-08-18T08:52:30.300+08:00 WARN 22900 --- [crm-app] [main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.crm.**.mapper]' package. Please check your configuration.
2026-08-18T08:52:32.376+08:00 INFO 22900 --- [crm-app] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8081 (http)
2026-08-18T08:52:32.410+08:00 INFO 22900 --- [crm-app] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-08-18T08:52:32.412+08:00 INFO 22900 --- [crm-app] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.31]
2026-08-18T08:52:32.530+08:00 INFO 22900 --- [crm-app] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-08-18T08:52:32.530+08:00 INFO 22900 --- [crm-app] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 5168 ms
2026-08-18T08:52:33.377+08:00 INFO 22900 --- [crm-app] [main] com.crm.auth.config.PermissionConfig : 数据权限受控表扫描完成:0 张
2026-08-18T08:52:33.680+08:00 INFO 22900 --- [crm-app] [main] com.crm.base.config.CustomIdGenerator : 雪花ID生成器初始化完成,workerId=0, datacenterId=0
2026-08-18T08:52:35.836+08:00 INFO 22900 --- [crm-app] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2026-08-18T08:52:35.994+08:00 INFO 22900 --- [crm-app] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.3.Final
2026-08-18T08:52:36.097+08:00 INFO 22900 --- [crm-app] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2026-08-18T08:52:37.157+08:00 INFO 22900 --- [crm-app] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2026-08-18T08:52:37.212+08:00 INFO 22900 --- [crm-app] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-08-18T08:52:37.935+08:00 INFO 22900 --- [crm-app] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@28c75c93
2026-08-18T08:52:37.977+08:00 INFO 22900 --- [crm-app] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-08-18T08:52:38.135+08:00 WARN 22900 --- [crm-app] [main] org.hibernate.orm.deprecation : HHH90000025: MySQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2026-08-18T08:52:40.696+08:00 INFO 22900 --- [crm-app] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2026-08-18T08:52:44.927+08:00 INFO 22900 --- [crm-app] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-08-18T08:52:45.297+08:00 WARN 22900 --- [crm-app] [main] c.c.a.controller.DebugTokenController : ============================================================
2026-08-18T08:52:45.298+08:00 WARN 22900 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 调试 token 接口 /api/auth/debug/token 已启用(verify profile)
2026-08-18T08:52:45.298+08:00 WARN 22900 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 严禁用于生产环境!任何知道 userId 的人可冒充该用户。
2026-08-18T08:52:45.298+08:00 WARN 22900 --- [crm-app] [main] c.c.a.controller.DebugTokenController : 参见 docs/adr/0014-verify-profile-debug-token-endpoint.md
2026-08-18T08:52:45.299+08:00 WARN 22900 --- [crm-app] [main] c.c.a.controller.DebugTokenController : ============================================================
2026-08-18T08:52:47.670+08:00 WARN 22900 --- [crm-app] [main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: a7b67971-5c51-49a9-8332-48abbb4a4eb8
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2026-08-18T08:52:47.716+08:00 INFO 22900 --- [crm-app] [main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
2026-08-18T08:52:49.985+08:00 INFO 22900 --- [crm-app] [main] c.c.a.c.DataScopeModuleStartupValidator : 数据权限模块校验通过:0 张受控表,4 个注册模块
2026-08-18T08:52:50.068+08:00 INFO 22900 --- [crm-app] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8081 (http) with context path '/'
2026-08-18T08:52:50.161+08:00 INFO 22900 --- [crm-app] [main] com.crm.app.CrmAppApplication : Started CrmAppApplication in 23.799 seconds (process running for 24.662)
2026-08-18T08:52:50.168+08:00 INFO 22900 --- [crm-app] [main] com.crm.auth.config.DataInitializer : 执行权限数据初始化检查...
2026-08-18T08:52:51.147+08:00 INFO 22900 --- [crm-app] [main] com.crm.auth.config.DataInitializer : 权限数据初始化检查完成
2026-08-18T08:52:51.148+08:00 INFO 22900 --- [crm-app] [main] c.c.d.config.DictPermissionInitializer : 执行数据字典权限种子化检查...
2026-08-18T08:52:51.542+08:00 INFO 22900 --- [crm-app] [main] c.c.d.config.DictPermissionInitializer : 数据字典权限种子化检查完成:10 个权限点
2026-08-18T08:52:51.543+08:00 INFO 22900 --- [crm-app] [main] c.c.r.config.RulePermissionInitializer : 执行线索规则权限种子化检查(仅菜单)...
2026-08-18T08:52:51.646+08:00 INFO 22900 --- [crm-app] [main] c.c.r.config.RulePermissionInitializer : 线索规则权限种子化检查完成:仅菜单(0 个 button)
2026-08-18T08:52:51.646+08:00 INFO 22900 --- [crm-app] [main] c.crm.rule.config.RegionDataInitializer : 执行行政区划全量种子导入检查...
2026-08-18T08:52:53.181+08:00 INFO 22900 --- [crm-app] [main] c.crm.rule.config.RegionDataInitializer : 行政区划种子导入完成:对账 3432 行(省 34 / 市 342 / 区 3056),存量 3432 行,新增 0 行
2026-08-18T08:52:53.181+08:00 INFO 22900 --- [crm-app] [main] c.c.l.config.LeadPermissionInitializer : 执行线索管理权限种子化检查(仅菜单)...
2026-08-18T08:52:53.516+08:00 INFO 22900 --- [crm-app] [main] c.c.l.config.LeadPermissionInitializer : 线索管理权限种子化检查完成:4 个菜单(0 个 button)
2026-08-18T08:52:53.877+08:00 WARN 22900 --- [crm-app] [main] com.crm.file.config.MinioConfig : MinIO bucket [crm] 初始化失败,文件功能可能不可用:The Access Key Id you provided does not exist in our records.
2026-08-18T08:52:53.878+08:00 INFO 22900 --- [crm-app] [main] c.crm.dict.config.DictDataInitializer : 执行内置字典幂等初始化检查...
2026-08-18T08:52:57.110+08:00 INFO 22900 --- [crm-app] [main] c.crm.dict.config.DictDataInitializer : 内置字典幂等初始化检查完成:6 个分组
2026-08-18T08:53:19.240+08:00 INFO 22900 --- [crm-app] [http-nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-08-18T08:53:19.241+08:00 INFO 22900 --- [crm-app] [http-nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-08-18T08:53:19.243+08:00 INFO 22900 --- [crm-app] [http-nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
2026-08-18T08:53:21.533+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-1] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:03:03.880+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-4] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:03:04.226+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-3] c.c.b.a.GlobalExceptionHandlerAdvice : 业务异常:code=61017, msg=已有组织同步任务进行中,请稍后再试
2026-08-18T09:05:34.986+08:00 INFO 22900 --- [crm-app] [http-nio-8081-exec-2] c.c.a.service.impl.DtOrgSyncServiceImpl : 钉钉组织同步完成:部门 812(新增 812 更新 0),用户 6046(新增 6046 更新 0),负责人反写 358
2026-08-18T09:07:00.145+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-7] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:09:38.097+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-1] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:15:45.300+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-9] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:16:00.892+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-4] c.c.a.controller.DebugTokenController : [verify] 为 userId=739564171091247104 签发调试 token
2026-08-18T09:16:59.057+08:00 WARN 22900 --- [crm-app] [http-nio-8081-exec-7] c.c.b.a.GlobalExceptionHandlerAdvice : 业务异常:code=65001, msg=省份不能为空

9
tmp/bomscan-all.ps1

@ -0,0 +1,9 @@
# 全工程 BOM 扫描(AGENTS.md:批量编辑 .java 后、mvn compile 前必跑)
$hits = Get-ChildItem -Path 'd:\code\crm-backend-matt' -Recurse -Filter *.java |
Where-Object { $_.FullName -notmatch '\\target\\' } |
Where-Object {
$b = [IO.File]::ReadAllBytes($_.FullName)
$b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF
}
if ($hits) { $hits | ForEach-Object { Write-Output ("BOM: " + $_.FullName) } }
else { Write-Output "BOM files: 0" }

12
tmp/dict-list-out.txt

@ -0,0 +1,12 @@
===== lead_source (0 椤? =====
===== brand (0 椤? =====
===== product (0 椤? =====
===== scene (0 椤? =====

15
tmp/dict-list.ps1

@ -0,0 +1,15 @@
# 拉取线索四组字典的 item 编码列表
$base = "http://127.0.0.1:8081"
$token = (Get-Content "d:\code\crm-backend-matt\tmp\dt-token.txt" -Raw).Trim()
$h = @{ Authorization = "Bearer $token" }
$out = @()
foreach ($g in @("lead_source","brand","product","scene")) {
$r = Invoke-RestMethod -Method Get -Uri "$base/api/dict/item/enabled-list?groupCode=$g" -Headers $h -TimeoutSec 30
$out += "===== $g ($($r.data.Count) 项) ====="
$r.data | ForEach-Object { $out += (" {0}`t{1}" -f $_.code, $_.name) }
$out += ""
}
$out | Out-File -Encoding utf8 "d:\code\crm-backend-matt\tmp\dict-list-out.txt"
$out | ForEach-Object { Write-Output $_ }

5
tmp/dt-build.ps1

@ -0,0 +1,5 @@
# 重新打包全反应堆(钉钉配置已更新,需打进 fat jar)
$env:JAVA_HOME = "C:\Users\luowj\.jdks\jdk-17.0.15"
Set-Location "d:\code\crm-backend-matt"
& mvn.cmd -s settings.xml clean package -DskipTests
exit $LASTEXITCODE

6
tmp/dt-diag-out.txt

@ -0,0 +1,6 @@
[1] new accessToken len=32
[2] listbypage(new tok): {"errcode":15,"sub_code":"isp.top-remote-connection-error","sub_msg":"远程服务不存在","errmsg":"Remote service error[subcode=isp.top-remote-connection-error,submsg=远程服务不存在]","request_id":"16kl2tmeqs96j"}
[3] v2 listsub(new tok): {"errcode":0,"errmsg":"ok","result":[{"auto_add_user":true,"create_dept_group":true,"dept_id":641717583,"ext":"{\"faceCount\":\"9009\"}","name":"职员","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":40315553,"ext":"{\"faceCount\":\"1820\"}","name":"研发中心","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":74653337,"ext":"{\"faceCount\":\"278\"}","name":"行政稽查部","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":40246694,"ext":"{\"faceCount\":\"4581\"}","name":"生产一厂","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":40295595,"ext":"{\"faceCount\":\"834\"}","name":"生产二厂","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":82340054,"ext":"{\"faceCount\":\"999\"}","name":"生产三厂(LED)","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":82311073,"ext":"{\"faceCount\":\"610\"}","name":"生产四厂(升和)","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":91758600,"ext":"{\"faceCount\":\"523\"}","name":"生产五厂(泰晟)","parent_id":1},{"auto_add_user":false,"create_dept_group":false,"dept_id":97176224,"name":"工厂管理部","parent_id":1},{"auto_add_user":false,"create_dept_group":false,"dept_id":533865333,"ext":"{\"faceCount\":\"5\"}","name":"研发权限组","parent_id":1},{"auto_add_user":false,"create_dept_group":false,"dept_id":846681342,"name":"临时组","parent_id":1},{"auto_add_user":false,"create_dept_group":false,"dept_id":996215779,"ext":"{\"faceCount\":\"5\"}","name":"蓝领新入职待分配","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":1047325516,"name":"算法安全应急小组","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":1048336200,"name":"西安保伦","parent_id":1},{"auto_add_user":true,"create_dept_group":true,"dept_id":1048470215,"name":"湖北爱递思","parent_id":1},{"auto_add_user":false,"create_dept_group":false,"dept_id":1089798037,"ext":"{\"faceCount\":\"78\"}","name":"实训实习","parent_id":1}],"request_id":"16l7c4xtyv6tg"}
[4] gettoken errcode=0 errmsg=ok oldToken len=32
[5] listbypage(old tok): {"errcode":15,"sub_code":"isp.top-remote-connection-error","sub_msg":"远程服务不存在","errmsg":"Remote service error[subcode=isp.top-remote-connection-error,submsg=远程服务不存在]","request_id":"15rn8y1050e7p"}
[6] v2 user/list(old tok): {"errcode":0,"errmsg":"ok","result":{"has_more":false,"list":[{"active":true,"admin":false,"avatar":"","boss":false,"dept_id_list":[1],"dept_order":176174473195764512,"disable_status":false,"email":"","exclusive_account":true,"exclusive_account_corp_id":"ding13a2ff8718e6bcdd","exclusive_account_corp_name":"广东保伦电子股份有限公司","exclusive_account_type":"dingtalk","extension":"{\"Agoal\":\"[查看目标](https://agoal.dingtalk.com/goal/okr/executor/67f80e263cda750e12230994?corpid=ding13a2ff8718e6bcdd)\"}","hide_mobile":true,"hired_date":1577808000000,"job_number":"","leader":false,"login_id":"gzitc1993","mobile":"18520392797","name":"专用账户","nickname":"专用账户","remark":"","state_code":"86","telephone":"","title":"","unionid":"Ns9xFU1ILdrbpfFg79iiYTAiEiE","userid":"988183640","work_place":""}]},"request_id":"16klfalvxtdfx"}

44
tmp/dt-diag.ps1

@ -0,0 +1,44 @@
# 钉钉同步失败诊断:对比新旧 token、不同旧版 topapi 接口的可用性
$appKey = "ding4eootvq4jzas96lr"
$appSecret = "UI6XcD8GFPE_W_yo2eQkMuoSsTEf1whpZYEXrsDA7CV7bkJp40B3VNcETWS1aGgg"
$out = @()
# 1. 新版 accessToken
$newTokResp = Invoke-RestMethod -Method Post -Uri "https://api.dingtalk.com/v1.0/oauth2/accessToken" -ContentType "application/json" -Body (@{appKey=$appKey;appSecret=$appSecret} | ConvertTo-Json)
$newToken = $newTokResp.accessToken
$out += "[1] new accessToken len=$($newToken.Length)"
# 2. 新版 token 调旧版 listbypage(复现应用内的失败)
$body = '{"department_id":1,"offset":0,"size":10}'
try {
$r2 = Invoke-RestMethod -Method Post -Uri "https://oapi.dingtalk.com/topapi/user/listbypage?access_token=$newToken" -ContentType "application/json" -Body $body
$out += "[2] listbypage(new tok): " + ($r2 | ConvertTo-Json -Depth 5 -Compress)
} catch { $out += "[2] listbypage(new tok) HTTP EX: " + $_.Exception.Message }
# 3. 新版 token 调 v2 listsub(部门接口对照)
try {
$r3 = Invoke-RestMethod -Method Post -Uri "https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=$newToken" -ContentType "application/json" -Body '{"dept_id":1}'
$out += "[3] v2 listsub(new tok): " + ($r3 | ConvertTo-Json -Depth 5 -Compress)
} catch { $out += "[3] v2 listsub(new tok) HTTP EX: " + $_.Exception.Message }
# 4. 旧版 gettoken
$oldTok = Invoke-RestMethod -Method Get -Uri "https://oapi.dingtalk.com/gettoken?appkey=$appKey&appsecret=$appSecret"
$oldToken = $oldTok.access_token
$out += "[4] gettoken errcode=$($oldTok.errcode) errmsg=$($oldTok.errmsg) oldToken len=$($oldToken.Length)"
# 5. 旧版 token 调 listbypage(对照:若成功则说明是新旧 token 兼容问题)
if ($oldToken) {
try {
$r5 = Invoke-RestMethod -Method Post -Uri "https://oapi.dingtalk.com/topapi/user/listbypage?access_token=$oldToken" -ContentType "application/json" -Body $body
$out += "[5] listbypage(old tok): " + ($r5 | ConvertTo-Json -Depth 5 -Compress)
} catch { $out += "[5] listbypage(old tok) HTTP EX: " + $_.Exception.Message }
# 6. 旧版 token 调 v2 版用户接口(另一种可选实现)
try {
$r6 = Invoke-RestMethod -Method Post -Uri "https://oapi.dingtalk.com/topapi/v2/user/list?access_token=$oldToken" -ContentType "application/json" -Body '{"dept_id":1,"cursor":0,"size":10}'
$out += "[6] v2 user/list(old tok): " + ($r6 | ConvertTo-Json -Depth 5 -Compress)
} catch { $out += "[6] v2 user/list(old tok) HTTP EX: " + $_.Exception.Message }
}
$out | Out-File -Encoding utf8 "d:\code\crm-backend-matt\tmp\dt-diag-out.txt"
$out | ForEach-Object { Write-Output $_ }

3
tmp/dt-run.ps1

@ -0,0 +1,3 @@
# 启动 crm-app(verify profile,后台运行)
Set-Location "d:\code\crm-backend-matt"
& "C:\Users\luowj\.jdks\jdk-17.0.15\bin\java.exe" -jar crm-app\target\crm-app-1.0.0-SNAPSHOT.jar --spring.profiles.active=verify

33
tmp/dt-sync.ps1

@ -0,0 +1,33 @@
# 拉取钉钉组织数据:调试 token -> POST /api/system/sync/dingtalk
$base = "http://127.0.0.1:8081"
$userId = "739564171091247104"
# 1. verify profile 调试 token
$tokResp = Invoke-RestMethod -Method Get -Uri "$base/api/auth/debug/token?userId=$userId" -TimeoutSec 30
$token = $tokResp.data
if (-not $token) {
Write-Output "=== TOKEN FAIL ==="
$tokResp | ConvertTo-Json -Depth 5
exit 1
}
$token | Out-File -Encoding ascii "d:\code\crm-backend-matt\tmp\dt-token.txt"
Write-Output "token OK (len=$($token.Length))"
# 2. 触发钉钉组织同步(阻塞,组织规模大时较慢)
try {
$syncResp = Invoke-RestMethod -Method Post -Uri "$base/api/system/sync/dingtalk" `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/x-www-form-urlencoded" -TimeoutSec 900
Write-Output "=== SYNC OK ==="
$syncResp | ConvertTo-Json -Depth 6
} catch {
Write-Output "=== SYNC FAIL ==="
$resp = $_.Exception.Response
if ($resp) {
try {
$sr = New-Object IO.StreamReader($resp.GetResponseStream())
Write-Output ("HTTP " + [int]$resp.StatusCode)
Write-Output $sr.ReadToEnd()
} catch { Write-Output $_.Exception.Message }
} else { Write-Output $_.Exception.Message }
}

13
tmp/dt-test-build.ps1

@ -0,0 +1,13 @@
# 1) crm-auth 单测验证(-am 同 reactor 构建依赖模块,避免陈旧 jar)
# 2) 全反应厨 clean package -DskipTests
$env:JAVA_HOME = "C:\Users\luowj\.jdks\jdk-17.0.15"
Set-Location "d:\code\crm-backend-matt"
Write-Output "===== crm-auth tests ====="
& mvn.cmd -s settings.xml -pl crm-auth -am test
if ($LASTEXITCODE -ne 0) { Write-Output "TEST FAIL"; exit 1 }
Write-Output "===== full package ====="
& mvn.cmd -s settings.xml clean package -DskipTests
if ($LASTEXITCODE -ne 0) { Write-Output "PACKAGE FAIL"; exit 1 }
Write-Output "ALL OK"

16
tmp/dt-test-build2.ps1

@ -0,0 +1,16 @@
# 验证字典权限调整:crm-dict 测试 + 全反应应重新打包
$env:JAVA_HOME = "C:\Users\luowj\.jdks\jdk-17.0.15"
Set-Location "d:\code\crm-backend-matt"
# 停掉残留 8081 java 进程(避免锁 fat jar)
$c = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
if ($c) { $c | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } ; Start-Sleep 2 }
Write-Output "===== crm-dict tests ====="
& mvn.cmd -s settings.xml -pl crm-dict -am test
if ($LASTEXITCODE -ne 0) { Write-Output "TEST FAIL"; exit 1 }
Write-Output "===== full package ====="
& mvn.cmd -s settings.xml clean package -DskipTests
if ($LASTEXITCODE -ne 0) { Write-Output "PACKAGE FAIL"; exit 1 }
Write-Output "ALL OK"

2
tmp/dt-token.txt

@ -1 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI3Mzk1NjQxNzEwOTEyNDcxMDQiLCJqdGkiOiI1MzYwYTM2ZWZlNzk0MjM1YTZkYTQyNGJhYmNjNTkwNiJ9.42V42Zsh3GV78G1MYHAleTcsy9SQ2QMCsSo0zDIkQJ4
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI3Mzk1NjQxNzEwOTEyNDcxMDQiLCJqdGkiOiI0NDk3NmY5YmM5MDk0ZWU5YWU5MmJkYTE0NDdkMGRjZSJ9.DGYUjmq2qJcR-C9WUiQWM60CWCuHqADrK2BLLOOnShA

24
tmp/dt-verify.ps1

@ -0,0 +1,24 @@
# 验证钉钉同步落库:部门树 + 用户统计
$base = "http://127.0.0.1:8081"
$userId = "739564171091247104"
$tokResp = Invoke-RestMethod -Method Get -Uri "$base/api/auth/debug/token?userId=$userId" -TimeoutSec 30
$token = $tokResp.data
$headers = @{ Authorization = "Bearer $token" }
# 1. 用户统计(需 ADMIN)
try {
$stats = Invoke-RestMethod -Method Get -Uri "$base/api/system/users/stats" -Headers $headers -TimeoutSec 30
Write-Output "=== users/stats ==="
$stats | ConvertTo-Json -Depth 6 -Compress
} catch { Write-Output ("users/stats EX: " + $_.Exception.Message) }
# 2. 部门树(统计节点数)
try {
$tree = Invoke-RestMethod -Method Get -Uri "$base/api/system/depts/tree" -Headers $headers -TimeoutSec 30
$count = 0
function Walk($nodes) {
foreach ($n in $nodes) { $script:count++; if ($n.children) { Walk $n.children } }
}
Walk $tree.data
Write-Output "=== depts/tree: nodes=$count ==="
} catch { Write-Output ("depts/tree EX: " + $_.Exception.Message) }

15
tmp/frontend-verify-out.txt

@ -0,0 +1,15 @@
[token] len=177
[OK] region/level(level=1) code=0 ok=True 鐪佹暟=34
[OK] region/list(parentId=744685756336832512) code=0 ok=True 甯傛暟=1
[OK] dict/enabled-list(lead_source) code=0 ok=True 椤规暟=43
[OK] dict/enabled-list(brand) code=0 ok=True 椤规暟=9
[OK] dict/enabled-list(product) code=0 ok=True 椤规暟=16
[OK] dict/enabled-list(scene) code=0 ok=True 椤规暟=18
[OK] rule/pool/page code=0 ok=True total=2 姹犳暟=2
[OK] system/depts/tree code=0 ok=True 鑺傜偣鏁?814
[OK] system/users/page code=0 ok=True total=6047 鏈〉=5
[OK] lead/page(MANAGE) code=0 ok=True total=9 鏈〉=9
[OK] lead/stats(MANAGE) code=0 ok=True data={"total":9,"waiting":2,"claimed":3,"converted":0,"todayNew":1,"undistributed":4,"feedbackValid":2,"feedbackInvalid":0,"feedbackNone":7}
[OK] lead/stats(PUBLIC_POOL) code=0 ok=True data={"total":2,"waiting":2,"claimed":0,"converted":0,"todayNew":0,"undistributed":0,"feedbackValid":0,"feedbackInvalid":0,"feedbackNone":2}
[OK] lead/stats(MY_LEAD) code=0 ok=True data={"total":3,"waiting":0,"claimed":3,"converted":0,"todayNew":0,"undistributed":0,"feedbackValid":2,"feedbackInvalid":0,"feedbackNone":1}
==== SUMMARY: FAIL=0 / total=13 ====

59
tmp/frontend-verify.ps1

@ -0,0 +1,59 @@
# 前端契约回归:模拟 demo 页面加载时的全部自动请求,验证后端链路
$base = "http://127.0.0.1:8081"
$userId = "739564171091247104"
$out = @()
function Check($label, $resp, $extra) {
$ok = $resp.success
$line = "[{0}] {1} code={2} ok={3} {4}" -f $(if($ok){"OK"}else{"FAIL"}), $label, $resp.code, $ok, $extra
$script:out += $line
Write-Output $line
}
# 0. 登录
$tok = (Invoke-RestMethod -Method Get -Uri "$base/api/auth/debug/token?userId=$userId" -TimeoutSec 30).data
$h = @{ Authorization = "Bearer $tok" }
$out += "[token] len=$($tok.Length)"
# 1. 省份
$r1 = Invoke-RestMethod -Method Get -Uri "$base/api/rule/region/level?level=1" -Headers $h -TimeoutSec 30
Check "region/level(level=1)" $r1 ("省数=" + $r1.data.Count)
$firstProvId = $r1.data[0].id
# 2. 市级联
$r2 = Invoke-RestMethod -Method Get -Uri "$base/api/rule/region/list?parentId=$firstProvId" -Headers $h -TimeoutSec 30
Check "region/list(parentId=$firstProvId)" $r2 ("市数=" + $r2.data.Count)
# 3. 字典4组
foreach ($g in @("lead_source","brand","product","scene")) {
$rd = Invoke-RestMethod -Method Get -Uri "$base/api/dict/item/enabled-list?groupCode=$g" -Headers $h -TimeoutSec 30
Check "dict/enabled-list($g)" $rd ("项数=" + $rd.data.Count)
}
# 4. 公海池
$rp = Invoke-RestMethod -Method Post -Uri "$base/api/rule/pool/page" -Headers $h -ContentType "application/x-www-form-urlencoded" -Body "current=1&size=50" -TimeoutSec 30
Check "rule/pool/page" $rp ("total=" + $rp.data.total + " 池数=" + $rp.data.content.Count)
# 5. 部门树
$rd2 = Invoke-RestMethod -Method Get -Uri "$base/api/system/depts/tree" -Headers $h -TimeoutSec 30
$cnt = 0; function Walk($n){ $script:cnt++; if($n.children){ $n.children | ForEach-Object { Walk $_ } } }
$rd2.data | ForEach-Object { Walk $_ }
Check "system/depts/tree" $rd2 ("节点数=" + $cnt)
# 6. 用户列表
$ru = Invoke-RestMethod -Method Post -Uri "$base/api/system/users/page" -Headers $h -ContentType "application/x-www-form-urlencoded" -Body "current=1&size=5" -TimeoutSec 30
Check "system/users/page" $ru ("total=" + $ru.data.total + " 本页=" + $ru.data.content.Count)
# 7. 线索列表(MANAGE)
$rl = Invoke-RestMethod -Method Post -Uri "$base/api/lead/page" -Headers $h -ContentType "application/x-www-form-urlencoded" -Body "current=1&size=10&viewType=MANAGE" -TimeoutSec 30
Check "lead/page(MANAGE)" $rl ("total=" + $rl.data.total + " 本页=" + $rl.data.content.Count)
# 8. 线索统计三视图
foreach ($v in @("MANAGE","PUBLIC_POOL","MY_LEAD")) {
$rs = Invoke-RestMethod -Method Post -Uri "$base/api/lead/stats" -Headers $h -ContentType "application/x-www-form-urlencoded" -Body "viewType=$v" -TimeoutSec 30
Check "lead/stats($v)" $rs ("data=" + ($rs.data | ConvertTo-Json -Compress))
}
$pass = ($out | Where-Object { $_ -like "[FAIL]*" }).Count
$out += "==== SUMMARY: FAIL=$pass / total=$($out.Count-1) ===="
$out | Out-File -Encoding utf8 "d:\code\crm-backend-matt\tmp\frontend-verify-out.txt"
Loading…
Cancel
Save