package pickup import ( "context" crand "crypto/rand" "encoding/json" "errors" "fmt" "math" "strconv" "strings" "time" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/listingstatus" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/notification" "hfb_sys/backend/internal/modules/wallet" "hfb_sys/backend/internal/timeutil" "hfb_sys/backend/pkg/money" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } // Create 创建提号订单(状态:提号中)。 // 与正常订单支付成功一致:listing 置 rented + in_transaction,从商品管理「已上架」中移出。 // 不结算、不动钱包。 func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } var createdID uint64 err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrListingUnavailable } return err } if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction { return ErrListingUnavailable } // 同一 listing 不能有进行中的提号 var cnt int64 if err := tx.Model(&model.AdminPickup{}). Where("listing_id = ? AND status = ?", listing.ID, StatusPickingUp). Count(&cnt).Error; err != nil { return err } if cnt > 0 { return ErrDuplicatePickup } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { return err } accountSource, sourceChannel, err := resolvePickupAccountSource(tx, listing.ID) if err != nil { return err } priceSnapshot := buildPickupPriceSnapshot(listing, account) accountSnapshot, err := makePickupAccountSnapshot(account, listing) if err != nil { return err } pickupNo, err := newPickupNo() if err != nil { return err } pickup := model.AdminPickup{ PickupNo: pickupNo, ListingID: listing.ID, AccountID: listing.AccountID, OwnerID: listing.OwnerID, AdminID: adminID, Platform: strings.TrimSpace(req.Platform), ShopName: strings.TrimSpace(req.ShopName), AccountSource: accountSource, SourceChannel: sourceChannel, ListingPriceCent: priceSnapshot.ListingPriceCent, OwnerPriceCent: priceSnapshot.OwnerPriceCent, WebsiteProfitCent: priceSnapshot.WebsiteProfitCent, ProfitAmountCent: req.ProfitAmountCent, SellerRatio: priceSnapshot.SellerRatio, BuyerRatio: priceSnapshot.BuyerRatio, AccountSnapshot: accountSnapshot, Status: StatusPickingUp, Remark: strings.TrimSpace(req.Remark), } if err := tx.Create(&pickup).Error; err != nil { return err } createdID = pickup.ID // 锁定商品:status=rented,从「已上架」列表移除,进入「已锁定」 fromStatus := listing.Status listing.InTransaction = true listing.Status = "rented" account.Status = "rented" if err := tx.Save(&listing).Error; err != nil { return err } if err := tx.Save(&account).Error; err != nil { return err } if err := listingstatus.AppendTransition( tx, &listing, fromStatus, listingstatus.SourcePickup, listingstatus.ActorAdmin, adminID, "管理员线下提号锁定", ); err != nil { return err } bid := pickup.ID if err := notification.Append(tx, notification.Entry{ UserID: listing.OwnerID, Type: "pickup", Title: "账号被管理员提号", Content: fmt.Sprintf("您的账号「%s」已被管理员提号(线下交易),等待完成结算。", account.Title), BizType: "pickup", BizID: &bid, }); err != nil { return err } if err := auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", ActorID: adminID, Action: "pickup_create", BizType: "admin_pickup", BizID: &bid, Meta: meta, Detail: map[string]any{ "pickup_no": pickupNo, "listing_id": listing.ID, "platform": pickup.Platform, "shop_name": pickup.ShopName, "account_source": pickup.AccountSource, "source_channel": pickup.SourceChannel, "profit_cent": pickup.ProfitAmountCent, }, }); err != nil { return err } return nil }) if err != nil { return nil, err } return r.FindByID(ctx, createdID) } // Complete 完成提号:给卖家加可用余额,listing 置 completed(售出终态,不可再上架)。 func (r *Repository) Complete(ctx context.Context, pickupID uint64, req CompleteRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } if req.SettleAmountCent <= 0 { return nil, ErrInvalidAmount } err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var pickup model.AdminPickup if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPickupNotFound } return err } if pickup.Status != StatusPickingUp { return ErrPickupNotPickingUp } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil { return err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, pickup.AccountID).Error; err != nil { return err } now := time.Now() pickup.SettleAmountCent = req.SettleAmountCent if req.ProfitAmountCent != nil { pickup.ProfitAmountCent = *req.ProfitAmountCent } pickup.Status = StatusCompleted pickup.CompleteRemark = strings.TrimSpace(req.CompleteRemark) pickup.CompletedAt = &now if err := tx.Save(&pickup).Error; err != nil { return err } // listing 置 completed(售出终态,不可再上架),与正常订单 completeAssets 对齐。 fromStatus := listing.Status listing.InTransaction = false listing.Status = "completed" listing.PublishedAt = nil account.Status = "offline" if err := tx.Save(&listing).Error; err != nil { return err } if err := tx.Save(&account).Error; err != nil { return err } if err := listingstatus.AppendTransition( tx, &listing, fromStatus, listingstatus.SourcePickup, listingstatus.ActorAdmin, adminID, "管理员提号完成", ); err != nil { return err } if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: pickup.OwnerID, Direction: "in", AmountCent: req.SettleAmountCent, BalanceType: "available", BizType: "admin_pickup_settle", BizNo: pickup.PickupNo, Remark: fmt.Sprintf("管理员提号结算:%s", pickup.PickupNo), }); err != nil { return err } bid := pickup.ID if err := notification.Append(tx, notification.Entry{ UserID: pickup.OwnerID, Type: "pickup", Title: "提号已完成,已结算到账", Content: fmt.Sprintf("账号「%s」提号完成,已结算 %s 到您的钱包。", account.Title, money.FormatWithSymbol(req.SettleAmountCent)), BizType: "pickup", BizID: &bid, }); err != nil { return err } if err := auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", ActorID: adminID, Action: "pickup_complete", BizType: "admin_pickup", BizID: &bid, Meta: meta, Detail: map[string]any{ "pickup_no": pickup.PickupNo, "settle_amount_cent": req.SettleAmountCent, "profit_amount_cent": pickup.ProfitAmountCent, }, }); err != nil { return err } return nil }) if err != nil { return nil, err } return r.FindByID(ctx, pickupID) } // UpdateProfit 修改提号中的线下利润,已完成和已取消的提号不可修改。 func (r *Repository) UpdateProfit(ctx context.Context, pickupID uint64, req UpdateProfitRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var pickup model.AdminPickup if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPickupNotFound } return err } if pickup.Status != StatusPickingUp { return ErrPickupNotPickingUp } oldProfitAmountCent := pickup.ProfitAmountCent pickup.ProfitAmountCent = req.ProfitAmountCent if err := tx.Model(&pickup).Update("profit_amount_cent", pickup.ProfitAmountCent).Error; err != nil { return err } bid := pickup.ID return auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", ActorID: adminID, Action: "pickup_profit_update", BizType: "admin_pickup", BizID: &bid, Meta: meta, Detail: map[string]any{ "pickup_no": pickup.PickupNo, "old_profit_amount_cent": oldProfitAmountCent, "new_profit_amount_cent": pickup.ProfitAmountCent, "reason": strings.TrimSpace(req.Reason), }, }) }) if err != nil { return nil, err } return r.FindByID(ctx, pickupID) } // Cancel 取消提号:恢复 listing 为 published + 解锁 in_transaction,账号恢复可租。 func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error { if r == nil || r.db == nil { return ErrDependencyUnavailable } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var pickup model.AdminPickup if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&pickup, pickupID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPickupNotFound } return err } if pickup.Status != StatusPickingUp { return ErrPickupNotPickingUp } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil { return err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, pickup.AccountID).Error; err != nil { return err } now := time.Now() pickup.Status = StatusCancelled pickup.CancelledAt = &now if err := tx.Save(&pickup).Error; err != nil { return err } // 取消提号:恢复上架,重新出现在商品管理「已上架」 fromStatus := listing.Status listing.InTransaction = false listing.Status = "published" account.Status = "published" if err := tx.Save(&listing).Error; err != nil { return err } if err := tx.Save(&account).Error; err != nil { return err } if err := listingstatus.AppendTransition( tx, &listing, fromStatus, listingstatus.SourcePickup, listingstatus.ActorAdmin, adminID, "取消提号恢复上架", ); err != nil { return err } bid := pickup.ID _ = notification.Append(tx, notification.Entry{ UserID: pickup.OwnerID, Type: "pickup", Title: "提号已取消", Content: fmt.Sprintf("提号订单 %s 已取消,原因:%s", pickup.PickupNo, reason), BizType: "pickup", BizID: &bid, }) _ = auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", ActorID: adminID, Action: "pickup_cancel", BizType: "admin_pickup", BizID: &bid, Meta: meta, Detail: map[string]any{"pickup_no": pickup.PickupNo, "reason": reason}, }) return nil }) } func (r *Repository) FindByID(ctx context.Context, id uint64) (*PickupDTO, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } var row pickupRow if err := r.baseQuery(ctx).Where("p.id = ?", id).First(&row).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPickupNotFound } return nil, err } dto := row.toDTO() if isEmptyPickupAccountSnapshot(dto.AccountSnapshot) { if snapshot, err := r.fallbackAccountSnapshot(ctx, row.ListingID, row.AccountID); err == nil { dto.AccountSnapshot = snapshot } } return &dto, nil } func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*PaginatedResult, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } db := r.baseQuery(ctx) if status := strings.TrimSpace(query.Status); status != "" { db = db.Where("p.status = ?", status) } if shopName := strings.TrimSpace(query.ShopName); shopName != "" { db = db.Where("p.shop_name = ?", shopName) } if accountSource := strings.TrimSpace(query.AccountSource); accountSource != "" { db = db.Where("p.account_source = ?", accountSource) } if kw := strings.TrimSpace(query.Keyword); kw != "" { like := "%" + kw + "%" db = db.Where("p.pickup_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ?", like, like, like) } return r.paginatePickups(db, query.Page, query.PageSize, false) } // ListShopNames 返回历史提号中已使用的店铺名称,供后台选择和筛选。 func (r *Repository) ListShopNames(ctx context.Context, platform string) ([]string, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } db := r.db.WithContext(ctx).Model(&model.AdminPickup{}). Where("shop_name <> ''") if value := strings.TrimSpace(platform); value != "" { db = db.Where("platform = ?", value) } names := make([]string, 0) if err := db.Distinct("shop_name").Order("shop_name ASC").Limit(200).Pluck("shop_name", &names).Error; err != nil { return nil, err } return names, nil } func (r *Repository) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } db := r.baseQuery(ctx).Where("p.owner_id = ?", ownerID) if status := strings.TrimSpace(query.Status); status != "" { db = db.Where("p.status = ?", status) } return r.paginatePickups(db, query.Page, query.PageSize, true) } func (r *Repository) paginatePickups(db *gorm.DB, page, pageSize int, sellerView bool) (*PaginatedResult, error) { page, pageSize = normalizePaging(page, pageSize) var total int64 if err := db.Count(&total).Error; err != nil { return nil, err } var rows []pickupRow if err := db.Order("p.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil { return nil, err } items := make([]PickupDTO, 0, len(rows)) for _, row := range rows { item := row.toDTO() item.AccountSnapshot = nil if sellerView { item.WebsiteProfitCent = 0 item.ProfitAmountCent = 0 item.BuyerRatio = 0 item.ShopName = "" item.SourceChannel = "" } items = append(items, item) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } // ListAvailableListings 可提号的 listing:已发布、已审核、未在交易中。 func (r *Repository) ListAvailableListings(ctx context.Context, query AvailableListingQuery) (*AvailableListingsResult, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } latestUploads := r.db.WithContext(ctx).Table("listing_uploads"). Select("listing_id, MAX(id) AS upload_id"). Where("listing_id IS NOT NULL"). Group("listing_id") db := r.db.WithContext(ctx).Table("rental_listings AS l"). Select(`l.id, l.listing_no, l.account_id, l.owner_id, l.price_cent AS listing_price_cent, a.title AS account_title, a.server_region, a.login_platform, a.haf_coin_amount, a.asset_summary, owner.phone AS owner_phone, l.handoff_mode, l.settlement_mode, CASE WHEN lu.id IS NULL THEN 0 ELSE 1 END AS is_external_upload, COALESCE(lu.source_channel, '') AS source_channel`). Joins("JOIN game_accounts AS a ON a.id = l.account_id"). Joins("JOIN users AS owner ON owner.id = l.owner_id"). Joins("LEFT JOIN (?) AS latest_upload ON latest_upload.listing_id = l.id", latestUploads). Joins("LEFT JOIN listing_uploads AS lu ON lu.id = latest_upload.upload_id"). Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false) if kw := strings.TrimSpace(query.Keyword); kw != "" { like := "%" + kw + "%" db = db.Where("l.listing_no LIKE ? OR a.title LIKE ?", like, like) } switch strings.TrimSpace(query.SourceType) { case ListingSourceExternal: db = db.Where("lu.id IS NOT NULL") case ListingSourceInternal: db = db.Where("lu.id IS NULL") } var total int64 if err := db.Count(&total).Error; err != nil { return nil, err } page, pageSize := normalizePaging(query.Page, query.PageSize) var rows []availableListingRow if err := db.Order("l.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil { return nil, err } items := make([]AvailableListingDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return &AvailableListingsResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) baseQuery(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx).Table("admin_pickups AS p"). Select("p.*, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone"). Joins("JOIN rental_listings AS l ON l.id = p.listing_id"). Joins("JOIN game_accounts AS a ON a.id = p.account_id"). Joins("JOIN users AS owner ON owner.id = p.owner_id") } type pickupRow struct { model.AdminPickup ListingNo string AccountTitle string ServerRegion string LoginPlatform string OwnerPhone string } func (row pickupRow) toDTO() PickupDTO { return PickupDTO{ ID: row.ID, PickupNo: row.PickupNo, ListingID: row.ListingID, ListingNo: row.ListingNo, AccountID: row.AccountID, AccountTitle: row.AccountTitle, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, OwnerID: row.OwnerID, OwnerPhone: row.OwnerPhone, AdminID: row.AdminID, Platform: row.Platform, ShopName: row.ShopName, AccountSource: row.AccountSource, SourceChannel: row.SourceChannel, ListingPriceCent: row.ListingPriceCent, OwnerPriceCent: row.OwnerPriceCent, WebsiteProfitCent: row.WebsiteProfitCent, ProfitAmountCent: row.ProfitAmountCent, SellerRatio: row.SellerRatio, BuyerRatio: row.BuyerRatio, AccountSnapshot: row.AccountSnapshot, SettleAmountCent: row.SettleAmountCent, Status: row.Status, Remark: row.Remark, CompleteRemark: row.CompleteRemark, CreatedAt: row.CreatedAt, CompletedAt: row.CompletedAt, CancelledAt: row.CancelledAt, } } type availableListingRow struct { ID uint64 ListingNo string AccountID uint64 AccountTitle string ServerRegion string LoginPlatform string OwnerID uint64 OwnerPhone string ListingPriceCent int64 HafCoinAmount int64 AssetSummary datatypes.JSON IsExternalUpload bool SourceChannel string HandoffMode string SettlementMode string } func (row availableListingRow) toDTO() AvailableListingDTO { snapshot := buildPickupPriceSnapshot( model.RentalListing{PriceCent: row.ListingPriceCent}, model.GameAccount{HafCoinAmount: row.HafCoinAmount, AssetSummary: row.AssetSummary}, ) accountSource := AccountSourceInternal if row.IsExternalUpload { accountSource = AccountSourceExternalPlatformManaged } return AvailableListingDTO{ ID: row.ID, ListingNo: row.ListingNo, AccountID: row.AccountID, AccountTitle: row.AccountTitle, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, OwnerID: row.OwnerID, OwnerPhone: row.OwnerPhone, ListingPriceCent: snapshot.ListingPriceCent, OwnerPriceCent: snapshot.OwnerPriceCent, OwnerPurePriceCent: snapshot.OwnerPurePriceCent, OwnerExtraItemPriceCent: snapshot.OwnerExtraItemPriceCent, OwnerTotalPriceCent: snapshot.OwnerTotalPriceCent, WebsiteProfitCent: snapshot.WebsiteProfitCent, SellerRatio: snapshot.SellerRatio, BuyerRatio: snapshot.BuyerRatio, AccountSource: accountSource, IsExternalUpload: row.IsExternalUpload, SourceChannel: row.SourceChannel, HandoffMode: row.HandoffMode, SettlementMode: row.SettlementMode, } } func resolvePickupAccountSource(tx *gorm.DB, listingID uint64) (string, string, error) { var upload model.ListingUpload err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error if errors.Is(err, gorm.ErrRecordNotFound) { return AccountSourceInternal, "", nil } if err != nil { return "", "", err } return AccountSourceExternalPlatformManaged, strings.TrimSpace(upload.SourceChannel), nil } func makePickupAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) { payload := map[string]any{ "listing_id": listing.ID, "listing_no": listing.ListingNo, "account_id": account.ID, "title": account.Title, "game_name": account.GameName, "server_region": account.ServerRegion, "login_platform": account.LoginPlatform, "rank_level": account.RankLevel, "haf_coin_amount": account.HafCoinAmount, "asset_summary": account.AssetSummary, "season_tags": account.SeasonTags, "screenshot_urls": account.ScreenshotURLS, "snapshot_version": 1, } raw, err := json.Marshal(payload) return datatypes.JSON(raw), err } func (r *Repository) fallbackAccountSnapshot(ctx context.Context, listingID uint64, accountID uint64) (datatypes.JSON, error) { var listing model.RentalListing if err := r.db.WithContext(ctx).First(&listing, listingID).Error; err != nil { return nil, err } var account model.GameAccount if err := r.db.WithContext(ctx).First(&account, accountID).Error; err != nil { return nil, err } return makePickupAccountSnapshot(account, listing) } func isEmptyPickupAccountSnapshot(raw datatypes.JSON) bool { trimmed := strings.TrimSpace(string(raw)) return trimmed == "" || trimmed == "null" || trimmed == "{}" } type pickupPriceSnapshot struct { ListingPriceCent int64 OwnerPriceCent int64 OwnerPurePriceCent int64 OwnerExtraItemPriceCent int64 OwnerTotalPriceCent int64 WebsiteProfitCent int64 SellerRatio float64 BuyerRatio float64 } func buildPickupPriceSnapshot(listing model.RentalListing, account model.GameAccount) pickupPriceSnapshot { summary := decodePickupAssetSummary(account.AssetSummary) breakdown := pickupPriceBreakdown(summary) listingPriceCent := maxPickupCent(listing.PriceCent, 0) ownerPurePriceCent := yuanToPickupCent(readPickupNumber(breakdown["seller_coin_base_price"])) ownerExtraItemPriceCent := yuanToPickupCent(readPickupNumber(breakdown["consumable_price"])) ownerTotalPriceCent := yuanToPickupCent(readPickupNumber(breakdown["seller_total_price"])) if ownerTotalPriceCent <= 0 && (ownerPurePriceCent > 0 || ownerExtraItemPriceCent > 0) { ownerTotalPriceCent = ownerPurePriceCent + ownerExtraItemPriceCent } if ownerTotalPriceCent <= 0 || (listingPriceCent > 0 && ownerTotalPriceCent > listingPriceCent) { ownerTotalPriceCent = listingPriceCent } ownerPurePriceCent, ownerExtraItemPriceCent = normalizePickupOwnerPriceSplit( ownerTotalPriceCent, ownerPurePriceCent, ownerExtraItemPriceCent, ) websiteProfitCent := yuanToPickupCent(readPickupNumber(breakdown["platform_markup_amount"])) if websiteProfitCent <= 0 { websiteProfitCent = listingPriceCent - ownerTotalPriceCent } if websiteProfitCent < 0 { websiteProfitCent = 0 } sellerRatio := readPickupNumber(breakdown["seller_ratio"]) if sellerRatio <= 0 { sellerRatio = ratioFromCoin(account.HafCoinAmount, readPickupNumber(breakdown["seller_coin_base_price"])) } buyerRatio := readPickupNumber(breakdown["buyer_ratio"]) if buyerRatio <= 0 { buyerRatio = readPickupNumber(summary["publish_ratio"]) } if buyerRatio <= 0 { buyerRatio = ratioFromCoin(account.HafCoinAmount, readPickupNumber(breakdown["buyer_coin_base_price"])) } return pickupPriceSnapshot{ ListingPriceCent: listingPriceCent, OwnerPriceCent: ownerTotalPriceCent, OwnerPurePriceCent: ownerPurePriceCent, OwnerExtraItemPriceCent: ownerExtraItemPriceCent, OwnerTotalPriceCent: ownerTotalPriceCent, WebsiteProfitCent: websiteProfitCent, SellerRatio: roundPickupRatio(sellerRatio), BuyerRatio: roundPickupRatio(buyerRatio), } } func normalizePickupOwnerPriceSplit(totalCent, pureCent, extraCent int64) (int64, int64) { if totalCent <= 0 { return 0, 0 } pureCent = maxPickupCent(pureCent, 0) extraCent = maxPickupCent(extraCent, 0) switch { case pureCent == 0 && extraCent == 0: pureCent = totalCent case pureCent == 0: pureCent = maxPickupCent(totalCent-extraCent, 0) case extraCent == 0: extraCent = maxPickupCent(totalCent-pureCent, 0) case pureCent+extraCent != totalCent: if pureCent > totalCent { pureCent = totalCent extraCent = 0 } else { extraCent = totalCent - pureCent } } return pureCent, extraCent } func decodePickupAssetSummary(raw datatypes.JSON) map[string]any { if len(raw) == 0 { return map[string]any{} } var summary map[string]any if err := json.Unmarshal(raw, &summary); err != nil { return map[string]any{} } return summary } func pickupPriceBreakdown(summary map[string]any) map[string]any { if summary == nil { return map[string]any{} } if breakdown, ok := summary["price_breakdown"].(map[string]any); ok { return breakdown } if raw, ok := summary["price_breakdown"].(string); ok && strings.TrimSpace(raw) != "" { var breakdown map[string]any if err := json.Unmarshal([]byte(raw), &breakdown); err == nil { return breakdown } } return map[string]any{} } func readPickupNumber(value any) float64 { switch typed := value.(type) { case float64: return typed case float32: return float64(typed) case int: return float64(typed) case int64: return float64(typed) case json.Number: number, err := typed.Float64() if err != nil { return 0 } return number case string: number, err := strconv.ParseFloat(strings.TrimSpace(typed), 64) if err != nil { return 0 } return number default: return 0 } } func yuanToPickupCent(value float64) int64 { if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return int64(math.Round(value * 100)) } func ratioFromCoin(coinAmount int64, basePrice float64) float64 { if coinAmount <= 0 || basePrice <= 0 { return 0 } return float64(coinAmount) / 10000 / basePrice } func roundPickupRatio(value float64) float64 { if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return math.Round(value*100) / 100 } func maxPickupCent(a, b int64) int64 { if a > b { return a } return b } func normalizePaging(page, pageSize int) (int, int) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } return page, pageSize } // newPickupNo 生成提号编号:PK + YYYYMMDDHHMMSS + 3位随机。 func newPickupNo() (string, error) { now := timeutil.ShanghaiNow() timeStr := now.Format("20060102150405") buf := make([]byte, 2) if _, err := crand.Read(buf); err != nil { return "", err } randomNum := (int(buf[0])<<8 | int(buf[1])) % 1000 return "PK" + timeStr + strconv.Itoa(1000 + randomNum)[1:], nil }