优化订单支付
This commit is contained in:
@@ -612,6 +612,9 @@ func normalizePaymentStatus(raw map[string]string) string {
|
||||
|
||||
func normalizeRefundStatus(raw map[string]string) string {
|
||||
value := strings.ToUpper(firstNonEmpty(raw["refund_status"], raw["trade_state"], raw["trade_status"], raw["status"]))
|
||||
if value == "" && responseOK(raw) {
|
||||
return "refunded"
|
||||
}
|
||||
switch value {
|
||||
case "SUCCESS", "REFUND_SUCCESS", "TRADE_SUCCESS", "REFUNDED", "S", "11":
|
||||
return "refunded"
|
||||
|
||||
@@ -77,6 +77,16 @@ func TestParsePayloadNormalizesAliases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRefundStatusTreatsSuccessCodeAsRefunded(t *testing.T) {
|
||||
status := normalizeRefundStatus(map[string]string{
|
||||
"code": "BBS00000",
|
||||
"msg": "成功",
|
||||
})
|
||||
if status != "refunded" {
|
||||
t.Fatalf("normalizeRefundStatus() = %q, want refunded", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePaymentOmitsCounterParamWhenPayModeBlank(t *testing.T) {
|
||||
body := captureCreatePaymentBody(t, "")
|
||||
reqData := body["req_data"].(map[string]any)
|
||||
|
||||
@@ -3,6 +3,7 @@ package dispute
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
@@ -268,6 +269,16 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
handoffRecord := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "admin_arbitration",
|
||||
Content: buildArbitrationHandoffContent(req, settlement),
|
||||
}
|
||||
if err := tx.Create(&handoffRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
||||
"dispute_id": row.ID,
|
||||
@@ -525,6 +536,44 @@ func arbitrateOrderStatus(result string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitrationSettlement) string {
|
||||
content := fmt.Sprintf("客服仲裁结果:%s", arbitrationResultLabel(req.Result))
|
||||
if req.Remark != "" {
|
||||
content += "\n处理说明:" + req.Remark
|
||||
}
|
||||
if settlement.RenterRefundAmount > 0 {
|
||||
content += fmt.Sprintf("\n退款给租客:¥%.2f", settlement.RenterRefundAmount)
|
||||
}
|
||||
if settlement.OwnerIncomeAmount > 0 {
|
||||
content += fmt.Sprintf("\n结算给号主:¥%.2f", settlement.OwnerIncomeAmount)
|
||||
}
|
||||
if settlement.DepositDeductAmount > 0 {
|
||||
content += fmt.Sprintf("\n押金扣除:¥%.2f", settlement.DepositDeductAmount)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func arbitrationResultLabel(result string) string {
|
||||
switch result {
|
||||
case "full_refund":
|
||||
return "全额退款"
|
||||
case "partial_refund":
|
||||
return "部分退款"
|
||||
case "deduct_deposit":
|
||||
return "扣押金"
|
||||
case "release_deposit":
|
||||
return "释放押金"
|
||||
case "compensate_owner":
|
||||
return "赔付号主"
|
||||
case "order_close":
|
||||
return "关闭订单"
|
||||
case "mark_abnormal":
|
||||
return "标记异常"
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func disputeType(input string, isCheckoutDispute bool) string {
|
||||
if isCheckoutDispute {
|
||||
return "checkout_dispute"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -76,3 +77,25 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi
|
||||
t.Fatalf("release frozen entry count = %d, want 1", releaseEntryCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArbitrationHandoffContent(t *testing.T) {
|
||||
content := buildArbitrationHandoffContent(ArbitrateRequest{
|
||||
Result: "partial_refund",
|
||||
Remark: "账号异常,退还部分租金。",
|
||||
}, arbitrationSettlement{
|
||||
RenterRefundAmount: 80,
|
||||
OwnerIncomeAmount: 120,
|
||||
})
|
||||
|
||||
wantParts := []string{
|
||||
"客服仲裁结果:部分退款",
|
||||
"处理说明:账号异常,退还部分租金。",
|
||||
"退款给租客:¥80.00",
|
||||
"结算给号主:¥120.00",
|
||||
}
|
||||
for _, part := range wantParts {
|
||||
if !strings.Contains(content, part) {
|
||||
t.Fatalf("handoff content = %q, want include %q", content, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package payment
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type StartPaymentRequest struct {
|
||||
PayWay string `json:"pay_way"`
|
||||
@@ -52,3 +56,46 @@ type NotifyResult struct {
|
||||
OK bool
|
||||
Message string
|
||||
}
|
||||
|
||||
type AdminPaymentQuery struct {
|
||||
UserID uint64
|
||||
OrderID uint64
|
||||
OrderNo string
|
||||
BizType string
|
||||
Status string
|
||||
Provider string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items interface{} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type AdminPaymentDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
PaymentNo string `json:"payment_no"`
|
||||
OrderID uint64 `json:"order_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
UserPhone string `json:"user_phone"`
|
||||
Provider string `json:"provider"`
|
||||
MerchantID string `json:"merchant_id"`
|
||||
ThirdOrderID string `json:"third_order_id"`
|
||||
ProviderOrderID string `json:"provider_order_id"`
|
||||
PayWay string `json:"pay_way"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
BizType string `json:"biz_type"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
RawRequest datatypes.JSON `json:"raw_request"`
|
||||
RawResponse datatypes.JSON `json:"raw_response"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||
NotifiedAt *time.Time `json:"notified_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -113,6 +113,19 @@ func (h *Handler) QueryRefundStatus(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminList(c *gin.Context) {
|
||||
query, ok := parseAdminPaymentQuery(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result, err := h.service.AdminList(query)
|
||||
if err != nil {
|
||||
writePaymentError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) LeshuaNotify(c *gin.Context) {
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -196,6 +209,35 @@ func parseID(c *gin.Context) (uint64, bool) {
|
||||
return id, true
|
||||
}
|
||||
|
||||
func parseAdminPaymentQuery(c *gin.Context) (AdminPaymentQuery, bool) {
|
||||
var query AdminPaymentQuery
|
||||
if raw := c.Query("user_id"); raw != "" {
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
response.BadRequest(c, "用户ID不正确")
|
||||
return query, false
|
||||
}
|
||||
query.UserID = value
|
||||
}
|
||||
if raw := c.Query("order_id"); raw != "" {
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
response.BadRequest(c, "订单ID不正确")
|
||||
return query, false
|
||||
}
|
||||
query.OrderID = value
|
||||
}
|
||||
query.OrderNo = c.Query("order_no")
|
||||
query.BizType = c.Query("biz_type")
|
||||
query.Status = c.Query("status")
|
||||
query.Provider = c.Query("provider")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
return query, true
|
||||
}
|
||||
|
||||
func writePaymentError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, ErrDependencyUnavailable):
|
||||
|
||||
@@ -180,6 +180,8 @@ func (r *Repository) Start(userID uint64, orderID uint64, req StartPaymentReques
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
|
||||
log.Printf("[payment] payment start order_id=%d order_no=%s payment_id=%d provider=%s amount_cent=%d third_order_id=%s",
|
||||
orderID, orderRow.OrderNo, payment.ID, runtimeConfig.Provider, payment.AmountCent, payment.ThirdOrderID)
|
||||
resp, err := runtimeConfig.Channel.CreatePayment(context.Background(), channelCreatePaymentRequest{
|
||||
ThirdOrderID: payment.ThirdOrderID,
|
||||
AmountCent: payment.AmountCent,
|
||||
@@ -193,10 +195,14 @@ func (r *Repository) Start(userID uint64, orderID uint64, req StartPaymentReques
|
||||
})
|
||||
if err != nil {
|
||||
_ = r.markPaymentFailed(payment.ID, nil, err.Error())
|
||||
log.Printf("[payment] payment request failed order_id=%d payment_id=%d provider=%s amount_cent=%d err=%v",
|
||||
orderID, payment.ID, runtimeConfig.Provider, payment.AmountCent, err)
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
_ = r.markPaymentFailed(payment.ID, resp.Raw, resp.ErrorMessage)
|
||||
log.Printf("[payment] payment rejected order_id=%d payment_id=%d provider=%s amount_cent=%d code=%s message=%s",
|
||||
orderID, payment.ID, runtimeConfig.Provider, payment.AmountCent, firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"]), resp.ErrorMessage)
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
if err := r.db.Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{
|
||||
@@ -216,6 +222,8 @@ func (r *Repository) Start(userID uint64, orderID uint64, req StartPaymentReques
|
||||
return nil, err
|
||||
}
|
||||
r.recordConfigUsage(runtimeConfig, latest)
|
||||
log.Printf("[payment] payment result order_id=%d order_no=%s payment_id=%d provider=%s amount_cent=%d status=%s provider_order_id=%s",
|
||||
orderID, orderRow.OrderNo, latest.ID, runtimeConfig.Provider, latest.AmountCent, latest.Status, latest.ProviderOrderID)
|
||||
dto := toDTO(*latest)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -254,6 +262,8 @@ func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymen
|
||||
_ = r.markPaymentFailed(payment.ID, nil, "payment channel unavailable")
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
log.Printf("[payment] wallet recharge start user_id=%d payment_id=%d provider=%s amount_cent=%d third_order_id=%s",
|
||||
userID, payment.ID, runtimeConfig.Provider, payment.AmountCent, payment.ThirdOrderID)
|
||||
resp, err := runtimeConfig.Channel.CreatePayment(context.Background(), channelCreatePaymentRequest{
|
||||
ThirdOrderID: payment.ThirdOrderID,
|
||||
AmountCent: payment.AmountCent,
|
||||
@@ -267,10 +277,14 @@ func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymen
|
||||
})
|
||||
if err != nil {
|
||||
_ = r.markPaymentFailed(payment.ID, nil, err.Error())
|
||||
log.Printf("[payment] wallet recharge request failed user_id=%d payment_id=%d provider=%s amount_cent=%d err=%v",
|
||||
userID, payment.ID, runtimeConfig.Provider, payment.AmountCent, err)
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
_ = r.markPaymentFailed(payment.ID, resp.Raw, resp.ErrorMessage)
|
||||
log.Printf("[payment] wallet recharge rejected user_id=%d payment_id=%d provider=%s amount_cent=%d code=%s message=%s",
|
||||
userID, payment.ID, runtimeConfig.Provider, payment.AmountCent, firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"]), resp.ErrorMessage)
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
if err := r.db.Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{
|
||||
@@ -290,6 +304,8 @@ func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymen
|
||||
return nil, err
|
||||
}
|
||||
r.recordConfigUsage(runtimeConfig, latest)
|
||||
log.Printf("[payment] wallet recharge result user_id=%d payment_id=%d provider=%s amount_cent=%d status=%s provider_order_id=%s",
|
||||
userID, latest.ID, runtimeConfig.Provider, latest.AmountCent, latest.Status, latest.ProviderOrderID)
|
||||
dto := toDTO(*latest)
|
||||
return &dto, nil
|
||||
}
|
||||
@@ -466,6 +482,8 @@ func (r *Repository) StartRefund(orderID uint64, refundAmountCent int64, bizType
|
||||
if err := r.db.Create(&refundOrder).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[payment] refund start order_id=%d order_no=%s payment_id=%d biz_type=%s provider=%s amount_cent=%d merchant_refund_id=%s origin_third_order_id=%s origin_provider_order_id=%s",
|
||||
orderID, originalPayment.OrderNo, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, merchantRefundID, originalPayment.ThirdOrderID, refundOriginProviderOrderID(originalPayment))
|
||||
r.recordConfigUsage(runtimeConfig, &refundOrder)
|
||||
if err := r.markOrderRefunding(orderID, refundAmountCent); err != nil {
|
||||
log.Printf("[payment] mark order refunding failed order_id=%d err=%v", orderID, err)
|
||||
@@ -477,7 +495,7 @@ func (r *Repository) StartRefund(orderID uint64, refundAmountCent int64, bizType
|
||||
}
|
||||
resp, err := runtimeConfig.Channel.CreateRefund(context.Background(), channelCreateRefundRequest{
|
||||
ThirdOrderID: originalPayment.ThirdOrderID,
|
||||
ProviderOrderID: originalPayment.ProviderOrderID,
|
||||
ProviderOrderID: refundOriginProviderOrderID(originalPayment),
|
||||
MerchantRefundID: merchantRefundID,
|
||||
RefundAmountCent: refundAmountCent,
|
||||
NotifyURL: runtimeConfig.NotifyURL,
|
||||
@@ -486,10 +504,14 @@ func (r *Repository) StartRefund(orderID uint64, refundAmountCent int64, bizType
|
||||
})
|
||||
if err != nil {
|
||||
_ = r.markRefundFailed(refundOrder.ID, orderID, refundAmountCent, map[string]string{"error": err.Error()})
|
||||
log.Printf("[payment] refund request failed order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d err=%v",
|
||||
orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, err)
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
_ = r.markRefundFailed(refundOrder.ID, orderID, refundAmountCent, resp.Raw)
|
||||
log.Printf("[payment] refund rejected order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d code=%s message=%s",
|
||||
orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, firstNonEmpty(resp.Raw["code"], resp.Raw["resp_code"], resp.Raw["result_code"]), resp.ErrorMessage)
|
||||
return nil, ErrPaymentUnavailable
|
||||
}
|
||||
|
||||
@@ -522,6 +544,8 @@ func (r *Repository) StartRefund(orderID uint64, refundAmountCent int64, bizType
|
||||
}
|
||||
refundOrder.Status = refundStatus
|
||||
refundOrder.ProviderOrderID = resp.ProviderRefundID
|
||||
log.Printf("[payment] refund result order_id=%d payment_id=%d biz_type=%s provider=%s amount_cent=%d status=%s provider_refund_id=%s",
|
||||
orderID, refundOrder.ID, bizType, runtimeConfig.Provider, refundAmountCent, refundStatus, resp.ProviderRefundID)
|
||||
|
||||
dto := toRefundDTO(refundOrder)
|
||||
return &dto, nil
|
||||
@@ -581,6 +605,51 @@ func (r *Repository) QueryRefundStatus(orderID uint64) (*RefundDTO, error) {
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AdminList(query AdminPaymentQuery) (*PaginatedResult, error) {
|
||||
db := r.db.Table("payment_orders AS p").
|
||||
Select("p.*, COALESCE(u.phone, '') AS user_phone").
|
||||
Joins("LEFT JOIN users AS u ON u.id = p.user_id")
|
||||
countDB := r.db.Model(&model.PaymentOrder{})
|
||||
if query.UserID > 0 {
|
||||
db = db.Where("p.user_id = ?", query.UserID)
|
||||
countDB = countDB.Where("user_id = ?", query.UserID)
|
||||
}
|
||||
if query.OrderID > 0 {
|
||||
db = db.Where("p.order_id = ?", query.OrderID)
|
||||
countDB = countDB.Where("order_id = ?", query.OrderID)
|
||||
}
|
||||
if query.OrderNo != "" {
|
||||
db = db.Where("p.order_no = ?", query.OrderNo)
|
||||
countDB = countDB.Where("order_no = ?", query.OrderNo)
|
||||
}
|
||||
if query.BizType != "" {
|
||||
db = db.Where("p.biz_type = ?", query.BizType)
|
||||
countDB = countDB.Where("biz_type = ?", query.BizType)
|
||||
}
|
||||
if query.Status != "" {
|
||||
db = db.Where("p.status = ?", query.Status)
|
||||
countDB = countDB.Where("status = ?", query.Status)
|
||||
}
|
||||
if query.Provider != "" {
|
||||
db = db.Where("p.provider = ?", query.Provider)
|
||||
countDB = countDB.Where("provider = ?", query.Provider)
|
||||
}
|
||||
var total int64
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (query.Page - 1) * query.PageSize
|
||||
var rows []adminPaymentRow
|
||||
if err := db.Order("p.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]AdminPaymentDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toDTO())
|
||||
}
|
||||
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
|
||||
}
|
||||
|
||||
// HandleRefundNotify 处理渠道退款通知。
|
||||
func (r *Repository) HandleRefundNotify(provider string, params map[string]string, rawPayload string, contentType string, authorization string) (*NotifyResult, error) {
|
||||
payment, err := r.findRefundPaymentForNotify(params)
|
||||
@@ -1004,6 +1073,55 @@ func toDTO(payment model.PaymentOrder) PaymentDTO {
|
||||
}
|
||||
}
|
||||
|
||||
type adminPaymentRow struct {
|
||||
model.PaymentOrder
|
||||
UserPhone string
|
||||
}
|
||||
|
||||
func (row adminPaymentRow) toDTO() AdminPaymentDTO {
|
||||
errorCode, errorMessage := paymentErrorSummary(row.Status, row.RawResponse)
|
||||
return AdminPaymentDTO{
|
||||
ID: row.ID,
|
||||
PaymentNo: row.PaymentNo,
|
||||
OrderID: row.OrderID,
|
||||
OrderNo: row.OrderNo,
|
||||
UserID: row.UserID,
|
||||
UserPhone: row.UserPhone,
|
||||
Provider: row.Provider,
|
||||
MerchantID: row.MerchantID,
|
||||
ThirdOrderID: row.ThirdOrderID,
|
||||
ProviderOrderID: row.ProviderOrderID,
|
||||
PayWay: row.PayWay,
|
||||
AmountCent: row.AmountCent,
|
||||
BizType: row.BizType,
|
||||
Status: row.Status,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMessage: errorMessage,
|
||||
RawRequest: row.RawRequest,
|
||||
RawResponse: row.RawResponse,
|
||||
PaidAt: row.PaidAt,
|
||||
NotifiedAt: row.NotifiedAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func paymentErrorSummary(status string, raw datatypes.JSON) (string, string) {
|
||||
if status != "failed" {
|
||||
return "", ""
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return "", ""
|
||||
}
|
||||
code := firstStringValue(payload, "code", "resp_code", "result_code", "error_code", "status")
|
||||
message := firstStringValue(payload, "msg", "message", "error", "error_message", "result_msg", "result_desc")
|
||||
return code, message
|
||||
}
|
||||
|
||||
func moneyCent(value float64) int64 {
|
||||
return int64(math.Round(value * 100))
|
||||
}
|
||||
@@ -1014,6 +1132,69 @@ func parseCent(value string) int64 {
|
||||
return amount
|
||||
}
|
||||
|
||||
func refundOriginProviderOrderID(payment model.PaymentOrder) string {
|
||||
if payment.Provider != "lakala" {
|
||||
return payment.ProviderOrderID
|
||||
}
|
||||
if tradeID := lakalaOriginTradeID(payment.RawResponse); tradeID != "" {
|
||||
return tradeID
|
||||
}
|
||||
return payment.ProviderOrderID
|
||||
}
|
||||
|
||||
func lakalaOriginTradeID(raw datatypes.JSON) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
if tradeID := firstStringValue(payload, "trade_no", "origin_trade_no"); tradeID != "" {
|
||||
return tradeID
|
||||
}
|
||||
value, ok := payload["order_trade_info_list"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
var items []map[string]any
|
||||
if err := json.Unmarshal([]byte(typed), &items); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, item := range items {
|
||||
if tradeID := firstStringValue(item, "trade_no", "origin_trade_no"); tradeID != "" {
|
||||
return tradeID
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if tradeID := firstStringValue(itemMap, "trade_no", "origin_trade_no"); tradeID != "" {
|
||||
return tradeID
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstStringValue(values map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
value, ok := values[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if text, ok := value.(string); ok && text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseChannelTime(value string) *time.Time {
|
||||
if value == "" {
|
||||
return nil
|
||||
|
||||
@@ -128,3 +128,32 @@ func TestParseChannelTimeUsesShanghaiWhenLocalIsUTC(t *testing.T) {
|
||||
t.Fatalf("parseChannelTime() = %s, want 20260607202700 in Asia/Shanghai", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefundOriginProviderOrderIDUsesLakalaTradeNo(t *testing.T) {
|
||||
payment := model.PaymentOrder{
|
||||
Provider: "lakala",
|
||||
ProviderOrderID: "26060811012001101011735013210",
|
||||
RawResponse: datatypes.JSON([]byte(`{
|
||||
"pay_order_no": "26060811012001101011735013210",
|
||||
"order_trade_info_list": "[{\"trade_no\":\"20260608110113230266224452004512\",\"pay_order_no\":\"26060811012001101011735013210\"}]"
|
||||
}`)),
|
||||
}
|
||||
|
||||
got := refundOriginProviderOrderID(payment)
|
||||
if got != "20260608110113230266224452004512" {
|
||||
t.Fatalf("refundOriginProviderOrderID() = %q, want trade_no", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefundOriginProviderOrderIDFallsBackToProviderOrderID(t *testing.T) {
|
||||
payment := model.PaymentOrder{
|
||||
Provider: "lakala",
|
||||
ProviderOrderID: "26060811012001101011735013210",
|
||||
RawResponse: datatypes.JSON([]byte(`{"pay_order_no":"26060811012001101011735013210"}`)),
|
||||
}
|
||||
|
||||
got := refundOriginProviderOrderID(payment)
|
||||
if got != payment.ProviderOrderID {
|
||||
t.Fatalf("refundOriginProviderOrderID() = %q, want fallback %q", got, payment.ProviderOrderID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,3 +106,19 @@ func (s *Service) QueryRefundStatus(orderID uint64) (*RefundDTO, error) {
|
||||
}
|
||||
return s.repo.QueryRefundStatus(orderID)
|
||||
}
|
||||
|
||||
func (s *Service) AdminList(query AdminPaymentQuery) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
if query.PageSize > 100 {
|
||||
query.PageSize = 100
|
||||
}
|
||||
return s.repo.AdminList(query)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package wallet
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -260,7 +260,7 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
|
||||
}
|
||||
|
||||
func roundWalletMoney(value float64) float64 {
|
||||
return math.Round(value)
|
||||
return money.Round(value)
|
||||
}
|
||||
|
||||
func toAccountDTO(account model.WalletAccount) *AccountDTO {
|
||||
|
||||
@@ -31,6 +31,34 @@ func TestApplyEntryRoundsMoneyBeforeComparing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEntryKeepsWalletMoneyAtJiaoPrecision(t *testing.T) {
|
||||
account := model.WalletAccount{
|
||||
UserID: 8,
|
||||
AvailableBalance: 0,
|
||||
}
|
||||
entry := Entry{
|
||||
UserID: 8,
|
||||
Direction: "in",
|
||||
Amount: 2.30,
|
||||
BalanceType: "available",
|
||||
}
|
||||
|
||||
balanceAfter, err := applyEntry(&account, entry)
|
||||
if err != nil {
|
||||
t.Fatalf("applyEntry() error = %v", err)
|
||||
}
|
||||
if balanceAfter != 2.3 {
|
||||
t.Fatalf("balanceAfter = %v, want 2.3", balanceAfter)
|
||||
}
|
||||
if account.AvailableBalance != 2.3 {
|
||||
t.Fatalf("AvailableBalance = %v, want 2.3", account.AvailableBalance)
|
||||
}
|
||||
|
||||
if rounded := roundWalletMoney(2.36); rounded != 2.4 {
|
||||
t.Fatalf("roundWalletMoney(2.36) = %v, want 2.4", rounded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLedgerNoUsesReadableFormat(t *testing.T) {
|
||||
ledgerNo, err := newLedgerNo()
|
||||
if err != nil {
|
||||
|
||||
@@ -425,6 +425,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.GET("/disputes", requirePerm("dispute:view"), disputeHandler.AdminList)
|
||||
adminRoutes.POST("/disputes/:id/arbitrate", requirePerm("dispute:arbitrate"), disputeHandler.AdminArbitrate)
|
||||
adminRoutes.GET("/wallet/ledger", requirePerm("wallet:view"), walletHandler.AdminLedger)
|
||||
adminRoutes.GET("/payments", requirePerm("wallet:view"), paymentHandler.AdminList)
|
||||
|
||||
// 提现管理
|
||||
adminRoutes.GET("/withdrawals", requirePerm("withdrawal:list"), withdrawalHandler.AdminList)
|
||||
|
||||
Reference in New Issue
Block a user