feat(billing): 支付 P5.2 —— 微信支付 Native 渠道(扫码充值)
wechatpay-go v0.2.21。凭据全 env 注入(WECHAT_MCHID/MCH_CERT_SERIAL/
MCH_PRIVATE_KEY/APIV3_KEY/APPID/NOTIFY_URL),缺一渠道即隐藏——半配置/假凭据
只打日志不拖垮 gateway(用假私钥实测过降级)。
- internal/payment:Native 下单出 code_url、APIv3 回调验签解密、主动查单,
三者统一收敛为 QueryResult。
- 下单 POST /billing/orders {pack_id}(≥member+审计):金额/积分按在售包服务端
锁定进订单行,不信任客户端;渠道下单失败即作废,不留付不了的 pending。
- 到账两条路汇入同一个 MarkOrderPaid 幂等闸(CAS+唯一索引双闸,同 P5.1):
①公开回调路由(验签是唯一的门;金额与订单不符不入账);②前端轮询的
GET /billing/orders/:id 在 pending 时顺路主动查单——本地/内网收不到公网
回调也能确认到账,回调只是生产更快的通道。pending 超 30 分钟置 expired。
- Web 面:在售包卡片(渠道亮才出现)→扫码弹窗(qrcode 画 code_url,二维码底色
固定纯白——暗色主题下低对比码扫不出来)→2.5s 轮询→到账 toast+刷余额。
验证:go/tsc/vitest 全绿;无凭据+假凭据两种降级 live 四连
(channels 只剩 redeem/下单 400 引导兑换码/回调 503/兑换码闭环不受影响)。
⚠️ 真通道(prepay→扫码→回调/查单→入账)需真实商户号,未 live——用户配好
env 后用小额包实测,建议先配 ¥0.01 测试包走一单。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/sundynix/sundynix-gateway/internal/handler"
|
||||
"github.com/sundynix/sundynix-gateway/internal/middleware"
|
||||
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||
"github.com/sundynix/sundynix-gateway/internal/payment"
|
||||
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||
)
|
||||
|
||||
@@ -31,7 +33,8 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
r.Use(middleware.RateLimit(cache)) // 已认证按用户限流,否则按 IP(企业网多人共享 IP 不再互相拖累)
|
||||
r.Use(middleware.Guardrail(db)) // Harness: Input Guardrail(命中落库 guardrail_event)
|
||||
|
||||
h := handler.New(db, cache, bus, blobStore)
|
||||
// 微信支付渠道按 env 装配;未配置返回 nil → 渠道自动隐藏,下单路由 400 引导用兑换码。
|
||||
h := handler.New(db, cache, bus, blobStore).WithWechat(payment.NewWechatFromEnv(context.Background()))
|
||||
|
||||
// 可观测性根端点:Prometheus 抓取 + k8s 存活/就绪探针(不挂业务中间件鉴权)。
|
||||
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
||||
@@ -50,6 +53,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(job_id 寻址)
|
||||
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址)
|
||||
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx)
|
||||
api.POST("/billing/callback/wechat", h.WechatCallback) // 支付回调(渠道服务器带不了 Bearer;APIv3 验签是唯一的门)
|
||||
|
||||
// —— 受保护:owner 作用域业务,必须携带有效 JWT ——
|
||||
p := api.Group("", middleware.RequireAuth())
|
||||
@@ -105,10 +109,12 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.POST("/spaces/:id/archive", h.SpaceArchive) // 归档空间
|
||||
p.POST("/reports", middleware.RequireTenantRole(db, store.RoleMember), h.GenerateReport) // 报告生成(同样烧租户积分):viewer 只读拦下
|
||||
p.GET("/billing", h.Billing)
|
||||
// 充值(P5.1 兑换码):核销会动钱,≥member + 审计;查询全员可看。
|
||||
// 充值(P5.1 兑换码 + P5.2 微信 Native):动钱的 ≥member + 审计;查询全员可看。
|
||||
p.GET("/billing/packs", h.BillingPacks)
|
||||
p.GET("/billing/orders", h.BillingOrders)
|
||||
p.GET("/billing/orders/:id", h.BillingOrderStatus) // 轮询单态(pending 时顺路主动查单确认)
|
||||
p.POST("/billing/redeem", middleware.RequireTenantRole(db, store.RoleMember), middleware.Audit(db), h.BillingRedeem)
|
||||
p.POST("/billing/orders", middleware.RequireTenantRole(db, store.RoleMember), middleware.Audit(db), h.BillingCreateOrder)
|
||||
p.GET("/stats/overview", h.StatsOverview) // 工作台仪表盘聚合
|
||||
p.GET("/runs", h.Runs) // 运行历史(复盘)
|
||||
p.GET("/tasks/:id/replay", h.TaskReplay) // 历史运行复盘(持久化输出+轨迹,免 Redis TTL)
|
||||
|
||||
Reference in New Issue
Block a user