You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package model
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/service/users/token_cache_redis"
"goskeleton/app/utils/md5_encrypt"
"time"
)
// 本文件针对 sqlserver 数据库有效,请手动使用本文件的所有代码替换 users.go 中的所有代码即可
// 针对数据库选型为sqlserver的开发者使用
// 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码
// Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/
// gorm_v2 提供的语法+ ginskeleton 实践 http://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/model/button_cn_en.go
// 创建 userFactory
// 参数说明: 传递空值,默认使用 配置文件选项UseDbTypemysql
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
type UsersModel struct {
BaseModel
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"-"`
Phone string `json:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status"`
Token string `json:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// 表名
func (u *UsersModel) TableName() string {
return "tb_users"
}
// 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO tb_users(user_name,pass,last_login_ip) SELECT ?,?,? WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
// 用户登录,
func (u *UsersModel) Login(userName string, pass string) *UsersModel {
sql := "select top 1 id, user_name,real_name,pass,phone from tb_users where user_name=? "
result := u.Raw(sql, userName).First(u)
if result.Error == nil {
// 账号密码验证成功
if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) {
return u
}
} else {
variable.ZapLog.Error("根据账号查询单条记录出错:", zap.Error(result.Error))
}
return nil
}
//记录用户登陆login生成的token每次登陆记录一次token
func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64, clientIp string) bool {
sql := `
INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip)
SELECT ?,'login',? ,?,? WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=?)
`
//注意token的精确度为秒如果在一秒之内一个账号多次调用接口生成的token其实是相同的这样写入数据库第二次的影响行数为0知己实际上操作仍然是有效的。
//所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的
if u.Exec(sql, userId, token, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, token).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
return true
}
return false
}
//用户刷新token,条件检查: 相关token在过期的时间之内就符合刷新条件
func (u *UsersModel) OauthRefreshConditionCheck(userId int64, oldToken string) bool {
// 首先判断旧token在本系统自带的数据库已经存在才允许继续执行刷新逻辑
var oldTokenIsExists int
sql := "SELECT count(*) as counts FROM tb_oauth_access_tokens WHERE fr_user_id =? and token=? and GETDATE()<DATEADD(SS,?,expires_at)"
if u.Raw(sql, userId, oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")).First(&oldTokenIsExists).Error == nil && oldTokenIsExists == 1 {
return true
}
return false
}
//用户刷新token
func (u *UsersModel) OauthRefreshToken(userId, expiresAt int64, oldToken, newToken, clientIp string) bool {
sql := "UPDATE tb_oauth_access_tokens SET token=? ,expires_at=?,client_ip=?,updated_at=GETDATE(),action_name='refresh' WHERE fr_user_id=? AND token=?"
if u.Exec(sql, newToken, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, oldToken).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
go u.UpdateUserloginInfo(clientIp, userId)
return true
}
return false
}
// 更新用户登陆次数、最近一次登录ip、最近一次登录时间
func (u *UsersModel) UpdateUserloginInfo(last_login_ip string, userId int64) {
sql := "UPDATE tb_users SET login_times=isnull(login_times,0)+1,last_login_ip=?,last_login_time=? WHERE id=? "
_ = u.Exec(sql, last_login_ip, time.Now().Format(variable.DateFormat), userId)
}
//当用户更改密码后所有的token都失效必须重新登录
func (u *UsersModel) OauthResetToken(userId int, newPass, clientIp string) bool {
//如果用户新旧密码一致直接返回true不需要处理
userItem, err := u.ShowOneItem(userId)
if userItem != nil && err == nil && userItem.Pass == newPass {
return true
} else if userItem != nil {
sql := "UPDATE tb_oauth_access_tokens SET revoked=1,updated_at=GETDATE(),action_name='ResetPass',client_ip=? WHERE fr_user_id=? "
if u.Exec(sql, clientIp, userId).Error == nil {
return true
}
}
return false
}
//当tb_users 删除数据相关的token同步删除
func (u *UsersModel) OauthDestroyToken(userId int) bool {
//如果用户新旧密码一致直接返回true不需要处理
sql := "DELETE FROM tb_oauth_access_tokens WHERE fr_user_id=? "
//判断>=0, 有些没有登录过的用户没有相关token此语句执行影响行数为0但是仍然是执行成功
if u.Exec(sql, userId).Error == nil {
return true
}
return false
}
// 判断用户token是否在数据库存在+状态OK
func (u *UsersModel) OauthCheckTokenIsOk(userId int64, token string) bool {
sql := `
SELECT token FROM tb_oauth_access_tokens
WHERE fr_user_id=? AND revoked=0 AND expires_at>GETDATE()
ORDER BY expires_at DESC , updated_at DESC
OFFSET 0 ROW FETCH NEXT ? ROWS ONLY
`
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是查询类记得释放记录集
_ = rows.Close()
}()
if err == nil && rows != nil {
for rows.Next() {
var tempToken string
err := rows.Scan(&tempToken)
if err == nil {
if tempToken == token {
_ = rows.Close()
return true
}
}
}
}
return false
}
// 禁用一个用户的: 1.tb_users表的 status 设置为 0tb_oauth_access_tokens 表的所有token删除
// 禁用一个用户的token请求本质上就是把tb_users表的 status 字段设置为 0 即可)
func (u *UsersModel) SetTokenInvalid(userId int) bool {
sql := "delete from tb_oauth_access_tokens where fr_user_id=? "
if u.Exec(sql, userId).Error == nil {
if u.Exec("update tb_users set status=0 where id=?", userId).Error == nil {
return true
}
}
return false
}
//根据用户ID查询一条信息
func (u *UsersModel) ShowOneItem(userId int) (*UsersModel, error) {
sql := "SELECT top 1 id, user_name,pass, real_name, phone, status FROM tb_users WHERE status=1 and id=?"
result := u.Raw(sql, userId).First(u)
if result.Error == nil {
return u, nil
} else {
return nil, result.Error
}
}
// 查询数据之前统计条数
func (u *UsersModel) counts(userName string) (counts int64) {
sql := "SELECT count(*) as counts FROM tb_users WHERE status=1 and user_name like ?"
if res := u.Raw(sql, "%"+userName+"%").First(&counts); res.Error != nil {
variable.ZapLog.Error("UsersModel - counts 查询数据条数出错", zap.Error(res.Error))
}
return counts
}
// 查询(根据关键词模糊查询)
func (u *UsersModel) Show(userName string, limitStart, limitItems int) (counts int64, temp []UsersModel) {
if counts = u.counts(userName); counts > 0 {
sql := `
SELECT id, user_name, real_name, phone,last_login_ip, status, CONVERT(varchar(20), created_at, 120 ) as created_at, CONVERT(varchar(20), updated_at, 120 ) as updated_at
FROM tb_users WHERE status=1 and user_name like ? order by id desc OFFSET ? ROW FETCH NEXT ? ROWS ONLY
`
if res := u.Raw(sql, "%"+userName+"%", limitStart, limitItems).Find(&temp); res.RowsAffected > 0 {
return counts, temp
}
}
return 0, nil
}
//新增
func (u *UsersModel) Store(userName string, pass string, realName string, phone string, remark string) bool {
sql := "INSERT INTO tb_users(user_name,pass,real_name,phone,remark) SELECT ?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
if u.Exec(sql, userName, pass, realName, phone, remark, userName).RowsAffected > 0 {
return true
}
return false
}
//UpdateDataCheckUserNameIsUsed 更新前检查新的用户名是否已经存在(避免和别的账号重名)
func (u *UsersModel) UpdateDataCheckUserNameIsUsed(userId int, userName string) (exists int64) {
sql := "select count(*) as counts from tb_users where id!=? AND user_name=?"
_ = u.Raw(sql, userId, userName).First(&exists)
return exists
}
//更新
func (u *UsersModel) Update(id int, userName string, pass string, realName string, phone string, remark string, clientIp string) bool {
sql := "update tb_users set user_name=?,pass=?,real_name=?,phone=?,remark=? WHERE status=1 AND id=?"
if u.Exec(sql, userName, pass, realName, phone, remark, id).RowsAffected >= 0 {
if u.OauthResetToken(id, pass, clientIp) {
return true
}
}
return false
}
//删除用户以及关联的token记录
func (u *UsersModel) Destroy(id int) bool {
if u.Delete(u, id).Error == nil {
if u.OauthDestroyToken(id) {
return true
}
}
return false
}
// 后续两个函数专门处理用户 token 缓存到 redis 逻辑
func (u *UsersModel) ValidTokenCacheToRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
defer tokenCacheRedisFact.ReleaseRedisConn()
sql := `
SELECT token,CONVERT(varchar(20), expires_at, 120 ) as expires_at FROM tb_oauth_access_tokens
WHERE fr_user_id=? AND revoked=0 AND expires_at>getdate() ORDER BY expires_at DESC , updated_at DESC
OFFSET 0 ROW FETCH NEXT ? ROWS ONLY
`
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是获取原生结果集的查询,记得释放记录集
_ = rows.Close()
}()
var tempToken, expires string
if err == nil && rows != nil {
for i := 1; rows.Next(); i++ {
err = rows.Scan(&tempToken, &expires)
if err == nil {
if ts, err := time.ParseInLocation(variable.DateFormat, expires, time.Local); err == nil {
tokenCacheRedisFact.SetTokenCache(ts.Unix(), tempToken)
// 因为每个用户的token是按照过期时间倒叙排列的第一个是有效期最长的将该用户的总键设置一个最大过期时间到期则自动清理避免不必要的数据残留
if i == 1 {
tokenCacheRedisFact.SetUserTokenExpire(ts.Unix())
}
} else {
variable.ZapLog.Error("expires_at 转换位时间戳出错", zap.Error(err))
}
}
}
}
// 缓存结束之后删除超过系统设置最大在线数量的token
tokenCacheRedisFact.DelOverMaxOnlineCache()
}
// DelTokenCacheFromRedis 用户密码修改后删除redis所有的token
func (u *UsersModel) DelTokenCacheFromRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
tokenCacheRedisFact.ClearUserToken()
tokenCacheRedisFact.ReleaseRedisConn()
}