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) } }