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 }