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
+10
View File
@@ -28,6 +28,8 @@ const (
FieldPayAmount = "pay_amount"
// FieldFeeRate holds the string denoting the fee_rate field in the database.
FieldFeeRate = "fee_rate"
// FieldBonusAmount holds the string denoting the bonus_amount field in the database.
FieldBonusAmount = "bonus_amount"
// FieldRechargeCode holds the string denoting the recharge_code field in the database.
FieldRechargeCode = "recharge_code"
// FieldOutTradeNo holds the string denoting the out_trade_no field in the database.
@@ -115,6 +117,7 @@ var Columns = []string{
FieldAmount,
FieldPayAmount,
FieldFeeRate,
FieldBonusAmount,
FieldRechargeCode,
FieldOutTradeNo,
FieldPaymentType,
@@ -166,6 +169,8 @@ var (
UserNameValidator func(string) error
// DefaultFeeRate holds the default value on creation for the "fee_rate" field.
DefaultFeeRate float64
// DefaultBonusAmount holds the default value on creation for the "bonus_amount" field.
DefaultBonusAmount float64
// RechargeCodeValidator is a validator for the "recharge_code" field. It is called by the builders before save.
RechargeCodeValidator func(string) error
// DefaultOutTradeNo holds the default value on creation for the "out_trade_no" field.
@@ -249,6 +254,11 @@ func ByFeeRate(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFeeRate, opts...).ToFunc()
}
// ByBonusAmount orders the results by the bonus_amount field.
func ByBonusAmount(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBonusAmount, opts...).ToFunc()
}
// ByRechargeCode orders the results by the recharge_code field.
func ByRechargeCode(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRechargeCode, opts...).ToFunc()
+45
View File
@@ -90,6 +90,11 @@ func FeeRate(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldEQ(FieldFeeRate, v))
}
// BonusAmount applies equality check predicate on the "bonus_amount" field. It's identical to BonusAmountEQ.
func BonusAmount(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldEQ(FieldBonusAmount, v))
}
// RechargeCode applies equality check predicate on the "recharge_code" field. It's identical to RechargeCodeEQ.
func RechargeCode(v string) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldEQ(FieldRechargeCode, v))
@@ -590,6 +595,46 @@ func FeeRateLTE(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldLTE(FieldFeeRate, v))
}
// BonusAmountEQ applies the EQ predicate on the "bonus_amount" field.
func BonusAmountEQ(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldEQ(FieldBonusAmount, v))
}
// BonusAmountNEQ applies the NEQ predicate on the "bonus_amount" field.
func BonusAmountNEQ(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldNEQ(FieldBonusAmount, v))
}
// BonusAmountIn applies the In predicate on the "bonus_amount" field.
func BonusAmountIn(vs ...float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldIn(FieldBonusAmount, vs...))
}
// BonusAmountNotIn applies the NotIn predicate on the "bonus_amount" field.
func BonusAmountNotIn(vs ...float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldNotIn(FieldBonusAmount, vs...))
}
// BonusAmountGT applies the GT predicate on the "bonus_amount" field.
func BonusAmountGT(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldGT(FieldBonusAmount, v))
}
// BonusAmountGTE applies the GTE predicate on the "bonus_amount" field.
func BonusAmountGTE(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldGTE(FieldBonusAmount, v))
}
// BonusAmountLT applies the LT predicate on the "bonus_amount" field.
func BonusAmountLT(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldLT(FieldBonusAmount, v))
}
// BonusAmountLTE applies the LTE predicate on the "bonus_amount" field.
func BonusAmountLTE(v float64) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldLTE(FieldBonusAmount, v))
}
// RechargeCodeEQ applies the EQ predicate on the "recharge_code" field.
func RechargeCodeEQ(v string) predicate.PaymentOrder {
return predicate.PaymentOrder(sql.FieldEQ(FieldRechargeCode, v))