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>
159 lines
5.2 KiB
Go
159 lines
5.2 KiB
Go
// Package store 数据模型与迁移。schema 设计见 doc/backend-architecture.html 第六章。
|
||
package store
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/datatypes"
|
||
)
|
||
|
||
type User struct {
|
||
ID string `gorm:"primaryKey;size:32"`
|
||
Nickname string `gorm:"size:64"`
|
||
AvatarURL string `gorm:"size:512"`
|
||
BalanceSeconds int64 `gorm:"not null;default:0"` // 冗余列,与 balance_ledger 同事务更新
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type WechatIdentity struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
OpenID string `gorm:"size:64;uniqueIndex;not null"`
|
||
UnionID string `gorm:"size:64;index"`
|
||
AppType string `gorm:"size:16;not null"` // web | mobile
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
// EmailIdentity 邮箱验证码登录身份(桌面端第二登录方式)。
|
||
type EmailIdentity struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
Email string `gorm:"size:255;uniqueIndex;not null"`
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type DurationPack struct {
|
||
ID string `gorm:"primaryKey;size:32"`
|
||
Minutes int `gorm:"not null"`
|
||
PriceCents int `gorm:"not null"`
|
||
UnitDesc string `gorm:"size:64"`
|
||
Tag string `gorm:"size:32"`
|
||
Sort int
|
||
Active bool `gorm:"not null;default:true"`
|
||
}
|
||
|
||
const (
|
||
OrderPending = "pending"
|
||
OrderPaid = "paid"
|
||
OrderClosed = "closed"
|
||
)
|
||
|
||
type Order struct {
|
||
ID string `gorm:"primaryKey;size:32"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
PackID string `gorm:"size:32;not null"`
|
||
PriceCents int `gorm:"not null"`
|
||
Channel string `gorm:"size:16;not null"` // native | app
|
||
Status string `gorm:"size:16;not null;index"` // pending | paid | closed
|
||
TransactionID *string `gorm:"size:64;uniqueIndex"` // 微信支付单号,幂等去重
|
||
PaidAt *time.Time
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
const (
|
||
LedgerPurchase = "purchase"
|
||
LedgerUsage = "usage"
|
||
LedgerGift = "gift"
|
||
)
|
||
|
||
// BalanceLedger 时长账本:只追加不修改,余额 = SUM(delta_seconds)。
|
||
type BalanceLedger struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
DeltaSeconds int64 `gorm:"not null"` // + 购买/赠送,− 用量
|
||
Reason string `gorm:"size:16;not null"`
|
||
OrderID *string `gorm:"size:32"`
|
||
SessionID *string `gorm:"size:64"`
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
// TrialUsage 每日免费试用:自然日(Asia/Shanghai)为键,无记录即未使用。
|
||
type TrialUsage struct {
|
||
UserID string `gorm:"primaryKey;size:32"`
|
||
Date string `gorm:"primaryKey;size:10"` // YYYY-MM-DD
|
||
UsedSeconds int `gorm:"not null;default:0"`
|
||
}
|
||
|
||
// ASRSession 识别会话用量明细(计费拆分 + 排障)。
|
||
type ASRSession struct {
|
||
ID string `gorm:"primaryKey;size:64"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
DeviceID string `gorm:"size:64"`
|
||
AudioSeconds int `gorm:"not null"`
|
||
TrialPart int `gorm:"not null"` // 试用扣减部分
|
||
BalancePart int `gorm:"not null"` // 余额扣减部分
|
||
ProviderMs int64 // Provider 报告的识别时长(ms),与 AudioSeconds 交叉校验/审计
|
||
Provider string `gorm:"size:16"`
|
||
Canceled bool
|
||
CreatedAt time.Time `gorm:"index"`
|
||
}
|
||
|
||
type Device struct {
|
||
ID string `gorm:"primaryKey;size:64"`
|
||
UserID string `gorm:"size:32;index"`
|
||
Platform string `gorm:"size:16"` // mac | win | ios | android
|
||
AppVersion string `gorm:"size:32"`
|
||
LastSeen time.Time
|
||
}
|
||
|
||
const (
|
||
FeedbackNew = "new"
|
||
FeedbackTriaged = "triaged"
|
||
FeedbackResolved = "resolved"
|
||
)
|
||
|
||
// Feedback 用户反馈(v1.1)。
|
||
type Feedback struct {
|
||
ID string `gorm:"primaryKey;size:32"`
|
||
UserID string `gorm:"size:32;index;not null"`
|
||
Content string `gorm:"type:text;not null"`
|
||
Images datatypes.JSON // OSS keys: ["fb/{id}/1.png", ...]
|
||
Diagnostics datatypes.JSON
|
||
Platform string `gorm:"size:16"`
|
||
AppVersion string `gorm:"size:32"`
|
||
Status string `gorm:"size:16;not null;default:new;index"`
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
// MetricEvent 客户端打点原始事件(v1.1,保留 90 天定期清理)。
|
||
type MetricEvent struct {
|
||
ID uint64 `gorm:"primaryKey"`
|
||
UserID *string `gorm:"size:32"`
|
||
DeviceID string `gorm:"size:64;not null"`
|
||
Platform string `gorm:"size:16"`
|
||
AppVersion string `gorm:"size:32"`
|
||
OSVersion string `gorm:"size:64"`
|
||
Event string `gorm:"size:64;not null;index"`
|
||
Props datatypes.JSON
|
||
ClientTs int64
|
||
ReceivedAt time.Time // BRIN 索引在 Open() 中按 PG 专属 DDL 创建
|
||
}
|
||
|
||
// MetricDaily 每日聚合(event × platform × date)。
|
||
type MetricDaily struct {
|
||
Date string `gorm:"primaryKey;size:10"`
|
||
Event string `gorm:"primaryKey;size:64"`
|
||
Platform string `gorm:"primaryKey;size:16"`
|
||
Count int64 `gorm:"not null"`
|
||
P50Ms float64 `gorm:"not null;default:0"`
|
||
P95Ms float64 `gorm:"not null;default:0"`
|
||
}
|
||
|
||
// AllModels 迁移清单(12 张表)。
|
||
func AllModels() []any {
|
||
return []any{
|
||
&User{}, &WechatIdentity{}, &EmailIdentity{}, &DurationPack{}, &Order{}, &BalanceLedger{},
|
||
&TrialUsage{}, &ASRSession{}, &Device{}, &Feedback{}, &MetricEvent{}, &MetricDaily{},
|
||
}
|
||
}
|