417 lines
13 KiB
Go
417 lines
13 KiB
Go
package service
|
||
|
||
import (
|
||
"dianshang/internal/model"
|
||
"dianshang/internal/repository"
|
||
"dianshang/pkg/jwt"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"math/rand"
|
||
"net/http"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// WeChatService 微信服务
|
||
type WeChatService struct {
|
||
userRepo *repository.UserRepository
|
||
pointsService *PointsService
|
||
db *gorm.DB
|
||
appID string
|
||
appSecret string
|
||
}
|
||
|
||
// NewWeChatService 创建微信服务实例
|
||
func NewWeChatService(db *gorm.DB, pointsService *PointsService, appID, appSecret string) *WeChatService {
|
||
// 初始化随机数种子
|
||
rand.Seed(time.Now().UnixNano())
|
||
|
||
return &WeChatService{
|
||
userRepo: repository.NewUserRepository(db),
|
||
pointsService: pointsService,
|
||
db: db,
|
||
appID: appID,
|
||
appSecret: appSecret,
|
||
}
|
||
}
|
||
|
||
// WeChatLoginResponse 微信登录响应
|
||
type WeChatLoginResponse struct {
|
||
OpenID string `json:"openid"`
|
||
SessionKey string `json:"session_key"`
|
||
UnionID string `json:"unionid"`
|
||
ErrCode int `json:"errcode"`
|
||
ErrMsg string `json:"errmsg"`
|
||
}
|
||
|
||
// WeChatUserInfo 微信用户信息
|
||
type WeChatUserInfo struct {
|
||
OpenID string `json:"openId"`
|
||
NickName string `json:"nickName"`
|
||
Gender int `json:"gender"`
|
||
City string `json:"city"`
|
||
Province string `json:"province"`
|
||
Country string `json:"country"`
|
||
AvatarURL string `json:"avatarUrl"`
|
||
Language string `json:"language"`
|
||
}
|
||
|
||
// Login 微信登录
|
||
func (s *WeChatService) Login(code string, ip string, userAgent string) (*model.User, string, error) {
|
||
// 验证输入参数
|
||
if code == "" {
|
||
return nil, "", errors.New("微信登录code不能为空")
|
||
}
|
||
|
||
fmt.Printf("开始微信登录流程: code=%s\n", code)
|
||
|
||
// 1. 调用微信API获取openid和session_key
|
||
wechatResp, err := s.getWeChatSession(code)
|
||
if err != nil {
|
||
s.logUserLogin(0, "wechat", false, fmt.Sprintf("获取微信会话失败: %v", err), ip, userAgent)
|
||
return nil, "", fmt.Errorf("获取微信会话失败: %v", err)
|
||
}
|
||
|
||
if wechatResp.ErrCode != 0 {
|
||
errorMsg := fmt.Sprintf("微信API返回错误: code=%d, msg=%s", wechatResp.ErrCode, wechatResp.ErrMsg)
|
||
s.logUserLogin(0, "wechat", false, errorMsg, ip, userAgent)
|
||
return nil, "", fmt.Errorf("微信登录失败: %s", wechatResp.ErrMsg)
|
||
}
|
||
|
||
fmt.Printf("成功获取微信会话: OpenID=%s\n", wechatResp.OpenID)
|
||
|
||
// 2. 查找或创建用户
|
||
user, err := s.findOrCreateUser(wechatResp)
|
||
if err != nil {
|
||
s.logUserLogin(0, "wechat", false, fmt.Sprintf("用户处理失败: %v", err), ip, userAgent)
|
||
return nil, "", fmt.Errorf("用户处理失败: %v", err)
|
||
}
|
||
|
||
// 3. 保存微信会话信息
|
||
if err := s.saveWeChatSession(user.ID, wechatResp); err != nil {
|
||
s.logUserLogin(user.ID, "wechat", false, fmt.Sprintf("保存会话失败: %v", err), ip, userAgent)
|
||
return nil, "", fmt.Errorf("保存会话失败: %v", err)
|
||
}
|
||
|
||
// 4. 生成自定义登录态(JWT token)
|
||
// 按照微信官方建议,生成自定义登录态用于维护用户登录状态
|
||
tokenExpiry := 7 * 24 * 3600 // 7天有效期,与session_key保持一致
|
||
token, err := jwt.GenerateToken(user.ID, "user", tokenExpiry)
|
||
if err != nil {
|
||
s.logUserLogin(user.ID, "wechat", false, fmt.Sprintf("生成token失败: %v", err), ip, userAgent)
|
||
return nil, "", fmt.Errorf("生成自定义登录态失败: %v", err)
|
||
}
|
||
|
||
// 5. 检查并给予每日首次登录积分
|
||
if s.pointsService != nil {
|
||
awarded, err := s.pointsService.CheckAndGiveDailyLoginPoints(user.ID)
|
||
if err != nil {
|
||
fmt.Printf("每日登录积分处理失败: %v\n", err)
|
||
} else if awarded {
|
||
fmt.Printf("用户 %d 获得每日首次登录积分\n", user.ID)
|
||
}
|
||
}
|
||
|
||
// 6. 记录登录日志
|
||
s.logUserLogin(user.ID, "wechat", true, "", ip, userAgent)
|
||
|
||
fmt.Printf("微信登录成功: UserID=%d, OpenID=%s, Token生成完成\n", user.ID, user.OpenID)
|
||
return user, token, nil
|
||
}
|
||
|
||
// LoginWithUserInfo 微信登录并更新用户信息
|
||
func (s *WeChatService) LoginWithUserInfo(code string, userInfo WeChatUserInfo, ip string, userAgent string) (*model.User, string, error) {
|
||
// 1. 先进行基本登录
|
||
user, token, err := s.Login(code, ip, userAgent)
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
|
||
// 2. 更新用户信息
|
||
if err := s.updateUserInfo(user.ID, userInfo); err != nil {
|
||
return nil, "", fmt.Errorf("更新用户信息失败: %v", err)
|
||
}
|
||
|
||
// 3. 重新获取用户信息
|
||
updatedUser, err := s.userRepo.GetByID(user.ID)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("获取用户信息失败: %v", err)
|
||
}
|
||
|
||
return updatedUser, token, nil
|
||
}
|
||
|
||
// getWeChatSession 获取微信会话(按照官方文档标准实现code2Session)
|
||
func (s *WeChatService) getWeChatSession(code string) (*WeChatLoginResponse, error) {
|
||
// 验证code格式
|
||
if code == "" {
|
||
return nil, errors.New("登录凭证code不能为空")
|
||
}
|
||
if len(code) < 10 {
|
||
return nil, errors.New("登录凭证code格式异常")
|
||
}
|
||
|
||
// 开发模式:如果AppSecret是占位符或为空,返回模拟数据
|
||
// 注意:当配置了真实的AppSecret时,会调用微信官方API
|
||
if s.appSecret == "your-wechat-app-secret" || s.appSecret == "your_wechat_appsecret" || s.appSecret == "" {
|
||
// 在开发模式下,使用固定的OpenID来模拟同一个微信用户
|
||
// 这样可以避免每次登录都创建新用户的问题
|
||
return &WeChatLoginResponse{
|
||
OpenID: "dev_openid_fixed_user_001", // 使用固定的OpenID
|
||
SessionKey: "dev_session_key_" + time.Now().Format("20060102150405"),
|
||
UnionID: "dev_unionid_fixed_user_001", // 使用固定的UnionID
|
||
ErrCode: 0,
|
||
ErrMsg: "",
|
||
}, nil
|
||
}
|
||
|
||
// 按照微信官方文档调用auth.code2Session接口
|
||
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||
s.appID, s.appSecret, code)
|
||
|
||
// 创建HTTP客户端,设置超时
|
||
client := &http.Client{
|
||
Timeout: 10 * time.Second,
|
||
}
|
||
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("调用微信API失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查HTTP状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("微信API返回异常状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取微信API响应失败: %v", err)
|
||
}
|
||
|
||
var wechatResp WeChatLoginResponse
|
||
if err := json.Unmarshal(body, &wechatResp); err != nil {
|
||
return nil, fmt.Errorf("解析微信API响应失败: %v", err)
|
||
}
|
||
|
||
// 检查微信API返回的错误
|
||
if wechatResp.ErrCode != 0 {
|
||
return nil, fmt.Errorf("微信API错误 [%d]: %s", wechatResp.ErrCode, wechatResp.ErrMsg)
|
||
}
|
||
|
||
// 验证必要字段
|
||
if wechatResp.OpenID == "" {
|
||
return nil, errors.New("微信API未返回OpenID")
|
||
}
|
||
if wechatResp.SessionKey == "" {
|
||
return nil, errors.New("微信API未返回SessionKey")
|
||
}
|
||
|
||
return &wechatResp, nil
|
||
}
|
||
|
||
// generateRandomUsername 生成随机用户名,格式为"用户xxxxxxxx"(包含字母和数字)
|
||
func (s *WeChatService) generateRandomUsername() string {
|
||
// 定义字符集:数字和小写字母
|
||
charset := "0123456789abcdefghijklmnopqrstuvwxyz"
|
||
|
||
// 生成8位随机字符串
|
||
randomSuffix := make([]byte, 8)
|
||
for i := range randomSuffix {
|
||
randomSuffix[i] = charset[rand.Intn(len(charset))]
|
||
}
|
||
|
||
return fmt.Sprintf("用户%s", string(randomSuffix))
|
||
}
|
||
|
||
// findOrCreateUser 查找或创建用户
|
||
func (s *WeChatService) findOrCreateUser(wechatResp *WeChatLoginResponse) (*model.User, error) {
|
||
// 验证必要参数
|
||
if wechatResp.OpenID == "" {
|
||
return nil, errors.New("微信OpenID不能为空")
|
||
}
|
||
|
||
// 先尝试通过openid查找用户
|
||
user, err := s.userRepo.GetByOpenID(wechatResp.OpenID)
|
||
if err != nil {
|
||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, fmt.Errorf("查询用户失败: %v", err)
|
||
}
|
||
} else {
|
||
// 用户已存在,检查状态
|
||
if user.Status == 0 {
|
||
return nil, errors.New("用户已被禁用,请联系客服")
|
||
}
|
||
fmt.Printf("找到已存在用户: ID=%d, OpenID=%s, Nickname=%s\n", user.ID, user.OpenID, user.Nickname)
|
||
return user, nil
|
||
}
|
||
|
||
// 用户不存在,创建新用户
|
||
fmt.Printf("用户不存在,开始创建新用户: OpenID=%s\n", wechatResp.OpenID)
|
||
|
||
// 生成随机用户名,格式为"用户xxxxxxxx"
|
||
randomUsername := s.generateRandomUsername()
|
||
|
||
user = &model.User{
|
||
OpenID: wechatResp.OpenID,
|
||
UnionID: wechatResp.UnionID,
|
||
Nickname: randomUsername,
|
||
Avatar: "", // 默认头像为空,后续可通过授权获取
|
||
Status: 1, // 1表示正常状态
|
||
Level: 1, // 初始等级为1
|
||
Gender: 0, // 0表示未知性别
|
||
}
|
||
|
||
if err := s.userRepo.Create(user); err != nil {
|
||
return nil, fmt.Errorf("创建用户失败: %v", err)
|
||
}
|
||
|
||
fmt.Printf("成功创建新用户: ID=%d, OpenID=%s, Nickname=%s\n", user.ID, user.OpenID, user.Nickname)
|
||
return user, nil
|
||
}
|
||
|
||
// saveWeChatSession 保存微信会话信息(安全存储session_key)
|
||
func (s *WeChatService) saveWeChatSession(userID uint, wechatResp *WeChatLoginResponse) error {
|
||
// session_key是敏感信息,需要安全存储
|
||
// 在生产环境中,建议对session_key进行加密存储
|
||
|
||
// 计算session_key过期时间(微信session_key有效期通常为7天)
|
||
sessionExpiry := time.Now().Add(7 * 24 * time.Hour)
|
||
|
||
// 简单示例:保存到用户表的额外字段中
|
||
// 在生产环境中,建议使用专门的会话表或Redis等缓存存储
|
||
updates := map[string]interface{}{
|
||
"open_id": wechatResp.OpenID,
|
||
"wechat_session_key": wechatResp.SessionKey, // 生产环境中应加密存储
|
||
"union_id": wechatResp.UnionID,
|
||
"session_expiry": sessionExpiry,
|
||
"updated_at": time.Now(),
|
||
}
|
||
|
||
if err := s.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||
return fmt.Errorf("保存微信会话信息失败: %v", err)
|
||
}
|
||
|
||
// 记录会话创建日志
|
||
fmt.Printf("用户 %d 的微信会话已保存,OpenID: %s, 过期时间: %s\n",
|
||
userID, wechatResp.OpenID, sessionExpiry.Format("2006-01-02 15:04:05"))
|
||
|
||
return nil
|
||
}
|
||
|
||
// updateUserInfo 更新用户信息
|
||
func (s *WeChatService) updateUserInfo(userID uint, userInfo WeChatUserInfo) error {
|
||
updates := map[string]interface{}{
|
||
"nickname": userInfo.NickName,
|
||
"avatar": userInfo.AvatarURL,
|
||
"gender": userInfo.Gender,
|
||
}
|
||
|
||
if err := s.userRepo.Update(userID, updates); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取用户的openid(从ai_users表中获取)
|
||
user, err := s.userRepo.GetByID(userID)
|
||
if err != nil {
|
||
return fmt.Errorf("获取用户信息失败: %v", err)
|
||
}
|
||
|
||
// 保存详细的微信用户信息
|
||
wechatUserInfo := struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
UserID uint `gorm:"column:user_id;not null;unique"`
|
||
OpenID string `gorm:"column:openid;not null;unique"`
|
||
Nickname string `gorm:"column:nickname"`
|
||
AvatarURL string `gorm:"column:avatar_url"`
|
||
Gender int `gorm:"column:gender"`
|
||
Country string `gorm:"column:country"`
|
||
Province string `gorm:"column:province"`
|
||
City string `gorm:"column:city"`
|
||
Language string `gorm:"column:language"`
|
||
CreatedAt time.Time `gorm:"column:created_at"`
|
||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||
}{
|
||
UserID: userID,
|
||
OpenID: user.OpenID, // 使用从数据库获取的openid
|
||
Nickname: userInfo.NickName,
|
||
AvatarURL: userInfo.AvatarURL,
|
||
Gender: userInfo.Gender,
|
||
Country: userInfo.Country,
|
||
Province: userInfo.Province,
|
||
City: userInfo.City,
|
||
Language: userInfo.Language,
|
||
}
|
||
|
||
return s.db.Table("ai_wechat_user_info").Save(&wechatUserInfo).Error
|
||
}
|
||
|
||
// logUserLogin 记录用户登录日志
|
||
func (s *WeChatService) logUserLogin(userID uint, loginType string, success bool, errorMsg string, ip string, userAgent string) {
|
||
status := 1
|
||
if !success {
|
||
status = 0
|
||
}
|
||
|
||
// 使用LogService创建登录日志
|
||
logService := NewLogService(s.db)
|
||
remark := loginType
|
||
if errorMsg != "" {
|
||
remark = fmt.Sprintf("%s: %s", loginType, errorMsg)
|
||
}
|
||
|
||
err := logService.CreateLoginLog(userID, ip, userAgent, status, remark)
|
||
if err != nil {
|
||
fmt.Printf("创建登录日志失败: %v\n", err)
|
||
}
|
||
}
|
||
|
||
// GetUserSession 获取用户会话信息
|
||
func (s *WeChatService) GetUserSession(userID uint) (map[string]interface{}, error) {
|
||
var user model.User
|
||
err := s.db.Where("id = ?", userID).First(&user).Error
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New("用户不存在")
|
||
}
|
||
return nil, fmt.Errorf("查询用户失败: %v", err)
|
||
}
|
||
|
||
// 检查session是否过期
|
||
if user.SessionExpiry != nil && user.SessionExpiry.Before(time.Now()) {
|
||
return nil, errors.New("会话已过期")
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"session_key": user.WeChatSessionKey,
|
||
"openid": user.OpenID,
|
||
"unionid": user.UnionID,
|
||
"expires_at": user.SessionExpiry,
|
||
}, nil
|
||
}
|
||
|
||
// ValidateSessionKey 验证session_key有效性
|
||
func (s *WeChatService) ValidateSessionKey(userID uint) (bool, error) {
|
||
session, err := s.GetUserSession(userID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
// 检查session_key是否存在
|
||
sessionKey, ok := session["session_key"].(string)
|
||
if !ok || sessionKey == "" {
|
||
return false, errors.New("session_key不存在")
|
||
}
|
||
|
||
// 检查过期时间
|
||
expiresAt, ok := session["expires_at"].(*time.Time)
|
||
if ok && expiresAt != nil && expiresAt.Before(time.Now()) {
|
||
return false, errors.New("session_key已过期")
|
||
}
|
||
|
||
return true, nil
|
||
} |