package repository import ( "context" "errors" "strings" "time" "github.com/topfans/backend/pkg/models" "gorm.io/gorm" ) // ErrDuplicateRegistry 唯一约束冲突 sentinel 错误 // // 当 InsertPeripheralRegistry 触发 PostgreSQL unique violation(错误码 23505, // 命中 uk_registry_owner_star_type_asset)时,返回本错误。 // service 层用 errors.Is(err, repository.ErrDuplicateRegistry) 识别后转 BizCodeAlreadyAdded。 // // 触发场景:ExistsRegistry 与 InsertPeripheralRegistry 之间存在 race condition, // 并发双击 mint 时两次 ExistsRegistry 都返回 false,两次都尝试 INSERT, // 后到达的请求撞唯一约束。 var ErrDuplicateRegistry = errors.New("peripheral: duplicate registry") // PeripheralRepository 周边验真 + 加入藏品的数据访问层 type PeripheralRepository struct { db *gorm.DB } // NewPeripheralRepository 创建 PeripheralRepository 实例 func NewPeripheralRepository(db *gorm.DB) *PeripheralRepository { return &PeripheralRepository{db: db} } // GetAssetForVerification 查 asset(已下架/已删除视为不存在) // 沿用现有 asset_repository.go:110 的过滤条件:is_active=true AND deleted_at IS NULL // not found 返 (nil, nil);service 层把 nil 视为"物品不存在或已下架",返 50003 func (r *PeripheralRepository) GetAssetForVerification(ctx context.Context, assetID int64) (*models.Asset, error) { if assetID <= 0 { return nil, errors.New("asset_id must be greater than 0") } var asset models.Asset err := r.db.WithContext(ctx). Where("id = ? AND is_active = ? AND deleted_at IS NULL", assetID, true). First(&asset).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } return &asset, nil } // GetPeripheralInfo 查 peripheral_info,not found 返 (nil, nil) // service 层把"peripheral_info 缺失"视为"非周边",返 50003 func (r *PeripheralRepository) GetPeripheralInfo(ctx context.Context, assetID int64) (*models.PeripheralInfo, error) { if assetID <= 0 { return nil, errors.New("asset_id must be greater than 0") } var info models.PeripheralInfo err := r.db.WithContext(ctx). Where("asset_id = ?", assetID). First(&info).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } return &info, nil } // GetPeripheralInfoByCode 按周边编号查(扫码入口,asset_id=NULL 时也能查) func (r *PeripheralRepository) GetPeripheralInfoByCode(ctx context.Context, code string) (*models.PeripheralInfo, error) { if code == "" { return nil, errors.New("code required") } var info models.PeripheralInfo err := r.db.WithContext(ctx). Where("code = ?", code). First(&info).Error if err == gorm.ErrRecordNotFound { return nil, nil } if err != nil { return nil, err } return &info, nil } // UpdatePeripheralInfoOnMint mint 时回填 asset_id 和 user_id func (r *PeripheralRepository) UpdatePeripheralInfoOnMint(ctx context.Context, peripheralID, assetID, userID int64) error { return r.db.WithContext(ctx). Model(&models.PeripheralInfo{}). Where("id = ?", peripheralID). Updates(map[string]interface{}{ "asset_id": assetID, "user_id": userID, }).Error } // ExistsRegistry 查重:同一用户同一 asset_id 同一 asset_type 是否已有记录 // 依赖 asset_registry 已有 uk_registry_owner_star_type_asset UNIQUE 约束 func (r *PeripheralRepository) ExistsRegistry(ctx context.Context, ownerUID, assetID int64, assetType string) (bool, error) { var count int64 err := r.db.WithContext(ctx). Model(&models.AssetRegistry{}). Where("owner_uid = ? AND asset_id = ? AND asset_type = ?", ownerUID, assetID, assetType). Limit(1). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // CountRecentMint 限频:近 since 时间内同一用户同一 asset_type 的 mint 数 func (r *PeripheralRepository) CountRecentMint(ctx context.Context, ownerUID int64, assetType string, since time.Duration) (int64, error) { var count int64 threshold := time.Now().Add(-since).UnixMilli() err := r.db.WithContext(ctx). Model(&models.AssetRegistry{}). Where("owner_uid = ? AND asset_type = ? AND created_at > ?", ownerUID, assetType, threshold). Count(&count).Error if err != nil { return 0, err } return count, nil } // InsertPeripheralRegistry INSERT 一条 peripheral mint // 返回 (newID, createdAtMs),createdAtMs 来自 BeforeCreate 钩子自动填的 time.Now().UnixMilli() // 用 GORM Create 写入并从模型取回自增 ID 与 created_at,避免 service 二次查询 // // ★ P1.3 修复:检测 PostgreSQL 唯一约束冲突(23505),返 sentinel 错误 // // ErrDuplicateRegistry,让 service 层能识别并转 BizCodeAlreadyAdded (50004)。 // 仅用字符串匹配("23505" / "unique constraint")即可,不依赖 pgconn 包, // 与项目其他位置(asset_like_service.go:67)的错误识别策略保持一致。 func (r *PeripheralRepository) InsertPeripheralRegistry(ctx context.Context, reg *models.AssetRegistry) (int64, int64, error) { if err := r.db.WithContext(ctx).Create(reg).Error; err != nil { if isUniqueViolationErr(err) { return 0, 0, ErrDuplicateRegistry } return 0, 0, err } return reg.ID, reg.CreatedAt, nil } // isUniqueViolationErr 检测 PostgreSQL 唯一约束冲突(错误码 23505) // // 不引入 pgconn 依赖,直接用字符串匹配 GORM 包装后的 error 字符串; // 与 asset_like_service.go:isUniqueConstraintViolation 保持一致。 func isUniqueViolationErr(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "23505") || strings.Contains(msg, "unique constraint") } // CreateAssetCopy mint 时复制一份新 asset,owner_uid 直接设给收藏者(而非改原 asset) // // 复制字段:star_id, name, cover_url, material_url, material_type, info // 不复制:owner_uid(用 newOwnerUID)、is_original(false)、status(active)、like_count(0) func (r *PeripheralRepository) CreateAssetCopy(ctx context.Context, original *models.Asset, newOwnerUID int64) (int64, error) { grade := int32(1) newAsset := &models.Asset{ OwnerUID: newOwnerUID, StarID: original.StarID, Name: original.Name, CoverURL: original.CoverURL, MaterialURL: original.MaterialURL, MaterialType: original.MaterialType, Info: original.Info, Grade: &grade, // 初始等级 1 TxHash: original.TxHash, Status: models.AssetStatusActive, IsOriginal: false, } if err := r.db.WithContext(ctx).Create(newAsset).Error; err != nil { return 0, err } return newAsset.ID, nil } // UpdateAssetOwner mint 时同步更新 assets.owner_uid,使藏品所有权转移给收藏者 func (r *PeripheralRepository) UpdateAssetOwner(ctx context.Context, assetID, newOwnerUID int64) error { return r.db.WithContext(ctx). Model(&models.Asset{}). Where("id = ?", assetID). Update("owner_uid", newOwnerUID).Error } // RefreshVerifyCount 异步刷新某周边 verify_count // 事务包裹:Step A COUNT(*) → Step B UPDATE assets.verify_count = ? func (r *PeripheralRepository) RefreshVerifyCount(ctx context.Context, assetID int64) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var count int64 if err := tx.Model(&models.AssetRegistry{}). Where("asset_id = ? AND asset_type = ?", assetID, "peripheral"). Count(&count).Error; err != nil { return err } return tx.Model(&models.Asset{}). Where("id = ?", assetID). Update("verify_count", count).Error }) }