532 lines
17 KiB
Go
532 lines
17 KiB
Go
//nolint:unused
|
||
package handlers
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
|
||
"tyapi-server/internal/application/user"
|
||
"tyapi-server/internal/application/user/dto/commands"
|
||
"tyapi-server/internal/application/user/dto/queries"
|
||
_ "tyapi-server/internal/application/user/dto/responses"
|
||
"tyapi-server/internal/config"
|
||
"tyapi-server/internal/shared/crypto"
|
||
"tyapi-server/internal/shared/interfaces"
|
||
"tyapi-server/internal/shared/middleware"
|
||
)
|
||
|
||
// UserHandler 用户HTTP处理器
|
||
type UserHandler struct {
|
||
appService user.UserApplicationService
|
||
response interfaces.ResponseBuilder
|
||
validator interfaces.RequestValidator
|
||
logger *zap.Logger
|
||
jwtAuth *middleware.JWTAuthMiddleware
|
||
config *config.Config
|
||
cache interfaces.CacheService
|
||
}
|
||
|
||
// NewUserHandler 创建用户处理器
|
||
func NewUserHandler(
|
||
appService user.UserApplicationService,
|
||
response interfaces.ResponseBuilder,
|
||
validator interfaces.RequestValidator,
|
||
logger *zap.Logger,
|
||
jwtAuth *middleware.JWTAuthMiddleware,
|
||
cfg *config.Config,
|
||
cache interfaces.CacheService,
|
||
) *UserHandler {
|
||
return &UserHandler{
|
||
appService: appService,
|
||
response: response,
|
||
validator: validator,
|
||
logger: logger,
|
||
jwtAuth: jwtAuth,
|
||
config: cfg,
|
||
cache: cache,
|
||
}
|
||
}
|
||
|
||
// decodedSendCodeData 解码后的请求数据结构
|
||
type decodedSendCodeData struct {
|
||
Phone string `json:"phone"`
|
||
Scene string `json:"scene"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
Nonce string `json:"nonce"`
|
||
Signature string `json:"signature"`
|
||
}
|
||
|
||
// SendCode 发送验证码
|
||
// @Summary 发送短信验证码
|
||
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景。需要提供有效的签名验证。只接收编码后的data字段(使用自定义编码方案)
|
||
// @Tags 用户认证
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body commands.SendCodeCommand true "发送验证码请求(包含data字段和可选的captchaVerifyParam字段)"
|
||
// @Success 200 {object} map[string]interface{} "验证码发送成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||
// @Failure 429 {object} map[string]interface{} "请求频率限制"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/send-code [post]
|
||
func (h *UserHandler) SendCode(c *gin.Context) {
|
||
var cmd commands.SendCodeCommand
|
||
|
||
// 绑定请求(包含data字段和可选的captchaVerifyParam字段)
|
||
if err := c.ShouldBindJSON(&cmd); err != nil {
|
||
h.response.BadRequest(c, "请求参数格式错误,必须提供data字段")
|
||
return
|
||
}
|
||
|
||
// 验证data字段不为空
|
||
if cmd.Data == "" {
|
||
h.response.BadRequest(c, "data字段不能为空")
|
||
return
|
||
}
|
||
|
||
// 解码自定义编码的数据
|
||
decodedData, err := h.decodeRequestData(cmd.Data)
|
||
if err != nil {
|
||
h.logger.Warn("解码请求数据失败",
|
||
zap.String("client_ip", c.ClientIP()),
|
||
zap.Error(err))
|
||
h.response.BadRequest(c, "请求数据解码失败")
|
||
return
|
||
}
|
||
|
||
// 验证必要字段
|
||
if decodedData.Phone == "" || decodedData.Scene == "" {
|
||
h.response.BadRequest(c, "手机号和场景不能为空")
|
||
return
|
||
}
|
||
|
||
// 如果启用了签名验证,进行签名校验(包含nonce唯一性检查,防止重放攻击)
|
||
if h.config.SMS.SignatureEnabled {
|
||
if err := h.verifyDecodedSignature(c.Request.Context(), decodedData); err != nil {
|
||
h.logger.Warn("短信发送签名验证失败",
|
||
zap.String("phone", decodedData.Phone),
|
||
zap.String("scene", decodedData.Scene),
|
||
zap.String("client_ip", c.ClientIP()),
|
||
zap.Error(err))
|
||
|
||
// 根据错误类型返回不同的用户友好消息(不暴露技术细节)
|
||
userMessage := h.getSignatureErrorMessage(err)
|
||
h.response.BadRequest(c, userMessage)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 构建SendCodeCommand用于调用应用服务
|
||
serviceCmd := &commands.SendCodeCommand{
|
||
Phone: decodedData.Phone,
|
||
Scene: decodedData.Scene,
|
||
Timestamp: decodedData.Timestamp,
|
||
Nonce: decodedData.Nonce,
|
||
Signature: decodedData.Signature,
|
||
CaptchaVerifyParam: cmd.CaptchaVerifyParam,
|
||
}
|
||
|
||
clientIP := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
|
||
if err := h.appService.SendCode(c.Request.Context(), serviceCmd, clientIP, userAgent); err != nil {
|
||
h.response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, nil, "验证码发送成功")
|
||
}
|
||
|
||
// decodeRequestData 解码自定义编码的请求数据
|
||
func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeData, error) {
|
||
// 使用自定义编码方案解码
|
||
decodedData, err := crypto.DecodeRequest(encodedData)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("自定义编码解码失败: %w", err)
|
||
}
|
||
|
||
// 解析JSON
|
||
var decoded decodedSendCodeData
|
||
if err := json.Unmarshal([]byte(decodedData), &decoded); err != nil {
|
||
return nil, fmt.Errorf("JSON解析失败: %w", err)
|
||
}
|
||
|
||
return &decoded, nil
|
||
}
|
||
|
||
// verifyDecodedSignature 验证解码后的签名(包含nonce唯一性检查,防止重放攻击)
|
||
func (h *UserHandler) verifyDecodedSignature(ctx context.Context, data *decodedSendCodeData) error {
|
||
// 构建参数map(包含signature字段,VerifySignature会自动排除它)
|
||
params := map[string]string{
|
||
"phone": data.Phone,
|
||
"scene": data.Scene,
|
||
"signature": data.Signature,
|
||
}
|
||
|
||
// 验证签名并检查nonce唯一性(防止重放攻击)
|
||
return crypto.VerifySignatureWithNonceCheck(
|
||
ctx,
|
||
params,
|
||
h.config.SMS.SignatureSecret,
|
||
data.Timestamp,
|
||
data.Nonce,
|
||
h.cache,
|
||
"sms:signature", // 缓存键前缀
|
||
)
|
||
}
|
||
|
||
// getSignatureErrorMessage 根据错误类型返回用户友好的错误消息(不暴露技术细节)
|
||
func (h *UserHandler) getSignatureErrorMessage(err error) string {
|
||
errMsg := err.Error()
|
||
|
||
// 根据错误消息内容判断错误类型,返回通用的用户友好消息
|
||
if strings.Contains(errMsg, "请求已被使用") || strings.Contains(errMsg, "重复提交") {
|
||
// 重放攻击:返回通用消息,不暴露具体原因
|
||
return "请求无效,请重新操作"
|
||
}
|
||
if strings.Contains(errMsg, "时间戳") || strings.Contains(errMsg, "过期") {
|
||
// 时间戳过期:返回通用消息
|
||
return "请求已过期,请重新操作"
|
||
}
|
||
if strings.Contains(errMsg, "签名") {
|
||
// 签名错误:返回通用消息
|
||
return "请求验证失败,请重新操作"
|
||
}
|
||
|
||
// 其他错误:返回通用消息
|
||
return "请求验证失败,请重新操作"
|
||
}
|
||
|
||
|
||
// Register 用户注册
|
||
// @Summary 用户注册
|
||
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||
// @Tags 用户认证
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body commands.RegisterUserCommand true "用户注册请求"
|
||
// @Success 201 {object} responses.RegisterUserResponse "注册成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||
// @Failure 409 {object} map[string]interface{} "手机号已存在"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/register [post]
|
||
func (h *UserHandler) Register(c *gin.Context) {
|
||
var cmd commands.RegisterUserCommand
|
||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||
return
|
||
}
|
||
|
||
resp, err := h.appService.Register(c.Request.Context(), &cmd)
|
||
if err != nil {
|
||
h.logger.Error("注册用户失败", zap.Error(err))
|
||
h.response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
h.response.Created(c, resp, "用户注册成功")
|
||
}
|
||
|
||
// LoginWithPassword 密码登录
|
||
// @Summary 用户密码登录
|
||
// @Description 使用手机号和密码进行用户登录,返回JWT令牌
|
||
// @Tags 用户认证
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body commands.LoginWithPasswordCommand true "密码登录请求"
|
||
// @Success 200 {object} responses.LoginUserResponse "登录成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||
// @Failure 401 {object} map[string]interface{} "用户名或密码错误"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/login-password [post]
|
||
func (h *UserHandler) LoginWithPassword(c *gin.Context) {
|
||
var cmd commands.LoginWithPasswordCommand
|
||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||
return
|
||
}
|
||
|
||
resp, err := h.appService.LoginWithPassword(c.Request.Context(), &cmd)
|
||
if err != nil {
|
||
h.logger.Error("密码登录失败", zap.Error(err))
|
||
h.response.Unauthorized(c, "用户名或密码错误")
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "登录成功")
|
||
}
|
||
|
||
// LoginWithSMS 短信验证码登录
|
||
// @Summary 用户短信验证码登录
|
||
// @Description 使用手机号和短信验证码进行用户登录,返回JWT令牌
|
||
// @Tags 用户认证
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body commands.LoginWithSMSCommand true "短信登录请求"
|
||
// @Success 200 {object} responses.LoginUserResponse "登录成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/login-sms [post]
|
||
func (h *UserHandler) LoginWithSMS(c *gin.Context) {
|
||
var cmd commands.LoginWithSMSCommand
|
||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||
return
|
||
}
|
||
|
||
resp, err := h.appService.LoginWithSMS(c.Request.Context(), &cmd)
|
||
if err != nil {
|
||
h.logger.Error("短信登录失败", zap.Error(err))
|
||
h.response.Unauthorized(c, err.Error())
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "登录成功")
|
||
}
|
||
|
||
// GetProfile 获取当前用户信息
|
||
// @Summary 获取当前用户信息
|
||
// @Description 根据JWT令牌获取当前登录用户的详细信息
|
||
// @Tags 用户管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Success 200 {object} responses.UserProfileResponse "用户信息"
|
||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/me [get]
|
||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||
userID := h.getCurrentUserID(c)
|
||
if userID == "" {
|
||
h.response.Unauthorized(c, "用户未登录")
|
||
return
|
||
}
|
||
|
||
resp, err := h.appService.GetUserProfile(c.Request.Context(), userID)
|
||
if err != nil {
|
||
h.logger.Error("获取用户资料失败", zap.Error(err))
|
||
h.response.NotFound(c, "用户不存在")
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "获取用户资料成功")
|
||
}
|
||
|
||
// ChangePassword 修改密码
|
||
// @Summary 修改密码
|
||
// @Description 使用旧密码、新密码确认和验证码修改当前用户的密码
|
||
// @Tags 用户管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param request body commands.ChangePasswordCommand true "修改密码请求"
|
||
// @Success 200 {object} map[string]interface{} "密码修改成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/me/password [put]
|
||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||
userID := h.getCurrentUserID(c)
|
||
if userID == "" {
|
||
h.response.Unauthorized(c, "用户未登录")
|
||
return
|
||
}
|
||
|
||
var cmd commands.ChangePasswordCommand
|
||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||
return
|
||
}
|
||
cmd.UserID = userID
|
||
|
||
if err := h.appService.ChangePassword(c.Request.Context(), &cmd); err != nil {
|
||
h.logger.Error("修改密码失败", zap.Error(err))
|
||
h.response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, nil, "密码修改成功")
|
||
}
|
||
|
||
// ResetPassword 重置密码
|
||
// @Summary 重置密码
|
||
// @Description 使用手机号、验证码和新密码重置用户密码(忘记密码时使用)
|
||
// @Tags 用户认证
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body commands.ResetPasswordCommand true "重置密码请求"
|
||
// @Success 200 {object} map[string]interface{} "密码重置成功"
|
||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/reset-password [post]
|
||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||
var cmd commands.ResetPasswordCommand
|
||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||
return
|
||
}
|
||
|
||
if err := h.appService.ResetPassword(c.Request.Context(), &cmd); err != nil {
|
||
h.logger.Error("重置密码失败", zap.Error(err))
|
||
h.response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, nil, "密码重置成功")
|
||
}
|
||
|
||
// ListUsers 管理员查看用户列表
|
||
// @Summary 管理员查看用户列表
|
||
// @Description 管理员查看用户列表,支持分页和筛选
|
||
// @Tags 用户管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param page query int false "页码" default(1)
|
||
// @Param page_size query int false "每页数量" default(10)
|
||
// @Param phone query string false "手机号筛选"
|
||
// @Param user_type query string false "用户类型筛选" Enums(user,admin)
|
||
// @Param is_active query bool false "是否激活筛选"
|
||
// @Param is_certified query bool false "是否已认证筛选"
|
||
// @Param company_name query string false "企业名称筛选"
|
||
// @Param start_date query string false "开始日期" format(date)
|
||
// @Param end_date query string false "结束日期" format(date)
|
||
// @Success 200 {object} responses.UserListResponse "用户列表"
|
||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/admin/list [get]
|
||
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||
// 检查管理员权限
|
||
userID := h.getCurrentUserID(c)
|
||
if userID == "" {
|
||
h.response.Unauthorized(c, "用户未登录")
|
||
return
|
||
}
|
||
|
||
// 构建查询参数
|
||
query := &queries.ListUsersQuery{
|
||
Page: 1,
|
||
PageSize: 10,
|
||
}
|
||
|
||
// 从查询参数中获取筛选条件
|
||
if page := c.Query("page"); page != "" {
|
||
if pageNum, err := strconv.Atoi(page); err == nil && pageNum > 0 {
|
||
query.Page = pageNum
|
||
}
|
||
}
|
||
|
||
if pageSize := c.Query("page_size"); pageSize != "" {
|
||
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 && size <= 1000 {
|
||
query.PageSize = size
|
||
}
|
||
}
|
||
|
||
query.Phone = c.Query("phone")
|
||
query.UserType = c.Query("user_type")
|
||
query.CompanyName = c.Query("company_name")
|
||
query.StartDate = c.Query("start_date")
|
||
query.EndDate = c.Query("end_date")
|
||
|
||
// 处理布尔值参数
|
||
if isActive := c.Query("is_active"); isActive != "" {
|
||
if active, err := strconv.ParseBool(isActive); err == nil {
|
||
query.IsActive = &active
|
||
}
|
||
}
|
||
|
||
if isCertified := c.Query("is_certified"); isCertified != "" {
|
||
if certified, err := strconv.ParseBool(isCertified); err == nil {
|
||
query.IsCertified = &certified
|
||
}
|
||
}
|
||
|
||
// 调用应用服务
|
||
resp, err := h.appService.ListUsers(c.Request.Context(), query)
|
||
if err != nil {
|
||
h.logger.Error("获取用户列表失败", zap.Error(err))
|
||
h.response.BadRequest(c, "获取用户列表失败")
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "获取用户列表成功")
|
||
}
|
||
|
||
// GetUserDetail 管理员获取用户详情
|
||
// @Summary 管理员获取用户详情
|
||
// @Description 管理员获取指定用户的详细信息
|
||
// @Tags 用户管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param user_id path string true "用户ID"
|
||
// @Success 200 {object} responses.UserDetailResponse "用户详情"
|
||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/admin/{user_id} [get]
|
||
func (h *UserHandler) GetUserDetail(c *gin.Context) {
|
||
// 检查管理员权限
|
||
userID := h.getCurrentUserID(c)
|
||
if userID == "" {
|
||
h.response.Unauthorized(c, "用户未登录")
|
||
return
|
||
}
|
||
|
||
// 获取路径参数中的用户ID
|
||
targetUserID := c.Param("user_id")
|
||
if targetUserID == "" {
|
||
h.response.BadRequest(c, "用户ID不能为空")
|
||
return
|
||
}
|
||
|
||
// 调用应用服务
|
||
resp, err := h.appService.GetUserDetail(c.Request.Context(), targetUserID)
|
||
if err != nil {
|
||
h.logger.Error("获取用户详情失败", zap.Error(err), zap.String("target_user_id", targetUserID))
|
||
h.response.BadRequest(c, "获取用户详情失败")
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "获取用户详情成功")
|
||
}
|
||
|
||
// GetUserStats 获取用户统计信息
|
||
// @Summary 获取用户统计信息
|
||
// @Description 管理员获取用户相关的统计信息
|
||
// @Tags 用户管理
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Success 200 {object} responses.UserStatsResponse "用户统计信息"
|
||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||
// @Failure 403 {object} map[string]interface{} "权限不足"
|
||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||
// @Router /api/v1/users/admin/stats [get]
|
||
func (h *UserHandler) GetUserStats(c *gin.Context) {
|
||
// 调用应用服务
|
||
resp, err := h.appService.GetUserStats(c.Request.Context())
|
||
if err != nil {
|
||
h.logger.Error("获取用户统计信息失败", zap.Error(err))
|
||
h.response.BadRequest(c, "获取用户统计信息失败")
|
||
return
|
||
}
|
||
|
||
h.response.Success(c, resp, "获取用户统计信息成功")
|
||
}
|
||
|
||
// getCurrentUserID 获取当前用户ID
|
||
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||
if userID, exists := c.Get("user_id"); exists {
|
||
if id, ok := userID.(string); ok {
|
||
return id
|
||
}
|
||
}
|
||
return ""
|
||
}
|