feat(payment): 充值赠送阶梯(按金额档位额外赠送到账额度)

- 后台支付设置新增「充值赠送阶梯」(满 X 送 Y%,RECHARGE_BONUS_TIERS)与 Markdown 活动文案
  (RECHARGE_BONUS_NOTICE):阈值按用户输入的支付金额命中(取不超过金额的最大档),
  赠送按到账基数(输入 × 充值倍率)计算;严格校验写入、宽松解析读取,订阅订单不参与
- 下单时按当时配置计算赠送并落库:payment_orders 新增 bonus_amount 列(迁移 241),
  amount 仍为到账总额,到账/兑换码/退款逻辑不变;推广返利基数改为 amount - bonus_amount
- checkout-info 下发 recharge_bonus_tiers / recharge_bonus_notice;创建订单与订单查询响应带 bonus_amount
- 充值页:快捷金额按钮右上角「+N%」角标 + 第二行到账金额,金额卡顶部渲染活动文案,
  订单摘要新增赠送额度行并在有赠送时始终显示到账余额;订单列表/详情/支付结果/成功面板显示含赠送
- 后台新增 RechargeBonusTierEditor(行内校验重复/非法阈值,自动排序并显示区间预览)
- 补后端单测、契约测试与前端 vitest,zh/en 文案
This commit is contained in:
feeeei
2026-10-02 00:25:41 +08:00
parent d6adebd22d
commit 1b1039f4fd
44 changed files with 1726 additions and 63 deletions
@@ -149,6 +149,8 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
BalanceRechargeMultiplier: cfg.BalanceRechargeMultiplier,
SubscriptionUSDToCNYRate: cfg.SubscriptionUSDToCNYRate,
RechargeFeeRate: cfg.RechargeFeeRate,
RechargeBonusTiers: cfg.RechargeBonusTiers,
RechargeBonusNotice: cfg.RechargeBonusNotice,
HelpText: cfg.HelpText,
HelpImageURL: cfg.HelpImageURL,
StripePublishableKey: cfg.StripePublishableKey,
@@ -166,6 +168,8 @@ type checkoutInfoResponse struct {
BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
SubscriptionUSDToCNYRate float64 `json:"subscription_usd_to_cny_rate"`
RechargeFeeRate float64 `json:"recharge_fee_rate"`
RechargeBonusTiers []service.RechargeBonusTier `json:"recharge_bonus_tiers"`
RechargeBonusNotice string `json:"recharge_bonus_notice"`
HelpText string `json:"help_text"`
HelpImageURL string `json:"help_image_url"`
StripePublishableKey string `json:"stripe_publishable_key"`
@@ -482,6 +486,7 @@ type PublicOrderResult struct {
Amount float64 `json:"amount"`
PayAmount float64 `json:"pay_amount"`
FeeRate float64 `json:"fee_rate"`
BonusAmount float64 `json:"bonus_amount"`
Currency string `json:"currency"`
PaymentType string `json:"payment_type"`
OrderType string `json:"order_type"`
@@ -517,6 +522,7 @@ func buildPublicOrderResult(order *dbent.PaymentOrder) PublicOrderResult {
Amount: order.Amount,
PayAmount: order.PayAmount,
FeeRate: order.FeeRate,
BonusAmount: order.BonusAmount,
Currency: service.PaymentOrderCurrency(order),
PaymentType: order.PaymentType,
OrderType: order.OrderType,
@@ -625,6 +631,7 @@ type PaymentOrderResult struct {
Amount float64 `json:"amount"`
PayAmount float64 `json:"pay_amount"`
FeeRate float64 `json:"fee_rate"`
BonusAmount float64 `json:"bonus_amount"`
Currency string `json:"currency"`
PaymentType string `json:"payment_type"`
OutTradeNo string `json:"out_trade_no"`
@@ -663,6 +670,7 @@ func sanitizePaymentOrderForResponse(order *dbent.PaymentOrder) *PaymentOrderRes
Amount: order.Amount,
PayAmount: order.PayAmount,
FeeRate: order.FeeRate,
BonusAmount: order.BonusAmount,
Currency: service.PaymentOrderCurrency(order),
PaymentType: order.PaymentType,
OutTradeNo: order.OutTradeNo,