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