Files
hfb_sys/backend/internal/modules/listing/mutation.go
T
ymlandClaude Opus 4.8 ad70b24fb6 提交2: 发布群核心功能实现
- 实现 EnsureListingConversation: 发布时建发布群,发欢迎语+二维码
- 实现 AddRenterToListingConversation: 付款后拉租客进群
- 实现 RemoveRenterFromListingConversation: 订单终态移出租客
- 实现消息可见性过滤: 租客只能看到 joined_at 之后的消息
- listing 模块注入 ListingChatCreator 接口,发布时建群
- ListingDTO 新增 listing_group_conversation_id 字段
- 付款流程改为拉租客进发布群(替代建订单群)
- 订单完成和取消时自动移出租客
- 创建 chat 适配器实现接口解耦

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:57:10 +08:00

327 lines
9.0 KiB
Go

package listing
import (
"context"
"errors"
"fmt"
"strings"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/gorm"
)
func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateRequest, reviewRequired bool) (*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
}
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)
if err != nil {
return err
}
conversationID = convID
}
dto = toDTO(account, listing)
dto.ListingGroupConversationID = conversationID
return nil
})
return dto, err
}
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
}
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)
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 listing.Status == "rented" || listing.InTransaction {
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 listing.Status == "rented" || listing.InTransaction {
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 listing.Status == "rented" || listing.InTransaction {
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
}