feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
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:
2026-04-07 17:39:36 +08:00
parent 7e489a19fb
commit 80c6f6c756
69 changed files with 3039 additions and 851 deletions

View File

@@ -1,11 +1,10 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
assetWalletSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_wallet"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
@@ -16,7 +15,8 @@ import (
// AssetWalletHandler 资产钱包处理器
// 提供管理端资产(卡/设备)钱包概况和流水查询接口
type AssetWalletHandler struct {
service *assetWalletSvc.Service
service *assetWalletSvc.Service
assetService *assetSvc.Service
}
// NewAssetWalletHandler 创建资产钱包处理器
@@ -24,26 +24,34 @@ func NewAssetWalletHandler(svc *assetWalletSvc.Service) *AssetWalletHandler {
return &AssetWalletHandler{service: svc}
}
// SetAssetService 注入资产解析服务用于通过标识符解析资产类型和ID
func (h *AssetWalletHandler) SetAssetService(svc *assetSvc.Service) {
h.assetService = svc
}
// GetWallet 查询资产钱包概况
// GET /api/admin/assets/:asset_type/:id/wallet
// GET /api/admin/assets/:identifier/wallet
func (h *AssetWalletHandler) GetWallet(c *fiber.Ctx) error {
userType := middleware.GetUserTypeFromContext(c.UserContext())
if userType == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "企业账号无权查看钱包信息")
}
assetType := c.Params("asset_type")
if assetType != "card" && assetType != "device" {
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
identifier := c.Params("identifier")
if identifier == "" {
return errors.New(errors.CodeInvalidParam, "标识符不能为空")
}
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
return errors.New(errors.CodeInvalidParam, "无效的资产ID")
if h.assetService == nil {
return errors.New(errors.CodeInternalError, "资产服务未配置")
}
result, err := h.service.GetWallet(c.UserContext(), assetType, uint(id))
asset, err := h.assetService.Resolve(c.UserContext(), identifier)
if err != nil {
return err
}
result, err := h.service.GetWallet(c.UserContext(), asset.AssetType, asset.AssetID)
if err != nil {
return err
}
@@ -52,22 +60,25 @@ func (h *AssetWalletHandler) GetWallet(c *fiber.Ctx) error {
}
// ListTransactions 查询资产钱包流水列表
// GET /api/admin/assets/:asset_type/:id/wallet/transactions
// GET /api/admin/assets/:identifier/wallet/transactions
func (h *AssetWalletHandler) ListTransactions(c *fiber.Ctx) error {
userType := middleware.GetUserTypeFromContext(c.UserContext())
if userType == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "企业账号无权查看钱包信息")
}
assetType := c.Params("asset_type")
if assetType != "card" && assetType != "device" {
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
identifier := c.Params("identifier")
if identifier == "" {
return errors.New(errors.CodeInvalidParam, "标识符不能为空")
}
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
return errors.New(errors.CodeInvalidParam, "无效的资产ID")
if h.assetService == nil {
return errors.New(errors.CodeInternalError, "资产服务未配置")
}
asset, err := h.assetService.Resolve(c.UserContext(), identifier)
if err != nil {
return err
}
var req dto.AssetWalletTransactionListRequest
@@ -79,7 +90,7 @@ func (h *AssetWalletHandler) ListTransactions(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "每页数量不能超过100")
}
result, err := h.service.ListTransactions(c.UserContext(), assetType, uint(id), &req)
result, err := h.service.ListTransactions(c.UserContext(), asset.AssetType, asset.AssetID, &req)
if err != nil {
return err
}