diff --git a/backend/internal/e2e/rental_flow_test.go b/backend/internal/e2e/rental_flow_test.go index f1a5f9d..ff0c791 100644 --- a/backend/internal/e2e/rental_flow_test.go +++ b/backend/internal/e2e/rental_flow_test.go @@ -216,7 +216,7 @@ func newFlowServices(db *gorm.DB) flowServices { return refund.Status, nil }), }) - paymentRepo = payment.NewRepository(db, configRepo, orderRepo, walletRepo) + paymentRepo = payment.NewRepository(db, configRepo, orderRepo) disputeRepo := dispute.NewRepository(db, dispute.Dependencies{ RefundStarter: dispute.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) { refund, err := paymentRepo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark) @@ -230,7 +230,7 @@ func newFlowServices(db *gorm.DB) flowServices { return flowServices{ listing: listing.NewService(listingRepo, fixedConfig{"listing.review_required": "true"}), order: order.NewService(orderRepo), - payment: payment.NewService(paymentRepo, "development"), + payment: payment.NewService(paymentRepo), paymentConfig: paymentconfig.NewService(configRepo), paymentAccount: paymentaccount.NewService(paymentaccount.NewRepository(db)), dispute: dispute.NewService(disputeRepo), diff --git a/backend/internal/modules/adminfinance/dashboard.go b/backend/internal/modules/adminfinance/dashboard.go index aef103c..de81166 100644 --- a/backend/internal/modules/adminfinance/dashboard.go +++ b/backend/internal/modules/adminfinance/dashboard.go @@ -26,10 +26,10 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ db := r.db.WithContext(ctx) var payment paymentSummaryRow if err := db.Table("payment_orders"). - Select(`COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, + Select(`COALESCE(SUM(CASE WHEN biz_type IN ('order_pay') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). @@ -84,10 +84,10 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi payments := make([]dailyPaymentRow, 0) if err := db.Table("payment_orders"). Select(`DATE(created_at) AS date, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). diff --git a/backend/internal/modules/payment/channel_status.go b/backend/internal/modules/payment/channel_status.go index d541b28..de49c15 100644 --- a/backend/internal/modules/payment/channel_status.go +++ b/backend/internal/modules/payment/channel_status.go @@ -34,21 +34,12 @@ func (r *Repository) updateChannelStatus(ctx context.Context, paymentID uint64, return r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(updates).Error } func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrder, status string, paidAt time.Time, raw map[string]string, source string) error { - if payment.Status != "paid" { - if payment.OrderID == 0 { - if r.walletRepo == nil { - return ErrDependencyUnavailable - } - if err := r.walletRepo.ConfirmRechargeFromChannel(ctx, payment.UserID, firstNonEmpty(payment.ProviderOrderID, payment.PaymentNo), payment.AmountCent); err != nil { - return err - } - } else { - if r.orderRepo == nil { - return ErrDependencyUnavailable - } - if err := r.orderRepo.ConfirmPaidFromChannel(ctx, payment.OrderID, firstNonEmpty(payment.ProviderOrderID, payment.PaymentNo)); err != nil { - return err - } + if payment.Status != "paid" && payment.OrderID != 0 { + if r.orderRepo == nil { + return ErrDependencyUnavailable + } + if err := r.orderRepo.ConfirmPaidFromChannel(ctx, payment.OrderID, firstNonEmpty(payment.ProviderOrderID, payment.PaymentNo)); err != nil { + return err } } updates := map[string]any{ diff --git a/backend/internal/modules/payment/dto.go b/backend/internal/modules/payment/dto.go index d748909..3cceb5a 100644 --- a/backend/internal/modules/payment/dto.go +++ b/backend/internal/modules/payment/dto.go @@ -11,12 +11,6 @@ type StartPaymentRequest struct { JSPayFlag string `json:"jspay_flag"` } -type WalletRechargePaymentRequest struct { - AmountCent int64 `json:"amount_cent"` - PayWay string `json:"pay_way"` - JSPayFlag string `json:"jspay_flag"` -} - type PaymentDTO struct { ID uint64 `json:"id"` PaymentNo string `json:"payment_no"` diff --git a/backend/internal/modules/payment/handler.go b/backend/internal/modules/payment/handler.go index 741ccff..d15701e 100644 --- a/backend/internal/modules/payment/handler.go +++ b/backend/internal/modules/payment/handler.go @@ -63,43 +63,6 @@ func (h *Handler) Query(c *gin.Context) { response.OK(c, item) } -func (h *Handler) WalletRecharge(c *gin.Context) { - userID, ok := currentUserID(c) - if !ok { - response.Unauthorized(c, "缺少用户上下文") - return - } - var req WalletRechargePaymentRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "充值金额不正确") - return - } - item, err := h.service.StartWalletRecharge(c.Request.Context(), userID, req, c.ClientIP()) - if err != nil { - writePaymentError(c, err) - return - } - response.OK(c, item) -} - -func (h *Handler) WalletRechargeQuery(c *gin.Context) { - userID, ok := currentUserID(c) - if !ok { - response.Unauthorized(c, "缺少用户上下文") - return - } - paymentID, ok := parseID(c) - if !ok { - return - } - item, err := h.service.QueryWalletRecharge(c.Request.Context(), userID, paymentID) - if err != nil { - writePaymentError(c, err) - return - } - response.OK(c, item) -} - func (h *Handler) QueryRefundStatus(c *gin.Context) { orderID, ok := parseID(c) if !ok { @@ -248,8 +211,6 @@ func writePaymentError(c *gin.Context, err error) { response.Error(c, http.StatusConflict, "payment_cannot_start", "当前订单不能支付") case errors.Is(err, ErrRefundCannotStart): response.Error(c, http.StatusConflict, "refund_cannot_start", "当前订单不能退款") - case errors.Is(err, ErrWalletRechargeDisabled): - response.Error(c, http.StatusGone, "wallet_recharge_disabled", "钱包充值已关闭") case errors.Is(err, ErrPaymentVerifyFailed): response.Error(c, http.StatusForbidden, "payment_verify_failed", "支付通知验签失败") case errors.Is(err, ErrPaymentNotFound), errors.Is(err, gorm.ErrRecordNotFound), order.IsNotFound(err): diff --git a/backend/internal/modules/payment/payment_start.go b/backend/internal/modules/payment/payment_start.go index d3e57a5..6d5ad54 100644 --- a/backend/internal/modules/payment/payment_start.go +++ b/backend/internal/modules/payment/payment_start.go @@ -103,87 +103,6 @@ func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, r dto := toDTO(*latest) return &dto, nil } -func (r *Repository) StartWalletRecharge(ctx context.Context, userID uint64, req WalletRechargePaymentRequest, clientIP string) (*PaymentDTO, error) { - amountCent := req.AmountCent - if userID == 0 || amountCent < moneyCent(MinWalletRechargeAmount) { - return nil, ErrPaymentCannotStart - } - runtimeConfig, err := r.defaultRuntimeConfig(ctx) - if err != nil { - return nil, ErrPaymentUnavailable - } - payment, err := r.createWalletRechargePayment(ctx, userID, amountCent, req, *runtimeConfig) - if err != nil { - return nil, err - } - if runtimeConfig.isMockMode() { - if err := r.confirmPaid(ctx, payment, "2", time.Now(), map[string]string{ - "mock": "true", - "third_order_id": payment.ThirdOrderID, - "leshua_order_id": payment.ProviderOrderID, - "status": "2", - }, channelSourceMock); err != nil { - return nil, err - } - latest, err := r.findPaymentByID(ctx, payment.ID) - if err != nil { - return nil, err - } - r.recordConfigUsage(ctx, runtimeConfig, latest) - dto := toDTO(*latest) - return &dto, nil - } - if runtimeConfig.Channel == nil { - _ = r.markPaymentFailed(ctx, 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(ctx, channelCreatePaymentRequest{ - ThirdOrderID: payment.ThirdOrderID, - AmountCent: payment.AmountCent, - PayWay: payment.PayWay, - JSPayFlag: payment.JSPayFlag, - NotifyURL: runtimeConfig.NotifyURL, - JumpURL: runtimeConfig.JumpURL, - ClientIP: clientIP, - Body: "钱包充值 " + payment.PaymentNo, - Attach: payment.PaymentNo, - }) - if err != nil { - _ = r.markPaymentFailed(ctx, 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(ctx, 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.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{ - "status": "paying", - "provider_order_id": resp.ProviderOrderID, - "pay_way": firstNonEmpty(resp.PayWay, payment.PayWay), - "td_code": resp.TDCode, - "jspay_url": resp.JSPayURL, - "jspay_info": resp.JSPayInfo, - "raw_request": jsonMap(resp.RawRequest), - "raw_response": jsonMap(withRawSource(resp.Raw, channelSourceCreate)), - }).Error; err != nil { - return nil, err - } - latest, err := r.findPaymentByID(ctx, payment.ID) - if err != nil { - return nil, err - } - r.recordConfigUsage(ctx, 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 -} func (r *Repository) preparePayment(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, *model.RentalOrder, error) { var paymentID uint64 var orderRow model.RentalOrder @@ -304,32 +223,4 @@ func newOrderPayment(row model.RentalOrder, amountCent int64, req StartPaymentRe } return payment, nil } -func (r *Repository) createWalletRechargePayment(ctx context.Context, userID uint64, amountCent int64, req WalletRechargePaymentRequest, runtimeConfig runtimePaymentConfig) (*model.PaymentOrder, error) { - paymentNo, err := newPaymentNo() - if err != nil { - return nil, err - } - payment := model.PaymentOrder{ - PaymentNo: paymentNo, - OrderID: 0, - OrderNo: paymentNo, - UserID: userID, - Provider: runtimeConfig.Provider, - MerchantID: runtimeConfig.MerchantID, - ThirdOrderID: paymentNo, - ProviderOrderID: "", - PayWay: firstNonEmpty(req.PayWay, runtimeConfig.PayWay, "ZFBZF"), - JSPayFlag: firstNonEmpty(req.JSPayFlag, runtimeConfig.JSPayFlag, "2"), - AmountCent: amountCent, - BizType: "wallet_recharge", - Status: "created", - } - if runtimeConfig.isMockMode() { - payment.ProviderOrderID = "MOCK" + paymentNo - payment.TDCode = "mock://payment/recharge/" + paymentNo - } - if err := r.db.WithContext(ctx).Create(&payment).Error; err != nil { - return nil, err - } - return &payment, nil -} + diff --git a/backend/internal/modules/payment/query.go b/backend/internal/modules/payment/query.go index f4c9a8a..90bf42d 100644 --- a/backend/internal/modules/payment/query.go +++ b/backend/internal/modules/payment/query.go @@ -6,39 +6,6 @@ import ( "hfb_sys/backend/internal/model" ) -func (r *Repository) QueryWalletRecharge(ctx context.Context, userID uint64, paymentID uint64) (*PaymentDTO, error) { - var payment model.PaymentOrder - if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ? AND order_id = 0", paymentID, userID).First(&payment).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, ErrPaymentNotFound - } - return nil, err - } - runtimeConfig, err := r.runtimeConfigForPayment(ctx, &payment) - if err != nil { - return nil, ErrPaymentUnavailable - } - if payment.Status == "paid" || runtimeConfig.isMockMode() { - dto := toDTO(payment) - return &dto, nil - } - if runtimeConfig.Channel == nil { - return nil, ErrPaymentUnavailable - } - resp, err := runtimeConfig.Channel.QueryPayment(ctx, payment.ThirdOrderID, payment.ProviderOrderID) - if err != nil { - return nil, err - } - if err := r.applyChannelStatus(ctx, &payment, resp.Status, resp.PayTime, resp.Raw, channelSourceQuery); err != nil { - return nil, err - } - latest, err := r.findPaymentByID(ctx, payment.ID) - if err != nil { - return nil, err - } - dto := toDTO(*latest) - return &dto, nil -} func (r *Repository) Query(ctx context.Context, userID uint64, orderID uint64) (*PaymentDTO, error) { var payment model.PaymentOrder if err := r.db.WithContext(ctx).Where("order_id = ? AND user_id = ? AND biz_type = ?", orderID, userID, "order_pay").Order("id DESC").First(&payment).Error; err != nil { diff --git a/backend/internal/modules/payment/repository.go b/backend/internal/modules/payment/repository.go index 8862618..da20095 100644 --- a/backend/internal/modules/payment/repository.go +++ b/backend/internal/modules/payment/repository.go @@ -8,14 +8,12 @@ import ( "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/internal/modules/paymentconfig" - "hfb_sys/backend/internal/modules/wallet" ) type Repository struct { db *gorm.DB configRepo *paymentconfig.Repository orderRepo *order.Repository - walletRepo *wallet.Repository } type runtimePaymentConfig struct { @@ -46,12 +44,11 @@ var refundBizTypes = []string{ "arbitration_refund", } -func NewRepository(db *gorm.DB, configRepo *paymentconfig.Repository, orderRepo *order.Repository, walletRepo *wallet.Repository) *Repository { +func NewRepository(db *gorm.DB, configRepo *paymentconfig.Repository, orderRepo *order.Repository) *Repository { return &Repository{ db: db, configRepo: configRepo, orderRepo: orderRepo, - walletRepo: walletRepo, } } func (c runtimePaymentConfig) isMockMode() bool { diff --git a/backend/internal/modules/payment/repository_integration_test.go b/backend/internal/modules/payment/repository_integration_test.go index 7cc4814..e9d8a07 100644 --- a/backend/internal/modules/payment/repository_integration_test.go +++ b/backend/internal/modules/payment/repository_integration_test.go @@ -271,39 +271,6 @@ func TestRefundAmountMustNotExceedOriginal(t *testing.T) { } } -// TestWalletRechargeEnabled 测试钱包充值开关 -func TestWalletRechargeEnabledInDevelopment(t *testing.T) { - svc := NewService(nil, "development") - if !svc.walletRechargeEnabled { - t.Fatal("wallet recharge should be enabled in development") - } -} - -func TestWalletRechargeDisabledInProduction(t *testing.T) { - svc := NewService(nil, "production") - if svc.walletRechargeEnabled { - t.Fatal("wallet recharge should be disabled in production") - } -} - -func TestWalletRechargeEnabledInTestEnv(t *testing.T) { - svc := NewService(nil, "test") - if !svc.walletRechargeEnabled { - t.Fatal("wallet recharge should be enabled in test env") - } -} - -// TestMinWalletRechargeAmountIsReasonable 测试最小充值金额合理性 -func TestMinWalletRechargeAmountIsReasonable(t *testing.T) { - if MinWalletRechargeAmount <= 0 { - t.Fatal("MinWalletRechargeAmount should be positive") - } - - if MinWalletRechargeAmount > 1.0 { - t.Fatalf("MinWalletRechargeAmount = %.2f, seems too high for minimum", MinWalletRechargeAmount) - } -} - // TestPaymentNotifyResultStructure 测试支付回调结果结构 func TestNotifyResultHasRequiredFields(t *testing.T) { result := NotifyResult{ diff --git a/backend/internal/modules/payment/repository_logic_test.go b/backend/internal/modules/payment/repository_logic_test.go index c663f91..b5b7098 100644 --- a/backend/internal/modules/payment/repository_logic_test.go +++ b/backend/internal/modules/payment/repository_logic_test.go @@ -146,13 +146,6 @@ func TestPaymentNoUniquenessAssumption(t *testing.T) { } } -// TestMinWalletRechargeAmount 测试最小充值金额常量 -func TestMinWalletRechargeAmountIsPositive(t *testing.T) { - if MinWalletRechargeAmount <= 0 { - t.Fatal("MinWalletRechargeAmount should be positive") - } -} - // TestPaymentDTOValidation 测试支付 DTO 基本结构 func TestPaymentDTOHasRequiredFields(t *testing.T) { dto := PaymentDTO{ @@ -293,7 +286,6 @@ func TestPaymentErrorsAreDefined(t *testing.T) { ErrPaymentVerifyFailed, ErrPaymentNotFound, ErrRefundCannotStart, - ErrWalletRechargeDisabled, } for i, err := range errors { diff --git a/backend/internal/modules/payment/service.go b/backend/internal/modules/payment/service.go index d2136be..a2aba2f 100644 --- a/backend/internal/modules/payment/service.go +++ b/backend/internal/modules/payment/service.go @@ -3,35 +3,23 @@ package payment import ( "context" "errors" - "strings" ) var ( - ErrDependencyUnavailable = errors.New("dependency unavailable") - ErrPaymentUnavailable = errors.New("payment unavailable") - ErrPaymentCannotStart = errors.New("payment cannot start") - ErrPaymentVerifyFailed = errors.New("payment verify failed") - ErrPaymentNotFound = errors.New("payment not found") - ErrRefundCannotStart = errors.New("refund cannot start") - ErrWalletRechargeDisabled = errors.New("wallet recharge disabled") + ErrDependencyUnavailable = errors.New("dependency unavailable") + ErrPaymentUnavailable = errors.New("payment unavailable") + ErrPaymentCannotStart = errors.New("payment cannot start") + ErrPaymentVerifyFailed = errors.New("payment verify failed") + ErrPaymentNotFound = errors.New("payment not found") + ErrRefundCannotStart = errors.New("refund cannot start") ) -const MinWalletRechargeAmount = 0.01 - type Service struct { - repo *Repository - walletRechargeEnabled bool + repo *Repository } -func NewService(repo *Repository, appEnv ...string) *Service { - env := "production" - if len(appEnv) > 0 { - env = strings.ToLower(strings.TrimSpace(appEnv[0])) - } - return &Service{ - repo: repo, - walletRechargeEnabled: env != "production", - } +func NewService(repo *Repository) *Service { + return &Service{repo: repo} } func (s *Service) Start(ctx context.Context, userID uint64, orderID uint64, req StartPaymentRequest, clientIP string) (*PaymentDTO, error) { @@ -54,26 +42,6 @@ func (s *Service) Query(ctx context.Context, userID uint64, orderID uint64) (*Pa return s.repo.Query(ctx, userID, orderID) } -func (s *Service) StartWalletRecharge(ctx context.Context, userID uint64, req WalletRechargePaymentRequest, clientIP string) (*PaymentDTO, error) { - if s.repo == nil { - return nil, ErrDependencyUnavailable - } - if !s.walletRechargeEnabled { - return nil, ErrWalletRechargeDisabled - } - return s.repo.StartWalletRecharge(ctx, userID, req, clientIP) -} - -func (s *Service) QueryWalletRecharge(ctx context.Context, userID uint64, paymentID uint64) (*PaymentDTO, error) { - if s.repo == nil { - return nil, ErrDependencyUnavailable - } - if userID == 0 || paymentID == 0 { - return nil, ErrPaymentNotFound - } - return s.repo.QueryWalletRecharge(ctx, userID, paymentID) -} - func (s *Service) HandleLeshuaNotify(ctx context.Context, params map[string]string, rawPayload string, contentType string) (*NotifyResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/modules/payment/service_test.go b/backend/internal/modules/payment/service_test.go index 04ab749..0c66679 100644 --- a/backend/internal/modules/payment/service_test.go +++ b/backend/internal/modules/payment/service_test.go @@ -71,21 +71,6 @@ func TestServiceStartRefundWithInvalidParams(t *testing.T) { } } -func TestServiceWalletRechargeDisabledInProduction(t *testing.T) { - svc := NewService(&Repository{}, "production") - _, err := svc.StartWalletRecharge(t.Context(), 1, WalletRechargePaymentRequest{AmountCent: 1000}, "127.0.0.1") - if !errors.Is(err, ErrWalletRechargeDisabled) { - t.Fatalf("StartWalletRecharge() error = %v, want ErrWalletRechargeDisabled", err) - } -} - -func TestServiceWalletRechargeEnabledInDevelopment(t *testing.T) { - svc := NewService(&Repository{}, "development") - if svc.walletRechargeEnabled != true { - t.Fatal("walletRechargeEnabled should be true in development") - } -} - // TestRefundBizTypeConstants 测试退款业务类型常量 func TestRefundBizTypesContainsExpectedValues(t *testing.T) { expected := []string{ diff --git a/backend/internal/modules/wallet/dto.go b/backend/internal/modules/wallet/dto.go index ca3979c..a945369 100644 --- a/backend/internal/modules/wallet/dto.go +++ b/backend/internal/modules/wallet/dto.go @@ -9,10 +9,6 @@ type AccountDTO struct { Status string `json:"status"` } -type RechargeRequest struct { - AmountCent int64 `json:"amount_cent" binding:"required"` -} - type WithdrawRequest struct { AmountCent int64 `json:"amount_cent" binding:"required"` } diff --git a/backend/internal/modules/wallet/handler.go b/backend/internal/modules/wallet/handler.go index 80f00b8..01a6e5f 100644 --- a/backend/internal/modules/wallet/handler.go +++ b/backend/internal/modules/wallet/handler.go @@ -62,25 +62,6 @@ func (h *Handler) Ledger(c *gin.Context) { response.OK(c, result) } -func (h *Handler) Recharge(c *gin.Context) { - userID, ok := currentUserID(c) - if !ok { - response.Unauthorized(c, "缺少用户上下文") - return - } - var req RechargeRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "充值金额不正确") - return - } - account, err := h.service.Recharge(c.Request.Context(), userID, req) - if err != nil { - writeWalletError(c, err) - return - } - response.OK(c, account) -} - func (h *Handler) Withdraw(c *gin.Context) { userID, ok := currentUserID(c) if !ok { @@ -155,8 +136,6 @@ func writeWalletError(c *gin.Context, err error) { response.BadRequest(c, "充值金额不正确") case errors.Is(err, ErrInsufficientBalance): response.Error(c, 409, "insufficient_balance", "钱包余额不足") - case errors.Is(err, ErrRechargeDisabled): - response.Error(c, 410, "wallet_recharge_disabled", "钱包充值已关闭") case errors.Is(err, ErrFeaturePending): response.Error(c, 501, "feature_pending", "提现功能待开发") default: diff --git a/backend/internal/modules/wallet/repository.go b/backend/internal/modules/wallet/repository.go index e5a40dc..7b20129 100644 --- a/backend/internal/modules/wallet/repository.go +++ b/backend/internal/modules/wallet/repository.go @@ -64,59 +64,6 @@ func (r *Repository) Ledger(ctx context.Context, userID uint64, page, pageSize i return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } -func (r *Repository) Recharge(ctx context.Context, userID uint64, amountCent int64) (*AccountDTO, error) { - err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - return AppendEntries(tx, Entry{ - UserID: userID, - Direction: "in", - AmountCent: amountCent, - BalanceType: "available", - BizType: "dev_recharge", - BizNo: "DEV", - Remark: "开发环境充值", - }) - }) - if err != nil { - return nil, err - } - return r.Account(ctx, userID) -} - -func (r *Repository) ConfirmRechargeFromChannel(ctx context.Context, userID uint64, bizNo string, amountCent int64) error { - if userID == 0 || amountCent <= 0 || bizNo == "" { - return ErrInvalidAmount - } - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if err := ensureAccount(tx, userID); err != nil { - return err - } - var account model.WalletAccount - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where("user_id = ?", userID). - First(&account).Error; err != nil { - return err - } - var existing int64 - if err := tx.Model(&model.WalletLedger{}). - Where("user_id = ? AND biz_type = ? AND biz_no = ?", userID, "channel_recharge", bizNo). - Count(&existing).Error; err != nil { - return err - } - if existing > 0 { - return nil - } - return AppendEntries(tx, Entry{ - UserID: userID, - Direction: "in", - AmountCent: amountCent, - BalanceType: "available", - BizType: "channel_recharge", - BizNo: bizNo, - Remark: "渠道充值入账", - }) - }) -} - // Withdraw 保留仓库能力;当前公开提现入口在 service 层标记为待开发,不会调用到这里。 func (r *Repository) Withdraw(ctx context.Context, userID uint64, amountCent int64) (*AccountDTO, error) { if userID == 0 || amountCent <= 0 { diff --git a/backend/internal/modules/wallet/repository_integration_test.go b/backend/internal/modules/wallet/repository_integration_test.go index 58aeeac..daa510c 100644 --- a/backend/internal/modules/wallet/repository_integration_test.go +++ b/backend/internal/modules/wallet/repository_integration_test.go @@ -64,113 +64,6 @@ func TestRepositoryAccountCreatesAccountIfNotExists(t *testing.T) { } } -// TestRepositoryRechargeIncreasesAvailableBalance 测试充值增加可用余额 -func TestRepositoryRechargeIncreasesAvailableBalance(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewRepository(db) - ctx := context.Background() - userID := uint64(1002) - - // 第一次充值 - account, err := repo.Recharge(ctx, userID, 10000) - if err != nil { - t.Fatalf("Recharge() error = %v", err) - } - if account.AvailableBalanceCent != 10000 { - t.Fatalf("第一次充值后余额 = %d, want 10000", account.AvailableBalanceCent) - } - - // 第二次充值 - account, err = repo.Recharge(ctx, userID, 5000) - if err != nil { - t.Fatalf("Recharge() error = %v", err) - } - if account.AvailableBalanceCent != 15000 { - t.Fatalf("第二次充值后余额 = %d, want 15000", account.AvailableBalanceCent) - } - - // 验证账本记录 - ledger, err := repo.Ledger(ctx, userID, 1, 10) - if err != nil { - t.Fatalf("Ledger() error = %v", err) - } - if ledger.Total != 2 { - t.Fatalf("账本记录数 = %d, want 2", ledger.Total) - } -} - -// TestRepositoryConfirmRechargeFromChannelIsIdempotent 测试渠道充值幂等性 -func TestRepositoryConfirmRechargeFromChannelIsIdempotent(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewRepository(db) - ctx := context.Background() - userID := uint64(1003) - bizNo := "PAY123456" - amount := int64(10000) - - // 第一次确认充值 - err := repo.ConfirmRechargeFromChannel(ctx, userID, bizNo, amount) - if err != nil { - t.Fatalf("第一次 ConfirmRechargeFromChannel() error = %v", err) - } - - account, _ := repo.Account(ctx, userID) - if account.AvailableBalanceCent != amount { - t.Fatalf("第一次充值后余额 = %d, want %d", account.AvailableBalanceCent, amount) - } - - // 第二次确认充值(相同 bizNo)应该幂等,不重复入账 - err = repo.ConfirmRechargeFromChannel(ctx, userID, bizNo, amount) - if err != nil { - t.Fatalf("第二次 ConfirmRechargeFromChannel() error = %v", err) - } - - account, _ = repo.Account(ctx, userID) - if account.AvailableBalanceCent != amount { - t.Fatalf("第二次充值后余额 = %d, want %d(应保持不变)", account.AvailableBalanceCent, amount) - } - - // 验证只有一条账本记录 - ledger, _ := repo.Ledger(ctx, userID, 1, 10) - if ledger.Total != 1 { - t.Fatalf("账本记录数 = %d, want 1(幂等)", ledger.Total) - } -} - -// TestRepositoryConfirmRechargeFromChannelRejectsInvalidParams 测试参数验证 -func TestRepositoryConfirmRechargeFromChannelRejectsInvalidParams(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewRepository(db) - - testCases := []struct { - name string - userID uint64 - bizNo string - amount int64 - wantError error - }{ - {"userID 为 0", 0, "BIZ123", 1000, ErrInvalidAmount}, - {"bizNo 为空", 1004, "", 1000, ErrInvalidAmount}, - {"amount 为 0", 1004, "BIZ123", 0, ErrInvalidAmount}, - {"amount 为负数", 1004, "BIZ123", -1000, ErrInvalidAmount}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := repo.ConfirmRechargeFromChannel(context.Background(), tc.userID, tc.bizNo, tc.amount) - if err != tc.wantError { - t.Fatalf("error = %v, want %v", err, tc.wantError) - } - }) - } -} - // TestAppendEntriesUpdatesBalanceCorrectly 测试 AppendEntries 余额计算 func TestAppendEntriesUpdatesBalanceCorrectly(t *testing.T) { db := setupTestDB(t) diff --git a/backend/internal/modules/wallet/service.go b/backend/internal/modules/wallet/service.go index d9aef74..fa96afe 100644 --- a/backend/internal/modules/wallet/service.go +++ b/backend/internal/modules/wallet/service.go @@ -10,12 +10,8 @@ var ( ErrInvalidAmount = errors.New("invalid amount") ErrInsufficientBalance = errors.New("insufficient balance") ErrFeaturePending = errors.New("feature pending") - ErrRechargeDisabled = errors.New("wallet recharge disabled") ) -// MinRechargeAmountCent 最小充值金额:1分 -const MinRechargeAmountCent = 1 - type Service struct { repo *Repository } @@ -38,13 +34,6 @@ func (s *Service) Ledger(ctx context.Context, userID uint64, page, pageSize int) return s.repo.Ledger(ctx, userID, page, pageSize) } -func (s *Service) Recharge(ctx context.Context, userID uint64, req RechargeRequest) (*AccountDTO, error) { - if s.repo == nil { - return nil, ErrDependencyUnavailable - } - return nil, ErrRechargeDisabled -} - func (s *Service) Withdraw(ctx context.Context, userID uint64, req WithdrawRequest) (*AccountDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/modules/wallet/service_test.go b/backend/internal/modules/wallet/service_test.go index f1b9ac0..d1e0db9 100644 --- a/backend/internal/modules/wallet/service_test.go +++ b/backend/internal/modules/wallet/service_test.go @@ -25,15 +25,6 @@ func TestServiceLedgerWithNilRepo(t *testing.T) { } } -func TestServiceRechargeIsDisabled(t *testing.T) { - svc := &Service{repo: &Repository{}} - ctx := context.Background() - _, err := svc.Recharge(ctx, 1, RechargeRequest{AmountCent: 100}) - if !errors.Is(err, ErrRechargeDisabled) { - t.Fatalf("Recharge() error = %v, want ErrRechargeDisabled", err) - } -} - func TestServiceWithdrawIsPending(t *testing.T) { svc := &Service{repo: &Repository{}} ctx := context.Background() diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index ae45e67..92c3a06 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -186,9 +186,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } if deps.DB != nil { - paymentRepo = payment.NewRepository(deps.DB, paymentConfigRepo, orderRepo, walletRepo) + paymentRepo = payment.NewRepository(deps.DB, paymentConfigRepo, orderRepo) } - paymentService := payment.NewService(paymentRepo, cfg.AppEnv) + paymentService := payment.NewService(paymentRepo) paymentHandler := payment.NewHandler(paymentService) var notificationRepo *notification.Repository if deps.DB != nil { @@ -363,9 +363,6 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { { walletRoutes.GET("/balance", walletHandler.Balance) walletRoutes.GET("/ledger", walletHandler.Ledger) - walletRoutes.POST("/recharge", walletHandler.Recharge) - walletRoutes.POST("/recharge/pay", paymentHandler.WalletRecharge) - walletRoutes.POST("/recharge/pay/:id/query", paymentHandler.WalletRechargeQuery) walletRoutes.POST("/withdraw", walletHandler.Withdraw) } diff --git a/frontend/src/features/admin/views/AdminOrderDetailView.vue b/frontend/src/features/admin/views/AdminOrderDetailView.vue index 86d1906..fb3b63a 100644 --- a/frontend/src/features/admin/views/AdminOrderDetailView.vue +++ b/frontend/src/features/admin/views/AdminOrderDetailView.vue @@ -162,7 +162,6 @@ function paymentBizTypeLabel(type: string) { admin_refund: '人工退款', cancel_refund: '取消退款', admin_close_refund: '客服关闭退款', - wallet_recharge: '钱包充值', } return map[type] || type } diff --git a/frontend/src/features/admin/views/AdminPaymentsView.vue b/frontend/src/features/admin/views/AdminPaymentsView.vue index 074599b..307d863 100644 --- a/frontend/src/features/admin/views/AdminPaymentsView.vue +++ b/frontend/src/features/admin/views/AdminPaymentsView.vue @@ -26,7 +26,7 @@ const filters = reactive({ const payAmount = computed(() => payments.value - .filter(item => item.biz_type === 'order_pay' || item.biz_type === 'wallet_recharge') + .filter(item => item.biz_type === 'order_pay') .reduce((sum, item) => sum + Number(item.amount_cent || 0), 0) ) const refundAmount = computed(() => @@ -97,7 +97,6 @@ function paymentStatusLabel(status: string) { function bizTypeLabel(type: string) { const map: Record = { order_pay: '订单支付', - wallet_recharge: '钱包充值', cancel_refund: '取消退款', admin_close_refund: '客服关闭退款', admin_refund: '人工退款', @@ -172,7 +171,6 @@ function jsonText(value: unknown) { - diff --git a/frontend/src/features/wallet/api/wallet.ts b/frontend/src/features/wallet/api/wallet.ts index f9a6389..8b76ad7 100644 --- a/frontend/src/features/wallet/api/wallet.ts +++ b/frontend/src/features/wallet/api/wallet.ts @@ -2,8 +2,6 @@ import { apiClient } from '@/shared/api/client' import type { ApiResponse, PaginatedResult } from '@/shared/types/types' import type { BalanceType, LedgerDirection, WalletStatus } from '@/shared/types/status' -import type { PaymentOrder } from '@/features/orders/api/orders' -import { yuanToCent } from '@/shared/utils/money' export interface WalletAccount { user_id: number @@ -41,26 +39,3 @@ export async function fetchWalletLedger(page = 1, pageSize = 20) { ) return data.data } - -export async function rechargeWallet(amountYuan: number) { - const amount_cent = yuanToCent(amountYuan) - const { data } = await apiClient.post>('/wallet/recharge', { - amount_cent, - }) - return data.data -} - -export async function startWalletRechargePayment(amountYuan: number) { - const amount_cent = yuanToCent(amountYuan) - const { data } = await apiClient.post>('/wallet/recharge/pay', { - amount_cent, - }) - return data.data -} - -export async function queryWalletRechargePayment(id: number) { - const { data } = await apiClient.post>( - `/wallet/recharge/pay/${id}/query` - ) - return data.data -} diff --git a/frontend/src/features/wallet/views/WalletView.vue b/frontend/src/features/wallet/views/WalletView.vue index 6c2c89d..af667d9 100644 --- a/frontend/src/features/wallet/views/WalletView.vue +++ b/frontend/src/features/wallet/views/WalletView.vue @@ -1,36 +1,28 @@