19281d9c42
后端(协议 → 存储 → 接口,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>
150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
package protocol
|
|
|
|
// ─── 认证 ────────────────────────────────────────────────────────────────────
|
|
|
|
type AuthQrResponse struct {
|
|
State string `json:"state"`
|
|
QrURL string `json:"qr_url"`
|
|
}
|
|
|
|
type AuthQrStatusResponse struct {
|
|
Status string `json:"status"` // pending | confirmed | expired
|
|
Token string `json:"token,omitempty"`
|
|
User *UserInfo `json:"user,omitempty"`
|
|
}
|
|
|
|
type AuthWechatRequest struct {
|
|
Code string `json:"code" binding:"required"`
|
|
}
|
|
|
|
// 邮箱验证码登录(桌面端第二登录方式)
|
|
type AuthEmailCodeRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
}
|
|
|
|
type AuthEmailRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Code string `json:"code" binding:"required,len=6"`
|
|
}
|
|
|
|
type AuthTokenResponse struct {
|
|
Token string `json:"token"`
|
|
User UserInfo `json:"user"`
|
|
}
|
|
|
|
type UserInfo struct {
|
|
UserID string `json:"user_id"`
|
|
NicknameMasked string `json:"nickname_masked"`
|
|
}
|
|
|
|
// ─── 用户与余额 ───────────────────────────────────────────────────────────────
|
|
|
|
// 账户状态:余额 > 0 → ok;余额 = 0 且试用未尽 → trial;皆尽 → quota。
|
|
const (
|
|
AccountOK = "ok"
|
|
AccountTrial = "trial"
|
|
AccountQuota = "quota"
|
|
)
|
|
|
|
type MeResponse struct {
|
|
UserID string `json:"user_id"`
|
|
NicknameMasked string `json:"nickname_masked"`
|
|
BalanceSeconds int64 `json:"balance_seconds"`
|
|
TrialDailyLimit int `json:"trial_daily_limit"`
|
|
TrialUsedToday int `json:"trial_used_today"`
|
|
AccountState string `json:"account_state"` // ok | trial | quota
|
|
}
|
|
|
|
// ─── 时长包与订单 ─────────────────────────────────────────────────────────────
|
|
|
|
type Pack struct {
|
|
ID string `json:"id"`
|
|
Minutes int `json:"minutes"`
|
|
PriceCents int `json:"price_cents"`
|
|
UnitDesc string `json:"unit_desc"`
|
|
Tag string `json:"tag,omitempty"`
|
|
}
|
|
|
|
type PacksResponse struct {
|
|
Packs []Pack `json:"packs"`
|
|
}
|
|
|
|
const (
|
|
PayChannelNative = "native" // 桌面扫码
|
|
PayChannelApp = "app" // 移动收银台
|
|
)
|
|
|
|
type CreateOrderRequest struct {
|
|
PackID string `json:"pack_id" binding:"required"`
|
|
Channel string `json:"channel" binding:"required"` // native | app
|
|
}
|
|
|
|
type CreateOrderResponse struct {
|
|
OrderID string `json:"order_id"`
|
|
CodeURL string `json:"code_url,omitempty"` // native
|
|
PayParams map[string]any `json:"pay_params,omitempty"` // app
|
|
}
|
|
|
|
type OrderStatusResponse struct {
|
|
Status string `json:"status"` // pending | paid | closed
|
|
BalanceSeconds int64 `json:"balance_seconds,omitempty"`
|
|
}
|
|
|
|
// ─── 反馈(v1.1)──────────────────────────────────────────────────────────────
|
|
|
|
const (
|
|
FeedbackMaxImages = 3
|
|
FeedbackMaxImageBytes = 5 << 20 // 5MB
|
|
FeedbackMaxContentLen = 1000
|
|
FeedbackDailyLimit = 10
|
|
)
|
|
|
|
type FeedbackResponse struct {
|
|
FeedbackID string `json:"feedback_id"`
|
|
}
|
|
|
|
// ─── 打点(v1.1)──────────────────────────────────────────────────────────────
|
|
|
|
const (
|
|
MetricsMaxBatch = 100
|
|
MetricsMaxPropBytes = 2048
|
|
)
|
|
|
|
type MetricEvent struct {
|
|
Event string `json:"event"`
|
|
Props map[string]any `json:"props,omitempty"`
|
|
ClientTs int64 `json:"client_ts"`
|
|
}
|
|
|
|
type MetricsBatchRequest struct {
|
|
DeviceID string `json:"device_id" binding:"required"`
|
|
Platform string `json:"platform" binding:"required"` // mac | win | ios | android
|
|
AppVersion string `json:"app_version"`
|
|
OSVersion string `json:"os_version"`
|
|
Events []MetricEvent `json:"events" binding:"required"`
|
|
}
|
|
|
|
// MetricWhitelist 事件白名单:未知事件名直接丢弃(见前端文档 9.1 指标清单)。
|
|
var MetricWhitelist = map[string]bool{
|
|
"asr.first_partial_ms": true,
|
|
"asr.release_to_commit_ms": true,
|
|
"audio.start_ms": true,
|
|
"ws.connect_ms": true,
|
|
"ws.reconnect": true,
|
|
"asr.session": true,
|
|
"inject.fail": true,
|
|
"onboarding.step": true,
|
|
"login.success": true,
|
|
"purchase.success": true,
|
|
"kb.enabled": true,
|
|
}
|
|
|
|
// ─── 应用更新 ─────────────────────────────────────────────────────────────────
|
|
|
|
type AppLatestResponse struct {
|
|
Version string `json:"version"`
|
|
Notes string `json:"notes,omitempty"`
|
|
URL string `json:"url"`
|
|
Force bool `json:"force"`
|
|
}
|