This commit is contained in:
2026-08-04 09:05:28 +08:00
parent fdb1e8af1c
commit 3afcff320a
116 changed files with 6749 additions and 749 deletions
+101 -111
View File
@@ -1,161 +1,151 @@
# 公共导出与下载中心接入说明
## 1. 目标与边界
本文描述当前已经实现的 RocketMQ + 事务 Outbox 版本。完整状态机、故障模型和容量门槛见[公共导出中心 RocketMQ 架构设计](./公共导出中心RocketMQ架构设计.md)。
`file-service` 统一承载异步导出任务、XLSX 文件生成结果、过期清理和下载鉴权。业务服务仍是业务数据唯一事实来源,通过内部、只读、分批接口向 File 提供导出数据。
## 1. 边界
- 浏览器不得拉取全部分页后自行生成 Excel。
- File 不跨库查询 IAM、Order、Warehouse 等业务数据库。
- 任务不保存用户 Bearer Token;服务间调用使用 `X-Internal-Service-Token`。
- 当前首个导出类型为 `CUSTOMER_LIST`,数据由 IAM 提供。
- 当前存储实现沿用 `FILE_STORAGE_ROOT` 下的共享目录;生产多实例必须挂载同一持久化目录,后续可替换为对象存储。
- `file-service` 统一承载任务、可靠调度、通用 XLSX、存储、过期清理、下载鉴权和指标。
- File 不跨库读取 IAM、Order、Warehouse 等业务数据;业务服务只实现标准只读 Provider。
- MQ 只传任务标识,不传筛选条件、业务行、Token 或文件。
- 一期存储是 `FILE_STORAGE_ROOT` 目录且按单节点部署;多实例前必须使用跨主机共享卷或对象存储。
- 当前首个类型为 `CUSTOMER_LIST`,由 IAM 提供数据。
## 2. 调用流程
## 2. 执行链路
```text
Admin/Client
Admin
-> POST /api/platform/v1/files/export-tasks
-> file_export_task(PENDING)
-> File worker 原子领取(RUNNING)
-> 业务服务 internal export API(最多 500 条/批)
-> SXSSFWorkbook 流式生成 XLSX
-> file_export_task(SUCCEEDED)
-> file_export_task_guard 行锁下复核重复提交及活动上限
-> 同一事务写 file_export_task + file_export_outbox
-> Publisher 投递 RocketMQ(失败无限退避)
-> Consumer 按 dispatch_version + 数据库租约领取(并发 4)
-> POST /internal/exports/v1/{TYPE}/batches
-> 通用 SXSSFWorkbook 流式写盘
-> 条件提交 SUCCEEDED
-> GET /api/platform/v1/files/export-tasks/{id}/download
```
任务状态只允许:
```text
PENDING -> RUNNING -> SUCCEEDED
-> FAILED -> PENDING(手工重试)
SUCCEEDED -> EXPIRED
PENDING -> RUNNING -> SUCCEEDED -> EXPIRED
-> PENDING(自动重试,dispatch_version + 1)
-> FAILED -> PENDING(人工重试,dispatch_version + 1)
```
服务重启后数据库中的 `PENDING` 会继续处理;超过配置阈值的 `RUNNING` 会重新排队。多个 File 实例通过 `PENDING` 条件更新原子领取,只有一个实例能成功。
消息补投保持同一版本;只有执行失败、执行租约恢复和人工重试递增版本。独立 watchdog 覆盖 Provider 拉取和最终写盘,最终成功 SQL 同时校验任务状态、版本、租约所有者与数据库 deadline。
## 3. 平台公开接口
Reconciler 负责补写缺失 Outbox、同版本重发长期 `SENT` 命令、恢复超时执行租约、释放超时 Outbox 租约及过期文件清理。旧 `ExportTaskWorker` 已删除,避免绕过版本和退避窗口。
所有接口都经过 Gateway 的现有 `/api/platform/v1/files/**` 路由。
## 3. 浏览器 API
### 3.1 创建导出任务
- `POST /api/platform/v1/files/export-tasks`
- `GET /api/platform/v1/files/export-tasks`
- `GET /api/platform/v1/files/export-tasks/{id}`
- `GET /api/platform/v1/files/export-tasks/{id}/download`
- `POST /api/platform/v1/files/export-tasks/{id}/retry`
`POST /api/platform/v1/files/export-tasks`
创建示例:
```json
{
"exportType": "CUSTOMER_LIST",
"format": "XLSX",
"taskName": "客户列表",
"parameters": {
"keyword": "柏悦",
"status": "ACTIVE"
}
"parameters": { "keyword": "柏悦", "status": "ACTIVE" }
}
```
创建接口按 `exportType` 校验来源权限。客户导出要求 `platform:customer:export`。筛选参数在创建时形成快照,后续页面修改筛选条件不会改变已提交任务。
创建按注册表校验访问域、启用状态和来源权限。客户导出要求 `platform:customer:export`;列表和下载要求 `platform:download:list`。默认只能访问本人任务,`platform:download:all` 可访问同一租户与应用域的全部任务。人工重试只允许 `FAILED`,并重新校验类型启用状态和来源权限。
### 3.2 下载中心列表
限制:单用户最多 3 个活动任务、单类型最多 5 个活动任务;同一用户 60 秒内相同类型和规范化参数返回已有活动任务。
`GET /api/platform/v1/files/export-tasks?page=1&size=20&exportType=CUSTOMER_LIST&status=SUCCEEDED&requestedFrom=2026-08-01&requestedTo=2026-08-03`
需要 `platform:download:list`。默认只返回当前登录用户创建的任务;拥有 `platform:download:all` 时可查看同一租户和应用域内所有用户任务。
### 3.3 任务详情、下载和重试
- `GET /api/platform/v1/files/export-tasks/{id}`
- `GET /api/platform/v1/files/export-tasks/{id}/download`
- `POST /api/platform/v1/files/export-tasks/{id}/retry`
下载仅允许 `SUCCEEDED` 且未过期的任务。重试仅允许 `FAILED`,并重新校验该导出类型的来源权限。服务端始终校验任务租户、应用域和申请人归属,前端按钮隐藏不能替代鉴权。
## 4. IAM 客户数据提供契约
File 调用:
`GET /internal/iam/exports/customers?cursor={lastId}&limit=500&keyword={keyword}&status={status}`
请求头:
## 4. Provider 契约
```http
POST /internal/exports/v1/{exportType}/batches
X-Internal-Service-Token: ${INTERNAL_SERVICE_TOKEN}
Content-Type: application/json
```
响应:
请求使用 `common-core` 的 `ExportBatchRequest`,必须包含 `taskId`、`tenantId`、`appCode`、`requestedBy`、`dispatchVersion`、`parameters`、`cursor`、`snapshotToken` 和 `limit`。Provider 必须按身份字段约束查询,不得信任 parameters 内的租户字段。
```json
{
"code": "200",
"data": {
"records": [],
"nextCursor": 190000000000000001,
"hasMore": false
}
}
响应使用 `ExportBatchResponse`:首批返回 `schemaVersion`、`schemaHash`、`snapshotToken`、`columns`、`rows`、游标和估算总行数;后续批次必须保持 `schemaHash`,可省略 `columns`。File 在 Feign 解码流读取阶段限制单批 4MB,再校验游标推进、列/行/文件上限,并支持 `STRING`、`INTEGER`、`DECIMAL`、`BOOLEAN`、`DATE`、`DATETIME` 六类 Excel 单元格。
IAM `CUSTOMER_LIST` Provider 强制 `tenantId=0`、`appCode=PLATFORM`,采用 HMAC 签名的最大商户 ID 快照。因此不会混入开始后新增客户,但不冻结期间的修改与删除。
新增类型只需:业务服务实现标准 Provider、在 `baiyue.file.export.definitions` 注册受控服务/路径/权限/访问域/容量参数、补权限和测试。不得修改公共 Controller、Consumer 或 Writer。
## 5. MQ 与 DLQ
- Topic:`${MQ_TOPIC_PREFIX}baiyue-file-export-command-v1`,4 个队列。
- Consumer:`${MQ_TOPIC_PREFIX}baiyue-file-export-worker-v1`,同步并发 4。
- Producer:`${MQ_TOPIC_PREFIX}baiyue-file-export-publisher-v1`。
- `eventId`、`taskId`、`dispatchVersion`、`schemaVersion` 同时写入 User Properties。
- 业务失败从不进入 DLQ;只有无法反序列化或不支持版本的消息不确认消费。
- DLQ Handler 不解析损坏 body,只从四个属性定位,并在同一事务内把 Outbox 置 `QUARANTINED`、当前 PENDING 任务置 `FAILED`。缺少定位属性只记录 CRITICAL 和 messageId,不误伤任务。
- DLQ Handler 的数据库处置失败持续重试,不允许因有限次数耗尽进入无人消费的二级 DLQ。
## 6. 配置与部署
关键环境变量:
```text
ROCKETMQ_NAME_SERVER
ROCKETMQ_BROKER_IP
MQ_TOPIC_PREFIX
FILE_EXPORT_CONSUMER_CONCURRENCY
FILE_EXPORT_RECONCILER_ENABLED
FILE_EXPORT_PUBLISHER_ENABLED
FILE_EXPORT_CONSUMER_ENABLED
FILE_EXPORT_RETENTION_DAYS
FILE_STORAGE_ROOT
```
IAM 按租户 ID 升序游标读取,只查询 `tenant_type=MERCHANT`,并复用客户列表的 `keyword/status` 语义。导出 DTO 与平台列表 DTO 分离,因此不会复用列表中的电话和邮箱脱敏结果。
`MQ_TOPIC_PREFIX` 是部署必填项但允许空值:测试环境显式填 `test-`,生产环境显式填空字符串。不能使用 `:-test-` 作为 Compose 默认值,否则生产空值会被重新替换成测试前缀。
## 5. 新增其他导出类型
当前 RocketMQ Compose 不映射 Broker 宿主机端口,File 与 Broker 必须同在 `infra-network`,因此 `ROCKETMQ_BROKER_IP` 填 `rocketmq-broker`。Topic 初始化会等待 `BaiyueCluster` 真正注册,任一幂等初始化命令失败后自动重试。
以订单导出为例:
File 容器初始 JVM 为 `-Xms1g -Xmx2g -XX:+ExitOnOutOfMemoryError`。Provider 单批连接/读取超时分别为 3 秒/15 秒;执行租约 300 秒、watchdog 60 秒、deadline 30 分钟、消费超时 90 分钟、单文件 50MB。
1. 数据所属服务提供带内部令牌校验的只读游标接口,不允许 File 直接访问其数据库。
2. 在 File 增加一个 `ExportTaskHandler` Bean:
- `exportType()` 返回稳定编码,例如 `FIRST_LEG_ORDER_LIST`;
- `requiredPermission()` 返回创建该任务所需的业务权限;
- `export(...)` 分批调用来源服务、流式写入文件并报告进度。
3. 增加对应 Feign Client 和导出专用 DTO,不复用面向页面的脱敏或分页 DTO。
4. 添加稳定权限资源、管理员授权和前端任务类型显示。
5. 增加处理器 XLSX 内容测试、来源接口筛选/游标测试和前端提交测试。
开发阶段确认无保留任务后,重建并执行 `file/src/main/resources/db/file.sql`。RocketMQ 使用 `deploy/docker-compose.rocketmq.yml`;脚本会先校验并展开 `ROCKETMQ_BROKER_IP`,再显式创建 4 队列命令 Topic、Worker 消费组、DLQ Handler 消费组以及可读写的 Worker DLQ。上线顺序:Broker -> SQL -> Nacos 注册表 -> IAM Provider -> File 三开关全关 -> Reconciler -> Publisher -> Consumer -> Admin。
不要在公共 Controller 中增加按类型展开的业务查询分支;类型扩展通过 Spring 注入的 `ExportTaskHandler` 注册表完成。
生产启用并发 4 前必须完成 4 个 20 万行任务并发压测;若 CPU、堆、磁盘、Provider 或 IAM 普通接口延迟不达设计门槛,回退并发 2。
## 6. 存储、保留与配置
## 7. 观测与验收
```yaml
baiyue:
file:
storage-root: ${FILE_STORAGE_ROOT}
export:
retention-days: ${FILE_EXPORT_RETENTION_DAYS:7}
worker-delay-ms: ${FILE_EXPORT_WORKER_DELAY_MS:2000}
stale-running-minutes: ${FILE_EXPORT_STALE_RUNNING_MINUTES:30}
Prometheus 指标包括任务创建/完成/耗时、队列年龄、Outbox 数量/年龄/最大尝试次数、Reconciler 动作与 `export_dlq_messages_total`;日志携带任务与版本标识但不打印完整数据或令牌。
最低验收:任务与 Outbox 同事务;Broker 停机恢复后追平;重复消息只生成一个有效文件;watchdog 覆盖最终写盘;deadline 竞态不能提交成功;同版本补投不递增版本;毒消息只隔离一次;IAM 快照不混入新增客户;生成的 XLSX 可真实打开且日期、金额为可计算类型;普通用户不能访问他人任务。
### 7.1 测试环境自动验收入口
`scripts/export-center-test-env-acceptance.ps1` 通过 Gateway 创建真实 `CUSTOMER_LIST` 任务,轮询终态,下载文件并校验 XLSX 的 ZIP 签名。Token 只通过参数或环境变量注入,脚本不会登录、部署、停止 Broker 或改数据库。
单任务冒烟:
```powershell
$env:ADMIN_ACCESS_TOKEN = '<测试环境平台用户Token>'
.\scripts\export-center-test-env-acceptance.ps1 `
-GatewayBaseUrl 'http://8.163.30.128:9000'
```
导出文件位于 `${FILE_STORAGE_ROOT}/exports/{taskId}/{uuid}.xlsx`。`object_key` 必须匹配服务端固定格式,任意绝对路径、`..` 或非 XLSX 后缀都会被拒绝。任务过期后先删除文件,再标记 `EXPIRED`。
并发 4 容量验收必须使用 4 个不同申请用户,避免 60 秒重复提交合并,并要求每个账号都具备 `platform:customer:export` 与 `platform:download:list`:
## 7. SQL 与部署顺序
```powershell
$tokens = @($env:EXPORT_TOKEN_1, $env:EXPORT_TOKEN_2, $env:EXPORT_TOKEN_3, $env:EXPORT_TOKEN_4)
.\scripts\export-center-test-env-acceptance.ps1 `
-GatewayBaseUrl 'http://8.163.30.128:9000' `
-AccessTokens $tokens -TaskCount 4 -MinimumRows 200000 -TimeoutSeconds 3600
```
新环境:
故障注入分两段执行:故障期间使用 `-CreateOnly` 创建并保存任务 ID;恢复 Broker、File 或 IAM 后,用相同申请人的 Token 和 `-TaskIds` 继续轮询与下载。这样停止/恢复基础设施仍由有权限的运维操作,脚本只验证任务是否最终追平:
1. 执行 `iam/src/main/resources/db/phase1-admin-rbac.sql` 及后续 IAM 权限脚本。
2. 执行 `file/src/main/resources/db/file.sql` 创建 `file_export_task`。
3. 部署 IAM,再部署 File,最后部署 Admin 静态资源。
```powershell
.\scripts\export-center-test-env-acceptance.ps1 `
-GatewayBaseUrl 'http://8.163.30.128:9000' -AccessTokens $tokens -TaskCount 4 -CreateOnly
现有测试环境可执行 `deploy/sql/2026-07-31一期回归测试环境增量迁移.sql`。脚本幂等创建任务表并补齐:
.\scripts\export-center-test-env-acceptance.ps1 `
-GatewayBaseUrl 'http://8.163.30.128:9000' -AccessTokens $tokens `
-TaskIds @('<taskId1>', '<taskId2>', '<taskId3>', '<taskId4>') -MinimumRows 200000
```
- `platform:customer:export`
- `platform:download:list`
- `platform:download:all`
File 启动前必须确认 `FILE_STORAGE_ROOT` 是持久化且可写的共享目录。
## 8. 错误与追踪
- 创建、列表和重试使用统一 `ApiResponse`,失败记录业务码、`requestId`、`traceId`。
- 下载接口成功返回文件流;失败仍由统一异常处理返回 JSON 错误。
- 来源服务不可用或返回非成功业务码时任务进入 `FAILED`,下载中心展示可理解的失败原因。
- File 日志使用任务 ID 和导出类型关联故障;不得打印内部令牌、Bearer Token 或完整导出数据。
## 9. 验收清单
- 创建任务后浏览器没有批量调用业务列表,也没有生成 HTML `.xls`。
- 当前筛选条件原样进入 `parameters_json`。
- 两个 worker 同时领取时只有一个能将任务从 `PENDING` 更新为 `RUNNING`。
- XLSX 可被标准办公软件打开,表头和数据列匹配。
- 普通用户看不到、下载不了其他用户任务;`platform:download:all` 用户可以。
- `FAILED` 可以重试,其他状态不能重试。
- 过期任务不能下载,文件被清理。
- 服务重启后 `PENDING` 继续执行,超时 `RUNNING` 能重新排队。
- Admin 下载中心覆盖加载、空数据、失败、处理中、完成、失败、过期和窄屏操作状态。
脚本通过只证明 API 创建、数据库状态机、MQ 触发、Provider、XLSX 写入和下载主链路。CPU/堆/磁盘、Provider P95/P99、IAM 普通接口退化比例仍须同时从 Prometheus/Grafana 取证;DLQ 毒消息必须由 `mqadmin` 按 §19.2 投递,不能由普通验收账号伪造。
File diff suppressed because it is too large Load Diff