276 lines
9.3 KiB
Go
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,
|
|
}
|
|
}
|