有效天数问题,充值单回调问题

This commit is contained in:
2026-05-08 11:46:34 +08:00
parent 7c85a8cf29
commit a93acc0784
12 changed files with 402 additions and 87 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
@@ -560,10 +561,7 @@ func buildClientPackageItem(
return dto.ClientPackageItem{}, false
}
validityDays := pkg.DurationDays
if validityDays <= 0 && pkg.DurationMonths > 0 {
validityDays = pkg.DurationMonths * 30
}
validityDays := packagepkg.CalculateValidityDays(pkg.CalendarType, time.Now(), pkg.DurationMonths, pkg.DurationDays)
dataAllowance := pkg.RealDataMB
if dataAllowance <= 0 {

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
@@ -16,6 +17,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"github.com/gofiber/fiber/v2"
@@ -300,12 +302,13 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
rechargeNo := generateClientRechargeNo()
paymentNo := generateClientPaymentNo()
// 先初始化微信支付实例并创建预支付订单,确认支付通道可用
// 先初始化生效支付通道并创建预支付订单,确认支付通道可用
// 避免先写入充值记录后支付初始化失败,导致产生孤儿记录
payResult, err := h.createJSAPIPayOrder(
payConfig, err := h.createClientRechargePayConfig(
resolved.SkipPermissionCtx,
config,
appID,
req.AppType,
paymentNo,
"资产钱包充值",
openID,
@@ -347,7 +350,6 @@ func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
}
payConfig := buildClientRechargePayConfig(appID, payResult)
resp := &dto.ClientRechargeResponse{
Recharge: dto.ClientRechargeResult{
RechargeID: rechargeOrder.ID,
@@ -607,33 +609,107 @@ func pickAppIDByType(config *model.WechatConfig, appType string) (string, error)
}
}
// createJSAPIPayOrder 根据支付配置的提供者类型路由到对应支付通道,发起 JSAPI 预下单。
// 与 client_order.Service.newPaymentProvider 保持相同的路由逻辑,支持 wechat_v2 和 wechatv3
func (h *ClientWalletHandler) createJSAPIPayOrder(
// createClientRechargePayConfig 根据当前生效支付配置创建充值预支付订单。
// provider_type 必须显式匹配,避免富友等渠道误落到微信直连
func (h *ClientWalletHandler) createClientRechargePayConfig(
ctx context.Context,
config *model.WechatConfig,
appID, orderNo, description, openID string,
appID, appType, orderNo, description, openID string,
amount int,
) (*wechat.JSAPIPayResult, error) {
) (dto.ClientRechargePayConfig, error) {
switch config.ProviderType {
case model.ProviderTypeWechatV2:
// 微信 v2仅需 APIv2Key无需证书序列号
svc, err := wechat.NewPaymentV2ServiceFromConfig(config, appID, h.logger)
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "初始化微信 v2 支付实例失败")
return dto.ClientRechargePayConfig{}, errors.Wrap(errors.CodeWechatPayFailed, err, "初始化微信 v2 支付实例失败")
}
return svc.CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
default:
result, err := svc.CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
if err != nil {
return dto.ClientRechargePayConfig{}, err
}
return buildClientRechargePayConfig(appID, result), nil
case model.ProviderTypeWechat:
// 微信 v3PowerWeChat需要证书序列号和 API v3 Key
cache := wechat.NewRedisCache(h.redis)
paymentApp, err := wechat.NewPaymentAppFromConfig(config, appID, cache, h.logger)
if err != nil {
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "初始化微信支付实例失败")
return dto.ClientRechargePayConfig{}, errors.Wrap(errors.CodeWechatPayFailed, err, "初始化微信支付实例失败")
}
return wechat.NewPaymentService(paymentApp, h.logger).CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
result, err := wechat.NewPaymentService(paymentApp, h.logger).CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
if err != nil {
return dto.ClientRechargePayConfig{}, err
}
return buildClientRechargePayConfig(appID, result), nil
case model.ProviderTypeFuiou:
return h.createFuiouRechargePayConfig(ctx, config, appID, appType, orderNo, description, openID, amount)
default:
return dto.ClientRechargePayConfig{}, errors.New(errors.CodeWechatConfigUnavailable, "不支持的支付渠道类型")
}
}
// createFuiouRechargePayConfig 使用富友支付配置创建充值预支付订单。
func (h *ClientWalletHandler) createFuiouRechargePayConfig(
ctx context.Context,
config *model.WechatConfig,
appID, appType, orderNo, description, openID string,
amount int,
) (dto.ClientRechargePayConfig, error) {
if strings.TrimSpace(config.FyInsCd) == "" ||
strings.TrimSpace(config.FyMchntCd) == "" ||
strings.TrimSpace(config.FyTermID) == "" ||
strings.TrimSpace(config.FyPrivateKey) == "" ||
strings.TrimSpace(config.FyPublicKey) == "" ||
strings.TrimSpace(config.FyAPIURL) == "" ||
strings.TrimSpace(config.FyNotifyURL) == "" ||
strings.TrimSpace(appID) == "" {
return dto.ClientRechargePayConfig{}, errors.New(errors.CodeFuiouPayFailed, "富友支付配置不完整")
}
client, err := fuiou.NewClient(
config.FyInsCd,
config.FyMchntCd,
config.FyTermID,
config.FyAPIURL,
config.FyNotifyURL,
config.FyPrivateKey,
config.FyPublicKey,
h.logger,
)
if err != nil {
return dto.ClientRechargePayConfig{}, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友支付客户端失败")
}
tradeType := "JSAPI"
if appType == "miniapp" {
tradeType = "LETPAY"
}
termIP := fuiou.GetServerIP()
if ip, ok := ctx.Value(constants.ContextKeyIP).(string); ok && strings.TrimSpace(ip) != "" {
termIP = ip
}
resp, err := client.WxPreCreate(orderNo, strconv.Itoa(amount), description, termIP, tradeType, appID, openID)
if err != nil {
h.logger.Error("富友充值预下单失败",
zap.String("payment_no", orderNo),
zap.String("trade_type", tradeType),
zap.Error(err),
)
return dto.ClientRechargePayConfig{}, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
}
return dto.ClientRechargePayConfig{
AppID: resp.SdkAppid,
Timestamp: resp.SdkTimestamp,
NonceStr: resp.SdkNoncestr,
PackageVal: resp.SdkPackage,
SignType: resp.SdkSigntype,
PaySign: resp.SdkPaysign,
}, nil
}
func generateClientRechargeNo() string {
timestamp := time.Now().Format("20060102150405")
randomNum := rand.Intn(1000000)

View File

@@ -260,18 +260,25 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
// FuiouPayCallback 富友支付回调(带签名验证)
// POST /api/callback/fuiou-pay
func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
body := c.Body()
if len(body) == 0 {
body, bodySource := fuiouCallbackPayload(c)
if strings.TrimSpace(string(body)) == "" {
return errors.New(errors.CodeFuiouCallbackInvalid, "回调请求体为空")
}
ctx := c.UserContext()
c.Set("Content-Type", "text/xml; charset=gbk")
c.Set("Content-Type", "text/plain; charset=utf-8")
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
preNotify, err := fuiou.ParseNotify(body)
if err != nil {
h.logger.Error("富友回调:预解析失败", zap.Error(err))
h.logger.Error("富友回调:预解析失败",
zap.Error(err),
zap.String("payload_source", bodySource),
zap.Int("raw_body_len", len(c.Body())),
zap.Int("payload_len", len(body)),
zap.String("content_type", c.Get("Content-Type")),
zap.String("query", string(c.Request().URI().QueryString())),
)
return c.Send(fuiou.BuildNotifyFailResponse("parse failed"))
}
@@ -283,6 +290,21 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
)
return c.Send(fuiou.BuildNotifyFailResponse("payment config unavailable"))
}
if cfg.ProviderType != model.ProviderTypeFuiou ||
strings.TrimSpace(preNotify.InsCd) != strings.TrimSpace(cfg.FyInsCd) ||
strings.TrimSpace(preNotify.MchntCd) != strings.TrimSpace(cfg.FyMchntCd) {
h.logger.Error("富友回调:支付配置与回调商户不匹配",
zap.String("order_no", preNotify.MchntOrderNo),
zap.Uint("config_id", cfg.ID),
zap.String("config_name", cfg.Name),
zap.String("callback_ins_cd", preNotify.InsCd),
zap.String("config_ins_cd", cfg.FyInsCd),
zap.String("callback_mchnt_cd", preNotify.MchntCd),
zap.String("config_mchnt_cd", cfg.FyMchntCd),
zap.String("provider_type", cfg.ProviderType),
)
return c.Send(fuiou.BuildNotifyFailResponse("payment config mismatch"))
}
client, err := fuiou.NewClient(
cfg.FyInsCd,
@@ -307,7 +329,16 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
zap.String("result_msg", notify.ResultMsg))
return c.Send(fuiou.BuildNotifySuccessResponse())
}
h.logger.Error("富友回调:验签或解析失败", zap.Error(err))
h.logger.Error("富友回调:验签或解析失败",
zap.Error(err),
zap.String("order_no", preNotify.MchntOrderNo),
zap.Uint("config_id", cfg.ID),
zap.String("config_name", cfg.Name),
zap.String("callback_ins_cd", preNotify.InsCd),
zap.String("config_ins_cd", cfg.FyInsCd),
zap.String("callback_mchnt_cd", preNotify.MchntCd),
zap.String("config_mchnt_cd", cfg.FyMchntCd),
)
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
}
@@ -345,3 +376,14 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
return c.Send(fuiou.BuildNotifySuccessResponse())
}
// fuiouCallbackPayload 提取富友回调载荷,兼容 body、form req 和 query req 三种来源。
func fuiouCallbackPayload(c *fiber.Ctx) ([]byte, string) {
if req := strings.TrimSpace(c.FormValue("req")); req != "" {
return []byte(req), "form:req"
}
if req := strings.TrimSpace(c.Query("req")); req != "" {
return []byte(req), "query:req"
}
return c.Body(), "body"
}