From 9d3bb13ecf69dc88e09daef1d43bd1827f21b608 Mon Sep 17 00:00:00 2001 From: wangkai <10903508@qq.com> Date: Fri, 23 Oct 2026 18:12:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(customer):=20=E6=96=B0=E5=A2=9EICC?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E4=BF=A1=E6=81=AF=E5=90=8C=E6=AD=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20-=20OAuth2=E5=AF=86=E7=A0=81=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E6=8B=89=E5=8F=96org=5Fbase=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E3=80=81=E6=8C=89icc=5Fcode=E5=B9=82=E7=AD=89upsert?= =?UTF-8?q?=E5=86=99=E5=85=A5=E5=B9=B6=E8=AE=B0=E5=BD=95icc=5Fsync=5Flog?= =?UTF-8?q?=EF=BC=8C=E6=B3=A8=E5=86=8C=E6=AF=8F10=E5=88=86=E9=92=9F?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=EF=BC=9B=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E6=A1=A3=E6=A1=88=E7=AE=A1=E7=90=86=E9=A1=B5=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=8B=E5=8A=A8'=E5=90=8C=E6=AD=A5=E5=AE=A2=E6=88=B7'?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=88POST=20/api/customers/sync/icc?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=90=8C=E6=AD=A5=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E5=88=97=E8=A1=A8=E4=B8=8E=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=EF=BC=9Bcustomer=E8=A1=A8=E6=96=B0=E5=A2=9Eicc=5Fcode=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=8F=8A=E5=94=AF=E4=B8=80=E7=B4=A2=E5=BC=95=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=AD=A5db=5Fseed=E4=B8=8E=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/add-icc-customer-sync/checklist.md | 12 + .trae/specs/add-icc-customer-sync/spec.md | 52 +++ .trae/specs/add-icc-customer-sync/tasks.md | 38 ++ client/src/api/customer.ts | 2 + client/src/views/customer/info/index.vue | 40 +- client/vite.config.ts | 1 + database/db_init.sql | 3 + database/db_seed.sql | 8 + .../migrations/20261016_icc_customer_sync.sql | 21 ++ server/.env | 9 +- server/package.json | 1 + server/src/config/config.default.ts | 7 + server/src/controller/customer.controller.ts | 14 + server/src/entity/customer.entity.ts | 7 + server/src/service/customer.service.ts | 2 +- .../src/service/icc_customer_sync.service.ts | 282 ++++++++++++++ server/src/service/job_scheduler.service.ts | 7 + server/src/task/icc_customer_auth.ts | 177 +++++++++ server/src/task/icc_customer_mapper.ts | 87 +++++ server/src/task/icc_customer_sync.task.ts | 17 + server/src/task/job_registry.ts | 5 +- server/yarn.lock | 5 + 系统设计/表结构新增及变更.md | 17 + 系统设计/鑫潼ICC平台接口文档.md | 350 ++++++++++++++++++ 24 files changed, 1159 insertions(+), 5 deletions(-) create mode 100644 .trae/specs/add-icc-customer-sync/checklist.md create mode 100644 .trae/specs/add-icc-customer-sync/spec.md create mode 100644 .trae/specs/add-icc-customer-sync/tasks.md create mode 100644 database/migrations/20261016_icc_customer_sync.sql create mode 100644 server/src/service/icc_customer_sync.service.ts create mode 100644 server/src/task/icc_customer_auth.ts create mode 100644 server/src/task/icc_customer_mapper.ts create mode 100644 server/src/task/icc_customer_sync.task.ts create mode 100644 系统设计/表结构新增及变更.md create mode 100644 系统设计/鑫潼ICC平台接口文档.md diff --git a/.trae/specs/add-icc-customer-sync/checklist.md b/.trae/specs/add-icc-customer-sync/checklist.md new file mode 100644 index 0000000..24c5be4 --- /dev/null +++ b/.trae/specs/add-icc-customer-sync/checklist.md @@ -0,0 +1,12 @@ +# Checklist + +- [x] customer 表迁移与 db_init.sql 均包含 icc_code 字段与唯一索引,且结构一致(migrations/20261016_icc_customer_sync.sql ↔ db_init.sql L194/L232/L213 ↔ customer.entity.ts) +- [x] customer 实体含 iccCode 字段定义(field=icc_code,STRING(64),可空) +- [x] ICC OAuth2 鉴权客户端可获取公钥并完成 RSA 加密、换取 access_token,token 有进程内缓存与过期续签(icc_customer_auth.ts,RSA 分块加解密冒烟验证通过) +- [x] 字段映射器正确映射 orgCode→iccCode、orgName→customerName、province/city/county,并解析分页响应(icc_customer_mapper.ts,冒烟验证映射与 PageVO/数组兼容解析均正确) +- [x] 同步服务按 org_base 过滤、分页全量拉取(maxRangeId=0、pageSize=1000 循环至末页),按 icc_code 幂等 upsert +- [x] 新增客户自动生成 customer_no(复用 CustomerService.generateCustomerNo,KH+YYYYMMDD+3位序号);已存在客户先比较四个映射字段,仅不一致才更新,其余字段保持不变 +- [x] 每次同步写 icc_sync_log(sync_type=customer,status/record_count/error_message),失败路径仅记日志不抛业务错误 +- [x] 定时任务已注册(job_registry L5/L37/L64 + db_seed.sql L411 sys_job 种子),默认 cron `*/10 * * * *`,调度器运行时已接线(job_scheduler.service.ts syncIccCustomers) +- [x] server/.env 含完整 ICC 配置键(BASE_URL/VERSION/CLIENT_ID/CLIENT_SECRET/USERNAME/PASSWORD) +- [x] `yarn build` 编译通过(mapper 修复后复验通过),任务代码路径(task → runtime → service)类型正确可运行;端到端写库同步建议在部署环境首次执行时由真实 ICC 接口确认 diff --git a/.trae/specs/add-icc-customer-sync/spec.md b/.trae/specs/add-icc-customer-sync/spec.md new file mode 100644 index 0000000..c6afc51 --- /dev/null +++ b/.trae/specs/add-icc-customer-sync/spec.md @@ -0,0 +1,52 @@ +# ICC 客户信息同步(org_base)Spec + +## Why +客户档案目前依赖手工维护,鑫潼 ICC 平台已存在基础组织(org_base)客户主数据。需按 ICC 平台接口文档,通过定时任务从 ICC 拉取 org_base 组织数据,按字段映射对 customer 客户表执行新增/变更,实现客户主数据自动化同步。 + +## What Changes +- **新增** `customer` 表 `icc_code` 字段(ICC 客户编码,唯一索引),作为 ICC 同步的幂等匹配键;同步修改实体、迁移脚本与 db_seed.sql(幂等一致) +- **新增** ICC OAuth2 密码模式鉴权客户端:获取公钥 → RSA(ECB/PKCS1) 加密密码 → `extend/token` 换取 access_token(2h 有效,进程内缓存过期自动续签) +- **新增** `IccCustomerSyncService`:从 `.env` 读取 ICC 配置,分页全量拉取 `organization/page`(`maxRangeId=0`,过滤 `orgType=org_base`),按字段映射对 customer 幂等 upsert(已存在记录先比较映射字段,不一致才变更),写 `icc_sync_log` +- **新增** 定时任务:注册 `icc_customer_sync.task` 到任务注册表与 `sys_job`(默认每 10 分钟,可配置) +- **安全**:全部 SQL 走 Sequelize 参数绑定;接口凭据经 `.env` 读取,不硬编码到代码;ICC 拉取失败仅记录 `icc_sync_log` 错误日志,不抛业务错误 + +## Impact +- Affected specs: ICC 数据同步(含现有 icc_line_sync 模式)、客户管理(customer 主数据) +- Affected code: + - server/src/entity/customer.entity.ts(新增 iccCode) + - server/src/task/icc_customer_mapper.ts(新增:字段映射集中配置) + - server/src/service/icc_customer_sync.service.ts(新增:OAuth2 鉴权 + 拉取 + upsert + 日志) + - server/src/task/icc_customer_sync.task.ts(新增:定时任务定义) + - server/src/task/job_registry.ts、server/src/service/job_scheduler.service.ts(注册任务与运行时) + - server/.env(新增 ICC 凭据配置项) + - database/migrations/(新增迁移文件)、database/db_seed.sql(customer 表结构与 sys_job 种子) + +## ADDED Requirements + +### Requirement: ICC 客户信息同步(org_base 定时任务) +系统 SHALL 通过定时任务从 ICC 平台同步 org_base 客户信息到 customer 表:OAuth2 鉴权获取 access_token → 分页全量拉取组织数据(maxRangeId=0,pageSize≤1000,循环至末页)→ 过滤 `orgType=org_base` → 按字段映射(`orgCode→iccCode`、`orgName→customerName`、`province→province`、`city→city`、`county→district`)幂等 upsert,并写 `icc_sync_log`(sync_type=`customer`)。 + +#### Scenario: 定时同步成功 +- **WHEN** 定时任务触发(默认 `*/10 * * * *`,可配置) +- **THEN** 拉取 ICC org_base 组织并新增/变更对应 customer 记录;新增客户自动生成 `customer_no`(沿用 KH+YYYYMMDD+序号规则);已存在客户(按 `icc_code` 匹配)先比较 customer_name/province/city/district 四个映射字段与现有值,仅当任一字段不一致时才执行更新,一致则跳过,其余手工字段(status/salesman_id/联系方式等)保持不变;`icc_sync_log` 写入成功记录 + +#### Scenario: 重复执行幂等 +- **WHEN** 同一 orgCode 连续两次同步均存在且 ICC 数据未变化 +- **THEN** 第二次执行按 icc_code 命中已有记录,比较后字段一致不执行 update,不产生重复客户、不产生无意义变更 + +#### Scenario: 全量分页拉取 +- **WHEN** 组织数据超过一页 +- **THEN** 按 pageNum 递增循环请求直至 currentPage==totalPage,全部合并后统一 upsert + +#### Scenario: ICC 鉴权失败 / 接口异常 +- **WHEN** 公钥获取失败、token 换取失败或组织接口返回非 0 code / HTTP 非 2xx +- **THEN** 同步失败仅写 `icc_sync_log`(status=0、error_message),业务不抛错;access_token 过期时自动重新走 OAuth2 鉴权 + +### Requirement: customer 表 icc_code 字段 +系统 SHALL 在 customer 表新增 `icc_code` 字段(字符串,可空,唯一索引)用于承载 ICC 客户编码;已有客户在首次同步时按 orgCode 匹配后回填该字段;该字段不参与客户档案页面手工编辑逻辑(同步来源字段)。 + +### Requirement: ICC 凭据环境配置 +系统 SHALL 通过 `.env` 读取 ICC 平台连接配置:`ICC_BASE_URL`(host)、`ICC_VERSION`、`ICC_CLIENT_ID`、`ICC_CLIENT_SECRET`、`ICC_USERNAME`、`ICC_PASSWORD`;业务代码不出现明文凭据。 + +## MODIFIED Requirements +(无既有功能被修改;现有 icc_line_sync 任务保持原行为不变) diff --git a/.trae/specs/add-icc-customer-sync/tasks.md b/.trae/specs/add-icc-customer-sync/tasks.md new file mode 100644 index 0000000..cadfea3 --- /dev/null +++ b/.trae/specs/add-icc-customer-sync/tasks.md @@ -0,0 +1,38 @@ +# Tasks + +- [x] Task 1: customer 表新增 icc_code 字段(数据库层) + - [x] 新建迁移文件 `database/migrations/20261016_icc_customer_sync.sql`:`ALTER TABLE customer ADD COLUMN icc_code`(幂等)+ 唯一索引 + - [x] 同步更新 `database/db_init.sql` 中 customer 建表结构(含 icc_code 与唯一索引),保持幂等一致 +- [x] Task 2: customer 实体新增 iccCode 字段 + - [x] `server/src/entity/customer.entity.ts` 增加 `iccCode`(STRING(64),field=icc_code,不设 unique 避免与迁移脚本索引重复) +- [x] Task 3: ICC OAuth2 鉴权客户端 + - [x] 封装公钥获取(GET `/evo-apigw/evo-oauth/{version}/oauth/public-key`) + - [x] 封装 RSA/ECB/PKCS1 分块加密(Node `crypto.publicEncrypt`,块长 117,Base64 输出,对齐文档 Java 示例)— 已冒烟验证加解密往返一致 + - [x] 封装 token 获取(POST `/evo-apigw/evo-oauth/{version}/oauth/extend/token`),进程内缓存 access_token,按 expires_in 过期自动续签 +- [x] Task 4: ICC 客户字段映射器 + - [x] `server/src/task/icc_customer_mapper.ts`:集中配置接口端点、org_base 过滤条件、字段映射(orgCode→iccCode、orgName→customerName、province/city/county)、PageVO 解析 — 修复读取 bug 后冒烟验证通过 +- [x] Task 5: IccCustomerSyncService 同步服务 + - [x] `server/src/service/icc_customer_sync.service.ts`:读取 `.env` 配置(缺失视为失败仅记日志) + - [x] 分页全量拉取(maxRangeId=0,pageSize=1000,循环至末页),过滤 orgType=org_base + - [x] 预取现有客户 icc_code 映射,按 iccCode 幂等 upsert:先比较 customer_name/province/city/district 四个映射字段,仅任一字段不一致才 update,一致则跳过;新增客户复用 CustomerService.generateCustomerNo(已改 public,KH+YYYYMMDD+3位序号);其余字段(status/salesman_id/联系方式等)不动 + - [x] 每次同步写 `icc_sync_log`(sync_type=customer,status/record_count/error_message);失败仅记录不抛出 +- [x] Task 6: 定时任务注册与调度器接线 + - [x] `server/src/task/icc_customer_sync.task.ts`:jobKey=`icc_customer_sync`,默认 cron `*/10 * * * *` + - [x] `server/src/task/job_registry.ts`:注册到 BUILTIN_JOBS,JobRuntime 增加 syncIccCustomers + - [x] `server/src/service/job_scheduler.service.ts`:buildRuntime 增加 `syncIccCustomers`,经匿名上下文注入 IccCustomerSyncService + - [x] `database/db_seed.sql` 新增 sys_job 种子(job_key=icc_customer_sync,按 job_key 幂等) +- [x] Task 7: .env 配置 + - [x] `server/.env` 增加 ICC_BASE_URL / ICC_VERSION / ICC_CLIENT_ID / ICC_CLIENT_SECRET / ICC_USERNAME / ICC_PASSWORD(凭据取自接口文档) +- [x] Task 8: 编译与运行验证 + - [x] `server` 目录执行 `yarn build` 通过(修复 mapper 后复验通过) + - [x] 冒烟验证:RSA 分块加解密往返一致、映射器字段映射与分页解析正确(无副作用,未连库);端到端写库同步需在部署环境首次执行时由真实 ICC 接口 + 数据库实际确认 + +# Task Dependencies +- [Task 1] 无依赖,可先行 +- [Task 2] 依赖 [Task 1](字段定义与库结构一致) +- [Task 3] 无依赖,可与 [Task 1] 并行 +- [Task 4] 依赖 [Task 3](需确认响应结构/鉴权头),也可并行开发 +- [Task 5] 依赖 [Task 1][Task 2][Task 3][Task 4] +- [Task 6] 依赖 [Task 5] +- [Task 7] 依赖 [Task 3](凭据键名一致),可并行 +- [Task 8] 依赖全部前置任务 diff --git a/client/src/api/customer.ts b/client/src/api/customer.ts index f334dd8..c4231c8 100644 --- a/client/src/api/customer.ts +++ b/client/src/api/customer.ts @@ -86,6 +86,8 @@ export const customerApi = { getCustomerStats: () => request.get('/customers/stats'), + syncIccCustomers: () => request.post('/customers/sync/icc'), + createCustomer: (data: CustomerCreateParams) => request.post('/customers', data), diff --git a/client/src/views/customer/info/index.vue b/client/src/views/customer/info/index.vue index 1472ce9..4ffc307 100644 --- a/client/src/views/customer/info/index.vue +++ b/client/src/views/customer/info/index.vue @@ -19,6 +19,10 @@

统一维护客户的基础资料、联系信息与业务阶段,支持快速检索与档案管理。

+ + + 同步客户 + 新增客户 @@ -323,13 +327,14 @@