Files

417 lines
13 KiB
Go
Raw Permalink Normal View History

2025-11-17 14:11:46 +08:00
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
}