dudu MVP:五端语音输入法初始提交
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s

- 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>
This commit is contained in:
wangjia
2026-06-12 00:38:37 +08:00
commit 40760aa884
252 changed files with 40789 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
// 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{},
}
}