topfans/backend/services/assetService/repository/peripheral_repo.go
2026-07-13 11:22:56 +08:00

213 lines
7.4 KiB
Go

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
})
}