package listing import ( "context" "errors" "fmt" "strings" "time" "hfb_sys/backend/internal/model" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool, publishCooldown time.Duration) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() if err := r.ensureCreateCooldown(tx, ownerID, publishCooldown, now); err != nil { return err } listingNo, err := r.nextListingNo(tx, now) if err != nil { return err } screenshots, err := marshalScreenshots(req.ScreenshotURLS) if err != nil { return err } assetSummary, err := marshalAssetSummary(req.AssetSummary) if err != nil { return err } listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) account := model.GameAccount{ OwnerID: ownerID, GameName: "delta_force", ServerRegion: req.ServerRegion, LoginPlatform: req.LoginPlatform, Title: req.Title, Description: req.Description, RankLevel: req.RankLevel, HafCoinAmount: req.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshots, Status: listingStatus, } if err := tx.Create(&account).Error; err != nil { return err } priceCent := normalizedListingPriceCent(req) listing := model.RentalListing{ ListingNo: listingNo, AccountID: account.ID, OwnerID: ownerID, PriceCent: priceCent, DepositAmountCent: req.DepositAmountCent, Status: listingStatus, ReviewStatus: reviewStatus, PublishedAt: publishedAt, } if err := tx.Create(&listing).Error; err != nil { return err } // 发布提交时建发布群 var conversationID uint64 if r.chatCreator != nil { convID, err := r.chatCreator.EnsureListingConversation(tx, listing, 0) if err != nil { return err } conversationID = convID } dto = toDTO(account, listing) dto.ListingGroupConversationID = conversationID return nil }) return dto, err } func (r *Repository) ensureCreateCooldown(tx *gorm.DB, ownerID uint64, cooldown time.Duration, now time.Time) error { if cooldown <= 0 { return nil } var user model.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&user, ownerID).Error; err != nil { return err } var latest model.RentalListing err := tx.Where("owner_id = ?", ownerID). Order("created_at DESC, id DESC"). First(&latest).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return err } remaining := latest.CreatedAt.Add(cooldown).Sub(now) if remaining > 0 { return PublishCooldownError{Cooldown: cooldown, Remaining: remaining} } return nil } type externalUploadCreate struct { UploaderName string ClientUploadTime *time.Time ClientIP string RawPayload []byte ParsedPayload []byte } func (r *Repository) CreateFromExternalUpload(ctx context.Context, upload externalUploadCreate, req CreateRequest) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { listingNo, err := r.nextListingNo(tx, time.Now()) if err != nil { return err } admin, err := r.findActiveUploadAdmin(tx, upload.UploaderName) if err != nil { return err } owner, err := r.ensureUploadOwnerUser(tx, admin) if err != nil { return err } screenshots, err := marshalScreenshots(req.ScreenshotURLS) if err != nil { return err } assetSummary, err := marshalAssetSummary(req.AssetSummary) if err != nil { return err } account := model.GameAccount{ OwnerID: owner.ID, GameName: "delta_force", ServerRegion: req.ServerRegion, LoginPlatform: req.LoginPlatform, Title: req.Title, Description: req.Description, RankLevel: req.RankLevel, HafCoinAmount: req.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshots, Status: "draft", } if err := tx.Create(&account).Error; err != nil { return err } priceCent := normalizedListingPriceCent(req) listing := model.RentalListing{ ListingNo: listingNo, AccountID: account.ID, OwnerID: owner.ID, PriceCent: priceCent, DepositAmountCent: req.DepositAmountCent, Status: "draft", ReviewStatus: "pending", } if err := tx.Create(&listing).Error; err != nil { return err } var conversationID uint64 if r.chatCreator != nil { convID, err := r.chatCreator.EnsureListingConversation(tx, listing, admin.ID) if err != nil { return err } conversationID = convID } matchedAdminID := admin.ID ownerID := owner.ID listingID := listing.ID uploadRow := model.ListingUpload{ UploaderName: upload.UploaderName, MatchedAdminID: &matchedAdminID, OwnerID: &ownerID, ClientUploadTime: upload.ClientUploadTime, ClientIP: upload.ClientIP, RawPayload: datatypes.JSON(upload.RawPayload), ParsedPayload: datatypes.JSON(upload.ParsedPayload), ListingID: &listingID, Status: "draft_created", } if err := tx.Create(&uploadRow).Error; err != nil { return err } dto = toDTO(account, listing) dto.ListingGroupConversationID = conversationID return nil }) return dto, err } func (r *Repository) Update(ctx context.Context, ownerID uint64, listingID uint64, req UpdateRequest, reviewRequired bool) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID) if err != nil { return err } if listingLockedForOwnerMutation(listing) { return ErrListingLocked } account.Title = req.Title account.Description = req.Description account.ServerRegion = req.ServerRegion account.LoginPlatform = req.LoginPlatform account.RankLevel = req.RankLevel account.HafCoinAmount = req.HafCoinAmount assetSummary, err := marshalAssetSummary(req.AssetSummary) if err != nil { return err } account.AssetSummary = assetSummary screenshots, err := marshalScreenshots(req.ScreenshotURLS) if err != nil { return err } account.ScreenshotURLS = screenshots listing.PriceCent = normalizedListingPriceCent(req) listing.DepositAmountCent = req.DepositAmountCent listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) listing.Status = listingStatus listing.ReviewStatus = reviewStatus listing.ReviewReason = "" listing.PublishedAt = publishedAt account.Status = listingStatus if err := tx.Save(account).Error; err != nil { return err } if err := tx.Save(listing).Error; err != nil { return err } dto = toDTO(*account, *listing) return nil }) return dto, err } func (r *Repository) SubmitReview(ctx context.Context, ownerID uint64, listingID uint64, reviewRequired bool) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID) if err != nil { return err } if listingLockedForOwnerMutation(listing) { return ErrListingLocked } listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) listing.Status = listingStatus listing.ReviewStatus = reviewStatus listing.ReviewReason = "" listing.PublishedAt = publishedAt account.Status = listingStatus if err := tx.Save(account).Error; err != nil { return err } if err := tx.Save(listing).Error; err != nil { return err } dto = toDTO(*account, *listing) return nil }) return dto, err } func (r *Repository) findActiveUploadAdmin(tx *gorm.DB, uploaderName string) (*model.AdminUser, error) { uploaderName = strings.TrimSpace(uploaderName) var admin model.AdminUser if err := tx.Where("username = ? AND status = ?", uploaderName, "active").First(&admin).Error; err == nil { return &admin, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } var admins []model.AdminUser if err := tx.Where("nickname = ? AND status = ?", uploaderName, "active").Limit(2).Find(&admins).Error; err != nil { return nil, err } switch len(admins) { case 0: return nil, ErrUploaderNotFound case 1: return &admins[0], nil default: return nil, ErrUploaderAmbiguous } } func (r *Repository) ensureUploadOwnerUser(tx *gorm.DB, admin *model.AdminUser) (*model.User, error) { phone := fmt.Sprintf("admin:%d", admin.ID) nickname := strings.TrimSpace(admin.Nickname) if nickname == "" { nickname = admin.Username } var user model.User err := tx.Where("phone = ?", phone).First(&user).Error if err == nil { updates := map[string]any{ "nickname": nickname, "status": "active", "realname_status": "verified", } if err := tx.Model(&user).Updates(updates).Error; err != nil { return nil, err } user.Nickname = nickname user.Status = "active" user.RealnameStatus = "verified" return &user, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } user = model.User{ Phone: phone, Nickname: nickname, RealnameStatus: "verified", RiskStatus: "normal", CreditScore: 100, Status: "active", } if err := tx.Create(&user).Error; err != nil { return nil, err } return &user, nil } func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint64) (*ListingDTO, error) { var dto *ListingDTO err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID) if err != nil { return err } if listingLockedForOwnerMutation(listing) { return ErrListingLocked } listing.Status = "offline" listing.ReviewStatus = "none" listing.ReviewReason = "号主已手动下架" listing.PublishedAt = nil account.Status = "offline" if err := tx.Save(account).Error; err != nil { return err } if err := tx.Save(listing).Error; err != nil { return err } dto = toDTO(*account, *listing) return nil }) return dto, err } func listingLockedForOwnerMutation(listing *model.RentalListing) bool { if listing == nil { return true } return listing.Status == "rented" || listing.Status == "completed" || listing.InTransaction }