40760aa884
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调 - desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导) - android:Compose 主 App + IME(键盘内录音直传) - ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写) - design/design-pipeline:设计系统 + token 导出 iOS/Android 主题 - doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.0 KiB
Go
101 lines
3.0 KiB
Go
// Package httpapi 路由装配。各业务模块在各自包内实现 handler,这里只负责挂载,
|
||
// 并按配置在真实外部依赖(微信/支付/OSS/ASR)与 mock 之间切换。
|
||
package httpapi
|
||
|
||
import (
|
||
"context"
|
||
"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 Provider(gummy 在 6B 实现 + DASHSCOPE_API_KEY 就绪后接入)。
|
||
func pickProvider(cfg config.Config) asr.Provider {
|
||
switch cfg.ASRProvider {
|
||
default:
|
||
return asr.NewMock()
|
||
}
|
||
}
|
||
|
||
// pickWechat 微信凭证未配置(#2A 申请中)时使用 mock。
|
||
func pickWechat(cfg config.Config) auth.WechatClient {
|
||
if cfg.WechatWebAppID == "" {
|
||
return auth.MockWechat{}
|
||
}
|
||
return auth.MockWechat{} // TODO(5C-真实): 开放平台实现,凭证就绪后替换
|
||
}
|
||
|
||
// pickPay 商户号未配置(#2B 申请中)时使用 mock。
|
||
func pickPay(cfg config.Config) billing.PayClient {
|
||
if cfg.WxPayMchID == "" {
|
||
return billing.MockPay{}
|
||
}
|
||
return billing.MockPay{} // TODO(7D-真实): wechatpay-go 实现,商户号就绪后替换
|
||
}
|
||
|
||
// pickStorage OSS 未配置时图片落本地盘。
|
||
func pickStorage(cfg config.Config) feedback.Storage {
|
||
if cfg.OSSBucket == "" {
|
||
return feedback.LocalStorage{Dir: "var/uploads"}
|
||
}
|
||
return feedback.LocalStorage{Dir: "var/uploads"} // 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}) })
|
||
|
||
authH := &auth.Handlers{DB: d.DB, RDB: d.RDB, JWT: d.JWT, Wechat: pickWechat(d.Cfg)}
|
||
billH := &billing.Handlers{DB: d.DB, Pay: pickPay(d.Cfg), Quota: d.Quota}
|
||
userH := &user.Handlers{DB: d.DB, Quota: d.Quota}
|
||
fbH := &feedback.Handlers{DB: d.DB, RDB: d.RDB, Storage: pickStorage(d.Cfg)}
|
||
telH := telemetry.New(d.DB, d.RDB)
|
||
gwH := &gateway.Handler{Provider: pickProvider(d.Cfg), 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.GET("/packs", billH.Packs)
|
||
v1.POST("/pay/notify", billH.Notify)
|
||
v1.POST("/metrics/batch", 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)
|
||
}
|