package service import ( "bytes" "context" "fmt" "math" "net/http" "net/url" "os" "strconv" "sync" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/smartwalle/alipay/v3" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" "github.com/wechatpay-apiv3/wechatpay-go/core/notify" "github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/services/payments" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" "github.com/wechatpay-apiv3/wechatpay-go/utils" "observer-server/biz/consts" "observer-server/biz/dao" "observer-server/biz/model/dto" "observer-server/biz/model/entity" "observer-server/common" ) // paymentService 支付渠道接入:统一下单、回调验签落授权(回调是唯一授权来源,confirm 仅加速刷新) type paymentService struct{} var Payment = &paymentService{} // ensureChannelConfigured 渠道未配置时拒绝下单(避免产生无法支付的订单) func (s *paymentService) ensureChannelConfigured(ctx context.Context, channel string) error { switch channel { case consts.ChannelWechat: if !wechatConf(ctx).configured() { return gerror.NewCode(common.CodePaymentNotConfigured, "微信支付未配置") } case consts.ChannelAlipay: if !alipayConf(ctx).configured() { return gerror.NewCode(common.CodePaymentNotConfigured, "支付宝未配置") } } return nil } // CreatePayParams 渠道统一下单,返回客户端拉起支付参数 func (s *paymentService) CreatePayParams(ctx context.Context, order *entity.PaymentOrder, planLabel, channel string) (*dto.PayParams, error) { switch channel { case consts.ChannelWechat: return s.wechatPrepay(ctx, order, planLabel) case consts.ChannelAlipay: return s.alipayPrepay(ctx, order, planLabel) } return nil, gerror.Newf("未知支付渠道: %s", channel) } // ---------- 微信支付(APIv3 APP 支付) ---------- type wechatConfig struct { appid string mchid string apiV3Key string serialNo string privateKey string notifyUrl string } func wechatConf(ctx context.Context) wechatConfig { return wechatConfig{ appid: g.Cfg().MustGet(ctx, "payment.wechat.appid", "").String(), mchid: g.Cfg().MustGet(ctx, "payment.wechat.mchid", "").String(), apiV3Key: g.Cfg().MustGet(ctx, "payment.wechat.apiV3Key", "").String(), serialNo: g.Cfg().MustGet(ctx, "payment.wechat.serialNo", "").String(), privateKey: g.Cfg().MustGet(ctx, "payment.wechat.privateKey", "").String(), notifyUrl: g.Cfg().MustGet(ctx, "payment.wechat.notifyUrl", "").String(), } } func (c wechatConfig) configured() bool { return c.appid != "" && c.mchid != "" && c.apiV3Key != "" && c.serialNo != "" && c.privateKey != "" && c.notifyUrl != "" } var ( wechatOnce sync.Once wechatClient *core.Client wechatNotifyHdl *notify.Handler wechatErr error ) // wechatClientInstance 懒加载微信客户端与回调处理器(配置静态,进程内只初始化一次) func wechatClientInstance() (*core.Client, *notify.Handler, error) { wechatOnce.Do(func() { ctx := context.Background() cfg := wechatConf(ctx) if !cfg.configured() { wechatErr = gerror.NewCode(common.CodePaymentNotConfigured, "微信支付未配置") return } key, err := utils.LoadPrivateKeyWithPath(cfg.privateKey) if err != nil { wechatErr = gerror.Wrap(err, "加载微信商户私钥失败") return } client, err := core.NewClient(ctx, option.WithWechatPayAutoAuthCipher(cfg.mchid, cfg.serialNo, key, cfg.apiV3Key)) if err != nil { wechatErr = gerror.Wrap(err, "初始化微信支付客户端失败") return } visitor := downloader.MgrInstance().GetCertificateVisitor(cfg.mchid) handler, err := notify.NewRSANotifyHandler(cfg.apiV3Key, verifiers.NewSHA256WithRSAVerifier(visitor)) if err != nil { wechatErr = gerror.Wrap(err, "初始化微信回调处理器失败") return } wechatClient, wechatNotifyHdl = client, handler }) return wechatClient, wechatNotifyHdl, wechatErr } func (s *paymentService) wechatPrepay(ctx context.Context, order *entity.PaymentOrder, planLabel string) (*dto.PayParams, error) { cfg := wechatConf(ctx) client, _, err := wechatClientInstance() if err != nil { return nil, err } svc := app.AppApiService{Client: client} resp, _, err := svc.PrepayWithRequestPayment(ctx, app.PrepayRequest{ Appid: core.String(cfg.appid), Mchid: core.String(cfg.mchid), Description: core.String("视野会员-" + planLabel), OutTradeNo: core.String(order.OrderId), NotifyUrl: core.String(cfg.notifyUrl), Amount: &app.Amount{Total: core.Int64(order.AmountCents)}, }) if err != nil { return nil, gerror.Wrap(err, "微信统一下单失败") } return &dto.PayParams{ PartnerId: strDeref(resp.PartnerId), PrepayId: strDeref(resp.PrepayId), NonceStr: strDeref(resp.NonceStr), TimeStamp: strDeref(resp.TimeStamp), Sign: strDeref(resp.Sign), PackageValue: strDeref(resp.Package), }, nil } // WechatNotify 微信支付回调:APIv3 验签+解密 → 商户校验 → 落授权 func (s *paymentService) WechatNotify(ctx context.Context, header http.Header, body []byte) error { cfg := wechatConf(ctx) if !cfg.configured() { return gerror.NewCode(common.CodePaymentNotConfigured, "微信支付未配置") } _, handler, err := wechatClientInstance() if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, "", bytes.NewReader(body)) if err != nil { return gerror.Wrap(err, "构造微信回调请求失败") } req.Header = header.Clone() txn := new(payments.Transaction) if _, err := handler.ParseNotifyRequest(ctx, req, txn); err != nil { return gerror.NewCode(common.CodeCallbackVerifyFailed, "微信回调验签/解密失败: "+err.Error()) } // 商户校验:解密后校验 appid/mchid 与配置一致(防跨商户重放) if txn.Appid == nil || *txn.Appid != cfg.appid || txn.Mchid == nil || *txn.Mchid != cfg.mchid { return gerror.NewCode(common.CodeCallbackMismatch, "微信回调商户不匹配") } if txn.OutTradeNo == nil || txn.TransactionId == nil { return gerror.Newf("微信回调缺少订单号") } return s.persistPaid(ctx, *txn.OutTradeNo, *txn.TransactionId, consts.ChannelWechat, 0) } // ---------- 支付宝(APP 支付) ---------- type alipayConfig struct { appid string privateKey string alipayPublicKey string sellerId string notifyUrl string } func alipayConf(ctx context.Context) alipayConfig { return alipayConfig{ appid: g.Cfg().MustGet(ctx, "payment.alipay.appid", "").String(), privateKey: g.Cfg().MustGet(ctx, "payment.alipay.privateKey", "").String(), alipayPublicKey: g.Cfg().MustGet(ctx, "payment.alipay.alipayPublicKey", "").String(), sellerId: g.Cfg().MustGet(ctx, "payment.alipay.sellerId", "").String(), notifyUrl: g.Cfg().MustGet(ctx, "payment.alipay.notifyUrl", "").String(), } } func (c alipayConfig) configured() bool { return c.appid != "" && c.privateKey != "" && c.alipayPublicKey != "" && c.sellerId != "" && c.notifyUrl != "" } var ( alipayOnce sync.Once alipayClient *alipay.Client alipayErr error ) // alipayClientInstance 懒加载支付宝客户端(密钥文件路径 → PEM 内容) func alipayClientInstance() (*alipay.Client, error) { alipayOnce.Do(func() { ctx := context.Background() cfg := alipayConf(ctx) if !cfg.configured() { alipayErr = gerror.NewCode(common.CodePaymentNotConfigured, "支付宝未配置") return } pri, err := os.ReadFile(cfg.privateKey) if err != nil { alipayErr = gerror.Wrap(err, "读取支付宝应用私钥失败") return } client, err := alipay.New(cfg.appid, string(pri), true) if err != nil { alipayErr = gerror.Wrap(err, "初始化支付宝客户端失败") return } pub, err := os.ReadFile(cfg.alipayPublicKey) if err != nil { alipayErr = gerror.Wrap(err, "读取支付宝公钥失败") return } if err := client.LoadAliPayPublicKey(string(pub)); err != nil { alipayErr = gerror.Wrap(err, "加载支付宝公钥失败") return } alipayClient = client }) return alipayClient, alipayErr } func (s *paymentService) alipayPrepay(ctx context.Context, order *entity.PaymentOrder, planLabel string) (*dto.PayParams, error) { cfg := alipayConf(ctx) client, err := alipayClientInstance() if err != nil { return nil, err } // 支付宝接口金额以「元」字符串传输(外部网关契约;内部存储与传输一律整数分) pay := alipay.TradeAppPay{} pay.Subject = "视野会员-" + planLabel pay.OutTradeNo = order.OrderId pay.TotalAmount = strconv.FormatFloat(float64(order.AmountCents)/100, 'f', 2, 64) pay.ProductCode = "QUICK_MSECURITY_PAY" pay.NotifyURL = cfg.notifyUrl orderStr, err := client.TradeAppPay(pay) if err != nil { return nil, gerror.Wrap(err, "支付宝统一下单失败") } return &dto.PayParams{OrderStr: orderStr}, nil } // AlipayNotify 支付宝回调:RSA2 验签 → 商户/金额校验 → 落授权 func (s *paymentService) AlipayNotify(ctx context.Context, values url.Values) error { cfg := alipayConf(ctx) if !cfg.configured() { return gerror.NewCode(common.CodePaymentNotConfigured, "支付宝未配置") } client, err := alipayClientInstance() if err != nil { return err } if err := client.VerifySign(ctx, values); err != nil { return gerror.NewCode(common.CodeCallbackVerifyFailed, "支付宝回调验签失败: "+err.Error()) } // 商户校验:app_id/seller_id 与配置一致(防跨商户重放) if values.Get("app_id") != cfg.appid || values.Get("seller_id") != cfg.sellerId { return gerror.NewCode(common.CodeCallbackMismatch, "支付宝回调商户不匹配") } status := values.Get("trade_status") if status != "TRADE_SUCCESS" && status != "TRADE_FINISHED" { g.Log().Infof(ctx, "支付宝回调非终态 %s,忽略", status) return nil } outTradeNo := values.Get("out_trade_no") tradeNo := values.Get("trade_no") if outTradeNo == "" || tradeNo == "" { return gerror.Newf("支付宝回调缺少订单号") } amount, err := strconv.ParseFloat(values.Get("total_amount"), 64) if err != nil { return gerror.Newf("支付宝回调金额格式错误: %s", values.Get("total_amount")) } // 回调金额(元)转分后与订单快照比对 cents := int64(math.Round(amount * 100)) return s.persistPaid(ctx, outTradeNo, tradeNo, consts.ChannelAlipay, cents) } // ---------- 回调落授权(唯一授权来源) ---------- // persistPaid 回调落授权:「查订单 → 算 expiresAt → 更新订单 → 写 license → 清缓存」, // 整条链路在事务 + 单写者下串行执行(SQLite 无 WAL,串行同时保证 expiresAt 只按一次现授权 // 状态计算,叠加语义不被并发重复累加)。 // 幂等:订单已 paid 直接成功(重复回调忽略);closed 仅告警不落授权。 func (s *paymentService) persistPaid(ctx context.Context, outTradeNo, tradeNo, channel string, amountCents int64) error { var phoneNum string err := common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { o, err := dao.PaymentOrder.GetByPkInTx(ctx, tx, outTradeNo) if err != nil { return err } if o == nil { return gerror.Newf("回调订单 %s 不存在", outTradeNo) } if o.Status == consts.OrderStatusPaid { return nil // 幂等:重复回调直接成功 } if o.Status != consts.OrderStatusCreated { g.Log().Errorf(ctx, "回调订单 %s 状态 %s,跳过落授权", outTradeNo, o.Status) return nil } // 金额一致性校验(支付宝回调带金额;微信回调无金额字段、以微信侧为准) if channel == consts.ChannelAlipay && amountCents != o.AmountCents { return gerror.NewCode(common.CodeCallbackMismatch, fmt.Sprintf("回调金额 %d 与订单金额 %d 不一致", amountCents, o.AmountCents)) } plan, err := common.GetPlan(ctx, o.PlanId) if err != nil { return err } if plan == nil { return gerror.Newf("订单 %s 套餐 %s 不存在", outTradeNo, o.PlanId) } // 续费叠加:base = max(现有到期, 当前时间),已过期则从当前时间起算 lic, err := dao.License.GetByPhoneInTx(ctx, tx, o.PhoneNum) if err != nil { return err } base := time.Now() if lic != nil && lic.ExpiresAt != nil && lic.ExpiresAt.Time.After(base) { base = lic.ExpiresAt.Time } expiresAt := License.CalcExpiresAt(base, plan.Days) if channel == consts.ChannelWechat { err = dao.PaymentOrder.UpdatePaidWechatInTx(ctx, tx, o.OrderId, tradeNo) } else { err = dao.PaymentOrder.UpdatePaidAlipayInTx(ctx, tx, o.OrderId, tradeNo) } if err != nil { return err } phoneNum = o.PhoneNum return dao.License.UpsertAuthInTx(ctx, tx, o.PhoneNum, expiresAt, gtime.Now()) }) }) if err != nil { return err } // 提交成功后清缓存(提交前清可能被并发授权查询以旧值回填) common.ClearCache(ctx, "license:"+phoneNum) return nil } // strDeref 解指针(SDK 返回的 *string 字段可能为 nil) func strDeref(p *string) string { if p == nil { return "" } return *p }