Files
smartmate/backend/services/userauth/dao/user.go
Losita b08ee17893 Version: 0.9.66.dev.260504
后端:
1. 阶段 2 user/auth 服务边界落地,新增 `cmd/userauth` go-zero zrpc 服务、`services/userauth` 核心实现、gateway user API/zrpc client 与 shared contracts/ports,迁移注册、登录、刷新 token、登出、JWT、黑名单和 token 额度治理
2. gateway 与启动装配切流,`cmd/all` 只保留边缘路由、鉴权和轻量组合,通过 userauth zrpc 访问核心用户能力;拆分 MySQL/Redis 初始化与 AutoMigrate 边界,`userauth` 自迁 `users` 和 token 记账幂等表,`all` 不再迁用户表
3. 清退 Gin 单体旧 user/auth DAO、model、service、router、middleware 和 JWT handler,并同步调整 agent/schedule/cache/outbox 相关调用依赖
4. 补齐 refresh token 防并发重放、MySQL 幂等 token 记账、额度 `>=` 拦截和 RPC 错误映射,避免重复记账与内部错误透出

文档:
1. 新增《学习计划论坛与Token商店PRD》
2026-05-04 15:20:47 +08:00

174 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dao
import (
"context"
"errors"
"strings"
"time"
userauthmodel "github.com/LoveLosita/smartflow/backend/services/userauth/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// UserDAO 是 user/auth 服务内部的 users 表访问层。
// 职责边界:只提供注册、登录和额度治理需要的最小读写能力,不暴露整张 users 表给 gateway。
type UserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{db: db}
}
// Create 创建新用户并初始化 token 额度字段。
func (r *UserDAO) Create(ctx context.Context, username, phoneNumber, password string) (*userauthmodel.User, error) {
user := &userauthmodel.User{
Username: username,
PhoneNumber: phoneNumber,
Password: password,
TokenLimit: 100000,
TokenUsage: 0,
LastResetAt: time.Now(),
}
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *UserDAO) IfUsernameExists(ctx context.Context, name string) (bool, error) {
err := r.db.WithContext(ctx).Where("username = ?", name).First(&userauthmodel.User{}).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return true, err
}
return true, nil
}
func (r *UserDAO) GetUserHashedPasswordByName(ctx context.Context, name string) (string, error) {
var user userauthmodel.User
if err := r.db.WithContext(ctx).Where("username = ?", name).First(&user).Error; err != nil {
return "", err
}
return user.Password, nil
}
func (r *UserDAO) GetUserIDByName(ctx context.Context, name string) (int, error) {
var user userauthmodel.User
if err := r.db.WithContext(ctx).Where("username = ?", name).First(&user).Error; err != nil {
return -1, err
}
return int(user.ID), nil
}
// GetUserTokenQuotaByID 只读取额度判断需要的字段。
func (r *UserDAO) GetUserTokenQuotaByID(ctx context.Context, id int) (*userauthmodel.User, error) {
var user userauthmodel.User
err := r.db.WithContext(ctx).
Select("id", "token_limit", "token_usage", "last_reset_at").
Where("id = ?", id).
First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// ResetUserTokenUsageIfDue 使用条件更新实现幂等懒重置。
func (r *UserDAO) ResetUserTokenUsageIfDue(ctx context.Context, id int, dueBefore time.Time, resetAt time.Time) (bool, error) {
result := r.db.WithContext(ctx).
Model(&userauthmodel.User{}).
Where("id = ? AND (last_reset_at IS NULL OR last_reset_at <= ?)", id, dueBefore).
Updates(map[string]interface{}{
"token_usage": 0,
"last_reset_at": resetAt,
})
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// AddTokenUsage 为用户 token 账本做增量累加。
// 职责边界:
// 1. 只做数据库累加,不负责额度判断与缓存刷新;
// 2. delta<=0 视为无操作,直接返回成功;
// 3. 由 service 层决定是否需要先做懒重置和后续 cache 回填。
func (r *UserDAO) AddTokenUsage(ctx context.Context, id int, delta int) (bool, error) {
if delta <= 0 {
return true, nil
}
result := r.db.WithContext(ctx).
Model(&userauthmodel.User{}).
Where("id = ?", id).
Update("token_usage", gorm.Expr("token_usage + ?", delta))
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// AdjustTokenUsageOnce 在同一个 MySQL 事务里完成“幂等占位 + token 用量增量”。
//
// 职责边界:
// 1. eventID 非空时先写入 user_token_usage_adjustments依赖主键冲突判断是否重复事件
// 2. 只有幂等占位写入成功后才更新 users.token_usage保证并发重放不会重复记账
// 3. 不负责 Redis 快照和封禁键维护,这些缓存语义仍由 service 层在事务成功后刷新。
func (r *UserDAO) AdjustTokenUsageOnce(ctx context.Context, eventID string, id int, delta int, dueBefore time.Time, resetAt time.Time) (*userauthmodel.User, bool, error) {
var quota userauthmodel.User
duplicated := false
trimmedEventID := strings.TrimSpace(eventID)
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if trimmedEventID != "" {
marker := userauthmodel.TokenUsageAdjustment{
EventID: trimmedEventID,
UserID: id,
TokenDelta: delta,
}
result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&marker)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
duplicated = true
return nil
}
}
resetResult := tx.Model(&userauthmodel.User{}).
Where("id = ? AND (last_reset_at IS NULL OR last_reset_at <= ?)", id, dueBefore).
Updates(map[string]interface{}{
"token_usage": 0,
"last_reset_at": resetAt,
})
if resetResult.Error != nil {
return resetResult.Error
}
updateResult := tx.Model(&userauthmodel.User{}).
Where("id = ?", id).
Update("token_usage", gorm.Expr("token_usage + ?", delta))
if updateResult.Error != nil {
return updateResult.Error
}
if updateResult.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return tx.Select("id", "token_limit", "token_usage", "last_reset_at").
Where("id = ?", id).
First(&quota).Error
})
if err != nil {
return nil, false, err
}
if duplicated {
return nil, true, nil
}
return &quota, false, nil
}