Files
ai_dianshang/server/internal/service/wechat.go
2025-11-17 13:32:54 +08:00

417 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 (
"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
}