Files
junhong_cmp_fiber/internal/query/asset/exchange_trace.go

229 lines
8.5 KiB
Go

// Package asset 提供资产详情读取投影。
package asset
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"go.uber.org/zap"
"gorm.io/gorm"
)
// ExchangeTraceQuery 查询资产单节点换货链路并投影关联资产可见性。
type ExchangeTraceQuery struct {
db *gorm.DB
logger *zap.Logger
}
// ExchangeTraceAssetRef 标识批量换货关系查询中的当前资产。
type ExchangeTraceAssetRef struct {
// AssetType 为统一资产详情或换货模型中的资产类型。
AssetType string
// AssetID 为资产数据库 ID。
AssetID uint
}
// NewExchangeTraceQuery 创建资产换货链路查询。
func NewExchangeTraceQuery(db *gorm.DB, logger *zap.Logger) *ExchangeTraceQuery {
if logger == nil {
logger = zap.NewNop()
}
return &ExchangeTraceQuery{db: db, logger: logger}
}
// Resolve 查询当前资产的前代与后代换货投影。
func (q *ExchangeTraceQuery) Resolve(ctx context.Context, assetType string, assetID uint) (*dto.AssetExchangeTrace, error) {
trace := &dto.AssetExchangeTrace{}
assetType = normalizeExchangeAssetType(assetType)
ref := ExchangeTraceAssetRef{AssetType: assetType, AssetID: assetID}
previousBatch, err := q.FindPreviousCompleted(ctx, []ExchangeTraceAssetRef{ref})
if err != nil {
return nil, q.wrapQueryError(constants.ExchangeTraceDirectionPrevious, assetType, assetID, err)
}
nextBatch, err := q.FindNextCompleted(ctx, []ExchangeTraceAssetRef{ref})
if err != nil {
return nil, q.wrapQueryError(constants.ExchangeTraceDirectionNext, assetType, assetID, err)
}
previousOrders := previousBatch[ref]
nextOrders := nextBatch[ref]
previous := q.selectLatest(constants.ExchangeTraceDirectionPrevious, assetType, assetID, previousOrders)
next := q.selectLatest(constants.ExchangeTraceDirectionNext, assetType, assetID, nextOrders)
visibility, err := q.loadVisibility(ctx, previous, next)
if err != nil {
q.logger.Error("查询换货关联资产可见性失败",
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Error(err))
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询换货关联资产可见性失败")
}
if previous != nil {
key := relatedAssetKey{assetType: previous.OldAssetType, assetID: previous.OldAssetID}
previousAssetID := previous.OldAssetID
trace.PreviousAsset = newTraceItem(previous.OldAssetType, &previousAssetID, previous.OldAssetIdentifier, previous.ExchangeNo, visibility[key])
}
if next != nil {
var canView bool
if next.NewAssetID != nil {
key := relatedAssetKey{assetType: next.NewAssetType, assetID: *next.NewAssetID}
canView = visibility[key]
} else {
q.logger.Warn("已完成换货记录缺少新资产 ID",
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Uint("exchange_id", next.ID),
zap.String("exchange_no", next.ExchangeNo))
}
trace.NextAsset = newTraceItem(next.NewAssetType, next.NewAssetID, next.NewAssetIdentifier, next.ExchangeNo, canView)
}
return trace, nil
}
func normalizeExchangeAssetType(assetType string) string {
if assetType == constants.AssetResolveTypeCard {
return constants.ExchangeAssetTypeIotCard
}
return assetType
}
type relatedAssetKey struct {
assetType string
assetID uint
}
// FindPreviousCompleted 批量查询当前资产作为新资产时的已完成换货记录。
func (q *ExchangeTraceQuery) FindPreviousCompleted(ctx context.Context, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
return q.findRelatedOrdersBatch(ctx, constants.ExchangeTraceDirectionPrevious, refs)
}
// FindNextCompleted 批量查询当前资产作为旧资产时的已完成换货记录。
func (q *ExchangeTraceQuery) FindNextCompleted(ctx context.Context, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
return q.findRelatedOrdersBatch(ctx, constants.ExchangeTraceDirectionNext, refs)
}
func (q *ExchangeTraceQuery) findRelatedOrdersBatch(ctx context.Context, direction string, refs []ExchangeTraceAssetRef) (map[ExchangeTraceAssetRef][]*model.ExchangeOrder, error) {
result := make(map[ExchangeTraceAssetRef][]*model.ExchangeOrder, len(refs))
if len(refs) == 0 {
return result, nil
}
var orders []*model.ExchangeOrder
query := q.db.WithContext(ctx).Where("status = ?", constants.ExchangeStatusCompleted)
assetConditions := q.db.Session(&gorm.Session{NewDB: true}).Where("1 = 0")
for _, ref := range refs {
ref.AssetType = normalizeExchangeAssetType(ref.AssetType)
if direction == constants.ExchangeTraceDirectionPrevious {
assetConditions = assetConditions.Or("new_asset_type = ? AND new_asset_id = ?", ref.AssetType, ref.AssetID)
} else {
assetConditions = assetConditions.Or("old_asset_type = ? AND old_asset_id = ?", ref.AssetType, ref.AssetID)
}
}
err := query.Where(assetConditions).
Order("completed_at DESC NULLS LAST, id DESC").
Find(&orders).Error
if err != nil {
return nil, err
}
for _, order := range orders {
ref := ExchangeTraceAssetRef{AssetType: order.OldAssetType, AssetID: order.OldAssetID}
if direction == constants.ExchangeTraceDirectionPrevious && order.NewAssetID != nil {
ref = ExchangeTraceAssetRef{AssetType: order.NewAssetType, AssetID: *order.NewAssetID}
}
result[ref] = append(result[ref], order)
}
return result, nil
}
func (q *ExchangeTraceQuery) selectLatest(direction string, assetType string, assetID uint, orders []*model.ExchangeOrder) *model.ExchangeOrder {
if len(orders) == 0 {
return nil
}
selected := orders[0]
if len(orders) > 1 {
q.logger.Warn("检测到同方向多条已完成换货记录",
zap.String("direction", string(direction)),
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Int("candidate_count", len(orders)),
zap.Uint("selected_exchange_id", selected.ID),
zap.String("selected_exchange_no", selected.ExchangeNo))
}
return selected
}
func (q *ExchangeTraceQuery) loadVisibility(ctx context.Context, previous, next *model.ExchangeOrder) (map[relatedAssetKey]bool, error) {
result := make(map[relatedAssetKey]bool, 2)
cardIDs, deviceIDs := relatedAssetIDs(previous, next)
if len(cardIDs) > 0 {
var cards []*model.IotCard
if err := q.db.WithContext(ctx).Select("id", "shop_id").Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
return nil, err
}
for _, card := range cards {
result[relatedAssetKey{assetType: constants.ExchangeAssetTypeIotCard, assetID: card.ID}] = canViewShopAsset(ctx, card.ShopID)
}
}
if len(deviceIDs) > 0 {
var devices []*model.Device
if err := q.db.WithContext(ctx).Select("id", "shop_id").Where("id IN ?", deviceIDs).Find(&devices).Error; err != nil {
return nil, err
}
for _, device := range devices {
result[relatedAssetKey{assetType: constants.ExchangeAssetTypeDevice, assetID: device.ID}] = canViewShopAsset(ctx, device.ShopID)
}
}
return result, nil
}
func canViewShopAsset(ctx context.Context, shopID *uint) bool {
if middleware.IsUnrestricted(ctx) {
return true
}
return shopID != nil && middleware.ContainsShopID(ctx, *shopID)
}
func relatedAssetIDs(previous, next *model.ExchangeOrder) ([]uint, []uint) {
cardIDs := make([]uint, 0, 2)
deviceIDs := make([]uint, 0, 2)
appendID := func(assetType string, assetID uint) {
if assetType == constants.ExchangeAssetTypeIotCard {
cardIDs = append(cardIDs, assetID)
} else if assetType == constants.ExchangeAssetTypeDevice {
deviceIDs = append(deviceIDs, assetID)
}
}
if previous != nil {
appendID(previous.OldAssetType, previous.OldAssetID)
}
if next != nil && next.NewAssetID != nil {
appendID(next.NewAssetType, *next.NewAssetID)
}
return cardIDs, deviceIDs
}
func (q *ExchangeTraceQuery) wrapQueryError(direction string, assetType string, assetID uint, err error) error {
q.logger.Error("查询资产换货关系失败",
zap.String("direction", string(direction)),
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Error(err))
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产换货关系失败")
}
func newTraceItem(assetType string, assetID *uint, identifier, exchangeNo string, canView bool) *dto.AssetExchangeTraceItem {
item := &dto.AssetExchangeTraceItem{
AssetType: assetType,
Identifier: identifier,
ExchangeNo: exchangeNo,
CanView: canView,
}
if canView && assetID != nil {
item.AssetID = assetID
}
return item
}