2025-12-10 18:33:37 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-06 10:28:56 +08:00
|
|
|
"encoding/json"
|
2025-12-10 18:33:37 +08:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand"
|
2026-03-06 10:28:56 +08:00
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
2025-12-10 18:33:37 +08:00
|
|
|
|
2026-03-06 11:03:41 +08:00
|
|
|
"gitea.com/texpixel/document_ai/config"
|
|
|
|
|
model "gitea.com/texpixel/document_ai/internal/model/user"
|
2026-01-27 21:56:21 +08:00
|
|
|
"gitea.com/texpixel/document_ai/internal/storage/cache"
|
|
|
|
|
"gitea.com/texpixel/document_ai/internal/storage/dao"
|
|
|
|
|
"gitea.com/texpixel/document_ai/pkg/common"
|
|
|
|
|
"gitea.com/texpixel/document_ai/pkg/log"
|
|
|
|
|
"gitea.com/texpixel/document_ai/pkg/sms"
|
2025-12-17 20:43:08 +08:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2025-12-10 18:33:37 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type UserService struct {
|
|
|
|
|
userDao *dao.UserDao
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewUserService() *UserService {
|
|
|
|
|
return &UserService{
|
|
|
|
|
userDao: dao.NewUserDao(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) GetSmsCode(ctx context.Context, phone string) (string, error) {
|
|
|
|
|
limit, err := cache.GetUserSendSmsLimit(ctx, phone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "get user send sms limit error", "error", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if limit >= cache.UserSendSmsLimitCount {
|
|
|
|
|
return "", errors.New("sms code send limit reached")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user, err := svc.userDao.GetByPhone(dao.DB.WithContext(ctx), phone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "get user error", "error", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
user = &dao.User{Phone: phone}
|
|
|
|
|
err = svc.userDao.Create(dao.DB.WithContext(ctx), user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "create user error", "error", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
|
|
|
|
err = sms.SendMessage(&sms.SendSmsRequest{PhoneNumbers: phone, SignName: sms.Signature, TemplateCode: sms.TemplateCode, TemplateParam: fmt.Sprintf(sms.TemplateParam, code)})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "send message error", "error", err)
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cacheErr := cache.SetUserSmsCode(ctx, phone, code)
|
|
|
|
|
if cacheErr != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "set user sms code error", "error", cacheErr)
|
|
|
|
|
}
|
|
|
|
|
cacheErr = cache.SetUserSendSmsLimit(ctx, phone)
|
|
|
|
|
if cacheErr != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetSmsCode", "msg", "set user send sms limit error", "error", cacheErr)
|
|
|
|
|
}
|
|
|
|
|
return code, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) VerifySmsCode(ctx context.Context, phone, code string) (uid int64, err error) {
|
|
|
|
|
user, err := svc.userDao.GetByPhone(dao.DB.WithContext(ctx), phone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "VerifySmsCode", "msg", "get user error", "error", err, "phone", phone)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
log.Error(ctx, "func", "VerifySmsCode", "msg", "user not found", "phone", phone)
|
|
|
|
|
return 0, errors.New("user not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
storedCode, err := cache.GetUserSmsCode(ctx, phone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "VerifySmsCode", "msg", "get user sms code error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
if storedCode != code {
|
|
|
|
|
log.Error(ctx, "func", "VerifySmsCode", "msg", "invalid sms code", "phone", phone, "code", code, "storedCode", storedCode)
|
|
|
|
|
return 0, errors.New("invalid sms code")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cacheErr := cache.DeleteUserSmsCode(ctx, phone)
|
|
|
|
|
if cacheErr != nil {
|
|
|
|
|
log.Error(ctx, "func", "VerifySmsCode", "msg", "delete user sms code error", "error", cacheErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) GetUserInfo(ctx context.Context, uid int64) (*dao.User, error) {
|
|
|
|
|
user, err := svc.userDao.GetByID(dao.DB.WithContext(ctx), uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "GetUserInfo", "msg", "get user error", "error", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if user == nil {
|
|
|
|
|
log.Warn(ctx, "func", "GetUserInfo", "msg", "user not found", "uid", uid)
|
|
|
|
|
return &dao.User{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
|
}
|
2025-12-17 20:43:08 +08:00
|
|
|
|
|
|
|
|
func (svc *UserService) RegisterByEmail(ctx context.Context, email, password string) (uid int64, err error) {
|
|
|
|
|
existingUser, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), email)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "RegisterByEmail", "msg", "get user by email error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
if existingUser != nil {
|
|
|
|
|
log.Warn(ctx, "func", "RegisterByEmail", "msg", "email already registered", "email", email)
|
|
|
|
|
return 0, common.ErrEmailExists
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "RegisterByEmail", "msg", "hash password error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := &dao.User{
|
|
|
|
|
Email: email,
|
|
|
|
|
Password: string(hashedPassword),
|
|
|
|
|
}
|
|
|
|
|
err = svc.userDao.Create(dao.DB.WithContext(ctx), user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "RegisterByEmail", "msg", "create user error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) LoginByEmail(ctx context.Context, email, password string) (uid int64, err error) {
|
|
|
|
|
user, err := svc.userDao.GetByEmail(dao.DB.WithContext(ctx), email)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "LoginByEmail", "msg", "get user by email error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
log.Warn(ctx, "func", "LoginByEmail", "msg", "user not found", "email", email)
|
|
|
|
|
return 0, common.ErrEmailNotFound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Warn(ctx, "func", "LoginByEmail", "msg", "password mismatch", "email", email)
|
|
|
|
|
return 0, common.ErrPasswordMismatch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.ID, nil
|
|
|
|
|
}
|
2026-03-06 10:28:56 +08:00
|
|
|
|
|
|
|
|
type googleTokenResponse struct {
|
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
|
IDToken string `json:"id_token"`
|
|
|
|
|
ExpiresIn int `json:"expires_in"`
|
|
|
|
|
TokenType string `json:"token_type"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 11:03:41 +08:00
|
|
|
func (svc *UserService) googleHTTPClient() *http.Client {
|
|
|
|
|
if config.GlobalConfig.Google.Proxy == "" {
|
|
|
|
|
return &http.Client{}
|
|
|
|
|
}
|
|
|
|
|
proxyURL, err := url.Parse(config.GlobalConfig.Google.Proxy)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return &http.Client{}
|
|
|
|
|
}
|
|
|
|
|
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 10:28:56 +08:00
|
|
|
func (svc *UserService) ExchangeGoogleCodeAndGetUserInfo(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*model.GoogleUserInfo, error) {
|
|
|
|
|
tokenURL := "https://oauth2.googleapis.com/token"
|
|
|
|
|
formData := url.Values{
|
|
|
|
|
"client_id": {clientID},
|
|
|
|
|
"client_secret": {clientSecret},
|
|
|
|
|
"code": {code},
|
|
|
|
|
"grant_type": {"authorization_code"},
|
|
|
|
|
"redirect_uri": {redirectURI},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 11:03:41 +08:00
|
|
|
client := svc.googleHTTPClient()
|
|
|
|
|
resp, err := client.PostForm(tokenURL, formData)
|
2026-03-06 10:28:56 +08:00
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "exchange code failed", "error", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
var tokenResp googleTokenResponse
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
|
|
|
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "decode token response failed", "error", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tokenResp.AccessToken == "" {
|
|
|
|
|
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "no access token in response")
|
|
|
|
|
return nil, errors.New("no access token in response")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userInfo, err := svc.getGoogleUserInfo(ctx, tokenResp.AccessToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "get user info failed", "error", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &model.GoogleUserInfo{
|
|
|
|
|
ID: userInfo.ID,
|
|
|
|
|
Email: userInfo.Email,
|
|
|
|
|
Name: userInfo.Name,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) getGoogleUserInfo(ctx context.Context, accessToken string) (*model.GoogleUserInfo, error) {
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
|
|
2026-03-06 11:03:41 +08:00
|
|
|
client := svc.googleHTTPClient()
|
2026-03-06 10:28:56 +08:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
var userInfo model.GoogleUserInfo
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &userInfo, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (svc *UserService) FindOrCreateGoogleUser(ctx context.Context, userInfo *model.GoogleUserInfo) (uid int64, err error) {
|
|
|
|
|
existingUser, err := svc.userDao.GetByGoogleID(dao.DB.WithContext(ctx), userInfo.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by google id error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if existingUser != nil {
|
|
|
|
|
return existingUser.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
existingUser, err = svc.userDao.GetByEmail(dao.DB.WithContext(ctx), userInfo.Email)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by email error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if existingUser != nil {
|
|
|
|
|
existingUser.GoogleID = userInfo.ID
|
|
|
|
|
err = svc.userDao.Update(dao.DB.WithContext(ctx), existingUser)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "update user google id error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return existingUser.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user := &dao.User{
|
|
|
|
|
Email: userInfo.Email,
|
|
|
|
|
GoogleID: userInfo.ID,
|
|
|
|
|
Username: userInfo.Name,
|
|
|
|
|
}
|
|
|
|
|
err = svc.userDao.Create(dao.DB.WithContext(ctx), user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "create user error", "error", err)
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user.ID, nil
|
|
|
|
|
}
|