Files
smartmate/backend/dao/user.go
LoveLosita bc56d471a8 Version: 0.6.7.dev.260317
 feat(agent): 新增 Token 配额门禁中间件(Redis 快照 + 封禁键 + 7 天懒重置)

- 🚪 在 `POST /api/v1/agent/chat` 挂载 `TokenQuotaGuard`,在请求进入业务逻辑前完成额度校验
-  新增 Redis 配额快照与封禁键机制:超额用户命中封禁键后可快速拦截,降低重复查库带来的开销
- 🗃️ 新增用户配额 DAO 能力:按需读取 `token_limit`、`token_usage`、`last_reset_at`,并支持基于“到期条件更新”的懒重置
- 🔄 实现 7 天懒重置策略:用户访问时若检测到配额周期已到期,则重置 `token_usage` 并清理封禁状态
- 🚫 新增超额响应码 `40051`,用于标识 `token usage exceeds limit`
2026-03-17 19:46:08 +08:00

133 lines
3.7 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"
"time"
"github.com/LoveLosita/smartflow/backend/model"
"gorm.io/gorm"
)
// UserDAO 用户数据访问对象
// 负责用户相关的数据库操作
type UserDAO struct {
// 这是一个口袋,用来装数据库连接实例
db *gorm.DB
}
// NewUserDAO 创建UserDAO实例
// NewUserDAO 接收一个 *gorm.DB并把它塞进结构体的口袋里
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{
db: db,
}
}
func (r *UserDAO) WithTx(tx *gorm.DB) *UserDAO {
return &UserDAO{db: tx}
}
// Create 创建新用户
// 插入新用户信息到数据库
func (r *UserDAO) Create(username, phoneNumber, password string) (*model.User, error) {
// 创建User实例
user := &model.User{
Username: username,
PhoneNumber: phoneNumber,
Password: password, // 注意:实际项目中应该对密码进行加密处理
TokenLimit: 100000, // 默认值
TokenUsage: 0, // 初始使用量为0
LastResetAt: time.Now(), // 设置为当前时间
}
// 插入数据
if err := r.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *UserDAO) IfUsernameExists(name string) (bool, error) {
err := r.db.Where("username = ?", name).First(&model.User{}).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return true, err
}
return true, nil
}
func (r *UserDAO) GetUserHashedPasswordByName(name string) (string, error) {
var user model.User
err := r.db.Where("username = ?", name).First(&user).Error
if err != nil {
return "", err
}
return user.Password, nil
}
func (r *UserDAO) GetUserIDByName(name string) (int, error) {
var user model.User
err := r.db.Where("username = ?", name).First(&user).Error
if err != nil {
return -1, err
}
return int(user.ID), nil
}
func (r *UserDAO) GetUserByID(id int) (*model.User, error) {
var user model.User
err := r.db.Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// GetUserTokenQuotaByID 查询用户 token 配额快照(仅查询配额相关字段)。
//
// 职责边界:
// 1. 只返回 token_limit / token_usage / last_reset_at 等“额度判断必需字段”;
// 2. 不负责做超额判断与重置判断(由中间件统一决策);
// 3. 不返回密码等敏感字段,避免把无关信息带入鉴权链路。
func (r *UserDAO) GetUserTokenQuotaByID(ctx context.Context, id int) (*model.User, error) {
var user model.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 在“已到重置窗口”时执行懒重置。
//
// 输入输出语义:
// 1. dueBefore判定“到期可重置”的截止时间通常是 now-7d
// 2. resetAt本次重置写入的时间戳
// 3. 返回值 bool
// - true 表示本次调用实际执行了重置;
// - false 表示条件未命中(尚未到期或记录不存在)。
//
// 并发与幂等说明:
// 1. 使用条件更新WHERE last_reset_at <= dueBefore保证并发下最多一次成功重置
// 2. 重复调用是安全的,未命中条件时不会破坏现有统计。
func (r *UserDAO) ResetUserTokenUsageIfDue(ctx context.Context, id int, dueBefore time.Time, resetAt time.Time) (bool, error) {
result := r.db.WithContext(ctx).
Model(&model.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
}