381 lines
12 KiB
Go
381 lines
12 KiB
Go
// 认证模块:微信登录、用户信息获取和更新
|
||
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 "访客"
|
||
}
|
||
}
|