Files
dudu/server/internal/httpapi/router.go
T
wangjia 19281d9c42
ci / server (push) Failing after 10s
ci / design-system (push) Failing after 10s
feat(auth+desktop): 邮箱验证码登录 + 登录窗改版(微信/邮箱双方式,无边框)
后端(协议 → 存储 → 接口,E2E 测试全绿):
- protocol:AuthEmailCodeRequest / AuthEmailRequest DTO
- store:EmailIdentity 表(邮箱唯一索引)+ authmail:* Redis key
- auth/email.go:POST /v1/auth/email/code 发 6 位码(crypto/rand,
  60s 冷却 SetNX,10 分钟 TTL);POST /v1/auth/email 常量时间比对、
  码一次性、邮箱建号(昵称取前缀)→ JWT(与微信登录同响应形态)
- Mailer 接口 + MockMailer(验证码打日志供联调;SMTP 未配置自动降级,
  装配结果入启动日志,上线前须换真实实现)
- TestEmailLogin E2E:发码/冷却 429/错码拒绝/登录/token 可用/码防复用

桌面(Rust 命令 + UI 改版):
- api.rs:login_email_code / login_email(成功保存 token + ws 重连,
  与扫码登录同路径)
- 登录窗改版(原型 LoginPurchase 先行,dark 截图验收):logo + 分段
  切换(微信扫码 / 邮箱登录,与设置页同控件语言)+ 邮箱表单
  (60s 倒计时、错误文案、未注册自动建号提示)
- 二维码真渲染:qrcode 库画 canvas(前景/背景取主题 token),
  替换原 URL 文本占位;过期态半透明化
- 登录窗无边框化:transparent + Overlay 标题栏 + 原生贴形阴影;
  抽公共 WindowFrame 组件(设置窗同步重构复用)

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

137 lines
5.2 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)
authH := &auth.Handlers{DB: d.DB, RDB: d.RDB, JWT: d.JWT, Wechat: wechat, Mailer: mailer}
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)
}