bindbox-game/internal/api/admin/pay_refund_admin.go
邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

291 lines
9.3 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
}
// 全额退款:恢复订单中使用的优惠券(优先使用结构化明细表)
var cnt int64
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM order_coupons WHERE order_id=?", order.ID).Scan(&cnt).Error
if cnt > 0 {
type ocRow struct {
UserCouponID int64
AppliedAmount int64
}
var rows []ocRow
_ = h.repo.GetDbR().Raw("SELECT user_coupon_id, applied_amount FROM order_coupons WHERE order_id=?", order.ID).Scan(&rows).Error
for _, r := range rows {
if r.UserCouponID > 0 && r.AppliedAmount > 0 {
_ = h.repo.GetDbW().Exec("UPDATE user_coupons SET balance_amount=COALESCE(balance_amount,0)+?, status=1, used_order_id=0, used_at=NULL WHERE id=? AND user_id=?", r.AppliedAmount, r.UserCouponID, order.UserID).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)
}
}