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
+103
View File
@@ -0,0 +1,103 @@
package auth
import (
"context"
"errors"
"strings"
"github.com/google/uuid"
"gorm.io/gorm"
"dudu/server/internal/store"
)
// WechatIdentityInfo 微信授权换取的身份。
type WechatIdentityInfo struct {
OpenID string
UnionID string
Nickname string
Avatar string
}
// WechatClient 微信 OAuth 客户端抽象:真实实现走开放平台 API(2A 凭证就绪后接入),
// 开发期用 MockWechat。
type WechatClient interface {
// ExchangeCode code 换身份;appType: web | mobile
ExchangeCode(ctx context.Context, code, appType string) (WechatIdentityInfo, error)
}
// MockWechat 开发期 mockcode 直接映射 openid(同 code 幂等同一用户),
// unionid = openid 加前缀,昵称取 code 前 8 字符。
type MockWechat struct{}
func (MockWechat) ExchangeCode(_ context.Context, code, _ string) (WechatIdentityInfo, error) {
if code == "" {
return WechatIdentityInfo{}, errors.New("empty code")
}
nick := code
if len(nick) > 8 {
nick = nick[:8]
}
return WechatIdentityInfo{
OpenID: "mock-open-" + code,
UnionID: "mock-union-" + code,
Nickname: nick,
}, nil
}
// MaskNickname 昵称脱敏:"wangjia" → "wang***";中文取首字 + ***。
func MaskNickname(n string) string {
r := []rune(strings.TrimSpace(n))
if len(r) == 0 {
return "用户***"
}
keep := 4
if r[0] > 0x2E80 { // CJK 起始附近,中文名只保留首字
keep = 1
}
if len(r) < keep {
keep = len(r)
}
return string(r[:keep]) + "***"
}
// FindOrCreateUser unionid 优先关联(同一用户多端 openid 归一),其次 openid。
func FindOrCreateUser(db *gorm.DB, info WechatIdentityInfo, appType string) (*store.User, error) {
var ident store.WechatIdentity
err := db.Where("open_id = ?", info.OpenID).First(&ident).Error
if err == nil {
var u store.User
return &u, db.First(&u, "id = ?", ident.UserID).Error
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
var user store.User
err = db.Transaction(func(tx *gorm.DB) error {
// unionid 已存在 → 关联到既有用户
if info.UnionID != "" {
var other store.WechatIdentity
if err := tx.Where("union_id = ?", info.UnionID).First(&other).Error; err == nil {
if err := tx.First(&user, "id = ?", other.UserID).Error; err != nil {
return err
}
return tx.Create(&store.WechatIdentity{
UserID: user.ID, OpenID: info.OpenID, UnionID: info.UnionID, AppType: appType,
}).Error
}
}
// 新用户
user = store.User{
ID: strings.ReplaceAll(uuid.NewString(), "-", "")[:24],
Nickname: info.Nickname, AvatarURL: info.Avatar,
}
if err := tx.Create(&user).Error; err != nil {
return err
}
return tx.Create(&store.WechatIdentity{
UserID: user.ID, OpenID: info.OpenID, UnionID: info.UnionID, AppType: appType,
}).Error
})
return &user, err
}