Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat(pay): 添加支付API基础结构 feat(miniapp): 创建支付测试小程序页面与配置 feat(wechatpay): 配置微信支付参数与证书 fix(guild): 修复成员列表查询条件 docs: 更新代码规范文档与需求文档 style: 统一前后端枚举显示与注释格式 refactor(admin): 重构用户奖励发放接口参数处理 test(title): 添加称号效果参数验证测试
182 lines
8.3 KiB
Go
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 })
|
|
}
|
|
} |