# 七月迭代联调交付说明

## 交付状态

本说明覆盖 `deliver-july-iteration-confirmed-scope` 的企业微信、Gateway、Redis/Asynq、对象存储和前端联调输入。生产代码、增量迁移和 OpenAPI 已生成至可联调状态；仓库迁移已新增至 `000196`，测试环境下次部署时由 API 启动脚本自动执行，尚未执行自动化测试、完整构建、LSP 或真实外部环境闭环。

接口统一使用 `{code,msg,data,timestamp}` 响应。完整请求和响应结构以 `docs/admin-openapi.yaml` 为准。

## 企业微信联调

### 环境和管理配置

| 配置 | 来源 | 说明 |
| --- | --- | --- |
| `JUNHONG_WECOM_BASE_URL` | 环境变量 | 默认 `https://qyapi.weixin.qq.com` |
| `JUNHONG_WECOM_TIMEOUT` | 环境变量 | 默认 `10s` |
| `corp_id`、`agent_id`、Secret | `POST /api/admin/wecom/applications` | 管理端按明文提交并保存；超级管理员读取时返回明文，业务日志不记录 |
| 回调 Token、EncodingAESKey | 同上 | Token 按明文提交；EncodingAESKey 固定 43 位 |
| 默认发起人 | `PUT /api/admin/wecom/applications/:id/default-creator` | 必须先同步可见成员，再从当前可见成员中选择 |
| `template_id` 和控件映射 | `PUT /api/admin/wecom/scenes/:business_type` | 模板必须先在企微后台创建，本系统只校验和绑定 |

部署侧还必须在企微管理后台配置：自建应用审批权限、应用可见通讯录范围、调用接口的可信 IP，以及公开可访问的回调地址：

```text
GET/POST {API_PUBLIC_BASE_URL}/api/callback/wecom/approval/{application_id}
```

### 配置和联调顺序

1. 部署 API 和 Worker，并确认数据库迁移已执行至最新版本。
2. 创建应用配置并调用 `POST /api/admin/wecom/applications/:id/test` 验证取 token。
3. 调用 `POST /api/admin/wecom/applications/:id/members/sync`，再分页查询成员。
4. 为内部系统账号调用 `PUT /api/admin/accounts/:id/wecom-binding` 绑定 `(corp_id,userid)`。
5. 选择一个当前可见成员作为默认发起人。代理退款、代理相关业务以及未绑定内部员工均使用该成员发起，但本地业务单仍保存真实提交人。
6. 分别配置 `refund_approval`、`offline_recharge_approval` 的 `template_id` 和控件映射。选择控件必须填写企微模板真实 option key。
7. 在企微后台配置回调 URL、Token、EncodingAESKey，完成 GET 校验。
8. 分别提交一笔退款和员工线下代充值，验证 `applyevent`、`sp_no`、回调、详情同步、标准终态和业务终态。

Integration Log 不记录 Secret、access_token、media_id、附件正文或完整企微响应；普通业务字段和业务快照按明文保存，便于业务处理。

### 结果未知和旧入口

- `applyevent` 超时或断连后标记为结果未知，不自动创建第二张审批单；Worker 通过时间窗单号和详情查询恢复。
- `wecom:approval:recovery` 每 2 分钟触发，详情同步使用 `wecom:approval:sync`。
- 在真实退款和线下代充值闭环、存量 provider 清单均验证前，保持 `JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED=true` 和 `JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED=true`。
- 切换后前端只读展示 `approval_provider`、`approval_status`、`approval_status_name`，不得再提供企微记录的本地人工通过、驳回或线下确认按钮。

## Gateway 卡限速联调

| 环境变量 | 说明 |
| --- | --- |
| `JUNHONG_GATEWAY_BASE_URL` | Gateway API 基础地址 |
| `JUNHONG_GATEWAY_APP_ID` | Gateway 应用 ID |
| `JUNHONG_GATEWAY_APP_SECRET` | Gateway 应用密钥 |
| `JUNHONG_GATEWAY_TIMEOUT` | 超时秒数，允许 5～300，默认配置为 60 |

接口固定为：

```text
PUT /api/admin/iot-cards/:iccid/speed-tier
```

请求只允许 `code=-1..8`，Gateway 业务参数只有卡 ICCID 对应的 `cardNo` 和档位 `code`。设备没有限速接口，也不得通过设备绑定关系间接限速。

联调需覆盖全部档位、越权卡、明确失败和超时。超时返回结果未知时，按 Integration Log 中的 ICCID 到 Gateway 运维侧核对当前档位，再决定是否人工重试；系统不会盲目自动重发。

## Redis、Asynq 和 Worker

API 与 Worker 必须连接同一 Redis/Asynq。新增或复用的任务如下：

| 任务类型 | 队列 | 用途 |
| --- | --- | --- |
| `wecom:approval:sync` | `wecom:approval` | 拉取企微详情并同步标准决策 |
| `wecom:approval:recovery` | `wecom:approval` | 结果未知恢复和未终态轮询 |
| `asset:package:batch_order` | 同名独立队列 | 单列 CSV 批量订购 |
| `device:import` | 同名独立队列 | 原设备导入和设备 CSV 批量分配共用任务外壳，通过 `operation_type` 分支 |
| `package:expiry:reminder` | `data:cleanup` | 每日 15/7/3 天套餐临期提醒 |
| `export:dispatch/shard/finalize` | 各自既有队列 | 六类新增 datasource 继续复用导出流水线 |

