feat: 实现资产实名认证策略功能 (asset-realname-policy)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m44s

- 新增 realname_policy 字段到 tb_iot_card、tb_device、tb_iot_card_import_task、tb_device_import_task 四张表
- 策略枚举:none(无需实名)、before_order(先实名后充值/购买)、after_order(先充值/购买后实名)
- C端充值拦截:before_order 策略下未实名用户禁止充值
- C端订单拦截:before_order 策略下未实名用户禁止购买套餐
- C端实名链接拦截:after_order 策略下无充值/订单用户禁止实名认证
- Admin API:PATCH /api/admin/assets/:identifier/realname-mode
- 新增错误码 CodeRealnameNotAvailable=1189
This commit is contained in:
2026-04-17 17:03:41 +08:00
parent e0a37f4a11
commit 505b30aa8f
12 changed files with 623 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
-- 回滚:删除 realname_policy 字段
ALTER TABLE tb_device_import_task DROP COLUMN IF EXISTS realname_policy;
ALTER TABLE tb_iot_card_import_task DROP COLUMN IF EXISTS realname_policy;
ALTER TABLE tb_device DROP COLUMN IF EXISTS realname_policy;
ALTER TABLE tb_iot_card DROP COLUMN IF EXISTS realname_policy;

View File

@@ -0,0 +1,19 @@
-- 新增 realname_policy 字段到四张表
-- 实名认证策略:(none=无需实名, before_order=先实名后充值/购买, after_order=先充值/购买后实名)
-- 默认值为 none存量数据不受影响
-- tb_iot_card 新增 realname_policy 字段
ALTER TABLE tb_iot_card ADD COLUMN IF NOT EXISTS realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
COMMENT ON COLUMN tb_iot_card.realname_policy IS '实名认证策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)';
-- tb_device 新增 realname_policy 字段
ALTER TABLE tb_device ADD COLUMN IF NOT EXISTS realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
COMMENT ON COLUMN tb_device.realname_policy IS '实名认证策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)';
-- tb_iot_card_import_task 新增 realname_policy 字段
ALTER TABLE tb_iot_card_import_task ADD COLUMN IF NOT EXISTS realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
COMMENT ON COLUMN tb_iot_card_import_task.realname_policy IS '导入批次实名策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)';
-- tb_device_import_task 新增 realname_policy 字段
ALTER TABLE tb_device_import_task ADD COLUMN IF NOT EXISTS realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
COMMENT ON COLUMN tb_device_import_task.realname_policy IS '导入批次实名策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)';

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-17

View File

