Files
dudu/server/internal/httpapi/router.go
T
wangjia 8266c575aa
ci / server (push) Failing after 10s
ci / design-system (push) Failing after 10s
fix(auth): 固定验证码改为显式 AUTH_EMAIL_DEV_CODE 环境开关(安全评审反馈)
不再从 mailer 运行时类型推导认证材料:固定码仅当显式设置
AUTH_EMAIL_DEV_CODE 时生效(run-dev.sh 设 888888,生产部署不经
该脚本即为随机码),设置时启动打印醒目告警。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:42:11 +08:00

140 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package httpapi 路由装配。各业务模块在各自包内实现 handler,这里只负责挂载,
// 并按配置在真实外部依赖(微信/支付/OSS/ASR)与 mock 之间切换。
package httpapi
import (
"context"
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"dudu/server/internal/asr"
"dudu/server/internal/auth"
"dudu/server/internal/billing"
"dudu/server/internal/config"
"dudu/server/internal/feedback"
"dudu/server/internal/gateway"
"dudu/server/internal/quota"
"dudu/server/internal/telemetry"
"dudu/server/internal/user"
)
type Deps struct {
Cfg config.Config
DB *gorm.DB
RDB *redis.Client
JWT *auth.JWT
Quota *quota.Manager
}
// pickProvider 按配置选择 ASR Provider17E)。ASR_PROVIDER=gummy 且 DASHSCOPE_API_KEY
// 就绪时装配真实 gummy;缺 key 则告警并降级 mock。返回值 kind 为装配结果("gummy"/"mock")。
func pickProvider(cfg config.Config) (asr.Provider, string) {
switch cfg.ASRProvider {
case "gummy":
if cfg.DashScopeAPIKey != "" {
return asr.NewGummy(cfg.DashScopeAPIKey), "gummy"
}
slog.Warn("ASR_PROVIDER=gummy but DASHSCOPE_API_KEY empty, falling back to mock")
return asr.NewMock(), "mock"
default:
return asr.NewMock(), "mock"
}
}
// pickWechat 微信凭证未配置(#2A 申请中)时使用 mock。返回 kind="mock"(真实实现就绪前恒为 mock)。
func pickWechat(cfg config.Config) (auth.WechatClient, string) {
if cfg.WechatWebAppID == "" {
return auth.MockWechat{}, "mock"
}
return auth.MockWechat{}, "mock" // TODO(5C-真实): 开放平台实现,凭证就绪后替换
}
// pickMailer SMTP 未配置时使用 mock(验证码打日志,供联调)。返回 kind。
// ⚠️ 上线前必须配置真实 SMTP,否则邮箱登录形同虚设。
func pickMailer(cfg config.Config) (auth.Mailer, string) {
if cfg.SMTPHost == "" {
return auth.MockMailer{}, "mock"
}
return auth.MockMailer{}, "mock" // TODO(邮箱登录-真实): SMTP 实现,凭证就绪后替换
}
// pickPay 商户号未配置(#2B 申请中)时使用 mock。返回 kind="mock"。
// ⚠️⚠️ 上线前必须替换为真实 PayClientMockPay 不验签,/v1/pay/notify 等于公开充值接口。
func pickPay(cfg config.Config) (billing.PayClient, string) {
if cfg.WxPayMchID == "" {
return billing.MockPay{}, "mock"
}
// TODO(7D-真实)【上线阻断项】: wechatpay-go 实现 + APIv3 验签,商户号就绪后替换,否则充值接口可被伪造。
return billing.MockPay{}, "mock"
}
// pickStorage OSS 未配置时图片落本地盘。返回 kind="local"OSS 就绪后改为 "oss")。
func pickStorage(cfg config.Config) (feedback.Storage, string) {
if cfg.OSSBucket == "" {
return feedback.LocalStorage{Dir: "var/uploads"}, "local"
}
return feedback.LocalStorage{Dir: "var/uploads"}, "local" // TODO(8C-真实): OSS 实现
}
func Register(r *gin.Engine, d *Deps) {
r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) })
provider, providerKind := pickProvider(d.Cfg)
wechat, wechatKind := pickWechat(d.Cfg)
pay, payKind := pickPay(d.Cfg)
storage, storageKind := pickStorage(d.Cfg)
mailer, mailerKind := pickMailer(d.Cfg)
// 启动时打印各外部依赖的装配结果(real/mock),便于部署核对(17E)。
slog.Info("dependency assembly",
"asr_provider", providerKind, "wechat", wechatKind, "pay", payKind,
"storage", storageKind, "mailer", mailerKind)
if d.Cfg.EmailDevCode != "" {
slog.Warn("⚠️ AUTH_EMAIL_DEV_CODE 已设置:邮箱验证码为固定值,任何人可登录任意邮箱账号——生产环境严禁")
}
authH := &auth.Handlers{DB: d.DB, RDB: d.RDB, JWT: d.JWT, Wechat: wechat, Mailer: mailer, DevCode: d.Cfg.EmailDevCode}
billH := &billing.Handlers{DB: d.DB, Pay: pay, Quota: d.Quota}
userH := &user.Handlers{DB: d.DB, Quota: d.Quota, AppVersions: d.Cfg.AppLatest}
fbH := &feedback.Handlers{DB: d.DB, RDB: d.RDB, Storage: storage}
telH := telemetry.New(d.DB, d.RDB)
gwH := &gateway.Handler{Provider: provider, Quota: d.Quota, RDB: d.RDB}
// 后台任务
bg := context.Background()
billH.StartWatchdog(bg)
d.Quota.StartDailyReconcile(bg)
telH.StartDailyAggregate(bg)
v1 := r.Group("/v1")
// 🔓 无需登录
v1.POST("/auth/qr", authH.CreateQr)
v1.GET("/auth/qr/:state", authH.PollQr)
v1.GET("/auth/wechat/callback", authH.QrCallback)
v1.POST("/auth/wechat", authH.MobileLogin)
v1.POST("/auth/email/code", authH.EmailCode)
v1.POST("/auth/email", authH.EmailLogin)
v1.GET("/packs", billH.Packs)
// ⚠️ 部署前必须切换到真实 PayClient(验签)!当前 MockPay 不验签,
// /v1/pay/notify 等于一个无鉴权的公开充值接口(任意人可伪造支付成功回调充值)。
// 上线前务必让 pickPay 返回带 APIv3 验签的真实实现(见 pickPay 的 TODO(7D-真实))。
v1.POST("/pay/notify", billH.Notify)
// metrics/batch 可匿名上报;OptionalMiddleware 在带合法 JWT 时注入 user_id 以关联用户(17F)。
v1.POST("/metrics/batch", d.JWT.OptionalMiddleware(), telH.Batch)
v1.GET("/app/latest", userH.AppLatest)
// 需登录
authed := v1.Group("", d.JWT.Middleware())
authed.POST("/auth/logout", authH.Logout)
authed.GET("/me", userH.Me)
authed.POST("/orders", billH.CreateOrder)
authed.GET("/orders/:id", billH.OrderStatus)
authed.POST("/feedback", fbH.Submit)
authed.GET("/asr/stream", gwH.HandleWS)
}