企业授权增强与资产列表扩展
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>
This commit is contained in:
2026-06-17 16:23:03 +09:00
parent cf36f1447f
commit 3b7b856e48
37 changed files with 1260 additions and 84 deletions

View File

@@ -0,0 +1,28 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
修复卡企业授权的数据一致性问题,使其与设备授权保持一致:一张卡在同一时间只能授权给一个企业。
分两步:
**第一步:数据库迁移**
`tb_enterprise_card_authorization` 表上新增部分唯一索引,约束同一张卡不能同时存在两条有效授权记录(`revoked_at IS NULL AND deleted_at IS NULL`)。迁移执行前需先查询是否存在脏数据(同一 card_id 有多条 revoked_at IS NULL 的记录),若有需在迁移脚本中先行清理。
**第二步service 层校验补充**
卡授权 service`BatchAuthorize`)在执行授权前,增加"目标卡是否已授权给其他企业"的校验。目前代码只跳过已授权给同一企业的卡,不阻止授权给第二个企业。新逻辑:若目标卡已存在有效授权(`revoked_at IS NULL`)且授权对象不是当前企业,则将该卡加入失败列表并给出明确错误原因(如"卡已授权给其他企业,请先收回")。
## Acceptance criteria
- [x] `tb_enterprise_card_authorization` 表存在部分唯一索引,约束 `(card_id) WHERE revoked_at IS NULL AND deleted_at IS NULL`
- [x] 尝试将同一张卡授权给第二个企业时,接口返回失败,错误信息明确说明"已授权给其他企业"
- [x] 已撤回revoked_at 有值)的历史记录不受唯一约束影响,可正常查询
- [x] 同一张卡授权给同一企业时仍返回"已授权给该企业"的原有错误,行为不变
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,22 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/iot-cards/standalone` 接口的查询条件中新增网络状态过滤。
Request DTO 新增可选字段 `network_status *int`0=停机1=开机不传时不过滤。Store 层 `applyStandaloneFilters` 函数补充对应的 `network_status = ?` WHERE 条件。
## Acceptance criteria
- [x] 传入 `network_status=0` 时只返回停机的卡
- [x] 传入 `network_status=1` 时只返回开机的卡
- [x] 不传 `network_status` 时返回全部(与改动前行为一致)
- [x] 网络状态过滤可与现有其他过滤条件ICCID、运营商等叠加使用
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,34 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
在 standalone 卡列表响应和设备列表响应中暴露换货相关字段,使运营人员能直接在列表中识别资产的换货历史。
**`StandaloneIotCardResponse` 新增三个字段:**
- `asset_status int`业务状态1=在库, 2=已销售, 3=已换货, 4=已停用)
- `asset_status_name string`:对应中文名称
- `generation int`资产世代编号初始值1每次换货+旧资产转新后 +1
**`DeviceResponse` 同步新增相同三个字段。**
语义约定(写入注释和 description
- `asset_status = 3`:该资产已换货且尚未执行"旧资产转新",不再流通
- `generation > 1`:该资产历史上曾执行过换货后转新,目前仍在使用
Service 层组装响应时直接从 Model 取值,`asset_status_name` 通过 constants 中的方法转换。
## Acceptance criteria
- [x] `/api/admin/iot-cards/standalone` 响应中每条记录包含 `asset_status``asset_status_name``generation` 三个字段
- [x] `/api/admin/devices` 响应中每条记录包含相同三个字段
- [x] 已换货未转新的卡/设备返回 `asset_status=3``asset_status_name="已换货"`
- [x] 经过换货转新重入库的卡/设备返回 `generation=2`(或更高)且 `asset_status=1`
- [x] 普通未经历换货的资产返回 `generation=1`
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,23 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/shops/{shop_id}/main-wallet/transactions` 接口的查询条件中新增资产标识精确检索。
`MainWalletTransactionListRequest` 新增可选字段 `asset_identifier string`,非空时对 `asset_identifier` 列做精确匹配(`= ?`,不做模糊匹配)。`asset_identifier` 字段存储的是下单时资产的标识符快照ICCID 或虚拟号),空字符串时不过滤。
## Acceptance criteria
- [x] 传入有效的 ICCID 时只返回该卡相关的主钱包流水
- [x] 传入有效的设备虚拟号时只返回该设备相关的主钱包流水
- [x] 传入不存在的标识符时返回空列表total=0不报错
- [x] 不传 `asset_identifier` 时行为与改动前完全一致
- [x] 可与 `transaction_type``start_date``end_date` 等现有条件叠加使用
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,23 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/refunds` 接口的查询条件中新增资产标识精确检索。
`RefundListRequest` 新增可选字段 `asset_identifier string`,非空时对退款记录的 `asset_identifier` 列做精确匹配(`= ?`。该字段存储的是下单时资产的标识符快照ICCID 或虚拟号),空字符串时不过滤。
## Acceptance criteria
- [x] 传入有效的 ICCID 时只返回该卡相关的退款申请
- [x] 传入有效的设备虚拟号时只返回该设备相关的退款申请
- [x] 传入不存在的标识符时返回空列表total=0不报错
- [x] 不传 `asset_identifier` 时行为与改动前完全一致
- [x] 可与 `status``order_id``shop_id` 等现有条件叠加使用
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,33 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/iot-cards/standalone` 接口中新增企业维度过滤条件,并在响应中返回当前有效授权的企业信息。
**Request 新增过滤条件(`ListStandaloneIotCardRequest`**
- `authorized_enterprise_id *uint`按企业ID过滤只匹配 `tb_enterprise_card_authorization``revoked_at IS NULL` 的有效授权
- `is_authorized_to_enterprise *bool`true=只返回当前已授权给某企业的卡false=只返回未授权任何企业的卡
**Response 新增字段(`StandaloneIotCardResponse`**
- `authorized_enterprise_id *uint`当前有效授权的企业ID未授权时为 null
- `authorized_enterprise_name string`:对应企业名称,未授权时为空字符串
Store 层在 `applyStandaloneFilters` 中增加对 `authorized_enterprise_id``is_authorized_to_enterprise` 的处理,通过子查询 `tb_enterprise_card_authorization``revoked_at IS NULL AND deleted_at IS NULL`实现过滤。响应组装时批量查询企业名称IN 查询),不逐条查询。
## Acceptance criteria
- [x] 传入 `authorized_enterprise_id` 时只返回当前有效授权给该企业的卡
- [x] 传入 `is_authorized_to_enterprise=true` 时只返回已授权给某企业的卡
- [x] 传入 `is_authorized_to_enterprise=false` 时只返回未授权任何企业的卡
- [x] 已撤回的历史授权不影响过滤结果(已撤回视为未授权)
- [x] 响应中每张卡包含 `authorized_enterprise_id``authorized_enterprise_name`,未授权卡对应字段为 null/空字符串
- [x] 企业名称通过批量查询获取,不触发 N+1 查询
- [x] 新增过滤条件可与现有条件ICCID、运营商、店铺等叠加使用
## Blocked by
- `issues/01-card-enterprise-auth-unique-constraint.md`

View File

@@ -0,0 +1,32 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/devices` 接口中新增企业维度过滤条件,并在响应中返回当前有效授权的企业信息。
**Request 新增过滤条件(`ListDeviceRequest`**
- `authorized_enterprise_id *uint`按企业ID过滤只匹配 `tb_enterprise_device_authorization``revoked_at IS NULL` 的有效授权
- `is_authorized_to_enterprise *bool`true=只返回当前已授权给某企业的设备false=只返回未授权任何企业的设备
**Response 新增字段(`DeviceResponse`**
- `authorized_enterprise_id *uint`当前有效授权的企业ID未授权时为 null
- `authorized_enterprise_name string`:对应企业名称,未授权时为空字符串
Store 层通过子查询或 JOIN `tb_enterprise_device_authorization` 实现过滤。响应组装时批量查询企业名称IN 查询),不逐条查询。
## Acceptance criteria
- [x] 传入 `authorized_enterprise_id` 时只返回当前有效授权给该企业的设备
- [x] 传入 `is_authorized_to_enterprise=true` 时只返回已授权给某企业的设备
- [x] 传入 `is_authorized_to_enterprise=false` 时只返回未授权任何企业的设备
- [x] 已撤回的历史授权不影响过滤结果(已撤回视为未授权)
- [x] 响应中每台设备包含 `authorized_enterprise_id``authorized_enterprise_name`,未授权设备对应字段为 null/空字符串
- [x] 企业名称通过批量查询获取,不触发 N+1 查询
## Blocked by
None - can start immediately

View File

@@ -0,0 +1,42 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
将企业卡授权和收回接口升级为支持 list/range/filter 三种模式批量选取卡,替代原来只接受精确 ICCID 列表的方式。
**涉及接口:**
- `POST /api/admin/enterprises/{id}/allocate-cards`
- `POST /api/admin/enterprises/{id}/recall-cards`
**`AllocateCardsReq` 重构为:**
- `selection_type string`(必填,`list``range``filter`
- list 模式:`iccids []string`ICCID 列表最多1000个
- range 模式:`iccid_start string` + `iccid_end string`(号段范围)
- filter 模式过滤字段:`iccid string`(模糊)、`batch_no string``carrier_id *uint``shop_id *uint``shop_ids []uint`
- `remark string`(备注,所有模式均可选)
**`RecallCardsReq` 重构为:**
- `selection_type string`(必填,`list``range``filter`
- list 模式:`iccids []string`
- range 模式:`iccid_start string` + `iccid_end string`
- filter 模式过滤字段:`iccid string`(模糊)、`batch_no string``carrier_id *uint`
filter 和 range 模式下allocate 操作的候选卡集受操作者权限约束(代理用户只能授权自己店铺的卡),与 `applyStandaloneFilters` 中的 `ApplyShopFilter` 逻辑保持一致。
## Acceptance criteria
- [x] `allocate-cards` 接口接受 `selection_type=list` + `iccids`,行为与改动前一致
- [x] `allocate-cards` 接口接受 `selection_type=range` + 号段,批量授权号段内所有匹配的卡
- [x] `allocate-cards` 接口接受 `selection_type=filter` + 过滤条件,批量授权所有匹配的卡
- [x] `recall-cards` 接口同样支持三种模式,分别正确收回对应卡的企业授权
- [x] filter/range 模式下代理用户只能操作自己店铺的卡,超出范围的卡进入失败列表
- [x] 任何模式下尝试授权"已授权给其他企业"的卡,该卡进入失败列表并附带原因
- [x] 响应中 `success_count``fail_count``failed_items` 准确反映实际执行结果
## Blocked by
- `issues/01-card-enterprise-auth-unique-constraint.md`

View File

@@ -0,0 +1,38 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
将企业设备授权和收回接口升级为支持 list/filter 两种模式批量选取设备,替代原来只接受精确设备号列表的方式。
**涉及接口:**
- `POST /api/admin/enterprises/{id}/allocate-devices`
- `POST /api/admin/enterprises/{id}/recall-devices`
**`AllocateDevicesReq` 重构为:**
- `selection_type string`(必填,`list``filter`
- list 模式:`device_nos []string`(设备虚拟号列表)
- filter 模式过滤字段:`virtual_no string`(模糊)、`batch_no string``shop_id *uint`
**`RecallDevicesReq` 重构为:**
- `selection_type string`(必填,`list``filter`
- list 模式:`device_nos []string`
- filter 模式过滤字段:`virtual_no string`(模糊)、`batch_no string`
filter 模式下allocate 操作的候选设备集受操作者权限约束代理用户只能授权自己店铺的设备。filter 模式命中的设备数量无硬性上限,但单次事务处理建议分批,超大批次需记录日志。
## Acceptance criteria
- [x] `allocate-devices` 接口接受 `selection_type=list` + `device_nos` 列表,行为与改动前一致
- [x] `allocate-devices` 接口接受 `selection_type=filter` + 过滤条件,批量授权所有匹配设备
- [x] `recall-devices` 接口接受 `selection_type=list` + `device_nos` 列表,行为与改动前一致
- [x] `recall-devices` 接口接受 `selection_type=filter` + 过滤条件,批量收回所有匹配设备
- [x] filter 模式下代理用户只能操作自己店铺的设备,超出范围的设备进入失败列表
- [x] 响应中 `success_count``fail_count``failed_items` 准确反映实际执行结果
## Blocked by
None - can start immediately