@@ -0,0 +1,86 @@
## Context
系统存在一段注释掉的实名拦截代码(`REALNAME-03`,位于 `internal/service/client_order/service.go` 第 143 行),原逻辑为"普通卡(`card_category=normal`)需实名才能购买套餐,行业卡(`industry`)无需实名"。由于业务还未明确,该逻辑被整体注释,由网关侧临时托管。
现在业务明确了两种场景:**先实名后充值**(充值/购买前拦截未实名资产)与**先充值后实名**(充值/购买放行,实名链接在未充值前拦截)。且行业卡在特定业务下也可能需要实名,原来依赖 `card_category` 的硬编码逻辑不够灵活。
需要在资产层面(`IotCard``Device`)引入可配置的 `realname_policy` 字段,取代原有硬编码逻辑,支持导入时批量初始化策略、后台管理接口修改策略、所有查询接口返回策略字段、以及 C 端三接口按策略执行拦截。
## Goals / Non-Goals
**Goals:**
-`IotCard``Device` 模型上新增 `realname_policy` 字段string 枚举)
- IoT 卡导入和设备导入支持批量初始化 `realname_policy`
- 后台新增 `PATCH /api/admin/assets/:identifier/realname-mode` 接口修改单资产策略
- C 端充值C3/C4、购买套餐/订单D1/D4、实名链接E1按策略拦截
- 所有涉及卡/设备/资产的查询接口返回 `realname_policy` 字段
- 存量数据迁移默认值为 `none`,不影响现有行为
**Non-Goals:**
- 不修改登录层的实名检查(已有机制,由网关托管)
- 不实现用户通知after_order 实名提醒等)
- 不修改 Carrier 层的 `realname_link_type`(运营商提供实名链接的方式,是另一个维度)
- 不实现按套餐粒度的实名策略(只到资产粒度)
## Decisions
**决策 1`realname_policy` 使用 string 枚举而非 bool**
`string` 而非 `bool`(如 `realname_required`):业务存在三种状态(无需实名 / 先实名后充值 / 先充值后实名bool 只能表达两种。枚举值:`none` / `before_order` / `after_order`
**决策 2设备卡场景以 Device.realname_policy 为准**
当一张卡绑定到设备时(设备卡),取 `Device.realname_policy`;未绑定设备时(单卡),取 `IotCard.realname_policy`。这与现有系统中"设备层面的验证规则高于卡层面"的设计一致(登录前的设备验证已体现此原则)。
判断逻辑封装在 Service 层公共方法 `GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string`,避免在多个 Service 中重复实现。
**决策 3after_order 场景下实名链接的前置条件**
`after_order` 模式下,用户调用 `GET /api/c/v1/realname/link` 时,系统检查该资产是否存在有效充值或已支付订单记录(`status=2` 的订单或 `status=2` 的充值单)。无记录则拦截;有记录则放行。
逻辑:先充值才有资格实名,避免用户跳过充值直接实名。检查范围:该资产当前 generation 内的充值记录或套餐订单记录。
**决策 4后台更新接口复用现有资产解析器**
遵循 `UpdatePollingStatus` 的模式(`PATCH /api/admin/assets/:identifier/polling-status`
- 接口路径:`PATCH /api/admin/assets/:identifier/realname-mode`
- 通过 `assetService.Resolve()` 将标识符解析为 asset_type + asset_id
- asset_type=card → 更新 `IotCard.realname_policy`
- asset_type=device → 更新 `Device.realname_policy`
- 单接口覆盖两种资产类型,与现有模式完全一致
**决策 5存量数据默认值为 `none`**
迁移时所有存量卡和设备的 `realname_policy` 默认值为 `none`(无需实名),保持与现有行为完全一致(目前实名拦截已被注释,等于放行)。运营方后续按需批量更新。
**决策 6命名区分**
`RealnamePolicy`(资产的实名策略:何时需要实名)与现有 `RealnameLinkType`运营商提供实名链接的方式none/template/gateway是两个独立维度字段名不同不产生混淆。DTO 中 response 字段名为 `realname_policy`
## Risks / Trade-offs
**[风险 1] after_order 拦截查询增加数据库压力**
→ 缓解:查询只取 `COUNT(*) > 0`命中索引generation + asset_id + status可加 Redis 缓存key 为 `realname:after_order_eligible:{asset_type}:{asset_id}:{generation}`,充值/购买成功时主动写入TTL 24h。初期数据量小可不加缓存待观察后决定。
**[风险 2] DTO 改动面大,遗漏字段**
→ 缓解:所有涉及卡/设备/资产的 DTO 已在探索阶段完整盘点tasks.md 按文件逐一列举,不依赖记忆。
**[风险 3] 存量数据全为 `none`,初始化后无法区分"未配置"与"主动设为 none"**
→ 可接受:`none` 本身是有效业务值(无需实名),与未配置语义相同,不需要额外区分。
## Migration Plan
1. 创建数据库迁移文件,为四张表新增 `realname_policy` 字段:
```sql
ALTER TABLE tb_iot_card ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
ALTER TABLE tb_device ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
ALTER TABLE tb_iot_card_import_task ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
ALTER TABLE tb_device_import_task ADD COLUMN realname_policy VARCHAR(20) NOT NULL DEFAULT 'none';
```
2. 迁移前REALNAME-03 注释代码继续保持注释(不影响线上)
3. 迁移后:代码上线,实名拦截以新字段为准;存量全为 `none`,等于放行,行为不变
4. 回滚策略:字段加 DEFAULT删除新增字段即可代码回滚到上一版本
## Open Questions
无(所有关键问题已在探索阶段与业务方确认)

View File

@@ -0,0 +1,48 @@
## Why
现有系统对实名认证的要求硬编码在卡业务类型(`card_category`)上,且当前拦截逻辑已被注释(`REALNAME-03`),由网关侧临时托管。业务侧实际存在两种截然不同的场景:**先实名后充值/购买**(实名是充值的前置条件)与**先充值/购买后实名**(充值不受限,实名在事后完成),且行业卡在特定业务下也需要实名。需要在资产(卡和设备)层面引入可配置的实名策略字段,彻底替代原有的硬编码逻辑,支持灵活的运营策略配置。
## What Changes
- **新增 `realname_policy` 字段**`IotCard``Device` 模型),枚举值:
- `none`:无需实名
- `before_order`:先实名后充值/购买(充值/购买前拦截未实名资产)
- `after_order`:先充值/购买后实名(充值/购买放行,实名链接在有订单前拦截)
- **生效优先级**:卡绑定设备时(设备卡)使用 `Device.realname_policy`,单卡使用 `IotCard.realname_policy`
- **C 端拦截**充值C3/C4、购买套餐/订单D1/D4按策略决定是否拦截实名链接E1`after_order` 模式下要求先有充值/订单记录才能获取链接
- **IoT 卡导入** 支持在导入时批量设置 `realname_policy`,并写入每张卡
- **设备导入** 支持在导入时设置 `realname_policy`,并写入每台设备
- **后台管理** 新增接口 `PATCH /api/admin/assets/:identifier/realname-mode` 支持逐资产修改策略
- **所有涉及卡/设备/资产的查询接口**(后台管理端 + C 端)返回 `realname_policy` 字段,便于前端展示和逻辑判断
- **废弃** `REALNAME-03` 注释代码,由新的策略逻辑替代
## Capabilities
### New Capabilities
- `asset-realname-policy`:资产实名认证策略——定义策略枚举、生效规则(单卡 vs 设备卡优先级、C 端三接口拦截逻辑、后台管理更新接口、以及所有查询接口返回该字段的规范
### Modified Capabilities
- `iot-card-import-task`IoT 卡导入请求新增 `realname_policy` 参数,导入任务模型记录该字段,批量初始化卡的策略值
- `device-import`:设备导入请求新增 `realname_policy` 参数,导入任务模型记录该字段,批量初始化设备的策略值
- `client-realname-link`E1 接口在 `after_order` 模式下新增"是否有有效充值/订单"的前置检查;`none` 模式正常返回链接
- `client-order-purchase`D1/D4 接口启用实名策略拦截(替代 REALNAME-03 注释逻辑),`before_order` + 未实名 → 返回 `CodeNeedRealname`
- `client-wallet-recharge`C3/C4 接口新增实名策略拦截,`before_order` + 未实名 → 返回 `CodeNeedRealname`
## Impact
**数据库**`tb_iot_card``tb_device``tb_iot_card_import_task``tb_device_import_task` 各新增 `realname_policy` 字段,迁移默认值 `none`(存量数据不影响现有行为)
**Model 层**`internal/model/iot_card.go``internal/model/device.go``internal/model/iot_card_import_task.go``internal/model/device_import_task.go`
**DTO 层**(新增 `realname_policy` 字段):
- 后台管理:`StandaloneIotCardResponse``DeviceResponse``AssetResolveResponse``AssetRealtimeStatusResponse``DeviceCardBindingResponse``BoundCardInfo``ImportTaskResponse`iot`DeviceImportTaskResponse`
- C 端:`AssetInfoResponse``BoundCardInfo`C端`DeviceCardItem`
- 导入请求:`ImportIotCardRequest``ImportDeviceRequest`
**Service 层**`client_order``client_wallet``client_realname` 新增策略检查逻辑;`iot_card``device` 新增 `UpdateRealnamePolicy` 方法;`iot_card_import``device_import` 传播策略字段至各资产记录
**常量**`pkg/constants/iot.go`(或新文件)新增 `RealnamePolicy*` 枚举常量
**路由 + 文档生成器**:新增 1 个后台管理接口,需同步更新 `cmd/api/docs.go``cmd/gendocs/main.go`

View File

@@ -0,0 +1,169 @@
## ADDED Requirements
### Requirement: 资产实名策略字段定义
系统 SHALL 在 `IotCard``Device` 模型上各新增 `realname_policy` 字段VARCHAR(20)NOT NULLDEFAULT 'none'),用于控制该资产的实名认证要求。
**枚举值**
- `none`:无需实名,充值/购买/实名链接均不受限
- `before_order`:先实名后充值/购买,充值/购买前若未实名则拦截并返回 `CodeNeedRealname`
- `after_order`:先充值/购买后实名,充值/购买放行;实名链接在无有效充值/订单记录前拦截
**常量定义**`pkg/constants/iot.go``pkg/constants/realname.go`
```go
const (
RealnmePolicyNone = "none" // 无需实名
RealnmePolicyBeforeOrder = "before_order" // 先实名后充值/购买
RealnmePolicyAfterOrder = "after_order" // 先充值/购买后实名
)
```
#### Scenario: 默认值为 none
- **WHEN** 新建 IotCard 或 Device 时未传入 realname_policy
- **THEN** 系统自动填充 `realname_policy = "none"`
#### Scenario: 存量数据迁移后行为不变
- **WHEN** 数据库迁移执行后,存量 IotCard 和 Device 的 realname_policy 均为 "none"
- **THEN** 充值、购买、实名链接行为与迁移前完全相同(均放行)
---
### Requirement: 生效策略优先级(设备卡 vs 单卡)
系统 SHALL 按以下规则确定一张卡的生效实名策略:
- **单卡**(该卡未绑定任何设备):使用 `IotCard.realname_policy`
- **设备卡**(该卡已绑定设备):使用 `Device.realname_policy`,忽略 `IotCard.realname_policy`
此规则 SHALL 封装为 Service 层公共方法 `GetEffectiveRealnamePolicy`,所有需要策略判断的场景均调用该方法,不得在多处重复实现。
#### Scenario: 设备卡使用设备策略
- **WHEN** IotCard.realname_policy="none" 且该卡绑定了 Device.realname_policy="before_order"
- **THEN** 生效策略为 "before_order"(设备策略覆盖卡策略)
#### Scenario: 单卡使用卡策略
- **WHEN** IotCard.realname_policy="before_order" 且该卡未绑定任何设备
- **THEN** 生效策略为 "before_order"
---
### Requirement: C 端充值接口实名策略拦截
系统 SHALL 在 C 端充值预检接口C3 `GET /api/c/v1/wallet/recharge-check`和充值下单接口C4 `POST /api/c/v1/wallet/recharge`)中,按生效实名策略执行以下逻辑:
- `before_order` + `real_name_status=0`未实名MUST 拦截,返回 `CodeNeedRealname`(错误码 1187
- `before_order` + `real_name_status=1`(已实名):放行
- `after_order`:放行(不检查实名状态)
- `none`:放行
#### Scenario: before_order 模式未实名时充值被拦截
- **WHEN** 资产 realname_policy="before_order" 且 real_name_status=0用户调用充值下单接口
- **THEN** 系统返回错误码 1187CodeNeedRealname
#### Scenario: before_order 模式已实名时充值放行
- **WHEN** 资产 realname_policy="before_order" 且 real_name_status=1用户调用充值下单接口
- **THEN** 系统正常创建充值订单
#### Scenario: after_order 模式充值放行
- **WHEN** 资产 realname_policy="after_order" 且 real_name_status=0用户调用充值下单接口
- **THEN** 系统正常创建充值订单,不检查实名状态
---
### Requirement: C 端购买套餐/订单接口实名策略拦截
系统 SHALL 在 C 端创建订单接口D1 `POST /api/c/v1/orders/create`和支付接口D4 `POST /api/c/v1/orders/:id/pay`按生效实名策略执行与充值相同的拦截规则。D1 中原 `REALNAME-03` 注释代码 SHALL 被删除,由新策略逻辑替代。
#### Scenario: before_order 模式未实名时购买套餐被拦截
- **WHEN** 资产 realname_policy="before_order" 且 real_name_status=0用户调用创建订单接口
- **THEN** 系统返回错误码 1187CodeNeedRealname订单不创建
#### Scenario: none 模式购买套餐放行
- **WHEN** 资产 realname_policy="none",用户调用创建订单接口
- **THEN** 系统正常创建订单,不检查实名状态
---
### Requirement: C 端实名链接接口策略拦截
系统 SHALL 在 C 端实名链接接口E1 `GET /api/c/v1/realname/link`)中,按生效实名策略执行以下逻辑:
- `none`:放行,正常返回实名链接(用户可自愿实名)
- `before_order`:放行,正常返回实名链接(引导用户先实名再充值)
- `after_order`:检查该资产当前 generation 内是否存在有效充值(`status=2`)或已支付订单(`payment_status=2`
- 有记录 → 放行
- 无记录 → 拦截,返回错误码(新错误码 `CodeRealnameNotAvailable`),消息为"请先完成充值或购买套餐后再进行实名认证"
#### Scenario: after_order 模式有充值记录时实名链接放行
- **WHEN** 资产 realname_policy="after_order" 且当前 generation 内存在 status=2 的充值记录
- **THEN** 系统正常返回实名链接
#### Scenario: after_order 模式无充值/订单记录时实名链接被拦截
- **WHEN** 资产 realname_policy="after_order" 且当前 generation 内无任何有效充值或已支付订单
- **THEN** 系统返回错误,消息为"请先完成充值或购买套餐后再进行实名认证"
#### Scenario: before_order 模式正常返回实名链接
- **WHEN** 资产 realname_policy="before_order"
- **THEN** 系统正常返回实名链接(引导用户完成实名)
---
### Requirement: 后台管理更新资产实名策略接口
系统 SHALL 提供 `PATCH /api/admin/assets/:identifier/realname-mode`,仅限后台管理端认证用户访问。接口通过 `assetService.Resolve()` 将标识符ICCID/虚拟号)解析为具体资产,按 asset_type 分别更新 `IotCard.realname_policy``Device.realname_policy`
**请求体**
```json
{ "realname_policy": "none | before_order | after_order" }
```
**响应体**
```json
{ "asset_type": "card | device", "asset_id": 123, "realname_policy": "before_order" }
```
#### Scenario: 通过 ICCID 更新单卡策略
- **WHEN** 管理员传入 identifier=ICCIDrealname_policy="before_order"
- **THEN** 系统更新对应 IotCard 的 realname_policy 为 "before_order",返回 asset_type="card"
#### Scenario: 通过设备号更新设备策略
- **WHEN** 管理员传入 identifier=设备虚拟号realname_policy="after_order"
- **THEN** 系统更新对应 Device 的 realname_policy 为 "after_order",返回 asset_type="device"
#### Scenario: 传入无效枚举值被拒绝
- **WHEN** 管理员传入 realname_policy="invalid_value"
- **THEN** 系统返回参数校验错误,提示实名策略值无效
---
### Requirement: 所有查询接口返回 realname_policy 字段
以下所有 DTO SHALL 新增 `realname_policy` 字段string及对应的 description 标签:
**后台管理端 DTO**
- `StandaloneIotCardResponse`IoT 卡详情/列表)
- `DeviceResponse`(设备详情/列表)
- `AssetResolveResponse`(统一资产解析)
- `AssetRealtimeStatusResponse`(资产实时状态)
- `DeviceCardBindingResponse`(设备绑卡记录)
- `BoundCardInfo`asset_dto.go 中的通用子结构)
- `ImportTaskResponse`iot 卡导入任务)
- `DeviceImportTaskResponse`(设备导入任务)
**C 端 DTO**
- `AssetInfoResponse`B1 资产信息)
- `BoundCardInfo`client_asset_dto.go 中的 C 端子结构)
- `DeviceCardItem`F1 设备卡列表项)
**description 标签统一格式**
```go
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
```
#### Scenario: 资产详情接口返回 realname_policy
- **WHEN** 后台管理员查询 IoT 卡详情或资产解析
- **THEN** 响应中包含 realname_policy 字段
#### Scenario: C 端资产信息接口返回 realname_policy
- **WHEN** C 端用户调用 GET /api/c/v1/asset/info
- **THEN** 响应中包含 realname_policy 字段(前端可据此展示提示)

