Files
2026-07-26 19:17:09 +08:00

276 lines
9.3 KiB
Go

package listing
import (
"context"
"math"
"strings"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
)
const maxExternalSaleRatioIncrement = 10.0
type externalAdjustmentResult struct {
Resources []any
ReferenceRatio float64
ReferenceCoinPrice float64
SaleRatio float64
SellerCoinPrice float64
SellerTotalPrice float64
ConsumablePrice float64
ActualDiscountAmount float64
PlatformPricing externalPlatformPricing
}
func (s *Service) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
meta AuditMeta,
) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if listingID == 0 || req.HafCoinAmount <= 0 || req.DepositAmountCent < 0 ||
req.AWMBullets < 0 || req.Level6Helmets < 0 || req.Level6Armor < 0 ||
strings.TrimSpace(req.Reason) == "" {
return nil, ErrExternalAdjustment
}
switch req.AdjustMode {
case ExternalAdjustModeDiscountAmount:
if req.DiscountAmountCent < 0 {
return nil, ErrExternalAdjustment
}
case ExternalAdjustModeSaleRatio:
if req.SaleRatio <= 0 || math.IsNaN(req.SaleRatio) || math.IsInf(req.SaleRatio, 0) {
return nil, ErrExternalAdjustment
}
default:
return nil, ErrExternalAdjustment
}
config, err := s.externalSalePriceConfig(ctx)
if err != nil {
return nil, err
}
return s.repo.AdjustExternalListing(ctx, adminID, listingID, req, config, meta)
}
func (r *Repository) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
config salePriceConfig,
meta AuditMeta,
) (*ListingDTO, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
listing, account, err := r.findForReviewUpdate(tx, listingID)
if err != nil {
return err
}
if listing.InTransaction || listing.Status == "rented" || listing.Status == "completed" {
return ErrListingLocked
}
switch listing.Status {
case "draft", "published", "offline", "abnormal":
default:
return ErrListingLocked
}
var uploadCount int64
if err := tx.Model(&model.ListingUpload{}).Where("listing_id = ?", listing.ID).Count(&uploadCount).Error; err != nil {
return err
}
if uploadCount == 0 {
return ErrExternalListingRequired
}
summary := decodeAssetSummary(account.AssetSummary)
if summary == nil {
summary = map[string]any{}
}
breakdown := ensurePriceBreakdown(summary)
referenceRatio := readSummaryNumber(breakdown["seller_reference_ratio"])
if referenceRatio <= 0 {
referenceRatio = readSummaryNumber(breakdown["seller_ratio"])
}
if referenceRatio <= 0 {
return ErrExternalAdjustment
}
before := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
adjustment, err := calculateExternalListingAdjustment(req, summary, referenceRatio, config)
if err != nil {
return err
}
if adjustment.ConsumablePrice > 0 && req.DepositAmountCent <= yuanToCent(adjustment.ConsumablePrice) {
return ErrDepositTooLow
}
now := time.Now()
summary["resources"] = adjustment.Resources
summary["publish_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["seller_reference_ratio"] = adjustment.ReferenceRatio
breakdown["seller_ratio"] = adjustment.SaleRatio
breakdown["accelerated_sale_ratio"] = adjustment.SaleRatio
breakdown["seller_coin_base_price"] = adjustment.SellerCoinPrice
breakdown["seller_total_price"] = adjustment.SellerTotalPrice
breakdown["consumable_price"] = adjustment.ConsumablePrice
breakdown["buyer_coin_base_price"] = adjustment.PlatformPricing.BuyerCoinBasePrice
breakdown["buyer_total_price"] = adjustment.PlatformPricing.BuyerTotalPrice
breakdown["buyer_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["platform_markup_amount"] = adjustment.PlatformPricing.PlatformMarkupPrice
breakdown["platform_rule_type"] = adjustment.PlatformPricing.RuleType
breakdown["external_admin_adjust_mode"] = req.AdjustMode
breakdown["external_admin_discount_amount"] = adjustment.ActualDiscountAmount
breakdown["external_admin_adjust_reason"] = strings.TrimSpace(req.Reason)
breakdown["external_admin_adjusted_at"] = now.Format(time.RFC3339)
breakdown["external_admin_adjusted_by"] = adminID
summary["price_breakdown"] = breakdown
assetSummary, err := marshalAssetSummary(summary)
if err != nil {
return err
}
account.HafCoinAmount = req.HafCoinAmount
account.AssetSummary = assetSummary
account.Title = buildExternalListingTitle(
account.RankLevel,
readAssetString(summary, "season_insurance"),
float64(req.HafCoinAmount)/1000000,
account.LoginPlatform,
)
listing.PriceCent = yuanToCent(adjustment.PlatformPricing.BuyerTotalPrice)
listing.DepositAmountCent = req.DepositAmountCent
if err := tx.Save(account).Error; err != nil {
return err
}
if err := tx.Save(listing).Error; err != nil {
return err
}
after := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
return appendAuditLog(tx, adminID, "listing.adjust_external", "listing", listing.ID, meta, map[string]any{
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"adjust_mode": req.AdjustMode,
"requested_discount_amount_cent": req.DiscountAmountCent,
"requested_sale_ratio": req.SaleRatio,
"reason": strings.TrimSpace(req.Reason),
"before": before,
"after": after,
})
})
if err != nil {
return nil, err
}
return r.FindAdmin(ctx, listingID)
}
func calculateExternalListingAdjustment(
req AdminExternalListingAdjustRequest,
summary map[string]any,
referenceRatio float64,
config salePriceConfig,
) (externalAdjustmentResult, error) {
coinM := float64(req.HafCoinAmount) / 1000000
coinWan := float64(req.HafCoinAmount) / 10000
referenceCoinPrice := roundMoney(coinWan / referenceRatio)
if coinM <= 0 || coinWan <= 0 || referenceCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
saleRatio := roundRatio(req.SaleRatio)
if req.AdjustMode == ExternalAdjustModeDiscountAmount {
desiredCoinPrice := referenceCoinPrice - centToYuan(req.DiscountAmountCent)
if desiredCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
saleRatio = roundRatio(coinWan / desiredCoinPrice)
}
if saleRatio < referenceRatio || saleRatio > referenceRatio+maxExternalSaleRatioIncrement {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
sellerCoinPrice := roundMoney(coinWan / saleRatio)
if sellerCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
coreResources := externalResources(ExternalUploadInventory{
AWMBullets: req.AWMBullets,
Level6Helmets: req.Level6Helmets,
Level6Armor: req.Level6Armor,
})
resources := mergeExternalCoreResources(summary, coreResources)
consumablePrice := consumableValue(map[string]any{"resources": resources})
sellerTotalPrice := roundMoney(sellerCoinPrice + consumablePrice)
platformPricing := calculateExternalPlatformPricing(
coinM,
saleRatio,
sellerCoinPrice,
consumablePrice,
config,
)
if platformPricing.BuyerCoinBasePrice <= 0 || platformPricing.BuyerTotalPrice <= 0 || platformPricing.BuyerRatio <= 0 {
return externalAdjustmentResult{}, ErrInvalidPrice
}
return externalAdjustmentResult{
Resources: resources,
ReferenceRatio: referenceRatio,
ReferenceCoinPrice: referenceCoinPrice,
SaleRatio: saleRatio,
SellerCoinPrice: sellerCoinPrice,
SellerTotalPrice: sellerTotalPrice,
ConsumablePrice: consumablePrice,
ActualDiscountAmount: roundMoney(referenceCoinPrice - sellerCoinPrice),
PlatformPricing: platformPricing,
}, nil
}
func mergeExternalCoreResources(summary map[string]any, core []any) []any {
coreKeys := map[string]struct{}{"awmAmmo": {}, "helmet6": {}, "armor6": {}}
result := append([]any{}, core...)
rawResources, _ := summary["resources"].([]any)
for _, raw := range rawResources {
row, ok := raw.(map[string]any)
if !ok {
continue
}
key, _ := row["key"].(string)
if _, isCore := coreKeys[key]; isCore {
continue
}
result = append(result, row)
}
return result
}
func externalAdjustmentAuditSnapshot(
account *model.GameAccount,
listing *model.RentalListing,
summary map[string]any,
breakdown map[string]any,
) map[string]any {
return map[string]any{
"haf_coin_amount": account.HafCoinAmount,
"awm_bullets": resourceQuantity(summary, "awmAmmo"),
"level6_helmets": resourceQuantity(summary, "helmet6"),
"level6_armor": resourceQuantity(summary, "armor6"),
"deposit_amount_cent": listing.DepositAmountCent,
"reference_ratio": readSummaryNumber(breakdown["seller_reference_ratio"]),
"sale_ratio": readSummaryNumber(breakdown["seller_ratio"]),
"seller_coin_base_price": readSummaryNumber(breakdown["seller_coin_base_price"]),
"seller_total_price": readSummaryNumber(breakdown["seller_total_price"]),
"buyer_ratio": readSummaryNumber(breakdown["buyer_ratio"]),
"buyer_total_price": readSummaryNumber(breakdown["buyer_total_price"]),
"listing_price_cent": listing.PriceCent,
}
}