bindbox-game/internal/api/admin/pay_reconcile_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

182 lines
8.3 KiB
Go

package admin
import (
"net/http"
"time"
"encoding/json"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type billTransactionItem struct {
TransactionID string `json:"transaction_id"`
OutTradeNo string `json:"out_trade_no"`
AmountTotal int64 `json:"amount_total"`
}
type billRefundItem struct {
RefundNo string `json:"refund_no"`
OutTradeNo string `json:"out_trade_no"`
AmountRefund int64 `json:"amount_refund"`
}
type importBillRequest struct {
BillDate string `json:"bill_date"` // YYYY-MM-DD
Type string `json:"type"` // transactions | refunds
Items []map[string]any `json:"items"`
}
type importBillResponse struct {
Imported bool `json:"imported"`
Message string `json:"message"`
}
// ImportPaymentBill 导入支付账单并计算差异
func (h *handler) ImportPaymentBill() core.HandlerFunc {
return func(ctx core.Context) {
req := new(importBillRequest)
rsp := new(importBillResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
t, err := time.Parse("2006-01-02", req.BillDate)
if err != nil || (req.Type != "transactions" && req.Type != "refunds") {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid bill_date or type"))
return
}
_ = h.writeDB.PaymentBills.WithContext(ctx.RequestContext()).Create(&model.PaymentBills{ BillDate: t, Type: req.Type, Imported: true })
if req.Type == "transactions" {
var items []billTransactionItem
for _, v := range req.Items {
b, _ := json.Marshal(v)
var it billTransactionItem
_ = json.Unmarshal(b, &it)
if it.TransactionID != "" || it.OutTradeNo != "" { items = append(items, it) }
}
h.calcTransactionDiff(ctx, t, items)
} else {
var items []billRefundItem
for _, v := range req.Items {
b, _ := json.Marshal(v)
var it billRefundItem
_ = json.Unmarshal(b, &it)
if it.RefundNo != "" || it.OutTradeNo != "" { items = append(items, it) }
}
h.calcRefundDiff(ctx, t, items)
}
rsp.Imported = true
rsp.Message = "ok"
ctx.Payload(rsp)
}
}
func (h *handler) calcTransactionDiff(ctx core.Context, billDate time.Time, items []billTransactionItem) {
wechatIDs := make(map[string]billTransactionItem)
orderNos := make(map[string]billTransactionItem)
for _, it := range items { if it.TransactionID != "" { wechatIDs[it.TransactionID] = it }; if it.OutTradeNo != "" { orderNos[it.OutTradeNo] = it } }
start := billDate
end := billDate.Add(24*time.Hour)
locals, _ := h.readDB.PaymentTransactions.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.PaymentTransactions.SuccessTime.Gte(start), h.readDB.PaymentTransactions.SuccessTime.Lte(end)).Find()
matchedLocal := make(map[string]bool)
for _, lt := range locals {
it, ok := wechatIDs[lt.TransactionID]
if !ok {
if lt.OrderNo != "" { it, ok = orderNos[lt.OrderNo] }
}
if !ok {
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "MISSING_WECHAT", LocalTxID: lt.TransactionID, Detail: jsonStr(map[string]any{"order_no": lt.OrderNo}) })
continue
}
matchedLocal[lt.TransactionID] = true
if it.AmountTotal != lt.AmountTotal {
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "AMOUNT_MISMATCH", LocalTxID: lt.TransactionID, WechatTxID: it.TransactionID, Detail: jsonStr(map[string]any{"local": lt.AmountTotal, "wechat": it.AmountTotal}) })
}
}
for _, it := range items {
key := it.TransactionID
if key == "" { key = it.OutTradeNo }
if key == "" { continue }
if it.TransactionID != "" && matchedLocal[it.TransactionID] { continue }
// 未在本地找到
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "MISSING_LOCAL", WechatTxID: it.TransactionID, Detail: jsonStr(map[string]any{"out_trade_no": it.OutTradeNo}) })
}
}
func (h *handler) calcRefundDiff(ctx core.Context, billDate time.Time, items []billRefundItem) {
refundNos := make(map[string]billRefundItem)
orderNos := make(map[string]billRefundItem)
for _, it := range items { if it.RefundNo != "" { refundNos[it.RefundNo] = it }; if it.OutTradeNo != "" { orderNos[it.OutTradeNo] = it } }
start := billDate
end := billDate.Add(24*time.Hour)
locals, _ := h.readDB.PaymentRefunds.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.PaymentRefunds.CreatedAt.Gte(start), h.readDB.PaymentRefunds.CreatedAt.Lte(end)).Find()
matched := make(map[string]bool)
for _, lr := range locals {
it, ok := refundNos[lr.RefundNo]
if !ok {
if lr.OrderNo != "" { it, ok = orderNos[lr.OrderNo] }
}
if !ok {
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "MISSING_WECHAT_REFUND", LocalTxID: lr.RefundNo, Detail: jsonStr(map[string]any{"order_no": lr.OrderNo}) })
continue
}
matched[lr.RefundNo] = true
if it.AmountRefund != lr.AmountRefund {
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "REFUND_AMOUNT_MISMATCH", LocalTxID: lr.RefundNo, WechatTxID: it.RefundNo, Detail: jsonStr(map[string]any{"local": lr.AmountRefund, "wechat": it.AmountRefund}) })
}
}
for _, it := range items {
key := it.RefundNo
if key == "" { key = it.OutTradeNo }
if key == "" { continue }
if it.RefundNo != "" && matched[it.RefundNo] { continue }
_ = h.writeDB.PaymentBillDiff.WithContext(ctx.RequestContext()).Create(&model.PaymentBillDiff{ BillDate: billDate, DiffType: "MISSING_LOCAL_REFUND", WechatTxID: it.RefundNo, Detail: jsonStr(map[string]any{"out_trade_no": it.OutTradeNo}) })
}
}
func jsonStr(v any) string { b, _ := json.Marshal(v); return string(b) }
type listBillDiffRequest struct {
BillDate string `form:"bill_date"`
DiffType string `form:"diff_type"`
Page int `form:"page"`
Size int `form:"size"`
}
type listBillDiffResponse struct {
Total int64 `json:"total"`
List []map[string]any `json:"list"`
}
// ListPaymentBillDiff 查询账单差异
func (h *handler) ListPaymentBillDiff() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listBillDiffRequest)
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.PaymentBillDiff.WithContext(ctx.RequestContext()).ReadDB()
if req.BillDate != "" { if t, err := time.Parse("2006-01-02", req.BillDate); err==nil { q = q.Where(h.readDB.PaymentBillDiff.BillDate.Eq(t)) } }
if req.DiffType != "" { q = q.Where(h.readDB.PaymentBillDiff.DiffType.Eq(req.DiffType)) }
total, _ := q.Count()
items, _ := q.Order(h.readDB.PaymentBillDiff.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{
"bill_date": it.BillDate.Format("2006-01-02"),
"diff_type": it.DiffType,
"local_tx_id": it.LocalTxID,
"wechat_tx_id": it.WechatTxID,
"detail": it.Detail,
"created_at": it.CreatedAt,
})
}
ctx.Payload(&listBillDiffResponse{ Total: total, List: list })
}
}