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:
51
internal/store/postgres/asset_identifier_store.go
Normal file
51
internal/store/postgres/asset_identifier_store.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AssetIdentifierStore 全局资产标识符注册表存储层
|
||||
type AssetIdentifierStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAssetIdentifierStore 创建资产标识符注册表存储实例
|
||||
func NewAssetIdentifierStore(db *gorm.DB) *AssetIdentifierStore {
|
||||
return &AssetIdentifierStore{db: db}
|
||||
}
|
||||
|
||||
// Register 注册资产标识符
|
||||
// 在同一事务内调用,冲突时事务回滚
|
||||
func (s *AssetIdentifierStore) Register(ctx context.Context, identifier, assetType string, assetID uint) error {
|
||||
record := &model.AssetIdentifier{
|
||||
Identifier: identifier,
|
||||
AssetType: assetType,
|
||||
AssetID: assetID,
|
||||
}
|
||||
return s.db.WithContext(ctx).Create(record).Error
|
||||
}
|
||||
|
||||
// FindByIdentifier 通过标识符精确查找资产注册记录
|
||||
// 未命中时返回 nil, nil(非错误状态,调用方按需 fallback)
|
||||
func (s *AssetIdentifierStore) FindByIdentifier(ctx context.Context, identifier string) (*model.AssetIdentifier, error) {
|
||||
var record model.AssetIdentifier
|
||||
err := s.db.WithContext(ctx).Where("identifier = ?", identifier).First(&record).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// DeleteByAsset 删除资产时清理注册表(软删除时调用)
|
||||
// 删除该资产对应的所有注册记录,允许标识符被后续资产复用
|
||||
func (s *AssetIdentifierStore) DeleteByAsset(ctx context.Context, assetType string, assetID uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Where("asset_type = ? AND asset_id = ?", assetType, assetID).
|
||||
Delete(&model.AssetIdentifier{}).Error
|
||||
}
|
||||
@@ -93,6 +93,24 @@ func (s *ExchangeOrderStore) UpdateStatus(ctx context.Context, id uint, fromStat
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByNewAssetID 通过新资产类型和ID查询换货来源记录
|
||||
// 用于换货链追溯:找到将当前资产作为换货结果的换货单,从而得到前代资产信息
|
||||
// 若未找到则返回 nil, nil 表示该资产无前代
|
||||
func (s *ExchangeOrderStore) FindByNewAssetID(ctx context.Context, assetType string, assetID uint) (*model.ExchangeOrder, error) {
|
||||
var order model.ExchangeOrder
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("new_asset_id = ? AND new_asset_type = ?", assetID, assetType).
|
||||
Order("id DESC").
|
||||
First(&order).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeOrderStore) FindActiveByOldAsset(ctx context.Context, assetType string, assetID uint) (*model.ExchangeOrder, error) {
|
||||
var order model.ExchangeOrder
|
||||
query := s.db.WithContext(ctx).
|
||||
|
||||
@@ -133,6 +133,9 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
|
||||
if v, ok := filters["end_time"]; ok {
|
||||
query = query.Where("created_at <= ?", v)
|
||||
}
|
||||
if v, ok := filters["identifier"]; ok {
|
||||
query = query.Where("asset_identifier = ?", v)
|
||||
}
|
||||
if v, ok := filters["is_expired"]; ok {
|
||||
isExpired, _ := v.(bool)
|
||||
if isExpired {
|
||||
@@ -186,6 +189,28 @@ func (s *OrderStore) GenerateOrderNo() string {
|
||||
return fmt.Sprintf("ORD%s%06d", now.Format("20060102150405"), randomNum)
|
||||
}
|
||||
|
||||
// ListByAssetIdentifier 按资产标识符分页查询订单(倒序)
|
||||
// 用于资产历史订单查询,直接走 asset_identifier 快照字段精确匹配
|
||||
// 数据权限已在调用方(asset service)的 Resolve 环节检查,此处不重复过滤
|
||||
func (s *OrderStore) ListByAssetIdentifier(ctx context.Context, identifier string, page, pageSize int) ([]*model.Order, int64, error) {
|
||||
var orders []*model.Order
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Order{}).
|
||||
Where("asset_identifier = ?", identifier)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&orders).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
// FindExpiredOrders 查询已超时的待支付订单
|
||||
// 查询条件:expires_at <= NOW() AND payment_status = 1(待支付)
|
||||
// limit 参数限制每次批量处理的数量,避免一次性加载太多数据
|
||||
|
||||
Reference in New Issue
Block a user