联调时确认 Worker 启动日志已注册上述 Handler，且 `JUNHONG_WORKER_ROLE` 的部署拓扑为单实例 `all`，或严格一个 `leader` 加多个 `consumer`。

## System Config 初始化

迁移 `000195_seed_july_system_config` 初始化以下 6 个代码已注册配置；使用 `ON CONFLICT DO NOTHING`，不会覆盖环境管理员已经保存的值：

- `carrier_callback.ctcc_realname.enabled=false`
- `carrier_callback.cmcc_realname.enabled=false`
- `carrier_callback.cucc_realname.enabled=false`
- `carrier_callback.cucc_realname_removal.enabled=false`
- `c2b.payment.card_allowed_methods=["wallet","wechat","alipay"]`
- `c2b.payment.device_allowed_methods=["wallet","wechat","alipay"]`

部署后通过 `GET /api/admin/system-configs` 核对 6 项均为 `registered=true`。运营商回调默认关闭，完成对应运营商联调后再逐项开启；支付方式默认全选，可由超级管理员按卡和设备分别调整。

## 对象存储和 CSV

先调用 `POST /api/admin/storage/upload-url` 获取预签名地址，再把文件直传对象存储，业务接口只接收返回的 `file_key`。

| purpose | 业务 | 文件要求 |
| --- | --- | --- |
| `batch_purchase` | 资产套餐批量订购 | UTF-8 单列 CSV，最多 1000 行、10MB |
| `device_batch_allocation` | 设备分配代理或设置套餐系列 | UTF-8 单列 CSV，支持 VirtualNo、IMEI、SN，最多 1000 行、10MB |

对象存储需配置 `JUNHONG_STORAGE_PROVIDER`、S3 endpoint/region/bucket/access key/secret key、SSL/path style 和预签名有效期。Worker 必须具备下载相同私有对象的权限。

## 前端接口和字段

| 场景 | 前端接入要点 |
| --- | --- |
| 店铺登录限制 | `PUT /api/admin/shops/:id` 写 `client_login_disabled`；列表和详情同字段回显，只阻止新登录，不踢出已有 Token |
| 实名策略 | 卡、设备分别调用 `POST /api/admin/iot-cards/batch-update-realname-policy`、`POST /api/admin/devices/batch-update-realname-policy`；最多 500 条、全成全败 |
| C 端资产初始化 | 使用 `effective_realname_policy`、`realname_required`、`realname_status`、`allowed_payment_methods`，不要在前端重新推断策略 |
| 支付方式配置 | `GET/PUT /api/admin/system-configs` 管理 `c2b.payment.card_allowed_methods`、`c2b.payment.device_allowed_methods`；至少保留一种 |
| 临期资产 | `GET /api/admin/expiring-assets`，使用数量、高亮和优先标识 |
| 企微配置 | 使用应用、成员同步/选择、默认发起人、账号绑定和场景接口；模板节点和审批人规则仍在企微后台维护 |
| 退款/充值/换货 | 展示 `submitter_id/submitter_name`；退款和充值只读展示企微审批渠道及状态 |
| 订单/退款资产 | 卡展示 ICCID；设备优先 VirtualNo、缺失时展示 IMEI；订单同时展示 `purchase_role` |
| 批量订购 | 上传 `purpose=batch_purchase`，再调用 `POST /api/admin/asset-package-batch-orders`；任务列表和详情使用统一状态名称 |
| 设备批量分配 | 上传 `purpose=device_batch_allocation`，再调用 `POST /api/admin/devices/import/allocations`；任务查询复用设备导入任务接口并展示 `operation_type/operation_name/target_id/status_name` |
| 业务导出 | `POST /api/admin/export-tasks`；新增场景为 `iot_card`、`package`、`agent_wallet_transaction`、`agent_recharge`、`refund`、`exchange` |
| 卡固定限速 | 只在卡页面展示固定档位选择；设备页面不得出现限速入口 |

## 已知限制

- 本系统不创建企微模板、不配置审批节点或审批人规则，也不提供扫码绑定。
- 企微应用只能看到其可见范围内的成员；可见范围或可信 IP 错误会导致连接、绑定或提交失败。
- 卡限速不保存本地当前档位，结果未知必须人工向 Gateway 核对。
- CSV 任务逐行部分成功，不保证整批业务原子性；任务详情是失败和跳过原因的权威展示。
- 店铺登录限制不主动吊销既有 C 端 Token。
- 测试库此前已完成 `000182`～`000194` 结构落库和注释核对；`000195`～`000196` 将在下一次测试环境部署时执行，外部系统闭环和前端浏览器验收尚未完成。

## 发布和回滚

1. 发布目标数据库必须执行至 `000196`，并核对 `tb_system_config` 已出现 6 个七月迭代默认配置、`tb_wecom_application` 已使用明文凭据字段；测试环境由 API 容器启动脚本自动迁移。
2. 先部署数据库兼容代码、API 和 Worker，保持旧审批入口开启；再配置企微应用、模板、回调和默认发起人。
3. 真实审批闭环通过后，先隐藏前端旧按钮，再关闭旧入口配置并重启 API。
4. 回滚时先停止新批量任务和新审批提交，等待已领取任务结束，再回退应用版本。新增表和列暂不删除。
5. 已产生的审批、退款、钱包流水、订单、通知、Outbox、Integration Log、设备分配和限速审计均为业务事实，不得清表回滚；使用前向修复。
