Files
dudu/server/internal/store/models.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

159 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 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{},
}
}