07955ddf07
服务号「植趣 ZeeQ」已认证,走网页授权(snsapi_base,只拿 openid、用户无感), 不接管消息推送,副作用最小。 流程:PC 建 ticket → 二维码指向 /wx/mp?t= → 用户微信扫码 → 302 到微信授权页 → 回调 /api/v1/wx/mp/callback 用 code 换 openid → 找/建用户 → ticket 置 authorized → PC 轮询 /wx/mp/poll 拿到 authorized → 签发 JWT。ticket 一次性消费防重放。 - 配置(appid/secret/base_url)后台可改,secret AES 加密入库,与微信支付同一套 secrets; - ticket 存 Redis(短 TTL),无 Redis 时回退进程内内存(本地单实例可用,生产必须有 Redis); - User 加 wechat_openid。**部分唯一索引**(WHERE openid <> '')而非普通唯一: 存量邮箱用户该列是空串,普通唯一索引会让多个空串互撞、AutoMigrate 直接失败 —— 与之前 NULL 余额同类的坑,这次提前避开。 单测覆盖:授权 URL 拼接(含 #wechat_redirect 锚点必须在末尾)、secret 加密往返、 建号/查号、空 openid 不误命中存量用户。微信 API 调用依赖公网回调,本地测不了, 留待部署后真机扫码。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ErrUserExists 表示邮箱已注册(唯一约束冲突)。
|
|
var ErrUserExists = errors.New("email already registered")
|
|
|
|
// CreateUser 新建用户(密码已在调用方 bcrypt 哈希)。邮箱重复返回 ErrUserExists。
|
|
func (p *Postgres) CreateUser(ctx context.Context, email, name, passwordHash string) (*User, error) {
|
|
if p.db == nil {
|
|
return nil, errStoreDisabled
|
|
}
|
|
var existed int64
|
|
p.db.WithContext(ctx).Model(&User{}).Where("email = ?", email).Count(&existed)
|
|
if existed > 0 {
|
|
return nil, ErrUserExists
|
|
}
|
|
u := &User{Email: email, Name: name, PasswordHash: passwordHash}
|
|
if err := p.db.WithContext(ctx).Create(u).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// GetUserByEmail 按邮箱取用户(登录用)。
|
|
func (p *Postgres) GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
|
if p.db == nil {
|
|
return nil, errStoreDisabled
|
|
}
|
|
var u User
|
|
if err := p.db.WithContext(ctx).Where("email = ?", email).First(&u).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// GetUserByID 按主键取用户(鉴权后取当前用户)。
|
|
func (p *Postgres) GetUserByID(ctx context.Context, id string) (*User, error) {
|
|
if p.db == nil {
|
|
return nil, errStoreDisabled
|
|
}
|
|
var u User
|
|
if err := p.db.WithContext(ctx).First(&u, "id = ?", id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// GetUserByWechatOpenID 按微信 openid 查用户;不存在返回 (nil, nil)。
|
|
func (p *Postgres) GetUserByWechatOpenID(ctx context.Context, openID string) (*User, error) {
|
|
if p.db == nil {
|
|
return nil, errStoreDisabled
|
|
}
|
|
if openID == "" {
|
|
return nil, nil
|
|
}
|
|
var u User
|
|
err := p.db.WithContext(ctx).Where("wechat_openid = ?", openID).First(&u).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &u, nil
|
|
}
|
|
|
|
// CreateWechatUser 为首次微信登录的用户建号(无邮箱/密码)。name 是展示名(微信昵称或默认)。
|
|
func (p *Postgres) CreateWechatUser(ctx context.Context, openID, name string) (*User, error) {
|
|
if p.db == nil {
|
|
return nil, errStoreDisabled
|
|
}
|
|
if openID == "" {
|
|
return nil, errors.New("openid 必填")
|
|
}
|
|
u := &User{WechatOpenID: openID, Name: name}
|
|
if err := p.db.WithContext(ctx).Create(u).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// (测试辅助见 user_wechat_test.go)
|