626 lines
27 KiB
Markdown
626 lines
27 KiB
Markdown
# 新增需求 01:数据同步触发与轮询优化
|
||
|
||
> 状态:已合并至标准评审稿,本文保留为实施明细。
|
||
> 评审主文档:`../../7月迭代技术方案-标准评审稿.md`
|
||
> 范围:IoT 卡实名、流量、网络状态和设备实时数据同步。
|
||
|
||
## 一、已确认决策
|
||
|
||
1. 轮询、运营商回调、业务事件触发是三条相互隔离的自动同步通道。
|
||
2. 轮询始终保留,负责回调失效、事件漏网和上游偶发失败时的最终兜底。
|
||
3. 资产是否轮询只保留现有全局开关 `enable_polling`;不再设计“实名资格、流量资格、状态资格”等业务资格开关。
|
||
4. 轮询优化只区分活跃卡和不活跃卡,通过不同轮询间隔降低无效请求,不因业务类型直接永久排除某类卡。
|
||
5. 行业卡是否需要实名不能由 `card_category=industry` 推断,统一以运营商 `realname_link_type` 判断。
|
||
6. 业务事件触发不是新增一个显式同步接口,而是埋点到关键查询、停复机、支付、实名和设备操作用例中。
|
||
7. 每次事件默认形成三个独立尝试:立即、事件发生后 3 分钟、事件发生后 5 分钟;三次结束后由常规轮询继续兜底。
|
||
8. 不设置统一五分钟冷却。重复请求控制拆成同场景触发合并、单卡请求互斥和运营商最小请求间隔;Gateway 超频只记录当前失败,不建立退避状态。
|
||
9. 运营商实名回调百分百信任,不做来源真实性校验;但必须经过防腐层完成报文解析、标识转换和状态语义转换。
|
||
10. 解除实名第一版不修改业务状态,只保留回调入口、写入统一集成审计并返回运营商要求的成功报文。
|
||
11. 三条自动通道和现有手动刷新接口共享同一套“获取上游观测并应用到业务”的 DDD 用例,禁止各入口直接更新卡字段。
|
||
12. 同步执行记录属于全局审计的 Integration Log,不在本方案新建 `tb_card_sync_execution` 或独立同步监控系统。
|
||
13. 本次迁移触碰到的旧实名、流量和状态写逻辑必须删除或改为调用新用例,不保留长期双实现。
|
||
|
||
## 二、现状问题
|
||
|
||
当前代码存在以下明确问题:
|
||
|
||
- `internal/task/polling_realname_handler.go` 直接跳过所有行业卡,与“是否实名由运营商特性决定”冲突。
|
||
- `PollingRealnameHandler`、`PollingCarddataHandler` 和 `PollingCardStatusHandler` 同时承担 Gateway 查询、字段更新和业务联动。
|
||
- `internal/service/iot_card/service.go:RefreshCardDataFromGateway` 又实现一套实名、流量和网络状态同步。
|
||
- `ManualUpdateRealnameStatus` 单独实现实名状态变更联动,无法保证与轮询、回调行为一致。
|
||
- `ClientRealnameHandler`、部分设备 Handler 直接调用 Gateway,业务事件无法在统一用例边界埋点。
|
||
- 现有获取实名链接后的自动检查借用了 `ManualTriggerService`,把系统事件伪装成平台用户手工操作。
|
||
- 调度任务载荷只有 `card_id`,缺少触发场景、来源、序列和关联链路。
|
||
- `last_sync_time` 被实名、流量和状态共同覆盖,无法说明最后同步了什么。
|
||
- 现有显式刷新接口已经存在,再增加 `/card-sync/requests` 只会形成重复入口。
|
||
|
||
因此,本次不是增加更多同步 Service,而是先收口写侧用例,再把轮询、回调和事件触发接入同一边界。
|
||
|
||
## 三、目标架构
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Polling[轮询调度] --> Activity[按活跃度计算下次间隔]
|
||
Activity --> Request[RequestCardObservation]
|
||
|
||
Business[关键业务用例] --> Trigger[CreateSyncTriggerSeries]
|
||
Query[关键查询用例] --> Trigger
|
||
Trigger --> A1[立即尝试]
|
||
Trigger --> A2[3分钟后尝试]
|
||
Trigger --> A3[5分钟后尝试]
|
||
A1 --> Gate[请求协调器]
|
||
A2 --> Gate
|
||
A3 --> Gate
|
||
Gate --> Request
|
||
|
||
Manual[现有手动刷新接口] --> Request
|
||
|
||
Callback[运营商实名回调] --> ACL[运营商防腐层]
|
||
ACL --> RealObs[标准实名观测]
|
||
|
||
Request --> Gateway[Gateway Adapter]
|
||
Gateway --> Observation[标准化观测值]
|
||
RealObs --> Apply[ApplyCardObservation]
|
||
Observation --> Apply
|
||
|
||
Apply --> Card[保存卡状态]
|
||
Apply --> Events[记录领域事件和 Outbox]
|
||
Events --> Package[套餐激活与流量扣减]
|
||
Events --> StopResume[停复机评估]
|
||
Events --> ActivityMark[更新活跃标记]
|
||
Request --> Integration[统一 Integration Log]
|
||
ACL --> Integration
|
||
Events --> Audit[关键业务 Audit Event]
|
||
```
|
||
|
||
各入口边界:
|
||
|
||
| 入口 | 是否调用 Gateway | 执行方式 | 作用 |
|
||
|---|---:|---|---|
|
||
| 轮询 | 是 | 异步、持续重排 | 最终一致性兜底 |
|
||
| 运营商回调 | 否 | 同步应用状态,联动异步可靠投递 | 实名成功急速通道 |
|
||
| 业务事件触发 | 是 | 异步 `0/3/5` 三次 Best Effort | 提高正在操作资产的命中概率 |
|
||
| 现有手动刷新 | 是 | 保持现有响应契约 | 用户明确要求立即刷新 |
|
||
|
||
手动刷新不是第四种自动策略,也不能被业务埋点调用。
|
||
|
||
## 四、DDD 设计
|
||
|
||
### 4.1 目录
|
||
|
||
```text
|
||
internal/
|
||
├── domain/cardstate/
|
||
│ ├── card.go 卡状态聚合及状态转换
|
||
│ ├── observation.go 实名、流量、网络状态观测值
|
||
│ ├── events.go 状态变化领域事件
|
||
│ └── repository.go 聚合仓储接口
|
||
├── application/cardsync/
|
||
│ ├── request_observation.go 查询 Gateway 并获取标准观测
|
||
│ ├── apply_observation.go 在事务内应用观测和领域事件
|
||
│ ├── create_trigger_series.go 创建事件触发序列
|
||
│ ├── execute_trigger_attempt.go 执行单次阶梯尝试
|
||
│ ├── mark_activity.go 更新活跃标记
|
||
│ └── refresh_asset.go 承接现有显式刷新接口
|
||
├── infrastructure/adapter/gateway/
|
||
│ └── card_observation_adapter.go 复用现有 Gateway Client
|
||
├── infrastructure/adapter/carrier_callback/
|
||
│ ├── translator.go 防腐层统一接口
|
||
│ ├── cmcc.go
|
||
│ ├── cucc.go
|
||
│ └── ctcc.go
|
||
├── infrastructure/messaging/cardsync/
|
||
│ ├── trigger_publisher.go Outbox/Asynq 任务发布
|
||
│ └── trigger_handler.go 阶梯任务处理器
|
||
└── query/cardsync/
|
||
└── activity_query.go 活跃状态和下次轮询查询
|
||
```
|
||
|
||
### 4.2 实名要求判断
|
||
|
||
不得继续使用以下判断:
|
||
|
||
```go
|
||
if card.CardCategory == constants.CardCategoryIndustry {
|
||
// 跳过实名
|
||
}
|
||
```
|
||
|
||
现有 `tb_carrier.realname_link_type` 可以直接决定该运营商接入是否需要实名,不新增重复能力字段:
|
||
|
||
| `realname_link_type` | 实名要求 | 实名入口 |
|
||
|---|---|---|
|
||
| `none` | 不需要实名 | 不提供实名链接,不创建实名查询任务 |
|
||
| `template` | 需要实名 | 使用模板生成实名链接 |
|
||
| `gateway` | 需要实名 | 调用 Gateway 获取实名链接 |
|
||
|
||
- 资产 `realname_policy` 只决定“先实名后购买”或“先购买后实名”的业务顺序。
|
||
- `realname_link_type=none` 时,资产有效实名策略统一视为 `none`。
|
||
- `realname_link_type!=none` 时,资产 `realname_policy=none` 属于冲突数据,发布前列出并修正,禁止运行时静默选择其中一方。
|
||
- `card_category` 只保留业务分类和展示用途,不参与实名轮询、复机或套餐激活判断。
|
||
|
||
运营商是否配置回调 Adapter 只影响有没有回调急速通道,不改变 `realname_link_type` 对实名要求的判断。
|
||
|
||
### 4.3 标准化观测值
|
||
|
||
```go
|
||
type SyncSource string
|
||
|
||
const (
|
||
SyncSourcePolling SyncSource = "polling"
|
||
SyncSourceBusinessEvent SyncSource = "business_event"
|
||
SyncSourceOpenAPI SyncSource = "open_api"
|
||
SyncSourceManualRefresh SyncSource = "manual_refresh"
|
||
SyncSourceCarrierCallback SyncSource = "carrier_callback"
|
||
)
|
||
|
||
type ObservationMeta struct {
|
||
Source SyncSource
|
||
TriggerScene string
|
||
TriggerSeries string
|
||
Attempt int
|
||
Provider string
|
||
ObservedAt time.Time
|
||
RequestID string
|
||
CorrelationID string
|
||
}
|
||
|
||
type RealnameObservation struct {
|
||
CardID uint
|
||
ICCID string
|
||
Verified bool
|
||
Meta ObservationMeta
|
||
}
|
||
|
||
type TrafficObservation struct {
|
||
CardID uint
|
||
GatewayReadingMB float64
|
||
Meta ObservationMeta
|
||
}
|
||
|
||
type NetworkObservation struct {
|
||
CardID uint
|
||
CardStatus string
|
||
Extend string
|
||
GatewayIMEI string
|
||
Meta ObservationMeta
|
||
}
|
||
```
|
||
|
||
领域层不使用 `map[string]any` 传递核心状态,防止不同入口遗漏字段或混淆单位。
|
||
|
||
### 4.4 观测应用规则
|
||
|
||
`ApplyCardObservation` 是唯一允许把上游数据写入卡领域状态的入口:
|
||
|
||
1. 加载并锁定卡聚合。
|
||
2. 根据观测类型执行实名、流量或网络状态规则。
|
||
3. 状态未变化时只更新时间和观测来源,不重复产生业务事件。
|
||
4. 状态变化时保存聚合,并在同一事务写 Outbox 和关键 Audit Event。
|
||
5. 实名 `0 -> 1` 产生 `CardRealnamed`,触发卡级/设备级套餐激活和停复机评估。
|
||
6. 流量正增量产生 `CardTrafficIncreased`,由套餐应用服务扣减套餐流量。
|
||
7. 网络状态变化产生 `CardNetworkStatusChanged`,由停复机策略重新评估。
|
||
8. 状态变化或流量增加时更新卡活跃标记。
|
||
|
||
解除实名回调不构造 `Verified=false` 观测,不修改卡状态。
|
||
|
||
## 五、轮询优化:只做活跃度调频
|
||
|
||
### 5.1 全局开关
|
||
|
||
`enable_polling` 是资产是否参与自动轮询的唯一业务开关:
|
||
|
||
- `false`:从所有轮询队列移除,事件触发也不因此失效;用户主动操作仍可触发 Best Effort 同步。
|
||
- `true`:按照活跃度和运营商能力进入对应 Gateway 查询队列。
|
||
- 设备关闭轮询时,继续沿用现有设备与绑定卡级联规则。
|
||
|
||
不再提供“实名轮询开关、流量轮询开关、状态轮询开关”给业务人员配置。
|
||
|
||
### 5.2 活跃标记
|
||
|
||
卡增加或维护以下轮询调度字段:
|
||
|
||
```text
|
||
polling_activity_level active / inactive
|
||
last_polling_activity_at 最后活跃时间
|
||
last_polling_activity_scene 最后活跃场景
|
||
last_upstream_change_at 上游观测最后发生变化时间
|
||
```
|
||
|
||
以下情况标记为活跃:
|
||
|
||
| 场景 | 说明 |
|
||
|---|---|
|
||
| C 端、后台或开放接口查询资产实时信息 | 表明当前有人关心该资产 |
|
||
| 获取实名链接 | 表明即将发生实名操作 |
|
||
| 停机、复机、切卡、重启、恢复出厂设置 | 表明上游状态正在变化 |
|
||
| 套餐支付、激活、失效或流量重置 | 可能影响流量和停复机 |
|
||
| 运营商回调 | 上游已发生变化 |
|
||
| 轮询发现实名、流量或网络状态变化 | 卡当前确实活跃 |
|
||
|
||
卡在配置时间内没有业务活动,且连续轮询未发现上游变化时转为 `inactive`。该判断在每次重排队时完成,不新增全表扫描任务。
|
||
|
||
第一版建议配置而非写死:
|
||
|
||
```text
|
||
inactive_after = 30m
|
||
active_realname_interval = 现有实名默认间隔
|
||
active_traffic_interval = 现有流量默认间隔
|
||
active_network_interval = 现有状态默认间隔
|
||
inactive_realname_interval = 15m
|
||
inactive_traffic_interval = 15m
|
||
inactive_network_interval = 15m
|
||
```
|
||
|
||
现有 `tb_polling_config` 的有效间隔迁移为活跃卡默认值,但不再按 `card_condition/card_category` 匹配多套业务资格。第一版调整为一套全局活跃/不活跃间隔;运营商配置只描述接口能力和上游限频,不再决定某张卡是否“有资格”轮询。上线前使用生产数据做只读测算,确认 Gateway QPS 和最长兜底延迟后再调整数值。
|
||
|
||
### 5.3 调度规则
|
||
|
||
```text
|
||
enable_polling=false
|
||
-> 不入自动轮询队列
|
||
|
||
enable_polling=true + active
|
||
-> 使用全局活跃间隔
|
||
|
||
enable_polling=true + inactive
|
||
-> 使用全局不活跃间隔
|
||
|
||
realname_link_type=none
|
||
-> 不创建实名查询任务
|
||
```
|
||
|
||
已实名卡仍可低频查询实名状态,以发现上游实名逆转;未实名行业卡也不能因卡类别被排除。
|
||
|
||
## 六、业务事件触发
|
||
|
||
### 6.1 不是新增接口
|
||
|
||
本需求不新增 `POST /api/admin/card-sync/requests`,也不新增独立“事件同步”按钮。
|
||
|
||
现有接口保持:
|
||
|
||
```http
|
||
POST /api/admin/assets/:identifier/refresh
|
||
POST /api/c/v1/asset/refresh
|
||
```
|
||
|
||
现有批量轮询运维接口可以保留,但必须改为调用新的 Application 用例,不再自行维护另一套同步逻辑。
|
||
|
||
业务事件触发由 Application UseCase 或 Query 完成后调用内部端口:
|
||
|
||
```go
|
||
type SyncTriggerCommand struct {
|
||
Scene string
|
||
ResourceType string
|
||
ResourceID uint
|
||
CardIDs []uint
|
||
SyncTypes []string
|
||
ExpectedState map[string]any
|
||
Source SyncSource
|
||
RequestID string
|
||
CorrelationID string
|
||
}
|
||
```
|
||
|
||
写操作在业务成功后触发;有数据库事务的关键写操作通过 Outbox 发布触发事件,避免提交成功后进程退出造成埋点丢失。读操作只查询本地快照,不等待 Gateway;在返回前 Best Effort 写入 Asynq,入队失败不得改变原接口响应。
|
||
|
||
### 6.2 阶梯式尝试
|
||
|
||
每个触发序列默认创建三个 Asynq 任务:
|
||
|
||
| 尝试 | 计划时间 | Asynq 自动重试 |
|
||
|---:|---:|---:|
|
||
| 1 | 立即 | 0 |
|
||
| 2 | 事件发生后 3 分钟 | 0 |
|
||
| 3 | 事件发生后 5 分钟 | 0 |
|
||
|
||
每次任务都有确定性的 `series_id + attempt` 幂等键。单次失败不会额外重试,也不会取消后续两次;三次结束后由常规轮询兜底。
|
||
|
||
存在明确预期状态时允许提前完成序列:
|
||
|
||
- 复机后已观测为开机。
|
||
- 停机后已观测为停机。
|
||
- 获取实名链接后已通过回调或查询确认实名。
|
||
- 切卡后设备当前卡已经是目标 ICCID。
|
||
|
||
序列提前完成后,剩余任务启动时检查状态并直接记为 `completed`,不再请求 Gateway。
|
||
|
||
### 6.3 关键埋点
|
||
|
||
埋点放在应用用例成功边界,不散落在 Handler 的 `response.Success` 前后。现有 Handler 直接调用 Gateway 的路径在迁移时收口到 Application。
|
||
|
||
| 现有入口/用例 | 触发内容 | 预期状态 |
|
||
|---|---|---|
|
||
| `GET /api/c/v1/asset/info` | 资产绑定卡的实名、流量、网络状态 | 无 |
|
||
| `GET /api/admin/assets/:identifier/realtime-status` | 对应卡或设备绑定卡的实时数据 | 无 |
|
||
| `GET /api/open/v1/cards/traffic` | 流量 | 无 |
|
||
| `GET /api/open/v1/cards/status` | 网络状态 | 无 |
|
||
| `GET /api/open/v1/cards/realname-status` | 实名 | 无 |
|
||
| `GET /api/open/v1/devices/traffic` | 设备信息、绑定卡流量 | 无 |
|
||
| C 端和后台获取实名链接 | 实名 | 已实名 |
|
||
| 后台、C 端、开放接口停机/复机成功 | 网络状态 | 停机或开机 |
|
||
| 自动停复机 Gateway 调用成功 | 网络状态 | 停机或开机 |
|
||
| 订单支付、钱包购买套餐、套餐激活成功 | 实名、流量、网络状态 | 无 |
|
||
| 设备切卡或切换模式成功 | 设备信息、源卡和目标卡网络状态、目标卡流量 | 目标 ICCID |
|
||
| 设备重启、恢复出厂设置、WiFi 设置成功 | 设备信息、绑定卡网络状态 | 无 |
|
||
| 卡绑定/解绑、设备或卡分配/回收 | 只标记活跃;确有上游操作时再创建同步序列 | 无 |
|
||
|
||
补充规则:
|
||
|
||
- `POST /api/admin/assets/:identifier/refresh` 和 `POST /api/c/v1/asset/refresh` 已经直接同步,不再额外创建 `0/3/5` 序列。
|
||
- 运营商实名回调直接应用观测,不再反查 Gateway;回调成功后终止相同卡的待执行实名序列。
|
||
- 轮询发现状态变化只更新活跃标记,不反向创建新的事件序列。
|
||
- 同一次设备操作涉及多张绑定卡时使用同一 `correlation_id`,但每张卡独立限流和记录结果。
|
||
|
||
### 6.4 冷却、合并与限流
|
||
|
||
不使用一个固定五分钟 `SET NX` 键拦截所有事件。请求协调器只处理同场景合并、单卡互斥和最小请求间隔;Gateway 返回超频后不建立额外退避状态。
|
||
|
||
#### 同场景触发合并
|
||
|
||
```text
|
||
cardsync:series:{scene}:{resource_type}:{resource_id}:{sync_type}
|
||
```
|
||
|
||
- 同一场景、同一资源、同一同步类型已有未结束序列时,新触发合并到原序列。
|
||
- 合并只防止页面轮询、开放接口重试等重复创建大量 `0/3/5` 任务。
|
||
- 停复机、支付、切卡等不同业务场景不会被一个查询场景长期压制。
|
||
- 序列键只覆盖最后一次计划任务和短暂缓冲,不作为全局五分钟冷却。
|
||
|
||
#### 单卡请求互斥
|
||
|
||
```text
|
||
cardsync:inflight:{provider}:{sync_type}:{card_id}
|
||
```
|
||
|
||
- 只覆盖一次 Gateway 请求的执行时间,TTL 为请求超时加安全余量。
|
||
- 防止轮询、手动刷新和事件任务同时请求并重复应用同一观测。
|
||
- 轮询命中互斥时延迟短时间重排;事件尝试命中互斥时只跳过当前尝试,后续阶梯任务仍存在。
|
||
|
||
#### 运营商最小请求间隔
|
||
|
||
最小间隔按运营商接入和接口类型配置,例如实名、流量、状态可以不同,不能统一写死为五分钟。
|
||
|
||
- 默认兜底值建议为 10 秒,仅用于阻止近乎同时的重复调用。
|
||
- 如果上游明确给出更严格限制,以运营商配置为准。
|
||
- 高价值状态变更事件命中最小间隔时,延迟到最近允许时间,不直接丢弃整个序列。
|
||
|
||
#### 超频结果处理
|
||
|
||
- Gateway 返回超频时,当前尝试记录为 `rate_limited` 后直接结束。
|
||
- 不记录 `blocked_until`,不读取 `Retry-After`,也不执行 `30s/60s/120s` 退避。
|
||
- 不为当前尝试额外补发任务,原定 3 分钟、5 分钟尝试保持不变。
|
||
- 后续轮询仍按正常调度时间继续,是否再次超频由当时上游实际情况决定。
|
||
- 轮询、事件和手动刷新仍共享本系统的并发控制,避免本系统自身在同一时刻并发打满 Gateway。
|
||
|
||
因此,“5 分钟”是第三次尝试的计划时间,不是禁止新业务事件同步的冷却时间。
|
||
|
||
### 6.5 开放接口行为
|
||
|
||
开放接口继续返回本地快照,不等待 Gateway:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Agent as 代理系统
|
||
participant API as Open API
|
||
participant DB as PostgreSQL
|
||
participant Trigger as CreateSyncTriggerSeries
|
||
participant Worker as Sync Attempt Worker
|
||
|
||
Agent->>API: 查询实名/状态/流量
|
||
API->>DB: 查询当前本地快照
|
||
DB-->>API: 当前数据
|
||
API-->>Agent: 按现有结构立即返回
|
||
API->>Trigger: Best Effort 创建 0/3/5 序列
|
||
Trigger-->>Worker: 同场景重复请求自动合并
|
||
```
|
||
|
||
## 七、运营商实名回调防腐层
|
||
|
||
### 7.1 防腐层职责
|
||
|
||
“百分百信任回调”表示信任其业务结论,不表示让运营商报文直接进入领域模型。
|
||
|
||
```go
|
||
type CarrierRealnameCallbackTranslator interface {
|
||
TranslateSuccess(body []byte) (CarrierRealnameNotice, error)
|
||
TranslateRemoval(body []byte) (CarrierRealnameRemovalNotice, error)
|
||
SuccessResponse() CallbackResponse
|
||
FailureResponse(err error) CallbackResponse
|
||
}
|
||
```
|
||
|
||
各运营商 Adapter 负责:
|
||
|
||
- 解析 JSON、XML 或表单字段。
|
||
- 提取并校验 19/20 位 ICCID、MSISDN 等标识。
|
||
- 把运营商状态码翻译为“实名成功”或“解除实名通知”。
|
||
- 屏蔽运营商字段名、状态码和成功响应格式。
|
||
- 生成脱敏 Integration Log 摘要。
|
||
|
||
领域层只接收标准 `RealnameObservation`,不依赖移动、联通、电信的报文结构。
|
||
|
||
#### ICCID 19/20 位解析
|
||
|
||
回调必须复用现有双列存储和按长度路由规则:
|
||
|
||
```text
|
||
收到 19 位 ICCID
|
||
-> 原值查询 tb_iot_card.iccid_19
|
||
|
||
收到 20 位 ICCID
|
||
-> 原值查询 tb_iot_card.iccid_20
|
||
|
||
收到其他长度
|
||
-> invalid_payload,不进入实名用例
|
||
```
|
||
|
||
- 先去除首尾空白,再调用现有 ICCID Validator,只接受 19 位或 20 位字母数字值。
|
||
- 禁止把 20 位回调值截成 19 位后降级查询。
|
||
- 禁止为 19 位值自行补第 20 位校验位。
|
||
- 查询 Miss 时不切换另一列重试,记录原始长度、运营商和脱敏 ICCID 摘要。
|
||
- 复用 `IotCardStore.GetByICCID` 的长度路由语义;防腐层只负责提取和校验,不自己拼接模糊 SQL。
|
||
- `iccid_19` 必须在未删除卡中保持唯一。发布前检查重复前缀并建立 Partial Unique Index,避免 19 位回调错误命中任意一张卡。
|
||
- Repository 查询若发现多条匹配必须返回冲突,不允许使用 `First` 静默选择。
|
||
- Integration Log 的 `resource_key` 保存回调原始 19/20 位值,展示时按审计权限脱敏。
|
||
|
||
### 7.2 路由
|
||
|
||
```http
|
||
POST /api/callback/carriers/cmcc/realname
|
||
POST /api/callback/carriers/cucc/realname
|
||
POST /api/callback/carriers/ctcc/realname
|
||
|
||
POST /api/callback/carriers/cmcc/realname/remove
|
||
POST /api/callback/carriers/cucc/realname/remove
|
||
POST /api/callback/carriers/ctcc/realname/remove
|
||
```
|
||
|
||
- 成功回调:防腐层转换后直接调用 `ApplyCardObservation(Verified=true)`。
|
||
- 解除回调:只写统一 Integration Log,状态记为 `ignored`,不修改卡状态。
|
||
- 广电或其他没有回调能力的接入继续依赖事件和轮询。
|
||
- 不验证回调是否真的来自运营商,不执行 Gateway 二次查询。
|
||
- 不调用旧平台 `inner_callback`、第三方推送或删除实名接口。
|
||
|
||
### 7.3 响应原则
|
||
|
||
- 找到卡并应用成功:返回运营商要求的成功报文。
|
||
- 重复成功回调:幂等返回成功,不重复激活套餐。
|
||
- 找不到卡:Integration Log 记 `not_found`,仍返回成功,避免无意义高频重推。
|
||
- 报文无法解析:记录 `invalid_payload`;若运营商有固定成功应答要求,仍按接入约定返回,避免不可控重试风暴。
|
||
- 状态已落库但异步联动失败:返回成功,联动通过 Outbox 重试。
|
||
|
||
## 八、统一审计契约
|
||
|
||
同步运行数据不在本方案定义独立表和独立页面,统一由 `04-全局多视角审计方案.md` 管理。
|
||
|
||
本模块需要向审计系统提供:
|
||
|
||
| 记录 | 进入位置 |
|
||
|---|---|
|
||
| 每次 Gateway 实际请求或被限流、合并的尝试 | Integration Log |
|
||
| 每次运营商实名/解除实名回调 | Integration Log |
|
||
| 实名、流量、网络状态发生业务变化 | Audit Event + Integration Log |
|
||
| 手动刷新、手动实名修改 | Audit Event + Integration Log |
|
||
| 连续失败达到阈值、超频持续异常 | 风险 Audit Event |
|
||
| 普通高频轮询成功且数据未变化 | 仅 Integration Log |
|
||
|
||
必须携带:
|
||
|
||
```text
|
||
provider / operation / resource / trigger_scene / trigger_series
|
||
attempt / result / duration / request_id / correlation_id
|
||
state_changed / provider_code / error_summary
|
||
```
|
||
|
||
资产详情的“查看同步轨迹”跳转审计中心外部集成视角,不再建设 `/operations/card-sync` 独立监控页。
|
||
|
||
## 九、接口和前端调整
|
||
|
||
### 9.1 后端接口
|
||
|
||
本需求不新增显式同步接口。
|
||
|
||
现有手动刷新接口内部改为调用 `RefreshAssetUseCase`,响应结构和权限保持不变。现有轮询监控接口增加:
|
||
|
||
- 活跃卡数量、不活跃卡数量。
|
||
- 各活跃级别的实际轮询 QPS。
|
||
- 因互斥或运营商最小间隔而延迟的任务数,以及 Gateway 超频失败次数。
|
||
|
||
资产实时状态或详情 Query 增加可选调度信息:
|
||
|
||
```json
|
||
{
|
||
"polling": {
|
||
"enabled": true,
|
||
"activity_level": "active",
|
||
"last_activity_at": "2026-07-15T10:00:00+08:00",
|
||
"last_activity_scene": "client_asset_info",
|
||
"next_poll_at": "2026-07-15T10:01:00+08:00"
|
||
}
|
||
}
|
||
```
|
||
|
||
同步明细查询复用全局审计接口:
|
||
|
||
```http
|
||
GET /api/admin/audit/integrations
|
||
?provider=gateway
|
||
&resource_type=iot_card
|
||
&resource_key=89860...
|
||
&trigger_scene=
|
||
&trigger_series=
|
||
```
|
||
|
||
### 9.2 前端
|
||
|
||
保留现有资产刷新按钮,不新增第二个同步按钮。
|
||
|
||
资产详情增加紧凑的轮询状态展示:
|
||
|
||
```text
|
||
自动轮询:已开启
|
||
活跃状态:活跃
|
||
最后活跃:2分钟前,C端资产详情
|
||
下次兜底:约1分钟后
|
||
同步轨迹:查看
|
||
```
|
||
|
||
“查看”跳转:
|
||
|
||
```text
|
||
/operations/audit?tab=integrations&resource_type=iot_card&resource_key={identifier}
|
||
```
|
||
|
||
轮询监控页只增加活跃/不活跃分布和限流状态,不重复实现 Integration Log 表格、详情抽屉和导出。
|
||
|
||
## 十、代码迁移范围
|
||
|
||
### 10.1 必须删除或收口
|
||
|
||
- 删除 `PollingRealnameHandler` 按 `CardCategoryIndustry` 跳过实名的判断。
|
||
- 修正 `pkg/constants/iot.go` 中“普通卡必需实名、行业卡无需实名”的绝对化注释和依赖逻辑。
|
||
- `PollingRealnameHandler`、`PollingCarddataHandler`、`PollingCardStatusHandler` 只调用 Application 用例,不再直接更新卡或执行业务联动。
|
||
- 删除重复流量增量算法,只保留领域实现。
|
||
- `RefreshCardDataFromGateway` 改为调用 `RequestCardObservation + ApplyCardObservation`。
|
||
- `ManualUpdateRealnameStatus` 改为调用领域用例。
|
||
- 获取实名链接后的 `ManualTriggerService.TriggerSingle` 改为 `CreateSyncTriggerSeries`。
|
||
- C 端和后台设备操作中直接调用 Gateway 的路径迁入 Application,再在用例成功边界创建触发序列。
|
||
|
||
### 10.2 保留并复用
|
||
|
||
- Gateway Client 的加密、签名和 HTTP 封装。
|
||
- Redis 分片 Sorted Set 调度基础设施。
|
||
- Asynq Worker。
|
||
- 卡流量同步锁,迁移为通用请求互斥实现。
|
||
- 现有资产手动刷新接口和权限契约。
|
||
- 现有批量轮询运维能力;其执行逻辑和记录迁入新用例与统一审计后,再下线旧专用日志表。
|
||
|
||
### 10.3 数据变更
|
||
|
||
- 不新增实名要求字段,继续以 `tb_carrier.realname_link_type` 判断是否需要实名及链接生成方式。
|
||
- 发布前检查 `iccid_19` 重复数据并建立未删除数据范围内的唯一索引,保证 19 位回调精确命中。
|
||
- 卡增加轮询活跃标记字段,或由独立调度状态表保存;实现阶段根据写入频率决定,不能把高频调度心跳写入核心卡表。
|
||
- 现有 `tb_polling_config` 条件匹配迁移为单一全局活跃/不活跃策略,不再使用 `card_condition/card_category` 形成隐式轮询资格。
|
||
- 事件任务载荷增加 `scene/series_id/attempt/source/request_id/correlation_id/expected_state`。
|
||
- 不创建 `tb_card_sync_execution`。
|
||
|
||
## 十一、发布与人工验证
|
||
|
||
停机发布,API 与 Worker 同时切换:
|
||
|
||
1. 验证 `realname_link_type=none/template/gateway` 分别对应无需实名、模板实名和 Gateway 实名,不再按行业卡统一跳过。
|
||
2. 验证 `enable_polling=false` 会移除自动轮询,但业务事件和手动刷新仍能按各自规则工作。
|
||
3. 验证活跃卡使用活跃间隔,不活跃卡使用低频间隔,重新活跃后立即恢复。
|
||
4. 验证一次业务事件形成立即、3 分钟、5 分钟三个任务。
|
||
5. 验证停复机、实名和切卡达到预期状态后,剩余任务不再请求 Gateway。
|
||
6. 验证同场景高频查询只合并重复序列,不压制新的停复机或支付事件。
|
||
7. 验证单卡互斥只覆盖请求执行时间,不形成五分钟全局冷却。
|
||
8. 验证 Gateway 超频时当前尝试直接记为 `rate_limited`,不创建退避状态,后续阶梯任务仍保留。
|
||
9. 验证现有后台和 C 端刷新接口契约不变,且不额外创建阶梯序列。
|
||
10. 验证移动、联通、电信回调均先经过防腐层,19 位和 20 位 ICCID 分别按双列精确路由,重复成功回调不重复激活套餐。
|
||
11. 验证解除实名回调只写 Integration Log,不修改卡实名状态。
|
||
12. 验证开放接口仍立即返回本地数据,事件触发失败不改变响应。
|
||
13. 验证普通同步、状态变化、手动刷新和连续失败能够在审计中心按资源和触发序列查询。
|