bindbox-game/internal/api/admin/pay_refund_admin.go
邹方成 6ee627139c
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat: 新增支付测试小程序与微信支付集成
feat(pay): 添加支付API基础结构
feat(miniapp): 创建支付测试小程序页面与配置
feat(wechatpay): 配置微信支付参数与证书
fix(guild): 修复成员列表查询条件
docs: 更新代码规范文档与需求文档
style: 统一前后端枚举显示与注释格式
refactor(admin): 重构用户奖励发放接口参数处理
test(title): 添加称号效果参数验证测试
2025-11-17 00:42:08 +08:00

228 lines
9.9 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 (
"net/http"
"time"
"fmt"
"encoding/json"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
paypkg "bindbox-game/internal/pkg/pay"
)
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
}
}
// 记录积分按比例恢复(幂等增量)
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)
}
}