Files
ttstd-infinite-canvas/backend/internal/app/payment_products.go
T

161 lines
6.1 KiB
Go

// 充值商品:前台可购买的积分档位与管理端增改。
package app
import (
"fmt"
"strings"
"time"
"infinite-canvas/backend/internal/model"
)
func (s *Service) TopupProducts(actor *model.User) ([]model.TopupProduct, error) {
if actor == nil {
return nil, Unauthorized("请先登录")
}
if err := s.RequireFeature(FeatureCredits); err != nil {
return nil, err
}
products, err := s.repo.TopupProducts(false)
if err != nil {
return nil, err
}
return decorateTopupProducts(products, time.Now()), nil
}
func (s *Service) AdminTopupProducts(actor *model.User) ([]model.TopupProduct, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
products, err := s.repo.TopupProducts(true)
if err != nil {
return nil, err
}
return decorateTopupProducts(products, time.Now()), nil
}
func decorateTopupProducts(products []model.TopupProduct, now time.Time) []model.TopupProduct {
for index := range products {
product := &products[index]
if product.SaleStrategy == "" {
product.SaleStrategy = model.TopupSaleStrategyUnlimited
}
product.CanPurchase = product.Enabled
product.SaleStatus = "on_sale"
if !product.Enabled {
product.CanPurchase = false
product.SaleStatus = "disabled"
} else if product.SaleStrategy == model.TopupSaleStrategyInventory && product.StockRemaining <= 0 {
product.CanPurchase = false
product.SaleStatus = "sold_out"
} else if product.SaleStrategy == model.TopupSaleStrategyTimed {
if product.SaleStartAt != nil && now.Before(*product.SaleStartAt) {
product.CanPurchase = false
product.SaleStatus = "upcoming"
} else if product.SaleEndAt != nil && !now.Before(*product.SaleEndAt) {
product.CanPurchase = false
product.SaleStatus = "ended"
}
}
}
return products
}
func (s *Service) CreateTopupProduct(actor *model.User, request TopupProductRequest) (*model.TopupProduct, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
product, err := topupProductFromRequest(newID(), actor.ID, request, 0, 0)
if err != nil {
return nil, err
}
if err := s.repo.CreateTopupProduct(product); err != nil {
return nil, err
}
if err := s.appendAdminAudit(actor, "topup_product.create", "topup_product", product.ID, "创建积分充值商品", map[string]any{"amountFen": product.AmountFen, "creditsMicrocredits": product.CreditsMicrocredits, "saleStrategy": product.SaleStrategy}); err != nil {
return nil, err
}
return product, nil
}
func (s *Service) UpdateTopupProduct(actor *model.User, id string, request TopupProductRequest) (*model.TopupProduct, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
id = strings.TrimSpace(id)
var product *model.TopupProduct
// 已售数量必须在锁住商品行之后计算,否则会覆盖并发下单扣减或关单退回的库存。
err := s.repo.UpdateTopupProduct(id, func(existing *model.TopupProduct) (*model.TopupProduct, error) {
consumed := existing.StockTotal - existing.StockRemaining
if existing.SaleStrategy != model.TopupSaleStrategyInventory {
consumed = 0
}
built, buildErr := topupProductFromRequest(id, actor.ID, request, existing.StockTotal, consumed)
if buildErr != nil {
return nil, buildErr
}
product = built
return built, nil
})
if err != nil {
return nil, err
}
if err := s.appendAdminAudit(actor, "topup_product.update", "topup_product", product.ID, "更新积分充值商品", map[string]any{"enabled": product.Enabled, "saleStrategy": product.SaleStrategy}); err != nil {
return nil, err
}
return s.repo.TopupProduct(product.ID)
}
func topupProductFromRequest(id, actorID string, request TopupProductRequest, stockArgs ...int64) (*model.TopupProduct, error) {
var consumed int64
if len(stockArgs) > 1 {
consumed = stockArgs[1]
}
name := strings.TrimSpace(request.Name)
if name == "" || len([]rune(name)) > 120 {
return nil, BadAuthRequest("充值商品名称不能为空且不能超过 120 个字符")
}
if request.AmountFen <= 0 || request.AmountFen > 100_000_000 {
return nil, BadAuthRequest("充值金额必须为 1 分至 100 万元")
}
if request.CreditsMicrocredits <= 0 || request.CreditsMicrocredits > maxTopupCreditsMicrocredits {
return nil, BadAuthRequest("充值积分必须为 0.000001 至 10 亿积分")
}
strategy := request.SaleStrategy
if strategy == "" {
strategy = model.TopupSaleStrategyUnlimited
}
if strategy != model.TopupSaleStrategyUnlimited && strategy != model.TopupSaleStrategyPeriodic && strategy != model.TopupSaleStrategyInventory && strategy != model.TopupSaleStrategyTimed {
return nil, BadAuthRequest("上架策略无效")
}
if strategy == model.TopupSaleStrategyPeriodic && (request.PeriodDays <= 0 || request.PeriodDays > 3650 || request.PeriodPurchaseLimit <= 0 || request.PeriodPurchaseLimit > 100000) {
return nil, BadAuthRequest("周期性商品需设置有效的周期天数和购买次数")
}
if strategy == model.TopupSaleStrategyInventory && request.StockTotal <= 0 {
return nil, BadAuthRequest("库存性商品库存必须大于 0")
}
if strategy == model.TopupSaleStrategyTimed && (request.SaleStartAt == nil || request.SaleEndAt == nil || !request.SaleEndAt.After(*request.SaleStartAt)) {
return nil, BadAuthRequest("限时商品必须设置有效的发售和结束时间")
}
stockRemaining := int64(0)
if strategy == model.TopupSaleStrategyInventory {
if consumed < 0 {
consumed = 0
}
if request.StockTotal < consumed {
return nil, BadAuthRequest(fmt.Sprintf("库存不能低于已售数量(至少 %d)", consumed))
}
stockRemaining = request.StockTotal - consumed
}
return &model.TopupProduct{
ID: id, Name: name, Description: truncateRunes(strings.TrimSpace(request.Description), 500),
AmountFen: request.AmountFen, CreditsMicrocredits: request.CreditsMicrocredits,
Enabled: request.Enabled, SortOrder: request.SortOrder, SaleStrategy: strategy,
PeriodDays: request.PeriodDays, PeriodPurchaseLimit: request.PeriodPurchaseLimit,
StockTotal: request.StockTotal, StockRemaining: stockRemaining,
SaleStartAt: request.SaleStartAt, SaleEndAt: request.SaleEndAt,
CreatedBy: actorID, UpdatedBy: actorID,
}, nil
}