Files
OpenFlare/internal/model/users.go
T
2026-06-08 20:34:28 +08:00

235 lines
7.2 KiB
Go

/*
Copyright 2025 linux.do
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/linux-do/credit/internal/common"
"github.com/linux-do/credit/internal/util"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type TrustLevel uint8
const (
TrustLevelNewUser TrustLevel = iota
TrustLevelBasicUser
TrustLevelUser
TrustLevelActiveUser
TrustLevelLeader
)
// OAuthUserInfo 用户信息结构(同时支持 OIDC ID Token claims 和 UserEndpoint 响应)
type OAuthUserInfo struct {
Id uint64 `json:"id"`
Sub string `json:"sub"`
Username string `json:"username"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
Name string `json:"name"`
Active bool `json:"active"`
AvatarUrl string `json:"avatar_url"`
TrustLevel TrustLevel `json:"trust_level"`
}
// GetID 获取用户 ID
func (u *OAuthUserInfo) GetID() uint64 {
if u.Id != 0 {
return u.Id
}
// 从 sub 解析(OIDC 格式)
if u.Sub != "" {
if id, err := strconv.ParseUint(u.Sub, 10, 64); err == nil {
return id
}
}
return 0
}
// UserGamificationScoreResponse API响应
type UserGamificationScoreResponse struct {
User struct {
GamificationScore int64 `json:"gamification_score"`
} `json:"user"`
}
// LeaderboardResponse 排行榜 API 响应
type LeaderboardResponse struct {
Users []LeaderboardUser `json:"users"`
}
// LeaderboardUser 排行榜用户信息
type LeaderboardUser struct {
ID uint64 `json:"id"`
Username string `json:"username"`
TotalScore int64 `json:"total_score"`
}
type User struct {
ID uint64 `json:"id" gorm:"primaryKey;index:idx_users_active_bal_id,priority:3"`
Username string `json:"username" gorm:"size:64;uniqueIndex"`
Password string `json:"password,omitempty" gorm:"size:255"`
Nickname string `json:"nickname" gorm:"size:255"`
AvatarUrl string `json:"avatar_url" gorm:"size:255"`
TrustLevel TrustLevel `json:"trust_level" gorm:"index"`
PayScore int64 `json:"pay_score" gorm:"default:0;index"`
SignKey string `json:"sign_key" gorm:"size:64;uniqueIndex;not null"`
TotalReceive decimal.Decimal `json:"total_receive" gorm:"type:numeric(20,2);default:0"`
TotalPayment decimal.Decimal `json:"total_payment" gorm:"type:numeric(20,2);default:0"`
TotalTransfer decimal.Decimal `json:"total_transfer" gorm:"type:numeric(20,2);default:0"`
TotalCommunity decimal.Decimal `json:"total_community" gorm:"type:numeric(20,2);default:0"`
CommunityBalance decimal.Decimal `json:"community_balance" gorm:"type:numeric(20,2);default:0"`
AvailableBalance decimal.Decimal `json:"available_balance" gorm:"type:numeric(20,2);default:0;index:idx_users_active_bal_id,priority:2"`
PendingBalance decimal.Decimal `json:"pending_balance" gorm:"type:numeric(20,2);default:0"`
IsActive bool `json:"is_active" gorm:"default:true;index:idx_users_active_bal_id,priority:1"`
IsAdmin bool `json:"is_admin" gorm:"default:false"`
LastLoginAt time.Time `json:"last_login_at" gorm:"index"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
}
func (u *User) SetPassword(password string) error {
if password == "" {
u.Password = ""
return nil
}
hashed, err := util.HashPassword(password)
if err != nil {
return err
}
u.Password = hashed
return nil
}
func (u *User) CheckPassword(password string) bool {
if u.Password == "" || password == "" {
return false
}
return util.CheckPasswordHash(u.Password, password)
}
func (u *User) GetByID(tx *gorm.DB, id uint64) error {
if err := tx.Where("id = ?", id).First(u).Error; err != nil {
return err
}
return nil
}
// GetByIDs 批量查询用户
func GetByIDs(tx *gorm.DB, ids []uint64) ([]User, error) {
var users []User
if err := tx.Where("id IN ?", ids).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func (u *User) GetUserGamificationScore(ctx context.Context) (*UserGamificationScoreResponse, error) {
if u.Username == "dev_user" {
var response UserGamificationScoreResponse
response.User.GamificationScore = 12345
return &response, nil
}
url := fmt.Sprintf("https://linux.do/u/%s.json", u.Username)
resp, err := util.Request(ctx, http.MethodGet, url, nil, nil, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("获取用户积分失败,状态码: %d", resp.StatusCode)
}
var response UserGamificationScoreResponse
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("解析用户积分响应失败: %w", err)
}
return &response, nil
}
// GetLeaderboard 获取排行榜数据
func GetLeaderboard(ctx context.Context, page int) (*LeaderboardResponse, error) {
url := fmt.Sprintf("https://linux.do/leaderboard/1.json?period=all_time&page=%d", page)
resp, err := util.Request(ctx, http.MethodGet, url, nil, nil, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("获取排行榜失败,状态码: %d", resp.StatusCode)
}
var response LeaderboardResponse
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("解析排行榜响应失败: %w", err)
}
return &response, nil
}
// UpdateFromOAuthInfo 根据 OAuth 信息更新用户数据
func (u *User) UpdateFromOAuthInfo(oauthInfo *OAuthUserInfo) {
u.Username = oauthInfo.Username
u.Nickname = oauthInfo.Name
u.AvatarUrl = oauthInfo.AvatarUrl
u.IsActive = oauthInfo.Active
u.TrustLevel = oauthInfo.TrustLevel
u.LastLoginAt = time.Now()
if oauthInfo.Username == "dev_user" {
u.IsAdmin = true
}
}
// CheckActive 检查用户账户是否激活,未激活则返回错误
func (u *User) CheckActive() error {
if !u.IsActive {
return errors.New(common.BannedAccount)
}
return nil
}
// CreateUser 创建新用户
func (u *User) CreateUser(tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
now := time.Now()
newUser := User{
ID: oauthInfo.GetID(),
Username: oauthInfo.Username,
Nickname: oauthInfo.Name,
AvatarUrl: oauthInfo.AvatarUrl,
IsActive: oauthInfo.Active,
TrustLevel: oauthInfo.TrustLevel,
SignKey: util.GenerateUniqueIDSimple(),
LastLoginAt: now,
IsAdmin: oauthInfo.Username == "dev_user",
}
if err := tx.Create(&newUser).Error; err != nil {
return err
}
*u = newUser
return nil
}