Files
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

381 lines
12 KiB
Go
Raw Permalink 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 auth
import (
"strconv"
"time"
"com.sclktx/m/v2/internal/common/model"
"com.sclktx/m/v2/internal/common/response"
"com.sclktx/m/v2/internal/common/utils"
"com.sclktx/m/v2/internal/pkg/config"
"com.sclktx/m/v2/internal/pkg/wechat"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AuthHandler struct {
db *gorm.DB
cfg *config.Config
wxClient *wechat.MiniProgramClient
}
func NewAuthHandler(db *gorm.DB, cfg *config.Config, wxClient *wechat.MiniProgramClient) *AuthHandler {
return &AuthHandler{db: db, cfg: cfg, wxClient: wxClient}
}
type LoginRequest struct {
Code string `json:"code" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
UserInfo *UserInfoVO `json:"user_info"`
}
type UserInfoVO struct {
ID uint `json:"id"`
Openid string `json:"openid"`
Nickname string `json:"nickname"`
RealName string `json:"real_name"`
Phone string `json:"phone"`
AvatarURL string `json:"avatar_url"`
RoleCode string `json:"role"`
RoleName string `json:"role_name"`
Status int `json:"status"`
IsFormalEmployee bool `json:"is_formal_employee"`
DepartmentName string `json:"department_name,omitempty"`
Position string `json:"position,omitempty"`
}
// Login 微信登录code 换 openid → 查找或注册用户 → 生成 JWT
// @Summary 微信登录
// @Description 使用微信小程序 code 登录,返回 JWT token 和用户信息
// @Tags 认证
// @Accept json
// @Produce json
// @Param body body LoginRequest true "微信登录code"
// @Success 200 {object} response.Response{data=LoginResponse} "登录成功"
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误: "+err.Error())
return
}
// 调用微信 code2session 换取 openid
session, err := h.wxClient.Code2Session(req.Code)
if err != nil {
response.BadRequest(c, "微信登录失败: "+err.Error())
return
}
openid := session.OpenID
var user model.User
result := h.db.Where("openid = ?", openid).First(&user)
if result.Error != nil {
// 新用户注册,分配初始角色(默认访客)
user = model.User{Openid: openid, Role: "visitor", Status: 1, AvatarURL: h.cfg.System.DefaultAvatar}
if err := h.db.Create(&user).Error; err != nil {
response.InternalError(c, "创建用户失败")
return
}
}
roleCode := user.Role
if roleCode == "" {
roleCode = "visitor"
}
token, err := utils.GenerateToken(user.ID, roleCode, h.cfg.JWT.Secret, h.cfg.JWT.ExpireHours)
if err != nil {
response.InternalError(c, "生成令牌失败")
return
}
// 查询用户部门信息(从 UserDepartment 关联表获取)
deptName, position := "", ""
var ud model.UserDepartment
if err := h.db.Where("user_id = ?", user.ID).First(&ud).Error; err == nil {
deptName = ud.DepartmentName
position = ud.Position
}
response.Success(c, LoginResponse{
Token: token,
UserInfo: &UserInfoVO{
ID: user.ID, Openid: user.Openid, Nickname: user.Nickname,
RealName: user.RealName, Phone: user.Phone, AvatarURL: user.AvatarURL,
RoleCode: roleCode, RoleName: roleNameMap(roleCode),
Status: user.Status,
IsFormalEmployee: roleCode == "employee",
DepartmentName: deptName,
Position: position,
},
})
}
// GetUserInfo 获取当前用户信息
// @Summary 获取当前用户信息
// @Description 获取当前登录用户的详细信息,包含角色、部门、职位等
// @Tags 认证
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=UserInfoVO} "用户信息"
// @Router /auth/userinfo [get]
func (h *AuthHandler) GetUserInfo(c *gin.Context) {
userID := utils.GetUserID(c)
var user model.User
if err := h.db.First(&user, userID).Error; err != nil {
response.Unauthorized(c, "用户不存在,请重新登录")
return
}
roleCode := user.Role
if roleCode == "" {
roleCode = "visitor"
}
// 查询用户部门信息(从 UserDepartment 关联表获取)
deptName, position := "", ""
var ud model.UserDepartment
if err := h.db.Where("user_id = ?", userID).First(&ud).Error; err == nil {
deptName = ud.DepartmentName
position = ud.Position
}
response.Success(c, UserInfoVO{
ID: user.ID, Openid: user.Openid, Nickname: user.Nickname,
RealName: user.RealName, Phone: user.Phone, AvatarURL: user.AvatarURL,
RoleCode: roleCode, RoleName: roleNameMap(roleCode),
Status: user.Status,
IsFormalEmployee: roleCode == "employee",
DepartmentName: deptName,
Position: position,
})
}
// UpdateUserInfo 更新用户信息(部分更新)
// @Summary 更新用户信息
// @Description 部分更新当前用户的昵称、真实姓名、头像
// @Tags 认证
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body object{nickname=string,real_name=string,avatar_url=string} true "要更新的字段"
// @Success 200 {object} response.Response "更新成功"
// @Router /auth/userinfo [put]
func (h *AuthHandler) UpdateUserInfo(c *gin.Context) {
userID := utils.GetUserID(c)
var req struct {
Nickname string `json:"nickname"`
RealName string `json:"real_name"`
AvatarURL string `json:"avatar_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
updates := map[string]interface{}{}
if req.Nickname != "" {
updates["nickname"] = req.Nickname
}
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.AvatarURL != "" {
updates["avatar_url"] = req.AvatarURL
}
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
response.InternalError(c, "更新失败")
return
}
response.SuccessWithMessage(c, "更新成功", nil)
}
// UpdateAvatar 更新用户头像
// @Summary 更新用户头像
// @Description 接受微信图片 URL直接更新用户头像
// @Tags 用户
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param userId path int true "用户ID"
// @Param body body object{url=string} true "微信图片URL"
// @Success 200 {object} response.Response "成功"
// @Router /v1/user/{userId}/avatar [put]
func (h *AuthHandler) UpdateAvatar(c *gin.Context) {
userID, err := strconv.ParseUint(c.Param("userId"), 10, 64)
if err != nil {
response.BadRequest(c, "参数错误")
return
}
var req struct {
URL string `json:"url" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Update("avatar_url", req.URL).Error; err != nil {
response.InternalError(c, "更新失败")
return
}
response.SuccessWithMessage(c, "更新成功", nil)
}
// BindPhone 绑定手机号(微信解密)
// @Summary 绑定手机号
// @Description 通过微信加密数据解密并绑定手机号
// @Tags 认证
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body object{code=string,encrypted_data=string,iv=string} true "微信加密数据"
// @Success 200 {object} response.Response "绑定成功"
// @Router /auth/phone [post]
func (h *AuthHandler) BindPhone(c *gin.Context) {
userID := utils.GetUserID(c)
var req struct {
Code string `json:"code" binding:"required"`
EncryptedData string `json:"encrypted_data" binding:"required"`
Iv string `json:"iv" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
// 用 code 换取 session_key
session, err := h.wxClient.Code2Session(req.Code)
if err != nil {
response.BadRequest(c, "微信授权失败")
return
}
// 解密手机号
phone, err := h.wxClient.DecryptPhone(session.SessionKey, req.EncryptedData, req.Iv)
if err != nil {
response.BadRequest(c, "手机号解密失败")
return
}
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"phone": phone, "phone_verified": true,
}).Error; err != nil {
response.InternalError(c, "绑定失败")
return
}
// 自动匹配待确认员工:手机号匹配则升级为正式员工
var pending model.PendingEmployee
if err := h.db.Where("phone = ? AND is_matched = ?", phone, false).First(&pending).Error; err == nil {
now := time.Now()
// 创建 UserDepartment
ud := model.UserDepartment{
UserID: userID,
DepartmentID: pending.DepartmentID,
DepartmentName: pending.DepartmentName,
Position: pending.Position,
EmployeeType: pending.EmployeeType,
EmployeeStatus: 1,
}
h.db.Where("user_id = ?", userID).Assign(ud).FirstOrCreate(&model.UserDepartment{UserID: userID})
// 升级用户角色
h.db.Model(&model.User{}).Where("id = ?", userID).Update("role", "employee")
// 标记已匹配
h.db.Model(&pending).Updates(map[string]interface{}{
"is_matched": true,
"matched_user_id": &userID,
"matched_at": &now,
})
}
response.SuccessWithMessage(c, "绑定成功", nil)
}
// MockLogin 模拟登录(开发调试用):直接登录 id=1 的用户
// @Summary 模拟登录
// @Description 直接登录 id=1 的账号,返回与微信登录一致的 token 和用户信息
// @Tags 认证
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=LoginResponse} "登录成功"
// @Router /auth/mock-login [post]
func (h *AuthHandler) MockLogin(c *gin.Context) {
var user model.User
if err := h.db.First(&user, 1).Error; err != nil {
response.NotFound(c, "用户不存在")
return
}
roleCode := user.Role
if roleCode == "" {
roleCode = "visitor"
}
token, err := utils.GenerateToken(user.ID, roleCode, h.cfg.JWT.Secret, h.cfg.JWT.ExpireHours)
if err != nil {
response.InternalError(c, "生成令牌失败")
return
}
// 查询用户部门信息(从 UserDepartment 关联表获取)
deptName, position := "", ""
var ud model.UserDepartment
if err := h.db.Where("user_id = ?", user.ID).First(&ud).Error; err == nil {
deptName = ud.DepartmentName
position = ud.Position
}
response.Success(c, LoginResponse{
Token: token,
UserInfo: &UserInfoVO{
ID: user.ID, Openid: user.Openid, Nickname: user.Nickname,
RealName: user.RealName, Phone: user.Phone, AvatarURL: user.AvatarURL,
RoleCode: roleCode, RoleName: roleNameMap(roleCode),
Status: user.Status,
IsFormalEmployee: roleCode == "employee",
DepartmentName: deptName,
Position: position,
},
})
}
func (h *AuthHandler) RegisterRoutes(r *gin.RouterGroup) {
auth := r.Group("/auth")
{
auth.POST("/login", h.Login)
auth.POST("/mock-login", h.MockLogin)
}
}
// RegisterAuthRoutes 注册需要 JWT 认证的 auth 路由
func (h *AuthHandler) RegisterAuthRoutes(r *gin.RouterGroup) {
auth := r.Group("/auth")
{
auth.GET("/userinfo", h.GetUserInfo)
auth.PUT("/userinfo", h.UpdateUserInfo)
auth.POST("/phone", h.BindPhone)
}
// 头像更新
r.PUT("/user/:userId/avatar", h.UpdateAvatar)
}
// roleNameMap 角色编码 → 中文名
func roleNameMap(code string) string {
switch code {
case "employee":
return "员工"
case "guard":
return "保安"
default:
return "访客"
}
}