bindbox-game/internal/api/admin/pay_refund_admin.go
邹方成 e2782a69d3 feat: 添加对对碰游戏功能与Redis支持
refactor: 重构抽奖逻辑以支持可验证凭据
feat(redis): 集成Redis客户端并添加配置支持
fix: 修复订单取消时的优惠券和库存处理逻辑
docs: 添加对对碰游戏前端对接指南和示例JSON
test: 添加对对碰游戏模拟测试和验证逻辑
2025-12-21 17:31:32 +08:00

341 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package admin
import (
"encoding/json"
"fmt"
"net/http"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
paypkg "bindbox-game/internal/pkg/pay"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type createRefundRequest struct {
OrderNo string `json:"order_no" form:"order_no"`
Amount int64 `json:"amount" form:"amount"`
Reason string `json:"reason" form:"reason"`
}
type createRefundResponse struct {
OrderNo string `json:"order_no"`
Status string `json:"status"`
}
// CreateRefund 管理端创建退款(占位实现)
// 入参order_no、amount(分)、reason
// 行为将订单状态置为已退款并写入积分流水refund_restore(按订单积分抵扣金额恢复)
// 注意真实场景需调用微信退款API并根据结果更新状态本版本为占位保证编译与联调
func (h *handler) CreateRefund() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createRefundRequest)
rsp := new(createRefundResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
// 查询订单
order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(req.OrderNo)).First()
if err != nil || order == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 160001, "order not found"))
return
}
// 计算已退款与可退余额(分)
ledgers, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserPointsLedger.RefTable.Eq("orders"), h.readDB.UserPointsLedger.RefID.Eq(order.OrderNo)).Find()
var refundedSumCents int64
for _, l := range ledgers {
if l.Action == "refund_amount" {
refundedSumCents += l.Points * 100
}
}
refundable := order.ActualAmount - refundedSumCents
if refundable < 0 {
refundable = 0
}
if req.Amount <= 0 || req.Amount > refundable {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 160005, fmt.Sprintf("invalid refund amount, max=%d", refundable)))
return
}
// 调用微信真实退款
wc, err := paypkg.NewWechatPayClient(ctx.RequestContext())
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 160006, err.Error()))
return
}
refundNo := fmt.Sprintf("R%s-%d", order.OrderNo, time.Now().Unix())
refundID, status, err := wc.RefundOrder(ctx.RequestContext(), order.OrderNo, refundNo, req.Amount, order.ActualAmount, req.Reason)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 160007, err.Error()))
return
}
pr := &model.PaymentRefunds{
OrderID: order.ID,
OrderNo: order.OrderNo,
RefundNo: refundNo,
Channel: "wechat_jsapi",
Status: status,
AmountRefund: req.Amount,
Reason: req.Reason,
SuccessTime: time.Now(),
Raw: func() string {
b, _ := json.Marshal(map[string]any{"refund_id": refundID, "status": status})
return string(b)
}(),
}
if err := h.writeDB.PaymentRefunds.WithContext(ctx.RequestContext()).Create(pr); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160009, err.Error()))
return
}
// 更新订单状态为已退款
if req.Amount == order.ActualAmount-refundedSumCents {
_, err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.ID.Eq(order.ID)).Updates(map[string]any{
h.writeDB.Orders.Status.ColumnName().String(): 4,
h.writeDB.Orders.UpdatedAt.ColumnName().String(): time.Now(),
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160002, err.Error()))
return
}
// 全额退款:恢复订单中使用的优惠券(细化余额与状态逻辑)
type ocRow struct {
UserCouponID int64
AppliedAmount int64
DiscountType int32
DiscountValue int64
BalanceAmount int64
}
var rows []ocRow
_ = h.repo.GetDbR().Raw("SELECT oc.user_coupon_id, oc.applied_amount, sc.discount_type, sc.discount_value, uc.balance_amount FROM order_coupons oc JOIN user_coupons uc ON uc.id=oc.user_coupon_id JOIN system_coupons sc ON sc.id=uc.coupon_id WHERE oc.order_id=?", order.ID).Scan(&rows).Error
for _, r := range rows {
if r.UserCouponID > 0 && r.AppliedAmount > 0 {
newBal := r.BalanceAmount + r.AppliedAmount
if r.DiscountType == 1 { // 直金额券:判断回退后是否全满
if newBal >= r.DiscountValue {
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=?, status=1, used_order_id=0, used_at=NULL WHERE id=?", r.DiscountValue, r.UserCouponID).Error
newBal = r.DiscountValue
} else {
// 若金额未满,维持 status=2 (已使用/使用中)
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=?, status=2 WHERE id=?", newBal, r.UserCouponID).Error
}
} else { // 满减/折扣券:一律恢复为未使用
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET status=1, used_order_id=0, used_at=NULL WHERE id=?", r.UserCouponID).Error
newBal = 0
}
// 记录流水
ledger := &model.UserCouponLedger{
UserID: order.UserID,
UserCouponID: r.UserCouponID,
ChangeAmount: r.AppliedAmount,
BalanceAfter: newBal,
OrderID: order.ID,
Action: "refund_restore",
CreatedAt: time.Now(),
}
_ = h.repo.GetDbW().Create(ledger).Error
}
}
// 全额退款:回收中奖资产与奖品库存
type invRow struct {
ID int64
RewardID int64
}
var invs []invRow
_ = h.repo.GetDbR().Raw("SELECT id, reward_id FROM user_inventory WHERE order_id=? AND status=1", order.ID).Scan(&invs).Error
for _, inv := range invs {
// 更新状态为2 (作废)
_ = h.repo.GetDbW().Exec("UPDATE user_inventory SET status=2, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|refund_void') WHERE id=?", inv.ID).Error
// 恢复奖品库存 (ActivityRewardSettings)
if inv.RewardID > 0 {
_ = h.repo.GetDbW().Exec("UPDATE activity_reward_settings SET quantity = quantity + 1 WHERE id=?", inv.RewardID).Error
}
}
// 全额退款:取消待发货记录
_ = h.repo.GetDbW().Exec("UPDATE shipping_records SET status=4, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|refund_cancel') WHERE order_id=? AND status=1", order.ID).Error
// 全额退款:回退道具卡
var itemCardIDs []int64
_ = h.repo.GetDbR().Raw("SELECT user_item_card_id FROM activity_draw_effects WHERE draw_log_id IN (SELECT id FROM activity_draw_logs WHERE order_id=?)", order.ID).Scan(&itemCardIDs).Error
for _, icID := range itemCardIDs {
if icID > 0 {
_ = h.repo.GetDbW().Exec("UPDATE user_item_cards SET status=1, used_at=NULL, used_draw_log_id=0, used_activity_id=0, used_issue_id=0, updated_at=NOW(3) WHERE id=?", icID).Error
}
}
}
// 记录积分按比例恢复(幂等增量)
if order.PointsAmount > 0 {
restores, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(
h.readDB.UserPointsLedger.RefTable.Eq("orders"),
h.readDB.UserPointsLedger.RefID.Eq(order.OrderNo),
h.readDB.UserPointsLedger.Action.Eq("refund_restore"),
).Find()
var restoredPointsSum int64
for _, r := range restores {
restoredPointsSum += r.Points
}
totalRefundedAfter := refundedSumCents + req.Amount
if totalRefundedAfter > order.ActualAmount {
totalRefundedAfter = order.ActualAmount
}
restoreCentsTarget := (order.PointsAmount * totalRefundedAfter) / order.ActualAmount
restorePointsTarget := restoreCentsTarget / 100
toAdd := restorePointsTarget - restoredPointsSum
maxPoints := order.PointsAmount / 100
if toAdd < 0 {
toAdd = 0
}
if toAdd > (maxPoints - restoredPointsSum) {
toAdd = maxPoints - restoredPointsSum
}
if toAdd > 0 {
ledger := &model.UserPointsLedger{
UserID: order.UserID,
Action: "refund_restore",
Points: toAdd,
RefTable: "orders",
RefID: order.OrderNo,
Remark: req.Reason,
}
if err := h.writeDB.UserPointsLedger.WithContext(ctx.RequestContext()).Create(ledger); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160003, err.Error()))
return
}
}
}
// 记录退款金额
amountLedger := &model.UserPointsLedger{
UserID: order.UserID,
Action: "refund_amount",
Points: req.Amount / 100,
RefTable: "payment_refund",
RefID: refundID,
Remark: req.Reason,
}
if err := h.writeDB.UserPointsLedger.WithContext(ctx.RequestContext()).Create(amountLedger); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 160004, err.Error()))
return
}
rsp.OrderNo = order.OrderNo
rsp.Status = "success"
ctx.Payload(rsp)
}
}
type listRefundsRequest struct {
OrderNo string `form:"order_no"`
Status string `form:"status"`
Page int `form:"page"`
Size int `form:"size"`
}
type listRefundsResponse struct {
Total int64 `json:"total"`
List []map[string]any `json:"list"`
}
func (h *handler) ListRefunds() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listRefundsRequest)
if err := ctx.ShouldBindQuery(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.Size <= 0 || req.Size > 100 {
req.Size = 20
}
q := h.readDB.PaymentRefunds.WithContext(ctx.RequestContext()).ReadDB()
if req.OrderNo != "" {
q = q.Where(h.readDB.PaymentRefunds.OrderNo.Eq(req.OrderNo))
}
if req.Status != "" {
q = q.Where(h.readDB.PaymentRefunds.Status.Eq(req.Status))
}
total, _ := q.Count()
items, _ := q.Order(h.readDB.PaymentRefunds.ID.Desc()).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find()
var list []map[string]any
for _, it := range items {
list = append(list, map[string]any{
"refund_no": it.RefundNo,
"order_no": it.OrderNo,
"status": it.Status,
"channel": it.Channel,
"amount_refund": it.AmountRefund,
"reason": it.Reason,
"created_at": it.CreatedAt,
})
}
ctx.Payload(&listRefundsResponse{Total: total, List: list})
}
}
type getRefundDetailResponse struct {
Refund map[string]any `json:"refund"`
Order map[string]any `json:"order"`
}
func (h *handler) GetRefundDetail() core.HandlerFunc {
return func(ctx core.Context) {
refundNo := ctx.Param("refund_no")
if refundNo == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "missing refund_no"))
return
}
r, err := h.readDB.PaymentRefunds.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentRefunds.RefundNo.Eq(refundNo)).First()
if err != nil || r == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 160008, "refund not found"))
return
}
o, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(r.OrderNo)).First()
resp := &getRefundDetailResponse{
Refund: map[string]any{
"refund_no": r.RefundNo,
"order_no": r.OrderNo,
"status": r.Status,
"channel": r.Channel,
"amount_refund": r.AmountRefund,
"reason": r.Reason,
"created_at": r.CreatedAt,
},
Order: map[string]any{
"order_no": func() string {
if o != nil {
return o.OrderNo
}
return ""
}(),
"user_id": func() int64 {
if o != nil {
return o.UserID
}
return 0
}(),
"status": func() int32 {
if o != nil {
return o.Status
}
return 0
}(),
"actual_amount": func() int64 {
if o != nil {
return o.ActualAmount
}
return 0
}(),
"points_amount": func() int64 {
if o != nil {
return o.PointsAmount
}
return 0
}(),
},
}
ctx.Payload(resp)
}
}