Files
ai_wht_wechat/go_backend/service/auth_service.go
2026-01-06 19:36:42 +08:00

444 lines
13 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 service
import (
"ai_xhs/config"
"ai_xhs/database"
"ai_xhs/models"
"ai_xhs/utils"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"gorm.io/gorm"
)
type AuthService struct{}
func NewAuthService() *AuthService {
return &AuthService{}
}
// 微信手机号响应
type WxPhoneResponse struct {
PhoneInfo struct {
PhoneNumber string `json:"phoneNumber"`
PurePhoneNumber string `json:"purePhoneNumber"`
CountryCode string `json:"countryCode"`
} `json:"phone_info"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// 微信登录响应
type WxLoginResponse struct {
OpenID string `json:"openid"`
SessionKey string `json:"session_key"`
UnionID string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// WechatLogin 微信小程序登录
func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (string, *models.User, error) {
// 1. 调用微信API验证code
// 注意需要在配置文件中添加小程序的AppID和AppSecret
appID := config.AppConfig.Wechat.AppID
appSecret := config.AppConfig.Wechat.AppSecret
// 调试日志:打印配置信息
log.Printf("[微信登录] AppID: %s, AppSecret: %s (长度:%d)", appID, appSecret, len(appSecret))
// 如果没有配置微信AppID使用手机号登录逻辑
if appID == "" || appSecret == "" {
if phone == "" {
// 没有配置微信且没有手机号使用默认员工ID=1
return s.loginByEmployeeID(1)
}
// 使用手机号登录
return s.PhoneLogin(phone)
}
// 调用微信API
url := fmt.Sprintf(
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appID, appSecret, code,
)
// 调试日志打印请求URL隐藏密钥
log.Printf("[微信登录] 请求URL: https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=***&js_code=%s&grant_type=authorization_code", appID, code)
resp, err := http.Get(url)
if err != nil {
return "", nil, fmt.Errorf("调用微信API失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, fmt.Errorf("读取响应失败: %v", err)
}
// 调试日志:打印微信返回的原始响应
log.Printf("[微信登录] 微信API响应: %s", string(body))
var wxResp WxLoginResponse
if err := json.Unmarshal(body, &wxResp); err != nil {
return "", nil, fmt.Errorf("解析响应失败: %v", err)
}
if wxResp.ErrCode != 0 {
return "", nil, fmt.Errorf("微信登录失败: %s", wxResp.ErrMsg)
}
// 1.5 如果有 phoneCode调用微信API获取手机号
if phoneCode != "" {
accessTokenURL := fmt.Sprintf(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
appID, appSecret,
)
// 获取 access_token
accessTokenResp, err := http.Get(accessTokenURL)
if err != nil {
log.Printf("获取access_token失败: %v", err)
} else {
defer accessTokenResp.Body.Close()
accessTokenBody, _ := io.ReadAll(accessTokenResp.Body)
var tokenResult struct {
AccessToken string `json:"access_token"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(accessTokenBody, &tokenResult); err == nil && tokenResult.AccessToken != "" {
// 获取手机号
phoneURL := fmt.Sprintf(
"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s",
tokenResult.AccessToken,
)
phoneReqBody := map[string]string{"code": phoneCode}
phoneReqJSON, _ := json.Marshal(phoneReqBody)
phoneResp, err := http.Post(phoneURL, "application/json", bytes.NewBuffer(phoneReqJSON))
if err == nil {
defer phoneResp.Body.Close()
phoneBody, _ := io.ReadAll(phoneResp.Body)
var phoneResult WxPhoneResponse
if err := json.Unmarshal(phoneBody, &phoneResult); err == nil && phoneResult.ErrCode == 0 {
// 获取手机号成功,覆盖 phone 参数
phone = phoneResult.PhoneInfo.PurePhoneNumber
log.Printf("[微信登录] 获取手机号成功: %s", phone)
} else {
log.Printf("[微信登录] 获取手机号失败: %s", string(phoneBody))
}
}
}
}
}
// 2. 根据手机号查找用户(手机号必填)
if phone == "" {
return "", nil, errors.New("请提供手机号")
}
var employee models.User
// 通过手机号查找员工
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
if result.Error != nil {
// 手机号不存在,不允许登录
return "", nil, errors.New("手机号不存在,请联系管理员添加")
}
// 3. 检查微信绑定信息
// 如果该用户已绑定微信信息,必须与当前登录的微信信息一致(单设备登录)
if employee.WechatOpenID != nil && *employee.WechatOpenID != "" {
// 已绑定微信,检查是否一致
if *employee.WechatOpenID != wxResp.OpenID {
return "", nil, errors.New("该账号已在其他设备登录,请使用原设备登录或联系管理员")
}
// 如果有UnionID也需要检查一致性
if employee.WechatUnionID != nil && *employee.WechatUnionID != "" && wxResp.UnionID != "" {
if *employee.WechatUnionID != wxResp.UnionID {
return "", nil, errors.New("微信账号信息不匹配,请使用原设备登录")
}
}
log.Printf("[微信登录] 用户 %s (ID:%d) 微信验证通过", employee.Phone, employee.ID)
} else {
// 微信信息为空,说明是新用户首次登录,保存微信信息
log.Printf("[微信登录] 新用户首次登录,绑定微信信息: OpenID=%s, UnionID=%s", wxResp.OpenID, wxResp.UnionID)
employee.WechatOpenID = &wxResp.OpenID
if wxResp.UnionID != "" {
employee.WechatUnionID = &wxResp.UnionID
}
// 使用事务保存微信绑定信息并创建作者记录
err := database.DB.Transaction(func(tx *gorm.DB) error {
// 1. 保存微信绑定信息
if err := tx.Save(&employee).Error; err != nil {
return fmt.Errorf("保存微信绑定信息失败: %v", err)
}
// 2. 检查是否已存在作者记录(通过 created_user_id 和企业ID
var existingAuthor models.Author
result := tx.Where("created_user_id = ? AND enterprise_id = ?", employee.ID, employee.EnterpriseID).First(&existingAuthor)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 作者记录不存在,创建新记录
author := models.Author{
EnterpriseID: employee.EnterpriseID,
CreatedUserID: employee.ID,
Phone: employee.Phone,
AuthorName: employee.RealName,
Department: employee.Department,
Status: "active",
Channel: 1, // 1=小红书(默认渠道)
}
// 如果真实姓名为空,使用用户名
if author.AuthorName == "" {
author.AuthorName = employee.Username
}
if err := tx.Create(&author).Error; err != nil {
return fmt.Errorf("创建作者记录失败: %v", err)
}
log.Printf("[微信登录] 创建作者记录成功: ID=%d, Name=%s, Channel=1(小红书)", author.ID, author.AuthorName)
} else if result.Error != nil {
// 其他数据库错误
return fmt.Errorf("检查作者记录失败: %v", result.Error)
} else {
log.Printf("[微信登录] 作者记录已存在: ID=%d", existingAuthor.ID)
}
return nil
})
if err != nil {
return "", nil, err
}
log.Printf("[微信登录] 用户 %s (ID:%d) 微信绑定成功", employee.Phone, employee.ID)
}
// 4. 生成JWT token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 5. 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[微信登录] 存储token到Redis失败: %v", err)
// 不阻断登录流程,但记录错误
} else {
log.Printf("[微信登录] 用户%d token已存入Redis", employee.ID)
}
return token, &employee, nil
}
// PhoneLogin 手机号登录(用于测试或无微信配置时)
func (s *AuthService) PhoneLogin(phone string) (string, *models.User, error) {
var employee models.User
// 查找员工
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
if result.Error != nil {
return "", nil, errors.New("员工不存在或已被禁用")
}
// 生成token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[手机号登录] 存储token到Redis失败: %v", err)
}
return token, &employee, nil
}
// loginByEmployeeID 通过员工ID登录内部方法
func (s *AuthService) loginByEmployeeID(employeeID int) (string, *models.User, error) {
var employee models.User
result := database.DB.Where("id = ? AND status = ?", employeeID, "active").First(&employee)
if result.Error != nil {
return "", nil, errors.New("员工不存在或已被禁用")
}
// 生成token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[ID登录] 存储token到Redis失败: %v", err)
}
return token, &employee, nil
}
// PhonePasswordLogin 手机号密码登录
func (s *AuthService) PhonePasswordLogin(phone string, password string) (string, *models.User, error) {
if phone == "" || password == "" {
return "", nil, errors.New("手机号和密码不能为空")
}
var employee models.User
// 查找员工
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
if result.Error != nil {
return "", nil, errors.New("手机号或密码错误")
}
// 验证密码
if !utils.VerifyPassword(password, employee.Password) {
return "", nil, errors.New("手机号或密码错误")
}
// 生成token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[密码登录] 存储token到Redis失败: %v", err)
}
return token, &employee, nil
}
// CheckPhoneExists 检查手机号是否存在于user表中
func (s *AuthService) CheckPhoneExists(phone string) error {
var count int64
result := database.DB.Model(&models.User{}).Where("phone = ? AND status = ?", phone, "active").Count(&count)
if result.Error != nil {
return fmt.Errorf("查询用户信息失败: %v", result.Error)
}
if count == 0 {
return errors.New("手机号未注册,请联系管理员添加")
}
return nil
}
// XHSPhoneCodeLogin 小红书手机号验证码登录
func (s *AuthService) XHSPhoneCodeLogin(phone string, code string) (string, *models.User, error) {
if phone == "" || code == "" {
return "", nil, errors.New("手机号和验证码不能为空")
}
// 调用短信服务验证验证码
smsService := GetSmsService()
if err := smsService.VerifyCode(phone, code); err != nil {
return "", nil, err
}
var employee models.User
// 查找员工
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
if result.Error != nil {
// 用户不存在,不允许登录
return "", nil, errors.New("手机号未注册,请联系管理员添加")
}
// 生成token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[验证码登录] 存储token到Redis失败: %v", err)
}
return token, &employee, nil
}
// createNewUserFromPhone 从手机号创建新用户
func (s *AuthService) createNewUserFromPhone(phone string) (string, *models.User, error) {
// 使用事务创建用户和作者记录
var employee models.User
err := database.DB.Transaction(func(tx *gorm.DB) error {
// 1. 创建用户记录
employee = models.User{
Phone: phone,
Username: phone, // 默认用户名为手机号
Role: "user",
Status: "active",
EnterpriseID: 1, // 默认企业ID可根据实际调整
EnterpriseName: "默认企业", // 默认企业名称
}
if err := tx.Create(&employee).Error; err != nil {
return fmt.Errorf("创建用户失败: %v", err)
}
// 2. 创建作者记录
author := models.Author{
EnterpriseID: employee.EnterpriseID,
CreatedUserID: employee.ID,
Phone: employee.Phone,
AuthorName: employee.Username,
Department: "",
Status: "active",
Channel: 1, // 1=小红书
}
if err := tx.Create(&author).Error; err != nil {
return fmt.Errorf("创建作者记录失败: %v", err)
}
log.Printf("[手机号登录] 创建新用户成功: Phone=%s, UserID=%d, AuthorID=%d", phone, employee.ID, author.ID)
return nil
})
if err != nil {
return "", nil, err
}
// 生成token
token, err := utils.GenerateToken(employee.ID)
if err != nil {
return "", nil, fmt.Errorf("生成token失败: %v", err)
}
// 将token存入Redis
ctx := context.Background()
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
log.Printf("[新用户登录] 存储token到Redis失败: %v", err)
}
return token, &employee, nil
}