feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
主要变更: - 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等) - 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束 - 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析 - 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合 - 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝 - Excel 工具函数优化,前端导入指引文档同步更新 - 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no - 更新 OpenAPI 文档及相关 DTO Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -21,87 +21,78 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/realtime-status", handler.RealtimeStatus, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/realtime-status", handler.RealtimeStatus, RouteSpec{
|
||||
Summary: "资产实时状态",
|
||||
Description: "读取 DB/Redis 中的持久化状态,不调网关。asset_type 为 card 或 device。",
|
||||
Description: "读取 DB/Redis 中的持久化状态,不调网关。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetTypeIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: new(dto.AssetRealtimeStatusResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/:asset_type/:id/refresh", handler.Refresh, RouteSpec{
|
||||
Register(assets, doc, groupPath, "POST", "/:identifier/refresh", handler.Refresh, RouteSpec{
|
||||
Summary: "刷新资产状态",
|
||||
Description: "主动调网关同步最新状态。设备有30秒冷却期。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetTypeIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: new(dto.AssetRealtimeStatusResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/packages", handler.Packages, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/packages", handler.Packages, RouteSpec{
|
||||
Summary: "资产套餐列表",
|
||||
Description: "查询该资产所有套餐记录,含虚流量换算结果。支持分页(默认 page=1, page_size=50, 最大100)。",
|
||||
Description: "查询该资产所有套餐记录,含虚流量换算结果。支持分页。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetPackagesRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: new(dto.AssetPackagesResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/current-package", handler.CurrentPackage, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/current-package", handler.CurrentPackage, RouteSpec{
|
||||
Summary: "当前生效套餐",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetTypeIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: new(dto.AssetPackageResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/device/:device_id/stop", handler.StopDevice, RouteSpec{
|
||||
Summary: "设备停机",
|
||||
Description: "批量停机设备下所有已实名卡。设置1小时停机保护期。",
|
||||
Register(assets, doc, groupPath, "POST", "/:identifier/stop", handler.Stop, RouteSpec{
|
||||
Summary: "停机",
|
||||
Description: "设备批量停机设备下所有已实名卡;单卡手动停机。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.DeviceIDRequest),
|
||||
Output: new(dto.DeviceSuspendResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/device/:device_id/start", handler.StartDevice, RouteSpec{
|
||||
Summary: "设备复机",
|
||||
Description: "批量复机设备下所有已实名卡。设置1小时复机保护期。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.DeviceIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/card/:iccid/stop", handler.StopCard, RouteSpec{
|
||||
Summary: "单卡停机",
|
||||
Description: "手动停机单张卡(通过ICCID)。受设备保护期约束。",
|
||||
Register(assets, doc, groupPath, "POST", "/:identifier/start", handler.Start, RouteSpec{
|
||||
Summary: "复机",
|
||||
Description: "设备批量复机;单卡手动复机。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.CardICCIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/card/:iccid/start", handler.StartCard, RouteSpec{
|
||||
Summary: "单卡复机",
|
||||
Description: "手动复机单张卡(通过ICCID)。受设备保护期约束。",
|
||||
Register(assets, doc, groupPath, "PATCH", "/:identifier/deactivate", handler.Deactivate, RouteSpec{
|
||||
Summary: "停用资产",
|
||||
Description: "将资产状态设置为停用(不可逆)。仅支持库存中或已售出的资产。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.CardICCIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/wallet", walletHandler.GetWallet, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/wallet", walletHandler.GetWallet, RouteSpec{
|
||||
Summary: "资产钱包概况",
|
||||
Description: "查询指定卡或设备的钱包余额概况。企业账号禁止调用。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetTypeIDRequest),
|
||||
Input: new(dto.AssetIdentifierRequest),
|
||||
Output: new(dto.AssetWalletResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/wallet/transactions", walletHandler.ListTransactions, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/wallet/transactions", walletHandler.ListTransactions, RouteSpec{
|
||||
Summary: "资产钱包流水列表",
|
||||
Description: "分页查询指定资产的钱包收支流水,含充值/扣款来源编号。企业账号禁止调用。",
|
||||
Tags: []string{"资产管理"},
|
||||
@@ -110,9 +101,18 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "PATCH", "/:asset_type/:id/polling-status", handler.UpdatePollingStatus, RouteSpec{
|
||||
Register(assets, doc, groupPath, "GET", "/:identifier/orders", handler.Orders, RouteSpec{
|
||||
Summary: "资产历史订单",
|
||||
Description: "查询资产本代历史订单,支持 include_previous=true 参数通过换货链追溯前代订单(最多10代)。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.AssetOrdersRequest),
|
||||
Output: new(dto.AssetOrdersResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "PATCH", "/:identifier/polling-status", handler.UpdatePollingStatus, RouteSpec{
|
||||
Summary: "更新资产轮询状态",
|
||||
Description: "启用或禁用指定资产(IoT卡或设备)的定期状态轮询。asset_type 为 iot_card 或 device。",
|
||||
Description: "启用或禁用指定资产(IoT卡或设备)的定期状态轮询。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.UpdateAssetPollingStatusRequest),
|
||||
Output: new(dto.UpdateAssetPollingStatusResponse),
|
||||
|
||||
@@ -4,25 +4,9 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// registerAssetLifecycleRoutes 注册资产手动停用路由
|
||||
func registerAssetLifecycleRoutes(router fiber.Router, handler *admin.AssetLifecycleHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "PATCH", "/iot-cards/:id/deactivate", handler.DeactivateIotCard, RouteSpec{
|
||||
Summary: "手动停用IoT卡",
|
||||
Tags: []string{"IoT卡管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "PATCH", "/devices/:id/deactivate", handler.DeactivateDevice, RouteSpec{
|
||||
Summary: "手动停用设备",
|
||||
Tags: []string{"设备管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
// registerAssetLifecycleRoutes 注册资产生命周期路由
|
||||
func registerAssetLifecycleRoutes(_ fiber.Router, _ *admin.AssetLifecycleHandler, _ *openapi.Generator, _ string) {
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(devices, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||||
Register(devices, doc, groupPath, "DELETE", "/:virtual_no", handler.Delete, RouteSpec{
|
||||
Summary: "删除设备",
|
||||
Description: "仅平台用户可操作。删除设备时自动解绑所有卡(卡不会被删除)。",
|
||||
Tags: []string{"设备管理"},
|
||||
@@ -30,7 +30,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(devices, doc, groupPath, "GET", "/:id/cards", handler.ListCards, RouteSpec{
|
||||
Register(devices, doc, groupPath, "GET", "/:virtual_no/cards", handler.ListCards, RouteSpec{
|
||||
Summary: "获取设备绑定的卡列表",
|
||||
Tags: []string{"设备管理"},
|
||||
Input: new(dto.ListDeviceCardsRequest),
|
||||
@@ -38,7 +38,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(devices, doc, groupPath, "POST", "/:id/cards", handler.BindCard, RouteSpec{
|
||||
Register(devices, doc, groupPath, "POST", "/:virtual_no/cards", handler.BindCard, RouteSpec{
|
||||
Summary: "绑定卡到设备",
|
||||
Description: "仅平台用户可操作。用于导入后调整卡绑定关系(补卡、换卡)。",
|
||||
Tags: []string{"设备管理"},
|
||||
@@ -47,7 +47,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(devices, doc, groupPath, "DELETE", "/:id/cards/:cardId", handler.UnbindCard, RouteSpec{
|
||||
Register(devices, doc, groupPath, "DELETE", "/:virtual_no/cards/:iccid", handler.UnbindCard, RouteSpec{
|
||||
Summary: "解绑设备上的卡",
|
||||
Description: "仅平台用户可操作。解绑不改变卡的 shop_id。",
|
||||
Tags: []string{"设备管理"},
|
||||
@@ -88,7 +88,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
|
||||
|
||||
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
||||
- 必须包含列(首行为表头):
|
||||
- ` + "`virtual_no`" + `: 设备虚拟号(必填,全局唯一)
|
||||
- ` + "`virtual_no`" + `: 设备虚拟号(**必填**,全局唯一;**为空的行记为失败,不再静默跳过**)
|
||||
- ` + "`device_name`" + `: 设备名称
|
||||
- ` + "`device_model`" + `: 设备型号
|
||||
- ` + "`device_type`" + `: 设备类型
|
||||
|
||||
@@ -49,16 +49,18 @@ func registerIotCardRoutes(router fiber.Router, handler *admin.IotCardHandler, i
|
||||
### Excel 文件格式
|
||||
|
||||
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
||||
- 必填列:` + "`ICCID`" + `, ` + "`MSISDN`" + `
|
||||
- 可选列:` + "`virtual_no`" + `(虚拟号,表头关键字:virtual_no / VirtualNo / 虚拟号 / 设备号)
|
||||
- 必填列(表头关键字):
|
||||
- ` + "`ICCID`" + `(或:卡号 / 号码)
|
||||
- ` + "`MSISDN`" + `(或:接入号 / 手机号 / 电话)
|
||||
- ` + "`virtual_no`" + `(或:VirtualNo / 虚拟号 / 设备号)—— **必填,缺少此列将导致整批导入失败**
|
||||
- 首行为表头(可选,但建议包含)
|
||||
- 列格式:设置为文本格式(避免长数字被转为科学记数法)
|
||||
|
||||
#### virtual_no 列导入规则(只补空白)
|
||||
#### virtual_no 导入规则
|
||||
|
||||
- 该行 virtual_no 非空 + 数据库当前值为 NULL:填入新值
|
||||
- 该行 virtual_no 非空 + 数据库已有值:跳过(不覆盖)
|
||||
- 批次内有任意 virtual_no 与数据库现存值重复:**整批拒绝**`,
|
||||
- ` + "`virtual_no`" + ` 为必填字段,**不可为空**;为空的行记为失败并报告原因
|
||||
- Excel 缺少 ` + "`virtual_no`" + ` 列时,整批导入直接失败(返回"Excel 文件缺少 virtual_no 列,请使用最新模板")
|
||||
- ` + "`virtual_no`" + ` 与系统已有值重复时,该行记为失败`,
|
||||
Tags: []string{"IoT卡管理"},
|
||||
Input: new(dto.ImportIotCardRequest),
|
||||
Output: new(dto.ImportIotCardResponse),
|
||||
|
||||
Reference in New Issue
Block a user