229 lines
8.5 KiB
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
|
|
}
|