提案
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-01
|
||||
67
openspec/changes/add-open-api-device-endpoints/design.md
Normal file
67
openspec/changes/add-open-api-device-endpoints/design.md
Normal file
@@ -0,0 +1,67 @@
|
||||
## Context
|
||||
|
||||
现有代理开放接口(`internal/service/agent_open_api/service.go`)已实现卡维度的流量查询、状态查询、实名查询和钱包购买。设备维度的 Gateway 操作(切网、重启、恢复出厂)已在 `internal/service/device/gateway_service.go` 中实现,设备级套餐流量查询通过 `PackageUsageStore.ListByCarrier(ctx, "device", deviceID, ...)` 支持。
|
||||
|
||||
本次变更在现有 `agent_open_api` service 中扩展设备维度能力,复用已有的 device service 和 package usage store,不引入新的数据模型或迁移。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 新增 4 个代理开放接口:设备流量查询、切网、重启、恢复出厂
|
||||
- 复用现有 device service 的 Gateway 调用逻辑,不重复实现
|
||||
- 权限校验与卡接口保持一致(`shop_id IN SubordinateShopIDs`)
|
||||
- 设备标识符支持虚拟号和 IMEI
|
||||
|
||||
**Non-Goals:**
|
||||
- 不新增数据库表或迁移
|
||||
- 不修改现有卡接口
|
||||
- 不支持 SN 作为设备标识符(开放接口只暴露虚拟号/IMEI)
|
||||
- 不支持批量设备操作
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策一:agent_open_api service 注入 device.Service 依赖
|
||||
|
||||
**选择**:在 `agent_open_api.Service` 结构体中新增 `deviceService *device.Service` 字段,通过构造函数注入。
|
||||
|
||||
**理由**:device service 已封装了 Gateway 调用、审计日志和设备查询逻辑,直接复用避免重复实现。相比新建独立函数,注入 service 更符合项目分层规范。
|
||||
|
||||
**替代方案**:在 agent_open_api service 中直接注入 deviceStore 和 gatewayClient,自行实现权限校验和 Gateway 调用。缺点是重复了 device service 中已有的审计日志和错误处理逻辑。
|
||||
|
||||
### 决策二:设备权限校验方式
|
||||
|
||||
**选择**:查询设备后,手动检查 `device.ShopID != nil && SubordinateShopIDs 包含 *device.ShopID`。
|
||||
|
||||
**理由**:`ApplyShopFilter` 作用于 GORM query,而 device service 的 `GetByIdentifier` 内部已有自己的查询逻辑。为避免侵入 device service,在 agent_open_api service 层做显式权限校验,与卡接口的 `resolveOpenAPICard` 模式一致。
|
||||
|
||||
**实现**:新增 `resolveOpenAPIDevice(ctx, deviceNo)` 私有方法,复用 `device.Service.GetDeviceByIdentifier`,然后校验 shop_id。
|
||||
|
||||
### 决策三:设备流量查询复用 PackageUsageStore
|
||||
|
||||
**选择**:直接在 `agent_open_api.Service` 中调用已注入的 `packageUsageStore.ListByCarrier(ctx, "device", device.ID, &status)`。
|
||||
|
||||
**理由**:`packageUsageStore` 已在 agent_open_api service 中注入(用于卡流量查询),`ListByCarrier` 支持 `"device"` 类型,无需额外改动 store 层。
|
||||
|
||||
### 决策四:切网接口调用 device.Service.GatewaySwitchCard
|
||||
|
||||
**选择**:agent_open_api service 调用 `deviceService.GatewaySwitchCard(ctx, identifier, &dto.SwitchCardRequest{ICCID: req.ICCID})`。
|
||||
|
||||
**理由**:`GatewaySwitchCard` 已包含审计日志记录,开放接口复用可保证操作可追溯。
|
||||
|
||||
**注意**:`GatewaySwitchCard` 内部通过 `getGatewayDevice` 再次查询设备,存在两次查询。考虑到操作频率低,接受此开销,不做优化。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[风险] device service 审计日志中的操作者信息**:`GatewaySwitchCard` 等方法通过 `middleware.GetUserIDFromContext` 获取操作者 ID。开放接口认证中间件已将代理账号 ID 写入 `ContextKeyUserID`,审计日志可正确记录操作者。→ 无需额外处理。
|
||||
|
||||
- **[风险] 设备标识符两次查询**:`resolveOpenAPIDevice` 查一次,`GatewaySwitchCard` 内部再查一次。→ 接受,操作类接口频率低,影响可忽略。
|
||||
|
||||
- **[Trade-off] 不支持 SN**:admin 接口支持虚拟号/IMEI/SN,开放接口只支持虚拟号/IMEI。→ 简化外部接口,SN 是内部管理标识,不适合对外暴露。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
无数据库变更,无需迁移。直接部署新版本即可。
|
||||
|
||||
## Open Questions
|
||||
|
||||
(无)
|
||||
29
openspec/changes/add-open-api-device-endpoints/proposal.md
Normal file
29
openspec/changes/add-open-api-device-endpoints/proposal.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## Why
|
||||
|
||||
现有代理开放接口仅支持单卡维度的查询和操作,无法满足代理对设备维度的管理需求。代理需要通过开放接口查询设备套餐内流量、对多卡设备执行切网,以及对设备执行重启和恢复出厂操作。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **新增** `GET /api/open/v1/devices/traffic` — 查询设备套餐内流量,返回结构与 `/cards/traffic` 一致
|
||||
- **新增** `POST /api/open/v1/devices/switch-card` — 切网(多卡设备切换到指定 ICCID)
|
||||
- **新增** `POST /api/open/v1/devices/reboot` — 重启设备
|
||||
- **新增** `POST /api/open/v1/devices/reset` — 恢复出厂设置
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `open-api-device-traffic`: 代理开放接口设备套餐内流量查询,按设备标识符(虚拟号/IMEI)查询设备级 PackageUsage,返回生效/待生效套餐流量信息
|
||||
- `open-api-device-operations`: 代理开放接口设备操作,包括切网(switch-card)、重启(reboot)、恢复出厂(reset),复用 device service 现有 Gateway 调用,增加代理权限校验
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(无现有 spec 级别的需求变更)
|
||||
|
||||
## Impact
|
||||
|
||||
- `internal/handler/openapi/handler.go` — 新增 4 个 Handler 方法
|
||||
- `internal/routes/open.go` — 注册 4 条新路由
|
||||
- `internal/service/agent_open_api/service.go` — 新增设备流量查询和设备操作业务逻辑,注入 device service 依赖
|
||||
- `cmd/api/docs.go` 和 `cmd/gendocs/main.go` — 同步更新文档生成器
|
||||
- 权限校验:`device.ShopID IN 代理管辖店铺`,与现有卡权限逻辑一致
|
||||
@@ -0,0 +1,87 @@
|
||||
# open-api-device-operations Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 设备切网开放接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/open/v1/devices/switch-card` 接口,允许代理对多卡设备执行切网操作(切换到指定 ICCID)。请求 body MUST 包含 `device_no`(虚拟号或 IMEI)和 `iccid`(目标卡 ICCID)。
|
||||
|
||||
系统 MUST 校验该设备的 `shop_id` 在当前代理管辖店铺范围内,否则返回 `CodeForbidden`。权限校验通过后,系统 MUST 复用 `device.Service.GatewaySwitchCard` 执行切网,底层调用 Gateway 接口。
|
||||
|
||||
响应 data MUST 为空对象(操作成功即可)。
|
||||
|
||||
#### Scenario: 切网成功
|
||||
|
||||
- **WHEN** 代理对管辖范围内的多卡设备传入有效目标 ICCID 执行切网
|
||||
- **THEN** 系统调用 Gateway 切换设备到目标 ICCID,返回成功
|
||||
|
||||
#### Scenario: 无权限设备切网
|
||||
|
||||
- **WHEN** 代理对不在自己管辖店铺范围内的设备执行切网
|
||||
- **THEN** 系统返回 `CodeForbidden`,不调用 Gateway
|
||||
|
||||
#### Scenario: 设备标识符不存在
|
||||
|
||||
- **WHEN** 代理传入的 `device_no` 无法解析为任何设备
|
||||
- **THEN** 系统返回 `CodeForbidden`,消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备重启开放接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/open/v1/devices/reboot` 接口,允许代理对设备执行重启操作。请求 body MUST 包含 `device_no`(虚拟号或 IMEI)。
|
||||
|
||||
系统 MUST 校验该设备的 `shop_id` 在当前代理管辖店铺范围内,否则返回 `CodeForbidden`。权限校验通过后,系统 MUST 复用 `device.Service.GatewayRebootDevice` 执行重启。
|
||||
|
||||
响应 data MUST 为空对象。
|
||||
|
||||
#### Scenario: 重启成功
|
||||
|
||||
- **WHEN** 代理对管辖范围内的设备执行重启
|
||||
- **THEN** 系统调用 Gateway 重启设备,返回成功
|
||||
|
||||
#### Scenario: 无权限设备重启
|
||||
|
||||
- **WHEN** 代理对不在自己管辖店铺范围内的设备执行重启
|
||||
- **THEN** 系统返回 `CodeForbidden`,不调用 Gateway
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备恢复出厂开放接口
|
||||
|
||||
系统 SHALL 提供 `POST /api/open/v1/devices/reset` 接口,允许代理对设备执行恢复出厂设置操作。请求 body MUST 包含 `device_no`(虚拟号或 IMEI)。
|
||||
|
||||
系统 MUST 校验该设备的 `shop_id` 在当前代理管辖店铺范围内,否则返回 `CodeForbidden`。权限校验通过后,系统 MUST 复用 `device.Service.GatewayResetDevice` 执行恢复出厂。
|
||||
|
||||
响应 data MUST 为空对象。
|
||||
|
||||
#### Scenario: 恢复出厂成功
|
||||
|
||||
- **WHEN** 代理对管辖范围内的设备执行恢复出厂
|
||||
- **THEN** 系统调用 Gateway 恢复设备出厂设置,返回成功
|
||||
|
||||
#### Scenario: 无权限设备恢复出厂
|
||||
|
||||
- **WHEN** 代理对不在自己管辖店铺范围内的设备执行恢复出厂
|
||||
- **THEN** 系统返回 `CodeForbidden`,不调用 Gateway
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备操作开放接口权限校验
|
||||
|
||||
系统 SHALL 对所有设备操作开放接口(switch-card、reboot、reset)统一执行权限校验。校验逻辑 MUST 为:通过设备标识符(虚拟号或 IMEI)查询设备,若设备不存在或 `device.ShopID` 不在当前代理的 `SubordinateShopIDs` 中,则返回 `CodeForbidden`,消息为"无权限操作该资源或资源不存在",不区分"不存在"与"无权限"以防止信息泄露。
|
||||
|
||||
#### Scenario: 设备属于代理管辖店铺
|
||||
|
||||
- **WHEN** 设备的 `shop_id` 在代理的 `SubordinateShopIDs` 中
|
||||
- **THEN** 系统允许执行操作
|
||||
|
||||
#### Scenario: 设备不属于代理管辖店铺
|
||||
|
||||
- **WHEN** 设备的 `shop_id` 不在代理的 `SubordinateShopIDs` 中
|
||||
- **THEN** 系统返回 `CodeForbidden`,消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
#### Scenario: 设备 shop_id 为 NULL(平台库存)
|
||||
|
||||
- **WHEN** 设备的 `shop_id` 为 NULL(平台库存设备)
|
||||
- **THEN** 系统返回 `CodeForbidden`,代理无权操作平台库存设备
|
||||
@@ -0,0 +1,53 @@
|
||||
# open-api-device-traffic Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 设备套餐内流量查询开放接口
|
||||
|
||||
系统 SHALL 提供 `GET /api/open/v1/devices/traffic` 接口,按设备标识符查询设备级套餐内流量。请求参数 `device_no` MUST 支持虚拟号或 IMEI 中任一种可解析为设备的标识。系统 MUST 校验该设备的 `shop_id` 在当前代理管辖店铺范围内(`device.ShopID IN SubordinateShopIDs`),否则返回 `CodeForbidden`。
|
||||
|
||||
接口 MUST 只返回 `usage_type='device'` 的套餐使用记录,不返回单卡套餐信息。
|
||||
|
||||
响应 data MUST 包含:
|
||||
- `device_no`:请求传入的设备标识符
|
||||
- `active_total_flow_mb`:当前生效套餐总流量汇总
|
||||
- `active_used_flow_mb`:当前生效套餐已用流量汇总
|
||||
- `active_remaining_flow_mb`:当前生效套餐剩余流量汇总
|
||||
- `active_expires_at`:当前生效套餐中最晚过期时间
|
||||
- `active_packages`:当前生效套餐列表
|
||||
- `pending_packages`:待生效套餐列表
|
||||
|
||||
流量口径 MUST 与 `/cards/traffic` 保持一致:
|
||||
- 总流量使用 `data_limit_mb`
|
||||
- 已用流量使用 `min(data_usage_mb * display_gain_ratio_snapshot, data_limit_mb)`
|
||||
- 剩余流量使用 `max(data_limit_mb - used_flow_mb, 0)`
|
||||
- 响应 MUST NOT 包含虚流量、停机阈值等内部字段
|
||||
|
||||
`active_packages` 每项 MUST 包含:`package_code`、`total_flow_mb`、`used_flow_mb`、`remaining_flow_mb`、`expires_at`、`start_at`、`package_name`、`series_name`、`package_type`、`package_type_name`。
|
||||
|
||||
`pending_packages` 每项 MUST 包含:`package_code`、`total_flow_mb`、`package_name`、`series_name`、`package_type`、`package_type_name`、`valid_days`、`priority`。
|
||||
|
||||
#### Scenario: 查询有生效套餐的设备流量
|
||||
|
||||
- **WHEN** 代理查询自己管辖范围内的设备,且该设备存在生效中的设备级套餐
|
||||
- **THEN** 系统返回生效套餐列表、总流量、已用流量、剩余流量和最晚过期时间
|
||||
|
||||
#### Scenario: 查询待生效套餐
|
||||
|
||||
- **WHEN** 代理查询设备流量,且该设备存在待生效的设备级套餐
|
||||
- **THEN** 系统在 `pending_packages` 中返回套餐编码、真总流量、套餐名称、系列名称、套餐类型、有效天数和生效优先级
|
||||
|
||||
#### Scenario: 查询无套餐的设备
|
||||
|
||||
- **WHEN** 代理查询有权限但当前无生效或待生效设备级套餐的设备
|
||||
- **THEN** 系统返回空套餐列表,流量数值为 0
|
||||
|
||||
#### Scenario: 无权限设备
|
||||
|
||||
- **WHEN** 代理查询不在自己管辖店铺范围内的设备
|
||||
- **THEN** 系统返回 `CodeForbidden`,消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
#### Scenario: 设备标识符不存在
|
||||
|
||||
- **WHEN** 代理传入的 `device_no` 无法解析为任何设备
|
||||
- **THEN** 系统返回 `CodeForbidden`,消息为"无权限操作该资源或资源不存在"
|
||||
31
openspec/changes/add-open-api-device-endpoints/tasks.md
Normal file
31
openspec/changes/add-open-api-device-endpoints/tasks.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## 1. DTO 定义
|
||||
|
||||
- [x] 1.1 在 `internal/model/dto/` 新增设备开放接口请求/响应 DTO:`AgentOpenAPIDeviceQueryRequest`(含 `device_no` 字段)、`AgentOpenAPIDeviceSwitchCardRequest`(含 `device_no`、`iccid` 字段)、`AgentOpenAPIDeviceOperationRequest`(含 `device_no` 字段,用于 reboot/reset)、`AgentOpenAPIDeviceTrafficResponse`(结构与 `AgentOpenAPICardTrafficResponse` 一致,`card_no` 换成 `device_no`)
|
||||
|
||||
## 2. Service 层
|
||||
|
||||
- [x] 2.1 在 `agent_open_api.Service` 结构体中新增 `deviceService *device.Service` 字段,更新 `New()` 构造函数签名,在 `internal/bootstrap/services.go` 的 `agentOpenAPISvc.New(...)` 调用处传入 `s.Device`
|
||||
- [x] 2.2 在 `agent_open_api` service 中新增私有方法 `resolveOpenAPIDevice(ctx, deviceNo)`:调用 `deviceService.GetDeviceByIdentifier`,校验 `device.ShopID IN SubordinateShopIDs`,不存在或无权限统一返回 `CodeForbidden`
|
||||
- [x] 2.3 实现 `GetDeviceTraffic(ctx, req)`:调用 `resolveOpenAPIDevice` 获取设备,通过 `packageUsageStore.ListByCarrier(ctx, "device", device.ID, &activeStatus)` 和 `ListByCarrier(ctx, "device", device.ID, &pendingStatus)` 查询套餐,复用 `loadUsagePackageContext` 和 `buildTrafficItem`,组装 `AgentOpenAPIDeviceTrafficResponse`
|
||||
- [x] 2.4 实现 `SwitchDeviceCard(ctx, req)`:调用 `resolveOpenAPIDevice` 校验权限,再调用 `deviceService.GatewaySwitchCard(ctx, req.DeviceNo, &dto.SwitchCardRequest{ICCID: req.ICCID})`
|
||||
- [x] 2.5 实现 `RebootDevice(ctx, req)`:调用 `resolveOpenAPIDevice` 校验权限,再调用 `deviceService.GatewayRebootDevice(ctx, req.DeviceNo)`
|
||||
- [x] 2.6 实现 `ResetDevice(ctx, req)`:调用 `resolveOpenAPIDevice` 校验权限,再调用 `deviceService.GatewayResetDevice(ctx, req.DeviceNo)`
|
||||
|
||||
## 3. Handler 层
|
||||
|
||||
- [x] 3.1 在 `internal/handler/openapi/handler.go` 新增 `GetDeviceTraffic` Handler 方法(GET,QueryParser 解析 `AgentOpenAPIDeviceQueryRequest`)
|
||||
- [x] 3.2 新增 `SwitchDeviceCard` Handler 方法(POST,BodyParser 解析 `AgentOpenAPIDeviceSwitchCardRequest`)
|
||||
- [x] 3.3 新增 `RebootDevice` Handler 方法(POST,BodyParser 解析 `AgentOpenAPIDeviceOperationRequest`)
|
||||
- [x] 3.4 新增 `ResetDevice` Handler 方法(POST,BodyParser 解析 `AgentOpenAPIDeviceOperationRequest`)
|
||||
|
||||
## 4. 路由注册
|
||||
|
||||
- [x] 4.1 在 `internal/routes/open.go` 的 `RegisterOpenAPIRoutes` 中注册 4 条新路由:`GET /devices/traffic`、`POST /devices/switch-card`、`POST /devices/reboot`、`POST /devices/reset`,补充 Summary、Description(含 authDescription)、Input/Output、Tags、Auth、SecurityScheme
|
||||
|
||||
## 5. 文档生成器更新
|
||||
|
||||
- [x] 5.1 在 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 的 `bootstrap.Handlers{}` 初始化中,将 `AgentOpenAPI` 字段更新为 `openapiHandler.NewHandler(nil, nil)`(或对应的空值初始化),确保新增的 4 个 Handler 方法被文档生成器扫描到
|
||||
|
||||
## 6. 编译验证
|
||||
|
||||
- [x] 6.1 运行 `go build ./...` 确认无编译错误
|
||||
Reference in New Issue
Block a user