View File

@@ -0,0 +1,60 @@
## MODIFIED Requirements
### Requirement: D1 创建套餐购买订单接口
系统 SHALL 提供 `POST /api/c/v1/orders/create`,并且 MUST 要求个人客户认证。**[BREAKING]** 请求体改为使用资产标识符identifier废弃 `iot_card_id`/`device_id` 主键字段和 `order_type` 字段。请求体 MUST 包含 `identifier``package_ids[]``payment_method`。接口流程 MUST 按顺序执行:归属校验 → 套餐校验(含加油包前置)→ **实名策略检查(新增)** → OpenID 查询 → 幂等检查 → 强充检查 → 分流创建。
**实名策略检查(新增,替代 REALNAME-03 注释代码)**
- 获取生效实名策略(`GetEffectiveRealnamePolicy`
- 生效策略为 `before_order``real_name_status=0`未实名MUST 返回 `CodeNeedRealname`(错误码 1187消息"该套餐需实名认证后购买",订单不创建
- 生效策略为 `after_order``none`:跳过实名检查,继续后续流程
`REALNAME-03` 注释代码 SHALL 被删除,由此策略检查替代。
实名不满足时 MUST 返回 `NEED_REALNAME`。OpenID 缺失时 MUST 返回 `OPENID_NOT_FOUND`。幂等 MUST 使用 Redis 业务键 + 分布式锁。分流规则 MUST 为:
- 无强充:创建套餐订单并返回 `order_type="package"``order``pay_config`
- 需强充:创建充值单并返回 `order_type="recharge"``recharge``pay_config``linked_package_info`
**客户端订单创建请求CreateOrderRequest**
```json
{
"identifier": "string资产标识符ICCID 或 VirtualNo必填1-50字符",
"package_ids": "[uint](套餐 ID 列表必填1-10 个)",
"payment_method": "stringwallet|wechat|alipay必填"
}
```
**废弃字段**
- `iot_card_id`(原单卡购买时必填)
- `device_id`(原设备购买时必填)
- `order_type`(改为系统根据 identifier 解析结果自动填入)
响应体 MUST 包含前端可直接渲染字段。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在``NEED_REALNAME/该套餐需实名认证后购买``OPENID_NOT_FOUND/未找到微信授权信息,请先完成授权``IDEMPOTENT_CONFLICT/请求处理中,请勿重复提交``PACKAGE_NOT_AVAILABLE/套餐不可购买`
#### Scenario: 命中强充返回 recharge 结构
- **WHEN** 客户购买套餐触发强充要求
- **THEN** 系统返回 `order_type="recharge"`,包含充值单与关联套餐信息
#### Scenario: 个人客户使用 VirtualNo 购买设备套餐
- **WHEN** 个人客户发送 `{ identifier: "DEV-001", package_ids: [5], payment_method: "wechat" }`
- **THEN** 系统解析 identifier 为设备,创建设备购买订单,微信支付流程正常触发
#### Scenario: 个人客户使用 ICCID 购买单卡套餐
- **WHEN** 个人客户发送 `{ identifier: "898600XXXXX", package_ids: [3], payment_method: "wallet" }`
- **THEN** 系统解析为独立 IoT 卡(`is_standalone = true`),创建单卡购买订单
#### Scenario: 个人客户尝试为绑定设备的卡购买套餐
- **WHEN** 个人客户发送 identifier 对应一张 `is_standalone = false` 的卡
- **THEN** 系统返回错误"该卡已绑定设备,请前往设备页面购买套餐"
#### Scenario: 个人客户只能操作自己绑定的资产
- **WHEN** 个人客户发送的 identifier 对应的资产不属于该客户
- **THEN** 系统返回 HTTP 403"无权限操作该资源"
#### Scenario: before_order 策略未实名时购买被拦截
- **WHEN** 生效策略为 before_order 且 real_name_status=0用户调用创建订单接口
- **THEN** 系统返回 CodeNeedRealname1187订单不创建
#### Scenario: after_order 策略购买放行
- **WHEN** 生效策略为 after_order 且 real_name_status=0用户调用创建订单接口
- **THEN** 系统正常创建订单,不检查实名状态

View File

@@ -0,0 +1,45 @@
## MODIFIED Requirements
### Requirement: E1 获取实名跳转链接接口
系统 SHALL 提供 `GET /api/c/v1/realname/link?identifier=xxx&iccid=xxx`,并且 MUST 要求个人客户认证。该接口 MUST 支持两类入口:购买拦截入口与设备卡列表主动入口。目标卡定位 MUST 支持三种路径:
1. 标识符直达卡:直接使用该卡
2. 标识符为设备且传 `iccid`:定位对应设备下卡
3. 标识符为设备且未传 `iccid`:定位设备当前活跃卡
接口在确定目标卡后MUST 获取生效实名策略(`GetEffectiveRealnamePolicy`),按以下顺序执行前置检查:
**实名策略检查**(新增,在实名状态检查之前执行):
- 生效策略为 `after_order`:检查该资产当前 generation 内是否存在有效充值(`status=2`)或已支付订单(`payment_status=2`
- 无记录 → MUST 返回 `CodeRealnameNotAvailable`,消息"请先完成充值或购买套餐后再进行实名认证"
- 有记录 → 继续执行后续检查
- 生效策略为 `none``before_order`:不执行此检查,继续执行后续检查
**原有检查保留**
-`real_name_status=1` 时 MUST 返回"该卡已完成实名"错误
- 运营商实名模式 MUST 支持:`none`(不支持在线实名)、`template`(模板替换)、`gateway`(调用网关)
响应体 SHALL 至少包含 `realname_mode`(运营商链接类型)、`realname_url``card_info{iccid,msisdn,virtual_no}``expire_at`(可空)。
错误码/消息 MUST 至少包含(新增 `REALNAME_NOT_AVAILABLE``INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在``REALNAME_ALREADY_DONE/该卡已完成实名``REALNAME_NOT_SUPPORTED/该运营商暂不支持在线实名``GATEWAY_ERROR/获取实名链接失败``REALNAME_NOT_AVAILABLE/请先完成充值或购买套餐后再进行实名认证`
#### Scenario: 设备未传 iccid 自动选活跃卡
- **WHEN** 客户传入设备标识符且不传 `iccid`
- **THEN** 系统自动选择设备活跃卡并返回实名跳转链接
#### Scenario: after_order 模式有充值记录时放行
- **WHEN** 生效策略为 after_order 且当前 generation 内存在 status=2 的充值记录
- **THEN** 系统正常返回实名链接
#### Scenario: after_order 模式无记录时拦截
- **WHEN** 生效策略为 after_order 且当前 generation 内无任何有效充值或已支付订单
- **THEN** 系统返回错误码 CodeRealnameNotAvailable消息"请先完成充值或购买套餐后再进行实名认证"
#### Scenario: before_order 模式正常返回链接
- **WHEN** 生效策略为 before_order 且该卡尚未实名
- **THEN** 系统正常返回实名链接(引导用户先实名)
#### Scenario: none 模式正常返回链接
- **WHEN** 生效策略为 none
- **THEN** 系统正常返回实名链接(无策略限制,允许用户自愿实名)

View File

@@ -0,0 +1,43 @@
## MODIFIED Requirements
### Requirement: C3 充值预检接口
系统 SHALL 提供 `GET /api/c/v1/wallet/recharge-check?identifier=xxx`,并且 MUST 要求个人客户认证。接口 MUST 在资产解析与归属校验后,**新增实名策略检查**
- 获取生效实名策略(`GetEffectiveRealnamePolicy`
- 生效策略为 `before_order``real_name_status=0`MUST 返回 `CodeNeedRealname`
- 生效策略为 `after_order``none`:跳过实名检查,继续执行强充规则计算
通过实名检查后,接口 MUST 复用 `recharge.Service.GetRechargeCheck()` 计算强充规则。响应体 SHALL 包含 `need_force_recharge``force_recharge_amount``trigger_type``min_amount``max_amount``message`。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``FORBIDDEN/无权限操作该资产或资源不存在``NEED_REALNAME/该套餐需实名认证后充值`
#### Scenario: 返回强充预检结果
- **WHEN** 资产命中强充规则
- **THEN** 系统返回 `need_force_recharge=true` 与对应强充金额和触发类型
#### Scenario: before_order 未实名时充值预检被拦截
- **WHEN** 生效策略为 before_order 且 real_name_status=0
- **THEN** 系统返回 CodeNeedRealname不返回强充预检结果
---
### Requirement: C4 创建充值订单接口
系统 SHALL 提供 `POST /api/c/v1/wallet/recharge`,并且 MUST 要求个人客户认证。请求体 MUST 包含:`identifier``amount`100~10000000 分)、`payment_method=wechat``app_type`
接口 MUST 在归属校验后、OpenID 查询前,**新增实名策略检查**
- 生效策略为 `before_order``real_name_status=0`MUST 返回 `CodeNeedRealname`,充值订单不创建
- 生效策略为 `after_order``none`:继续执行后续流程
接口 MUST 禁止客户端传入 OpenID并由后端按 `customer_id + app_type` 查询 OpenID。订单创建时 MUST 写入:`operator_type=personal_customer` 与资产当前 `generation` 快照。响应体 SHALL 返回 `recharge``pay_config`,其中 `recharge` 至少含 `recharge_id``recharge_no``amount``status``pay_config` 为微信 JSAPI 拉起参数。错误码/消息 MUST 至少包含:`INVALID_PARAM/参数错误``OPENID_NOT_FOUND/未找到微信授权信息,请先完成授权``FORBIDDEN/无权限操作该资产或资源不存在``PAYMENT_NOT_SUPPORTED/仅支持微信支付``NEED_REALNAME/该套餐需实名认证后充值`
#### Scenario: 后端查 OpenID 并返回支付参数
- **WHEN** 客户传入合法参数且后端成功查询到 OpenID
- **THEN** 系统创建充值单并返回 `recharge + pay_config`
#### Scenario: before_order 未实名时充值订单创建被拦截
- **WHEN** 生效策略为 before_order 且 real_name_status=0用户调用充值下单接口
- **THEN** 系统返回 CodeNeedRealname1187充值单不创建
#### Scenario: after_order 模式充值放行
- **WHEN** 生效策略为 after_order 且 real_name_status=0用户调用充值下单接口
- **THEN** 系统正常创建充值单,不检查实名状态

View File

@@ -0,0 +1,31 @@
## ADDED Requirements
### Requirement: 设备导入批次支持实名策略配置
系统 SHALL 在设备导入请求(`ImportDeviceRequest`)中支持 `realname_policy` 参数,以批次为单位指定导入设备的实名策略,默认为 `none`
**请求字段新增**
- `realname_policy`:实名认证策略(枚举:`none` / `before_order` / `after_order`,可选,默认 `none`
**任务字段新增**
- `DeviceImportTask` 新增 `realname_policy` 字段VARCHAR(20) NOT NULL DEFAULT 'none'
**导入行为**
- 该批次导入的所有设备统一使用 `realname_policy` 的值写入 `Device.realname_policy`,不支持同一批次混用
- 导入任务记录该字段用于审计
#### Scenario: 不传 realname_policy 时默认为 none
- **WHEN** 设备导入请求未包含 `realname_policy` 字段
- **THEN** 导入的所有设备 `realname_policy``none`
#### Scenario: 指定 before_order 导入设备
- **WHEN** 设备导入请求中 `realname_policy` = `before_order`
- **THEN** 该批次导入的所有设备 `realname_policy` 均为 `before_order`
#### Scenario: 传入无效 realname_policy 被拒绝
- **WHEN** 设备导入请求中 `realname_policy` = `unknown`
- **THEN** 系统返回参数校验错误,提示实名策略值无效
#### Scenario: 设备导入任务响应返回 realname_policy
- **WHEN** 管理员查询设备导入任务列表或详情
- **THEN** 响应中包含 `realname_policy` 字段

View File

@@ -0,0 +1,31 @@
## ADDED Requirements
### Requirement: 导入批次支持实名策略配置
系统 SHALL 在 IoT 卡导入请求(`ImportIotCardRequest`)中支持 `realname_policy` 参数,以批次为单位指定导入卡的实名策略,默认为 `none`
**请求字段新增**
- `realname_policy`:实名认证策略(枚举:`none` / `before_order` / `after_order`,可选,默认 `none`
**任务字段新增**
- `IotCardImportTask` 新增 `realname_policy` 字段VARCHAR(20) NOT NULL DEFAULT 'none'
**导入行为**
- 该批次导入的所有卡统一使用 `realname_policy` 的值写入 `IotCard.realname_policy`,不支持同一批次混用
- 导入任务记录该字段用于审计
#### Scenario: 不传 realname_policy 时默认为 none
- **WHEN** 导入请求未包含 `realname_policy` 字段
- **THEN** 导入的所有卡 `realname_policy``none`,导入任务 `realname_policy``none`
#### Scenario: 指定 before_order 导入
- **WHEN** 导入请求中 `realname_policy` = `before_order`
- **THEN** 该批次导入的所有卡 `realname_policy` 均为 `before_order`
#### Scenario: 传入无效 realname_policy
- **WHEN** 导入请求中 `realname_policy` = `unknown`(非枚举值)
- **THEN** 系统返回参数校验错误,提示实名策略值无效
#### Scenario: 导入任务响应返回 realname_policy
- **WHEN** 管理员查询导入任务详情或列表
- **THEN** 响应中包含 `realname_policy` 字段,与创建时传入值一致

View File

@@ -0,0 +1,83 @@
## 1. 基础设施(常量 + 迁移 + Model
- [ ] 1.1 在 `pkg/constants/` 中定义 `RealnamePolicy` 枚举常量none / before_order / after_order及新增错误码 `CodeRealnameNotAvailable``pkg/errors/codes.go`
- [ ] 1.2 创建数据库迁移文件,为 `tb_iot_card` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
- [ ] 1.3 创建数据库迁移文件,为 `tb_device` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
- [ ] 1.4 创建数据库迁移文件,为 `tb_iot_card_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
- [ ] 1.5 创建数据库迁移文件,为 `tb_device_import_task` 新增 `realname_policy VARCHAR(20) NOT NULL DEFAULT 'none'`
- [ ] 1.6 在 `internal/model/iot_card.go``IotCard` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
- [ ] 1.7 在 `internal/model/device.go``Device` 结构体新增 `RealnamePolicy` 字段(含 gorm 标签和中文注释)
- [ ] 1.8 在 `internal/model/iot_card_import_task.go``IotCardImportTask` 结构体新增 `RealnamePolicy` 字段
- [ ] 1.9 在 `internal/model/device_import_task.go``DeviceImportTask` 结构体新增 `RealnamePolicy` 字段
- [ ] 1.10 执行迁移并验证四张表字段已正确添加
## 2. 公共策略判断逻辑Service 层)
- [ ] 2.1 在合适的公共 Service 或工具包中实现 `GetEffectiveRealnamePolicy(card *model.IotCard, device *model.Device) string`:设备卡取 device.RealnamePolicy单卡取 card.RealnamePolicy
- [ ] 2.2 实现检查 `after_order` 生效条件的方法:查询该资产当前 generation 内是否存在有效充值status=2或已支付订单payment_status=2返回 bool
## 3. IoT 卡导入模块改造
- [ ] 3.1 在 `ImportIotCardRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
- [ ] 3.2 更新 `iot_card_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `IotCardImportTask.realname_policy`
- [ ] 3.3 更新 IoT 卡导入 Worker 处理逻辑:创建 `IotCard` 记录时从任务的 `realname_policy` 写入卡的 `realname_policy`
- [ ] 3.4 在 `ImportTaskResponse`IoT 卡)中新增 `realname_policy` 字段(含 description 标签)
## 4. 设备导入模块改造
- [ ] 4.1 在 `ImportDeviceRequest` DTO 中新增 `realname_policy` 字段(含 validate、description 标签)
- [ ] 4.2 更新 `device_import` Service 的 CreateImportTask 方法:将请求中的 `realname_policy` 保存到 `DeviceImportTask.realname_policy`
- [ ] 4.3 更新设备导入 Worker 处理逻辑:创建 `Device` 记录时从任务的 `realname_policy` 写入设备的 `realname_policy`
- [ ] 4.4 在 `DeviceImportTaskResponse` 中新增 `realname_policy` 字段(含 description 标签)
## 5. 后台管理查询接口 DTO 添加字段
- [ ] 5.1 在 `StandaloneIotCardResponse``iot_card_dto.go`)中新增 `realname_policy` 字段,更新 Store/Service 中的 DTO 填充逻辑
- [ ] 5.2 在 `DeviceResponse``device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
- [ ] 5.3 在 `AssetResolveResponse``asset_dto.go`)中新增 `realname_policy` 字段,更新 `asset` Service 的 Resolve 方法填充逻辑
- [ ] 5.4 在 `AssetRealtimeStatusResponse``asset_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
- [ ] 5.5 在 `DeviceCardBindingResponse``device_dto.go`)中新增 `realname_policy` 字段,更新填充逻辑
- [ ] 5.6 在 `BoundCardInfo``asset_dto.go`,后台管理端公共子结构)中新增 `realname_policy` 字段,更新填充逻辑
## 6. C 端查询接口 DTO 添加字段
- [ ] 6.1 在 `AssetInfoResponse``client_asset_dto.go`)中新增 `realname_policy` 字段,更新 `client_asset` Service 的填充逻辑
- [ ] 6.2 在 C 端 `BoundCardInfo``client_asset_dto.go` 内嵌子结构)中新增 `realname_policy` 字段,更新填充逻辑
- [ ] 6.3 在 `DeviceCardItem``client_realname_device_dto.go`)中新增 `realname_policy` 字段,更新 `GetDeviceCards` 接口填充逻辑
## 7. C 端充值接口拦截C3/C4
- [ ] 7.1 在 `client_wallet` Service 的充值预检方法C3 对应逻辑在归属校验后插入实名策略检查before_order + 未实名 → 返回 CodeNeedRealname
- [ ] 7.2 在 `client_wallet` Service 的充值下单方法C4 对应逻辑在归属校验后、OpenID 查询前插入实名策略检查before_order + 未实名 → 返回 CodeNeedRealname
## 8. C 端购买套餐/订单接口拦截D1/D4
- [ ] 8.1 删除 `client_order` Service 中的 `REALNAME-03` 注释代码
- [ ] 8.2 在 `client_order` Service 的 CreateOrder 方法D1套餐校验之后、OpenID 查询之前插入实名策略检查
- [ ] 8.3 在 `client_order` Service 的 PayOrder 方法D4在执行支付前插入实名策略检查
## 9. C 端实名链接接口改造E1
- [ ] 9.1 在 `client_realname` Service 的 GetRealnameLink 方法中,目标卡定位后,`real_name_status=1` 检查前插入策略检查after_order + 无有效充值/订单 → 返回 CodeRealnameNotAvailable
- [ ] 9.2 验证 before_order 和 none 模式下实名链接接口行为不受影响
## 10. 后台管理更新接口UpdateRealnameMode
- [ ] 10.1 在 `iot_card` Store 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法
- [ ] 10.2 在 `device` Store 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法
- [ ] 10.3 在 `iot_card` Service 中新增 `UpdateRealnamePolicy(ctx, cardID uint, policy string) error` 方法(含幂等检查)
- [ ] 10.4 在 `device` Service 中新增 `UpdateRealnamePolicy(ctx, deviceID uint, policy string) error` 方法(含幂等检查)
- [ ] 10.5 在 `asset_dto.go` 中新增 `UpdateAssetRealnameModRequest``UpdateAssetRealnameModResponse` DTO
- [ ] 10.6 在 `asset` Handler`internal/handler/admin/asset.go`)中新增 `UpdateRealnameMode` 方法,遵循 `UpdatePollingStatus` 模式
- [ ] 10.7 在路由注册文件中注册新接口 `PATCH /api/admin/assets/:identifier/realname-mode`
- [ ] 10.8 更新文档生成器 `cmd/api/docs.go``cmd/gendocs/main.go`,添加新 Handler
## 11. 验证与收尾
- [ ] 11.1 运行 `lsp_diagnostics` 检查所有修改文件,确保无编译错误
- [ ] 11.2 使用 PostgreSQL MCP 验证四张表的字段已正确添加,存量数据 realname_policy 均为 'none'
- [ ] 11.3 手动验证 before_order 拦截:充值/购买接口在未实名时返回 CodeNeedRealname
- [ ] 11.4 手动验证 after_order 拦截:实名链接接口在无充值/订单时被拦截,有充值后放行
- [ ] 11.5 手动验证 none 模式:充值/购买/实名链接均正常放行
- [ ] 11.6 验证设备卡策略优先级:设备 before_order + 卡 none → 充值被拦截
- [ ] 11.7 验证后台更新接口:通过 ICCID 更新卡策略,通过设备号更新设备策略