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,8 +1,6 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
@@ -43,13 +41,17 @@ func (h *DeviceHandler) Delete(c *fiber.Ctx) error {
return errors.New(errors.CodeForbidden, "仅平台用户可删除设备")
}
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的设备ID")
virtualNo := c.Params("virtual_no")
if virtualNo == "" {
return errors.New(errors.CodeInvalidParam, "虚拟号不能为空")
}
if err := h.service.Delete(c.UserContext(), uint(id)); err != nil {
device, err := h.service.GetByVirtualNo(c.UserContext(), virtualNo)
if err != nil {
return err
}
if err := h.service.Delete(c.UserContext(), device.ID); err != nil {
return err
}
@@ -57,13 +59,17 @@ func (h *DeviceHandler) Delete(c *fiber.Ctx) error {
}
func (h *DeviceHandler) ListCards(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的设备ID")
virtualNo := c.Params("virtual_no")
if virtualNo == "" {
return errors.New(errors.CodeInvalidParam, "虚拟号不能为空")
}
result, err := h.service.ListBindings(c.UserContext(), uint(id))
device, err := h.service.GetByVirtualNo(c.UserContext(), virtualNo)
if err != nil {
return err
}
result, err := h.service.ListBindings(c.UserContext(), device.ID)
if err != nil {
return err
}
@@ -77,19 +83,23 @@ func (h *DeviceHandler) BindCard(c *fiber.Ctx) error {
return errors.New(errors.CodeForbidden, "仅平台用户可绑定卡到设备")
}
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
virtualNo := c.Params("virtual_no")
if virtualNo == "" {
return errors.New(errors.CodeInvalidParam, "虚拟号不能为空")
}
device, err := h.service.GetByVirtualNo(c.UserContext(), virtualNo)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的设备ID")
return err
}
var req dto.BindCardToDeviceRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
req.ID = uint(id)
req.ID = device.ID
result, err := h.service.BindCard(c.UserContext(), uint(id), &req)
result, err := h.service.BindCard(c.UserContext(), device.ID, &req)
if err != nil {
return err
}
@@ -103,19 +113,27 @@ func (h *DeviceHandler) UnbindCard(c *fiber.Ctx) error {
return errors.New(errors.CodeForbidden, "仅平台用户可解绑设备的卡")
}
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的设备ID")
virtualNo := c.Params("virtual_no")
if virtualNo == "" {
return errors.New(errors.CodeInvalidParam, "虚拟号不能为空")
}
cardIdStr := c.Params("cardId")
cardId, err := strconv.ParseUint(cardIdStr, 10, 64)
device, err := h.service.GetByVirtualNo(c.UserContext(), virtualNo)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的卡ID")
return err
}
result, err := h.service.UnbindCard(c.UserContext(), uint(id), uint(cardId))
iccid := c.Params("iccid")
if iccid == "" {
return errors.New(errors.CodeInvalidParam, "ICCID 不能为空")
}
card, cardErr := h.service.GetCardByICCID(c.UserContext(), iccid)
if cardErr != nil {
return cardErr
}
result, err := h.service.UnbindCard(c.UserContext(), device.ID, card.ID)
if err != nil {
return err
}