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>
151 lines
4.9 KiB
Go
151 lines
4.9 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
|
||
}
|
||
|
||
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 迁移清单(11 张表)。
|
||
func AllModels() []any {
|
||
return []any{
|
||
&User{}, &WechatIdentity{}, &DurationPack{}, &Order{}, &BalanceLedger{},
|
||
&TrialUsage{}, &ASRSession{}, &Device{}, &Feedback{}, &MetricEvent{}, &MetricDaily{},
|
||
}
|
||
}
|