refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
291 lines
9.3 KiB
Go
291 lines
9.3 KiB
Go
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)
|
||
}
|
||
}
|