All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
- 企业卡授权唯一约束:新增 DB 迁移(000154),卡级部分唯一索引防止同一张卡被多个企业同时持有,Service 层新增跨企业冲突检测 - 单卡列表新增 network_status 过滤参数 - 单卡/设备列表新增 asset_status、asset_status_name、generation 响应字段 - 单卡/设备列表新增企业维度过滤(authorized_enterprise_id、is_authorized_to_enterprise)及响应中企业授权信息(批量加载,无 N+1) - 主钱包流水/退款列表新增 asset_identifier 精确过滤参数 - 企业卡授权/收回接口升级为三模式(list/range/filter),企业设备授权/收回升级为双模式(list/filter) - 升级 sonic v1.14.2 → v1.15.2 以兼容 Go 1.26 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
9.5 KiB
Markdown
144 lines
9.5 KiB
Markdown
Status: ready-for-agent
|
||
|
||
# PRD:企业授权增强 & 资产列表扩展
|
||
|
||
## Problem Statement
|
||
|
||
平台运营人员在管理企业授权和查看资产状态时面临以下痛点:
|
||
|
||
1. **企业维度检索缺失**:卡列表和设备列表无法按企业维度过滤,也无法在列表中直接看到某张卡/设备被授权给了哪个企业,需要跨页面跳转查询。
|
||
2. **企业授权操作繁琐**:授权和收回接口只接受精确 ICCID/虚拟号列表,但前端列表有分页,量大时需一个个选,无法按批次号、号段等条件一次性批量操作。
|
||
3. **换货历史不可见**:经历过换货的旧卡/设备,在列表和详情中没有任何标记,运营无法判断当前资产是否为"换货遗留"状态或曾经历过换货后被重新激活(转新)的资产。
|
||
4. **主钱包流水和退款列表检索维度不足**:无法直接通过资产标识(ICCID 或虚拟号)快速定位某个资产相关的流水和退款记录。
|
||
5. **卡网络状态过滤缺失**:standalone 卡列表无法按网络状态(开机/停机)过滤,批量运维操作不便。
|
||
|
||
## Solution
|
||
|
||
对六个业务场景进行针对性扩展:
|
||
|
||
1. 在 standalone 卡列表和设备列表中新增企业维度过滤及企业信息返回字段
|
||
2. 将企业授权/收回接口升级为支持 list/range/filter 三(两)模式选资产
|
||
3. 在 standalone 卡列表和设备列表响应中暴露换货业务状态和资产世代编号
|
||
4. 主钱包流水列表新增资产标识精确检索
|
||
5. 退款列表新增资产标识精确检索
|
||
6. standalone 卡列表新增网络状态过滤
|
||
|
||
同时修复卡企业授权表的数据一致性问题(补唯一约束,与设备授权保持一致)。
|
||
|
||
## User Stories
|
||
|
||
1. 作为平台运营,我希望在 standalone 卡列表中输入企业 ID 进行过滤,以便快速找到已授权给该企业的所有卡
|
||
2. 作为平台运营,我希望在 standalone 卡列表中使用"是否授权企业"开关过滤,以便快速区分已授权和未授权的卡库存
|
||
3. 作为平台运营,我希望在 standalone 卡列表中直接看到每张卡被授权给哪个企业(企业ID和名称),以便不用跳转到企业详情页
|
||
4. 作为平台运营,我希望在设备列表中输入企业 ID 进行过滤,以便快速找到已授权给该企业的所有设备
|
||
5. 作为平台运营,我希望在设备列表中使用"是否授权企业"开关过滤,以便区分已授权和未授权的设备
|
||
6. 作为平台运营,我希望在设备列表中直接看到每台设备被授权给哪个企业,以便快速掌握设备归属
|
||
7. 作为平台运营,我希望在授权卡给企业时能通过 ICCID 号段范围一次性选取,以便高效授权大批量卡
|
||
8. 作为平台运营,我希望在授权卡给企业时能通过批次号、运营商等筛选条件选取,以便按业务维度批量授权
|
||
9. 作为平台运营,我希望在收回企业卡授权时同样支持 list/range/filter 三种模式,以便与授权操作保持一致的操作体验
|
||
10. 作为平台运营,我希望在授权设备给企业时能通过虚拟号模糊搜索和批次号筛选一次性选取,以便批量操作设备
|
||
11. 作为平台运营,我希望在收回企业设备授权时支持 list/filter 两种模式,以便批量收回
|
||
12. 作为平台运营,我希望在 standalone 卡列表中看到每张卡的业务状态(`asset_status`),以便识别已换货但尚未转新的"死档"卡
|
||
13. 作为平台运营,我希望在 standalone 卡列表中看到每张卡的世代编号(`generation`),以便识别曾换货后转新重入库的卡
|
||
14. 作为平台运营,我希望在设备列表中看到每台设备的业务状态和世代编号,以便同样掌握设备的换货历史
|
||
15. 作为平台运营,我希望在主钱包流水列表中输入 ICCID 或虚拟号精确检索,以便快速定位某资产的所有充值/扣款记录
|
||
16. 作为平台运营,我希望在退款列表中输入 ICCID 或虚拟号精确检索,以便快速找到某资产相关的所有退款申请
|
||
17. 作为平台运营,我希望在 standalone 卡列表中按网络状态(开机/停机)过滤,以便批量处理特定网络状态的卡
|
||
|
||
## Implementation Decisions
|
||
|
||
### 数据一致性修复(前置)
|
||
|
||
- **卡企业授权唯一约束**:在 `tb_enterprise_card_authorization` 表上补充部分唯一索引,约束同一张卡在同一时间只能有一条有效授权记录(`WHERE revoked_at IS NULL AND deleted_at IS NULL`),与设备授权表的 `uq_active_device_auth` 约束保持一致
|
||
- **service 层校验补充**:卡授权 service 在执行授权前,需增加"卡是否已授权给其他企业"的校验,目前只校验同一企业重复授权
|
||
|
||
### 需求1:standalone 卡列表 + 设备列表企业字段
|
||
|
||
**Request 新增过滤条件**(`ListStandaloneIotCardRequest` 和 `ListDeviceRequest`):
|
||
- `authorized_enterprise_id *uint`:按企业ID过滤,只匹配当前有效授权(`revoked_at IS NULL`)
|
||
- `is_authorized_to_enterprise *bool`:true=已授权给某企业,false=未授权任何企业
|
||
|
||
**Response 新增字段**(`StandaloneIotCardResponse` 和 `DeviceResponse`):
|
||
- `authorized_enterprise_id *uint`:未授权时为 null
|
||
- `authorized_enterprise_name string`:未授权时为空字符串
|
||
|
||
Store 层需通过 JOIN 或子查询 `tb_enterprise_card_authorization` / `tb_enterprise_device_authorization` 表获取企业ID,再批量查企业名称
|
||
|
||
### 需求2:企业授权/收回接口升级为多模式
|
||
|
||
参照 `AllocateStandaloneCardsRequest` / `RecallStandaloneCardsRequest` 的 `selection_type` 三模式设计:
|
||
|
||
**allocate-cards / recall-cards**(三模式:list / range / filter):
|
||
- `list`:`ICCIDs []string`,精确 ICCID 列表
|
||
- `range`:`ICCIDStart string` + `ICCIDEnd string`,号段范围
|
||
- `filter`:筛选条件
|
||
- allocate-cards filter 字段:`ICCID`(模糊)、`BatchNo`、`CarrierID`、`ShopID`/`ShopIDs`
|
||
- recall-cards filter 字段:`ICCID`(模糊)、`BatchNo`、`CarrierID`
|
||
|
||
**allocate-devices / recall-devices**(两模式:list / filter,不支持 range):
|
||
- `list`:`DeviceNos []string`,设备虚拟号列表
|
||
- `filter`:筛选条件
|
||
- allocate-devices filter 字段:`VirtualNo`(模糊)、`BatchNo`、`ShopID`
|
||
- recall-devices filter 字段:`VirtualNo`(模糊)、`BatchNo`
|
||
|
||
原有接口的请求结构需要破坏性变更(DTO 重构),需与前端联调确认过渡方案
|
||
|
||
### 需求3:换货状态字段
|
||
|
||
**`StandaloneIotCardResponse` 和 `DeviceResponse` 新增字段**:
|
||
- `asset_status int`:业务状态(1=在库, 2=已销售, 3=已换货, 4=已停用)
|
||
- `asset_status_name string`:对应中文名称
|
||
- `generation int`:资产世代编号(初始值1,每次换货+转新后+1)
|
||
|
||
语义约定:
|
||
- `asset_status = 3`:该资产已换货且尚未执行"旧资产转新",是永久性死档标记
|
||
- `generation > 1`:该资产历史上曾执行过换货+转新,目前仍在流通
|
||
|
||
### 需求4:主钱包流水资产标识检索
|
||
|
||
`MainWalletTransactionListRequest` 新增:
|
||
- `AssetIdentifier string`:精确匹配 `asset_identifier` 快照字段(ICCID 或虚拟号),空字符串时不过滤
|
||
|
||
### 需求5:退款列表资产标识检索
|
||
|
||
`RefundListRequest` 新增:
|
||
- `AssetIdentifier string`:精确匹配 `asset_identifier` 快照字段(ICCID 或虚拟号),空字符串时不过滤
|
||
|
||
### 需求6:standalone 卡列表网络状态过滤
|
||
|
||
`ListStandaloneIotCardRequest` 新增:
|
||
- `NetworkStatus *int`:网络状态过滤(0=停机, 1=开机),不传则不过滤
|
||
|
||
Store 层 `applyStandaloneFilters` 函数中补充 `network_status = ?` 条件
|
||
|
||
## Testing Decisions
|
||
|
||
本项目禁止自动化测试,验证方式为:
|
||
|
||
- **数据库验证**:通过 PostgreSQL MCP 工具验证授权表唯一约束是否生效、字段值是否正确写入
|
||
- **接口验证**:通过 curl/Postman 对各接口进行手动联调,重点覆盖以下场景:
|
||
- 企业 ID 过滤:确认只返回有效授权(revoked_at IS NULL)的卡/设备
|
||
- 是否授权企业过滤:true/false 结果集互补且无交集
|
||
- 授权时尝试将同一张卡授权给第二个企业,确认被拒绝
|
||
- allocate-cards 三种 selection_type 均能正确选取目标卡
|
||
- allocate-devices 两种 selection_type 均能正确选取目标设备
|
||
- asset_status=3 的卡在列表中可见且字段正确
|
||
- generation 字段在换货+转新后正确递增
|
||
- 主钱包流水 asset_identifier 精确匹配不漏不多
|
||
- 退款 asset_identifier 精确匹配不漏不多
|
||
- 网络状态过滤 0/1 结果集互补
|
||
|
||
## Out of Scope
|
||
|
||
- 企业卡/设备授权列表接口本身的改造(本次只改授权/收回操作接口)
|
||
- `AllocateCardsPreviewReq`(预览接口)是否同步升级为多模式(待后续评估)
|
||
- standalone 卡列表和设备列表的导出功能是否同步支持新增过滤条件
|
||
- 换货历史的完整时间线展示(仅暴露字段,不新增详情接口)
|
||
- `generation` 字段的过滤能力(暂只返回,不支持作为检索条件)
|
||
|
||
## Further Notes
|
||
|
||
- 需求1 的企业名称需要 N+1 防护:通过授权查询批量拿到 enterprise_id 后,用 IN 一次性查企业名称,不能逐条查
|
||
- 需求2 的 filter 模式对于 allocate-cards 场景,卡的范围天然受操作者权限约束(代理用户只能授权自己店铺的卡),Store 层已有 `middleware.ApplyShopFilter` 处理,filter 模式需要同样受此约束
|
||
- 卡唯一约束迁移执行前,需确认现有数据中是否存在一张卡同时有多条 `revoked_at IS NULL` 的记录,若有需先清理
|