213 lines
7.4 KiB
Go
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
|
|
})
|
|
}
|