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