优化后台商品审核价格调整
This commit is contained in:
@@ -55,6 +55,12 @@ type ReviewRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type AdminPriceAdjustRequest struct {
|
||||
BuyerRatio float64 `json:"buyer_ratio"`
|
||||
BuyerTotalPrice float64 `json:"buyer_total_price"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type AdminListQuery struct {
|
||||
OwnerID uint64
|
||||
Status string
|
||||
|
||||
@@ -203,6 +203,29 @@ func (h *Handler) Approve(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) AdjustReviewPrice(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req AdminPriceAdjustRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "调价参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.AdjustReviewPrice(adminID, id, req, auditMeta(c))
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Reject(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -472,6 +473,97 @@ func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) {
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req AdminPriceAdjustRequest, meta AuditMeta) (*ListingDTO, error) {
|
||||
var dto *ListingDTO
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
listing, account, err := r.findForReviewUpdate(tx, listingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status == "rented" || listing.InTransaction {
|
||||
return ErrListingLocked
|
||||
}
|
||||
summary := decodeAssetSummary(account.AssetSummary)
|
||||
if summary == nil {
|
||||
summary = map[string]any{}
|
||||
}
|
||||
breakdown := ensurePriceBreakdown(summary)
|
||||
coinWan := float64(account.HafCoinAmount) / 10000
|
||||
consumablePrice := readSummaryNumber(breakdown["consumable_price"])
|
||||
if consumablePrice <= 0 {
|
||||
consumablePrice = consumableValue(summary)
|
||||
}
|
||||
sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"])
|
||||
if sellerTotalPrice <= 0 {
|
||||
sellerTotalPrice = math.Max(0, listing.Price-consumablePrice)
|
||||
}
|
||||
sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"])
|
||||
if sellerCoinBasePrice <= 0 {
|
||||
sellerCoinBasePrice = math.Max(0, sellerTotalPrice-consumablePrice)
|
||||
}
|
||||
sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
|
||||
if sellerRatio <= 0 && sellerCoinBasePrice > 0 {
|
||||
sellerRatio = roundRatio(coinWan / sellerCoinBasePrice)
|
||||
}
|
||||
buyerCoinBasePrice, buyerTotalPrice, buyerRatio := calculateAdminAdjustedPrice(req, coinWan, consumablePrice)
|
||||
if buyerCoinBasePrice <= 0 || buyerTotalPrice <= 0 || buyerRatio <= 0 {
|
||||
return ErrInvalidPrice
|
||||
}
|
||||
|
||||
beforePrice := listing.Price
|
||||
beforeRatio := readSummaryNumber(breakdown["buyer_ratio"])
|
||||
if beforeRatio <= 0 && listing.Price > consumablePrice {
|
||||
beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice))
|
||||
}
|
||||
|
||||
listing.Price = buyerTotalPrice
|
||||
summary["publish_ratio"] = buyerRatio
|
||||
breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice)
|
||||
breakdown["seller_total_price"] = roundMoney(sellerTotalPrice)
|
||||
breakdown["seller_ratio"] = sellerRatio
|
||||
breakdown["buyer_coin_base_price"] = buyerCoinBasePrice
|
||||
breakdown["buyer_total_price"] = buyerTotalPrice
|
||||
breakdown["buyer_ratio"] = buyerRatio
|
||||
breakdown["platform_markup_amount"] = roundMoney(buyerTotalPrice - sellerTotalPrice)
|
||||
breakdown["platform_rule_type"] = "admin_adjusted"
|
||||
breakdown["admin_adjust_reason"] = strings.TrimSpace(req.Reason)
|
||||
breakdown["admin_adjusted_at"] = time.Now().Format(time.RFC3339)
|
||||
breakdown["admin_adjusted_by"] = adminID
|
||||
summary["price_breakdown"] = breakdown
|
||||
|
||||
assetSummary, err := marshalAssetSummary(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.AssetSummary = assetSummary
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "listing.adjust_review_price", "listing", listing.ID, meta, map[string]any{
|
||||
"listing_id": listing.ID,
|
||||
"account_id": account.ID,
|
||||
"owner_id": listing.OwnerID,
|
||||
"before_price": beforePrice,
|
||||
"after_price": listing.Price,
|
||||
"before_buyer_ratio": beforeRatio,
|
||||
"after_buyer_ratio": buyerRatio,
|
||||
"platform_markup": breakdown["platform_markup_amount"],
|
||||
"adjust_reason": req.Reason,
|
||||
"buyer_coin_base": buyerCoinBasePrice,
|
||||
"consumable_price": consumablePrice,
|
||||
"seller_total_price": sellerTotalPrice,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
dto = toDTO(*account, *listing)
|
||||
return nil
|
||||
})
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func (r *Repository) Reject(listingID uint64, req ReviewRequest) (*ListingDTO, error) {
|
||||
var dto *ListingDTO
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -1278,6 +1370,47 @@ func readSummaryNumber(value any) float64 {
|
||||
}
|
||||
}
|
||||
|
||||
func ensurePriceBreakdown(summary map[string]any) map[string]any {
|
||||
if summary == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
breakdown, ok := summary["price_breakdown"].(map[string]any)
|
||||
if ok {
|
||||
return breakdown
|
||||
}
|
||||
breakdown = map[string]any{}
|
||||
if raw, ok := summary["price_breakdown"].(map[string]interface{}); ok {
|
||||
for key, value := range raw {
|
||||
breakdown[key] = value
|
||||
}
|
||||
}
|
||||
return breakdown
|
||||
}
|
||||
|
||||
func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) {
|
||||
if req.BuyerTotalPrice > 0 {
|
||||
buyerTotalPrice := roundMoney(req.BuyerTotalPrice)
|
||||
buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice)
|
||||
if buyerCoinBasePrice <= 0 || coinWan <= 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice)
|
||||
}
|
||||
if req.BuyerRatio <= 0 || coinWan <= 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
buyerCoinBasePrice := roundMoney(coinWan / req.BuyerRatio)
|
||||
buyerTotalPrice := roundMoney(buyerCoinBasePrice + consumablePrice)
|
||||
return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice)
|
||||
}
|
||||
|
||||
func roundRatio(value float64) float64 {
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return math.Round(value*10) / 10
|
||||
}
|
||||
|
||||
func cleanScreenshotURLs(urls []string) []string {
|
||||
cleaned := make([]string, 0, len(urls))
|
||||
seen := make(map[string]struct{}, len(urls))
|
||||
|
||||
@@ -242,6 +242,16 @@ func (s *Service) Approve(id uint64) (*ListingDTO, error) {
|
||||
return s.repo.Approve(id)
|
||||
}
|
||||
|
||||
func (s *Service) AdjustReviewPrice(adminID uint64, id uint64, req AdminPriceAdjustRequest, meta AuditMeta) (*ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if req.BuyerRatio <= 0 && req.BuyerTotalPrice <= 0 {
|
||||
return nil, ErrInvalidPrice
|
||||
}
|
||||
return s.repo.AdjustReviewPrice(adminID, id, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) Reject(id uint64, req ReviewRequest) (*ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
@@ -14,8 +14,22 @@ func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
if value != 2 {
|
||||
t.Fatalf("expected 2, got %.2f", value)
|
||||
if value != 1.5 {
|
||||
t.Fatalf("expected 1.5, got %.2f", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) {
|
||||
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerRatio: 25}, 1000, 20)
|
||||
if base != 40 || total != 60 || ratio != 25 {
|
||||
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
|
||||
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPrice: 70}, 1000, 20)
|
||||
if base != 50 || total != 70 || ratio != 20 {
|
||||
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user