基础架构

This commit is contained in:
2025-07-13 16:36:20 +08:00
parent e3d64e7485
commit 807004f78d
128 changed files with 17232 additions and 11396 deletions

View File

@@ -0,0 +1,108 @@
package entities
import (
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// EnterpriseInfo 企业信息实体
// 存储用户在认证过程中验证后的企业信息,认证完成后不可修改
// 与用户是一对一关系,每个用户最多对应一个企业信息
type EnterpriseInfo struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"企业信息唯一标识"`
UserID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id" comment:"关联用户ID"`
// 企业四要素 - 企业认证的核心信息
CompanyName string `gorm:"type:varchar(255);not null" json:"company_name" comment:"企业名称"`
UnifiedSocialCode string `gorm:"type:varchar(50);not null;index" json:"unified_social_code" comment:"统一社会信用代码"`
LegalPersonName string `gorm:"type:varchar(100);not null" json:"legal_person_name" comment:"法定代表人姓名"`
LegalPersonID string `gorm:"type:varchar(50);not null" json:"legal_person_id" comment:"法定代表人身份证号"`
// 认证状态 - 各环节的验证结果
IsOCRVerified bool `gorm:"default:false" json:"is_ocr_verified" comment:"OCR验证是否通过"`
IsFaceVerified bool `gorm:"default:false" json:"is_face_verified" comment:"人脸识别是否通过"`
IsCertified bool `gorm:"default:false" json:"is_certified" comment:"是否已完成认证"`
VerificationData string `gorm:"type:text" json:"verification_data,omitempty" comment:"验证数据(JSON格式)"`
// OCR识别结果 - 从营业执照中自动识别的信息
OCRRawData string `gorm:"type:text" json:"ocr_raw_data,omitempty" comment:"OCR原始返回数据(JSON格式)"`
OCRConfidence float64 `gorm:"type:decimal(5,2)" json:"ocr_confidence,omitempty" comment:"OCR识别置信度(0-1)"`
// 认证完成时间
CertifiedAt *time.Time `json:"certified_at,omitempty" comment:"认证完成时间"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 关联关系
User *User `gorm:"foreignKey:UserID" json:"user,omitempty" comment:"关联的用户信息"`
}
// TableName 指定数据库表名
func (EnterpriseInfo) TableName() string {
return "enterprise_infos"
}
// IsComplete 检查企业四要素是否完整
// 验证企业名称、统一社会信用代码、法定代表人姓名、身份证号是否都已填写
func (e *EnterpriseInfo) IsComplete() bool {
return e.CompanyName != "" &&
e.UnifiedSocialCode != "" &&
e.LegalPersonName != "" &&
e.LegalPersonID != ""
}
// Validate 验证企业信息是否有效
// 这里可以添加企业信息的业务验证逻辑
// 比如统一社会信用代码格式验证、身份证号格式验证等
func (e *EnterpriseInfo) Validate() error {
if !e.IsComplete() {
return fmt.Errorf("企业信息不完整")
}
// 这里可以添加企业信息的业务验证逻辑
// 比如统一社会信用代码格式验证、身份证号格式验证等
return nil
}
// IsFullyVerified 检查是否已完成所有验证
func (e *EnterpriseInfo) IsFullyVerified() bool {
return e.IsOCRVerified && e.IsFaceVerified && e.IsCertified
}
// UpdateOCRVerification 更新OCR验证状态
func (e *EnterpriseInfo) UpdateOCRVerification(isVerified bool, rawData string, confidence float64) {
e.IsOCRVerified = isVerified
e.OCRRawData = rawData
e.OCRConfidence = confidence
}
// UpdateFaceVerification 更新人脸识别验证状态
func (e *EnterpriseInfo) UpdateFaceVerification(isVerified bool) {
e.IsFaceVerified = isVerified
}
// CompleteCertification 完成认证
func (e *EnterpriseInfo) CompleteCertification() {
e.IsCertified = true
now := time.Now()
e.CertifiedAt = &now
}
// IsReadOnly 检查企业信息是否只读(认证完成后不可修改)
func (e *EnterpriseInfo) IsReadOnly() bool {
return e.IsCertified
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (e *EnterpriseInfo) BeforeCreate(tx *gorm.DB) error {
if e.ID == "" {
e.ID = uuid.New().String()
}
return nil
}

View File

@@ -3,32 +3,35 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// SMSCode 短信验证码记录实体
// 记录用户发送的所有短信验证码,支持多种使用场景
// 包含验证码的有效期管理、使用状态跟踪、安全审计等功能
// @Description 短信验证码记录实体
type SMSCode struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"短信验证码记录唯一标识"`
Phone string `gorm:"type:varchar(20);not null;index" json:"phone" comment:"接收手机号"`
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"短信验证码记录唯一标识" example:"123e4567-e89b-12d3-a456-426614174000"`
Phone string `gorm:"type:varchar(20);not null;index" json:"phone" comment:"接收手机号" example:"13800138000"`
Code string `gorm:"type:varchar(10);not null" json:"-" comment:"验证码内容(不返回给前端)"`
Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene" comment:"使用场景"`
Used bool `gorm:"default:false" json:"used" comment:"是否已使用"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at" comment:"过期时间"`
Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene" comment:"使用场景" example:"register"`
Used bool `gorm:"default:false" json:"used" comment:"是否已使用" example:"false"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at" comment:"过期时间" example:"2024-01-01T00:05:00Z"`
UsedAt *time.Time `json:"used_at,omitempty" comment:"使用时间"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间" example:"2024-01-01T00:00:00Z"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间" example:"2024-01-01T00:00:00Z"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 额外信息 - 安全审计相关数据
IP string `gorm:"type:varchar(45)" json:"ip" comment:"发送IP地址"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent" comment:"客户端信息"`
IP string `gorm:"type:varchar(45)" json:"ip" comment:"发送IP地址" example:"192.168.1.1"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent" comment:"客户端信息" example:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"`
}
// SMSScene 短信验证码使用场景枚举
// 定义系统中所有需要使用短信验证码的业务场景
// @Description 短信验证码使用场景
type SMSScene string
const (
@@ -40,6 +43,14 @@ const (
SMSSceneUnbind SMSScene = "unbind" // 解绑手机号 - 解绑当前手机号
)
// BeforeCreate GORM钩子创建前自动生成UUID
func (s *SMSCode) BeforeCreate(tx *gorm.DB) error {
if s.ID == "" {
s.ID = uuid.New().String()
}
return nil
}
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (s *SMSCode) GetID() string {

View File

@@ -6,6 +6,7 @@ import (
"regexp"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@@ -23,6 +24,17 @@ type User struct {
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 关联关系
EnterpriseInfo *EnterpriseInfo `gorm:"foreignKey:UserID" json:"enterprise_info,omitempty" comment:"企业信息(认证后获得)"`
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.ID == "" {
u.ID = uuid.New().String()
}
return nil
}
// 实现 Entity 接口 - 提供统一的实体管理接口
@@ -273,3 +285,39 @@ func IsValidationError(err error) bool {
var validationErr *ValidationError
return errors.As(err, &validationErr)
}
// UserCache 用户缓存结构体
// 专门用于缓存序列化包含Password字段
type UserCache struct {
// 基础标识
ID string `json:"id" comment:"用户唯一标识"`
Phone string `json:"phone" comment:"手机号码(登录账号)"`
Password string `json:"password" comment:"登录密码(加密存储)"`
// 时间戳字段
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `json:"deleted_at" comment:"软删除时间"`
}
// ToCache 转换为缓存结构体
func (u *User) ToCache() *UserCache {
return &UserCache{
ID: u.ID,
Phone: u.Phone,
Password: u.Password,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
DeletedAt: u.DeletedAt,
}
}
// FromCache 从缓存结构体转换
func (u *User) FromCache(cache *UserCache) {
u.ID = cache.ID
u.Phone = cache.Phone
u.Password = cache.Password
u.CreatedAt = cache.CreatedAt
u.UpdatedAt = cache.UpdatedAt
u.DeletedAt = cache.DeletedAt
}

View File

@@ -1,293 +0,0 @@
package handlers
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/domains/user/dto"
"tyapi-server/internal/domains/user/services"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/middleware"
)
// UserHandler 用户HTTP处理器
type UserHandler struct {
userService interfaces.UserService
smsCodeService *services.SMSCodeService
response interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
jwtAuth *middleware.JWTAuthMiddleware
}
// NewUserHandler 创建用户处理器
func NewUserHandler(
userService interfaces.UserService,
smsCodeService *services.SMSCodeService,
response interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
logger *zap.Logger,
jwtAuth *middleware.JWTAuthMiddleware,
) *UserHandler {
return &UserHandler{
userService: userService,
smsCodeService: smsCodeService,
response: response,
validator: validator,
logger: logger,
jwtAuth: jwtAuth,
}
}
// SendCode 发送验证码
// @Summary 发送短信验证码
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body dto.SendCodeRequest true "发送验证码请求"
// @Success 200 {object} dto.SendCodeResponse "验证码发送成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 429 {object} map[string]interface{} "请求频率限制"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /users/send-code [post]
func (h *UserHandler) SendCode(c *gin.Context) {
var req dto.SendCodeRequest
// 验证请求体
if err := h.validator.BindAndValidate(c, &req); err != nil {
return // 响应已在验证器中处理
}
// 获取客户端信息
clientIP := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 发送验证码
if err := h.smsCodeService.SendCode(c.Request.Context(), req.Phone, req.Scene, clientIP, userAgent); err != nil {
h.logger.Error("发送验证码失败",
zap.String("phone", req.Phone),
zap.String("scene", string(req.Scene)),
zap.Error(err))
h.response.BadRequest(c, err.Error())
return
}
// 构建响应
response := &dto.SendCodeResponse{
Message: "验证码发送成功",
ExpiresAt: time.Now().Add(5 * time.Minute), // 5分钟过期
}
h.response.Success(c, response, "验证码发送成功")
}
// Register 用户注册
// @Summary 用户注册
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "用户注册请求"
// @Success 201 {object} dto.UserResponse "注册成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
// @Failure 409 {object} map[string]interface{} "手机号已存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /users/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req dto.RegisterRequest
// 验证请求体
if err := h.validator.BindAndValidate(c, &req); err != nil {
return // 响应已在验证器中处理
}
// 注册用户
user, err := h.userService.Register(c.Request.Context(), &req)
if err != nil {
h.logger.Error("注册用户失败", zap.Error(err))
h.response.BadRequest(c, err.Error())
return
}
// 返回响应
response := dto.FromEntity(user)
h.response.Created(c, response, "用户注册成功")
}
// LoginWithPassword 密码登录
// @Summary 用户密码登录
// @Description 使用手机号和密码进行用户登录返回JWT令牌
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body dto.LoginWithPasswordRequest true "密码登录请求"
// @Success 200 {object} dto.LoginResponse "登录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "认证失败"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /users/login-password [post]
func (h *UserHandler) LoginWithPassword(c *gin.Context) {
var req dto.LoginWithPasswordRequest
// 验证请求体
if err := h.validator.BindAndValidate(c, &req); err != nil {
return
}
// 用户登录
user, err := h.userService.LoginWithPassword(c.Request.Context(), &req)
if err != nil {
h.logger.Error("密码登录失败", zap.Error(err))
h.response.Unauthorized(c, "用户名或密码错误")
return
}
// 生成JWT token
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
if err != nil {
h.logger.Error("生成令牌失败", zap.Error(err))
h.response.InternalError(c, "生成访问令牌失败")
return
}
// 构建登录响应
loginResponse := &dto.LoginResponse{
User: dto.FromEntity(user),
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 86400, // 24小时从配置获取
LoginMethod: "password",
}
h.response.Success(c, loginResponse, "登录成功")
}
// LoginWithSMS 短信验证码登录
// @Summary 用户短信验证码登录
// @Description 使用手机号和短信验证码进行用户登录返回JWT令牌
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body dto.LoginWithSMSRequest true "短信登录请求"
// @Success 200 {object} dto.LoginResponse "登录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
// @Failure 401 {object} map[string]interface{} "认证失败"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /users/login-sms [post]
func (h *UserHandler) LoginWithSMS(c *gin.Context) {
var req dto.LoginWithSMSRequest
// 验证请求体
if err := h.validator.BindAndValidate(c, &req); err != nil {
return
}
// 用户登录
user, err := h.userService.LoginWithSMS(c.Request.Context(), &req)
if err != nil {
h.logger.Error("短信登录失败", zap.Error(err))
h.response.Unauthorized(c, err.Error())
return
}
// 生成JWT token
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
if err != nil {
h.logger.Error("生成令牌失败", zap.Error(err))
h.response.InternalError(c, "生成访问令牌失败")
return
}
// 构建登录响应
loginResponse := &dto.LoginResponse{
User: dto.FromEntity(user),
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 86400, // 24小时从配置获取
LoginMethod: "sms",
}
h.response.Success(c, loginResponse, "登录成功")
}
// GetProfile 获取当前用户信息
// @Summary 获取当前用户信息
// @Description 根据JWT令牌获取当前登录用户的详细信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} dto.UserResponse "用户信息"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 404 {object} map[string]interface{} "用户不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /users/me [get]
func (h *UserHandler) GetProfile(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
// 获取用户信息
user, err := h.userService.GetByID(c.Request.Context(), userID)
if err != nil {
h.logger.Error("获取用户资料失败", zap.Error(err))
h.response.NotFound(c, "用户不存在")
return
}
// 返回用户信息
response := dto.FromEntity(user)
h.response.Success(c, response, "获取用户资料成功")
}
// ChangePassword 修改密码
// @Summary 修改密码
// @Description 使用旧密码、新密码确认和验证码修改当前用户的密码
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.ChangePasswordRequest 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 /users/me/password [put]
func (h *UserHandler) ChangePassword(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
var req dto.ChangePasswordRequest
// 验证请求体
if err := h.validator.BindAndValidate(c, &req); err != nil {
return
}
// 修改密码
if err := h.userService.ChangePassword(c.Request.Context(), userID, &req); err != nil {
h.logger.Error("修改密码失败", zap.Error(err))
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, nil, "密码修改成功")
}
// 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 ""
}

View File

@@ -0,0 +1,22 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/shared/interfaces"
)
// EnterpriseInfoRepository 企业信息仓储接口
type EnterpriseInfoRepository interface {
interfaces.Repository[entities.EnterpriseInfo]
// 基础查询
GetByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfo, error)
GetByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string) (*entities.EnterpriseInfo, error)
// 业务操作
CheckUnifiedSocialCodeExists(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error)
UpdateVerificationStatus(ctx context.Context, userID string, isOCRVerified, isFaceVerified, isCertified bool) error
UpdateOCRData(ctx context.Context, userID string, rawData string, confidence float64) error
CompleteCertification(ctx context.Context, userID string) error
}

View File

@@ -0,0 +1,21 @@
package queries
// ListUsersQuery 用户列表查询参数
type ListUsersQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Phone string `json:"phone"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListSMSCodesQuery 短信验证码列表查询参数
type ListSMSCodesQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Phone string `json:"phone"`
Purpose string `json:"purpose"`
Status string `json:"status"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}

View File

@@ -1,148 +0,0 @@
package repositories
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/shared/interfaces"
)
// SMSCodeRepository 短信验证码仓储
type SMSCodeRepository struct {
db *gorm.DB
cache interfaces.CacheService
logger *zap.Logger
}
// NewSMSCodeRepository 创建短信验证码仓储
func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *SMSCodeRepository {
return &SMSCodeRepository{
db: db,
cache: cache,
logger: logger,
}
}
// Create 创建短信验证码记录
func (r *SMSCodeRepository) Create(ctx context.Context, smsCode *entities.SMSCode) error {
if err := r.db.WithContext(ctx).Create(smsCode).Error; err != nil {
r.logger.Error("创建短信验证码失败", zap.Error(err))
return err
}
// 缓存验证码
cacheKey := r.buildCacheKey(smsCode.Phone, smsCode.Scene)
r.cache.Set(ctx, cacheKey, smsCode, 5*time.Minute)
return nil
}
// GetValidCode 获取有效的验证码
func (r *SMSCodeRepository) GetValidCode(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error) {
// 先从缓存查找
cacheKey := r.buildCacheKey(phone, scene)
var smsCode entities.SMSCode
if err := r.cache.Get(ctx, cacheKey, &smsCode); err == nil {
return &smsCode, nil
}
// 从数据库查找最新的有效验证码
if err := r.db.WithContext(ctx).
Where("phone = ? AND scene = ? AND expires_at > ? AND used_at IS NULL",
phone, scene, time.Now()).
Order("created_at DESC").
First(&smsCode).Error; err != nil {
return nil, err
}
// 缓存结果
r.cache.Set(ctx, cacheKey, &smsCode, 5*time.Minute)
return &smsCode, nil
}
// MarkAsUsed 标记验证码为已使用
func (r *SMSCodeRepository) MarkAsUsed(ctx context.Context, id string) error {
now := time.Now()
if err := r.db.WithContext(ctx).
Model(&entities.SMSCode{}).
Where("id = ?", id).
Update("used_at", now).Error; err != nil {
r.logger.Error("标记验证码为已使用失败", zap.Error(err))
return err
}
r.logger.Info("验证码已标记为使用", zap.String("code_id", id))
return nil
}
// Update 更新验证码记录
func (r *SMSCodeRepository) Update(ctx context.Context, smsCode *entities.SMSCode) error {
if err := r.db.WithContext(ctx).Save(smsCode).Error; err != nil {
r.logger.Error("更新验证码记录失败", zap.Error(err))
return err
}
// 更新缓存
cacheKey := r.buildCacheKey(smsCode.Phone, smsCode.Scene)
r.cache.Set(ctx, cacheKey, smsCode, 5*time.Minute)
r.logger.Info("验证码记录更新成功", zap.String("code_id", smsCode.ID))
return nil
}
// GetRecentCode 获取最近的验证码记录(不限制有效性)
func (r *SMSCodeRepository) GetRecentCode(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error) {
var smsCode entities.SMSCode
if err := r.db.WithContext(ctx).
Where("phone = ? AND scene = ?", phone, scene).
Order("created_at DESC").
First(&smsCode).Error; err != nil {
return nil, err
}
return &smsCode, nil
}
// CleanupExpired 清理过期的验证码
func (r *SMSCodeRepository) CleanupExpired(ctx context.Context) error {
result := r.db.WithContext(ctx).
Where("expires_at < ?", time.Now()).
Delete(&entities.SMSCode{})
if result.Error != nil {
r.logger.Error("清理过期验证码失败", zap.Error(result.Error))
return result.Error
}
if result.RowsAffected > 0 {
r.logger.Info("清理过期验证码完成", zap.Int64("count", result.RowsAffected))
}
return nil
}
// CountRecentCodes 统计最近发送的验证码数量
func (r *SMSCodeRepository) CountRecentCodes(ctx context.Context, phone string, scene entities.SMSScene, duration time.Duration) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&entities.SMSCode{}).
Where("phone = ? AND scene = ? AND created_at > ?",
phone, scene, time.Now().Add(-duration)).
Count(&count).Error; err != nil {
r.logger.Error("统计最近验证码数量失败", zap.Error(err))
return 0, err
}
return count, nil
}
// buildCacheKey 构建缓存键
func (r *SMSCodeRepository) buildCacheKey(phone string, scene entities.SMSScene) string {
return fmt.Sprintf("sms_code:%s:%s", phone, string(scene))
}

View File

@@ -1,220 +0,0 @@
package repositories
import (
"context"
"errors"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/shared/interfaces"
)
// 定义错误常量
var (
// ErrUserNotFound 用户不存在错误
ErrUserNotFound = errors.New("用户不存在")
)
// UserRepository 用户仓储实现
type UserRepository struct {
db *gorm.DB
cache interfaces.CacheService
logger *zap.Logger
}
// NewUserRepository 创建用户仓储
func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *UserRepository {
return &UserRepository{
db: db,
cache: cache,
logger: logger,
}
}
// Create 创建用户
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
r.logger.Error("创建用户失败", zap.Error(err))
return err
}
// 清除相关缓存
r.deleteCacheByPhone(ctx, user.Phone)
r.logger.Info("用户创建成功", zap.String("user_id", user.ID))
return nil
}
// GetByID 根据ID获取用户
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entities.User, error) {
// 尝试从缓存获取
cacheKey := fmt.Sprintf("user:id:%s", id)
var user entities.User
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
return &user, nil
}
// 从数据库查询
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
r.logger.Error("根据ID查询用户失败", zap.Error(err))
return nil, err
}
// 缓存结果
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
return &user, nil
}
// FindByPhone 根据手机号查找用户
func (r *UserRepository) FindByPhone(ctx context.Context, phone string) (*entities.User, error) {
// 尝试从缓存获取
cacheKey := fmt.Sprintf("user:phone:%s", phone)
var user entities.User
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
return &user, nil
}
// 从数据库查询
if err := r.db.WithContext(ctx).Where("phone = ?", phone).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
r.logger.Error("根据手机号查询用户失败", zap.Error(err))
return nil, err
}
// 缓存结果
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
return &user, nil
}
// Update 更新用户
func (r *UserRepository) Update(ctx context.Context, user *entities.User) error {
if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
r.logger.Error("更新用户失败", zap.Error(err))
return err
}
// 清除相关缓存
r.deleteCacheByID(ctx, user.ID)
r.deleteCacheByPhone(ctx, user.Phone)
r.logger.Info("用户更新成功", zap.String("user_id", user.ID))
return nil
}
// Delete 删除用户
func (r *UserRepository) Delete(ctx context.Context, id string) error {
// 先获取用户信息用于清除缓存
user, err := r.GetByID(ctx, id)
if err != nil {
return err
}
if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除用户失败", zap.Error(err))
return err
}
// 清除相关缓存
r.deleteCacheByID(ctx, id)
r.deleteCacheByPhone(ctx, user.Phone)
r.logger.Info("用户删除成功", zap.String("user_id", id))
return nil
}
// SoftDelete 软删除用户
func (r *UserRepository) SoftDelete(ctx context.Context, id string) error {
// 先获取用户信息用于清除缓存
user, err := r.GetByID(ctx, id)
if err != nil {
return err
}
if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil {
r.logger.Error("软删除用户失败", zap.Error(err))
return err
}
// 清除相关缓存
r.deleteCacheByID(ctx, id)
r.deleteCacheByPhone(ctx, user.Phone)
r.logger.Info("用户软删除成功", zap.String("user_id", id))
return nil
}
// Restore 恢复软删除的用户
func (r *UserRepository) Restore(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Unscoped().Model(&entities.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
r.logger.Error("恢复用户失败", zap.Error(err))
return err
}
// 清除相关缓存
r.deleteCacheByID(ctx, id)
r.logger.Info("用户恢复成功", zap.String("user_id", id))
return nil
}
// List 分页获取用户列表
func (r *UserRepository) List(ctx context.Context, offset, limit int) ([]*entities.User, error) {
var users []*entities.User
if err := r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error; err != nil {
r.logger.Error("查询用户列表失败", zap.Error(err))
return nil, err
}
return users, nil
}
// Count 获取用户总数
func (r *UserRepository) Count(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&entities.User{}).Count(&count).Error; err != nil {
r.logger.Error("统计用户数量失败", zap.Error(err))
return 0, err
}
return count, nil
}
// ExistsByPhone 检查手机号是否存在
func (r *UserRepository) ExistsByPhone(ctx context.Context, phone string) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&entities.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil {
r.logger.Error("检查手机号是否存在失败", zap.Error(err))
return false, err
}
return count > 0, nil
}
// 私有辅助方法
// deleteCacheByID 根据ID删除缓存
func (r *UserRepository) deleteCacheByID(ctx context.Context, id string) {
cacheKey := fmt.Sprintf("user:id:%s", id)
if err := r.cache.Delete(ctx, cacheKey); err != nil {
r.logger.Warn("删除用户ID缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
}
}
// deleteCacheByPhone 根据手机号删除缓存
func (r *UserRepository) deleteCacheByPhone(ctx context.Context, phone string) {
cacheKey := fmt.Sprintf("user:phone:%s", phone)
if err := r.cache.Delete(ctx, cacheKey); err != nil {
r.logger.Warn("删除用户手机号缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
}
}

View File

@@ -0,0 +1,71 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// UserStats 用户统计信息
type UserStats struct {
TotalUsers int64
ActiveUsers int64
TodayRegistrations int64
TodayLogins int64
}
// UserRepository 用户仓储接口
type UserRepository interface {
interfaces.Repository[entities.User]
// 基础查询 - 直接使用实体
GetByPhone(ctx context.Context, phone string) (*entities.User, error)
// 复杂查询 - 使用查询参数
ListUsers(ctx context.Context, query *queries.ListUsersQuery) ([]*entities.User, int64, error)
// 业务操作
ValidateUser(ctx context.Context, phone, password string) (*entities.User, error)
UpdateLastLogin(ctx context.Context, userID string) error
UpdatePassword(ctx context.Context, userID string, newPassword string) error
CheckPassword(ctx context.Context, userID string, password string) (bool, error)
ActivateUser(ctx context.Context, userID string) error
DeactivateUser(ctx context.Context, userID string) error
// 统计信息
GetStats(ctx context.Context) (*UserStats, error)
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*UserStats, error)
}
// SMSCodeRepository 短信验证码仓储接口
type SMSCodeRepository interface {
interfaces.Repository[entities.SMSCode]
// 基础查询 - 直接使用实体
GetByPhone(ctx context.Context, phone string) (*entities.SMSCode, error)
GetLatestByPhone(ctx context.Context, phone string) (*entities.SMSCode, error)
GetValidByPhone(ctx context.Context, phone string) (*entities.SMSCode, error)
GetValidByPhoneAndScene(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error)
// 复杂查询 - 使用查询参数
ListSMSCodes(ctx context.Context, query *queries.ListSMSCodesQuery) ([]*entities.SMSCode, int64, error)
// 业务操作
CreateCode(ctx context.Context, phone string, code string, purpose string) (entities.SMSCode, error)
ValidateCode(ctx context.Context, phone string, code string, purpose string) (bool, error)
InvalidateCode(ctx context.Context, phone string) error
CheckSendFrequency(ctx context.Context, phone string, purpose string) (bool, error)
GetTodaySendCount(ctx context.Context, phone string) (int64, error)
// 统计信息
GetCodeStats(ctx context.Context, phone string, days int) (*SMSCodeStats, error)
}
// SMSCodeStats 短信验证码统计信息
type SMSCodeStats struct {
TotalSent int64
TotalValidated int64
SuccessRate float64
TodaySent int64
}

View File

@@ -1,29 +0,0 @@
package routes
import (
"tyapi-server/internal/domains/user/handlers"
"tyapi-server/internal/shared/middleware"
"github.com/gin-gonic/gin"
)
// UserRoutes 注册用户相关路由
func UserRoutes(router *gin.Engine, handler *handlers.UserHandler, authMiddleware *middleware.JWTAuthMiddleware) {
// 用户域路由组
usersGroup := router.Group("/api/v1/users")
{
// 公开路由(不需要认证)
usersGroup.POST("/send-code", handler.SendCode) // 发送验证码
usersGroup.POST("/register", handler.Register) // 用户注册
usersGroup.POST("/login-password", handler.LoginWithPassword) // 密码登录
usersGroup.POST("/login-sms", handler.LoginWithSMS) // 短信验证码登录
// 需要认证的路由
authenticated := usersGroup.Group("")
authenticated.Use(authMiddleware.Handle())
{
authenticated.GET("/me", handler.GetProfile) // 获取当前用户信息
authenticated.PUT("/me/password", handler.ChangePassword) // 修改密码
}
}
}

View File

@@ -0,0 +1,304 @@
package services
import (
"context"
"fmt"
"go.uber.org/zap"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/repositories"
)
// EnterpriseService 企业信息领域服务
type EnterpriseService struct {
userRepo repositories.UserRepository
enterpriseInfoRepo repositories.EnterpriseInfoRepository
logger *zap.Logger
}
// NewEnterpriseService 创建企业信息领域服务
func NewEnterpriseService(
userRepo repositories.UserRepository,
enterpriseInfoRepo repositories.EnterpriseInfoRepository,
logger *zap.Logger,
) *EnterpriseService {
return &EnterpriseService{
userRepo: userRepo,
enterpriseInfoRepo: enterpriseInfoRepo,
logger: logger,
}
}
// CreateEnterpriseInfo 创建企业信息
func (s *EnterpriseService) CreateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID string) (*entities.EnterpriseInfo, error) {
// 检查用户是否存在
_, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 检查用户是否已有企业信息
existingInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err == nil && existingInfo != nil {
return nil, fmt.Errorf("用户已有企业信息")
}
// 检查统一社会信用代码是否已存在
exists, err := s.enterpriseInfoRepo.CheckUnifiedSocialCodeExists(ctx, unifiedSocialCode, "")
if err != nil {
return nil, fmt.Errorf("检查企业信息失败: %w", err)
}
if exists {
return nil, fmt.Errorf("统一社会信用代码已存在")
}
// 创建企业信息
enterpriseInfo := &entities.EnterpriseInfo{
UserID: userID,
CompanyName: companyName,
UnifiedSocialCode: unifiedSocialCode,
LegalPersonName: legalPersonName,
LegalPersonID: legalPersonID,
}
*enterpriseInfo, err = s.enterpriseInfoRepo.Create(ctx, *enterpriseInfo)
if err != nil {
s.logger.Error("创建企业信息失败", zap.Error(err))
return nil, fmt.Errorf("创建企业信息失败: %w", err)
}
s.logger.Info("企业信息创建成功",
zap.String("user_id", userID),
zap.String("enterprise_id", enterpriseInfo.ID),
zap.String("company_name", companyName),
)
return enterpriseInfo, nil
}
// GetEnterpriseInfo 获取企业信息
func (s *EnterpriseService) GetEnterpriseInfo(ctx context.Context, userID string) (*entities.EnterpriseInfo, error) {
// 检查用户是否存在
_, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("企业信息不存在: %w", err)
}
return enterpriseInfo, nil
}
// UpdateEnterpriseInfo 更新企业信息(仅限未认证完成的情况)
func (s *EnterpriseService) UpdateEnterpriseInfo(ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID string) (*entities.EnterpriseInfo, error) {
// 检查用户是否存在
_, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 获取现有企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("企业信息不存在: %w", err)
}
// 检查企业信息是否已认证完成(认证完成后不可修改)
if enterpriseInfo.IsReadOnly() {
return nil, fmt.Errorf("企业信息已认证完成,不可修改")
}
// 检查统一社会信用代码是否已被其他用户使用
if unifiedSocialCode != enterpriseInfo.UnifiedSocialCode {
exists, err := s.enterpriseInfoRepo.CheckUnifiedSocialCodeExists(ctx, unifiedSocialCode, userID)
if err != nil {
return nil, fmt.Errorf("检查企业信息失败: %w", err)
}
if exists {
return nil, fmt.Errorf("统一社会信用代码已存在")
}
}
// 更新企业信息
enterpriseInfo.CompanyName = companyName
enterpriseInfo.UnifiedSocialCode = unifiedSocialCode
enterpriseInfo.LegalPersonName = legalPersonName
enterpriseInfo.LegalPersonID = legalPersonID
if err := s.enterpriseInfoRepo.Update(ctx, *enterpriseInfo); err != nil {
s.logger.Error("更新企业信息失败", zap.Error(err))
return nil, fmt.Errorf("更新企业信息失败: %w", err)
}
s.logger.Info("企业信息更新成功",
zap.String("user_id", userID),
zap.String("enterprise_id", enterpriseInfo.ID),
)
return enterpriseInfo, nil
}
// UpdateOCRVerification 更新OCR验证状态
func (s *EnterpriseService) UpdateOCRVerification(ctx context.Context, userID string, isVerified bool, rawData string, confidence float64) error {
// 获取企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return fmt.Errorf("企业信息不存在: %w", err)
}
// 更新OCR验证状态
enterpriseInfo.UpdateOCRVerification(isVerified, rawData, confidence)
if err := s.enterpriseInfoRepo.Update(ctx, *enterpriseInfo); err != nil {
s.logger.Error("更新OCR验证状态失败", zap.Error(err))
return fmt.Errorf("更新OCR验证状态失败: %w", err)
}
s.logger.Info("OCR验证状态更新成功",
zap.String("user_id", userID),
zap.Bool("is_verified", isVerified),
zap.Float64("confidence", confidence),
)
return nil
}
// UpdateFaceVerification 更新人脸识别验证状态
func (s *EnterpriseService) UpdateFaceVerification(ctx context.Context, userID string, isVerified bool) error {
// 获取企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return fmt.Errorf("企业信息不存在: %w", err)
}
// 更新人脸识别验证状态
enterpriseInfo.UpdateFaceVerification(isVerified)
if err := s.enterpriseInfoRepo.Update(ctx, *enterpriseInfo); err != nil {
s.logger.Error("更新人脸识别验证状态失败", zap.Error(err))
return fmt.Errorf("更新人脸识别验证状态失败: %w", err)
}
s.logger.Info("人脸识别验证状态更新成功",
zap.String("user_id", userID),
zap.Bool("is_verified", isVerified),
)
return nil
}
// CompleteEnterpriseCertification 完成企业认证
func (s *EnterpriseService) CompleteEnterpriseCertification(ctx context.Context, userID string) error {
// 获取企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return fmt.Errorf("企业信息不存在: %w", err)
}
// 检查是否已完成所有验证
if !enterpriseInfo.IsOCRVerified || !enterpriseInfo.IsFaceVerified {
return fmt.Errorf("企业信息验证未完成,无法完成认证")
}
// 完成认证
enterpriseInfo.CompleteCertification()
if err := s.enterpriseInfoRepo.Update(ctx, *enterpriseInfo); err != nil {
s.logger.Error("完成企业认证失败", zap.Error(err))
return fmt.Errorf("完成企业认证失败: %w", err)
}
s.logger.Info("企业认证完成",
zap.String("user_id", userID),
zap.String("enterprise_id", enterpriseInfo.ID),
)
return nil
}
// CheckUnifiedSocialCodeExists 检查统一社会信用代码是否存在
func (s *EnterpriseService) CheckUnifiedSocialCodeExists(ctx context.Context, unifiedSocialCode, excludeUserID string) (bool, error) {
return s.enterpriseInfoRepo.CheckUnifiedSocialCodeExists(ctx, unifiedSocialCode, excludeUserID)
}
// GetUserWithEnterpriseInfo 获取用户信息(包含企业信息)
func (s *EnterpriseService) GetUserWithEnterpriseInfo(ctx context.Context, userID string) (*entities.User, error) {
// 获取用户信息
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 获取企业信息(如果存在)
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
// 企业信息不存在是正常的,不是错误
s.logger.Debug("用户暂无企业信息", zap.String("user_id", userID))
} else {
user.EnterpriseInfo = enterpriseInfo
}
return &user, nil
}
// ValidateEnterpriseInfo 验证企业信息完整性
func (s *EnterpriseService) ValidateEnterpriseInfo(ctx context.Context, userID string) error {
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return fmt.Errorf("企业信息不存在: %w", err)
}
if err := enterpriseInfo.Validate(); err != nil {
return fmt.Errorf("企业信息验证失败: %w", err)
}
return nil
}
// GetEnterpriseInfoByUnifiedSocialCode 根据统一社会信用代码获取企业信息
func (s *EnterpriseService) GetEnterpriseInfoByUnifiedSocialCode(ctx context.Context, unifiedSocialCode string) (*entities.EnterpriseInfo, error) {
return s.enterpriseInfoRepo.GetByUnifiedSocialCode(ctx, unifiedSocialCode)
}
// IsEnterpriseCertified 检查用户是否已完成企业认证
func (s *EnterpriseService) IsEnterpriseCertified(ctx context.Context, userID string) (bool, error) {
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
// 没有企业信息,认为未认证
return false, nil
}
return enterpriseInfo.IsFullyVerified(), nil
}
// GetEnterpriseCertificationStatus 获取企业认证状态
func (s *EnterpriseService) GetEnterpriseCertificationStatus(ctx context.Context, userID string) (map[string]interface{}, error) {
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
if err != nil {
return map[string]interface{}{
"has_enterprise_info": false,
"is_certified": false,
"message": "用户暂无企业信息",
}, nil
}
status := map[string]interface{}{
"has_enterprise_info": true,
"is_certified": enterpriseInfo.IsFullyVerified(),
"is_readonly": enterpriseInfo.IsReadOnly(),
"ocr_verified": enterpriseInfo.IsOCRVerified,
"face_verified": enterpriseInfo.IsFaceVerified,
"certified_at": enterpriseInfo.CertifiedAt,
"company_name": enterpriseInfo.CompanyName,
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
"legal_person_name": enterpriseInfo.LegalPersonName,
"created_at": enterpriseInfo.CreatedAt,
"updated_at": enterpriseInfo.UpdatedAt,
}
return status, nil
}

View File

@@ -5,31 +5,32 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/infrastructure/external/sms"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/sms"
)
// SMSCodeService 短信验证码服务
type SMSCodeService struct {
repo *repositories.SMSCodeRepository
smsClient sms.Service
repo repositories.SMSCodeRepository
smsClient *sms.AliSMSService
cache interfaces.CacheService
config config.SMSConfig
appConfig config.AppConfig
logger *zap.Logger
}
// NewSMSCodeService 创建短信验证码服务
func NewSMSCodeService(
repo *repositories.SMSCodeRepository,
smsClient sms.Service,
repo repositories.SMSCodeRepository,
smsClient *sms.AliSMSService,
cache interfaces.CacheService,
config config.SMSConfig,
appConfig config.AppConfig,
logger *zap.Logger,
) *SMSCodeService {
return &SMSCodeService{
@@ -37,31 +38,25 @@ func NewSMSCodeService(
smsClient: smsClient,
cache: cache,
config: config,
appConfig: appConfig,
logger: logger,
}
}
// SendCode 发送验证码
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
// 1. 检查频率限制
if err := s.checkRateLimit(ctx, phone); err != nil {
return err
}
// 2. 生成验证码
// 1. 生成验证码
code := s.smsClient.GenerateCode(s.config.CodeLength)
// 3. 使用工厂方法创建SMS验证码记录
// 2. 使用工厂方法创建SMS验证码记录
smsCode, err := entities.NewSMSCode(phone, code, scene, s.config.ExpireTime, clientIP, userAgent)
if err != nil {
return fmt.Errorf("创建验证码记录失败: %w", err)
}
// 4. 设置ID
smsCode.ID = uuid.New().String()
// 5. 保存验证码
if err := s.repo.Create(ctx, smsCode); err != nil {
// 4. 保存验证码
*smsCode, err = s.repo.Create(ctx, *smsCode)
if err != nil {
s.logger.Error("保存短信验证码失败",
zap.String("phone", smsCode.GetMaskedPhone()),
zap.String("scene", smsCode.GetSceneName()),
@@ -69,7 +64,7 @@ func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entit
return fmt.Errorf("保存验证码失败: %w", err)
}
// 6. 发送短信
// 5. 发送短信
if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil {
// 记录发送失败但不删除验证码记录,让其自然过期
s.logger.Error("发送短信验证码失败",
@@ -79,8 +74,8 @@ func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entit
return fmt.Errorf("短信发送失败: %w", err)
}
// 7. 更新发送记录缓存
s.updateSendRecord(ctx, phone)
// 6. 更新发送记录缓存
s.updateSendRecord(ctx, phone, scene)
s.logger.Info("短信验证码发送成功",
zap.String("phone", smsCode.GetMaskedPhone()),
@@ -92,19 +87,33 @@ func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entit
// VerifyCode 验证验证码
func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error {
// 开发模式下跳过验证码校验
if s.appConfig.IsDevelopment() {
s.logger.Info("开发模式:验证码校验已跳过",
zap.String("phone", phone),
zap.String("scene", string(scene)),
zap.String("code", code))
return nil
}
// 1. 根据手机号和场景获取有效的验证码记录
smsCode, err := s.repo.GetValidCode(ctx, phone, scene)
smsCode, err := s.repo.GetValidByPhoneAndScene(ctx, phone, scene)
if err != nil {
return fmt.Errorf("验证码无效或已过期")
}
// 2. 使用实体的验证方法
// 2. 检查场景是否匹配
if smsCode.Scene != scene {
return fmt.Errorf("验证码错误或已过期")
}
// 3. 使用实体的验证方法
if err := smsCode.VerifyCode(code); err != nil {
return err
}
// 3. 保存更新后的验证码状态
if err := s.repo.Update(ctx, smsCode); err != nil {
// 4. 保存更新后的验证码状态
if err := s.repo.Update(ctx, *smsCode); err != nil {
s.logger.Error("更新验证码状态失败",
zap.String("code_id", smsCode.ID),
zap.Error(err))
@@ -120,10 +129,10 @@ func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, sce
// CanResendCode 检查是否可以重新发送验证码
func (s *SMSCodeService) CanResendCode(ctx context.Context, phone string, scene entities.SMSScene) (bool, error) {
// 1. 获取最近的验证码记录
recentCode, err := s.repo.GetRecentCode(ctx, phone, scene)
// 1. 获取最近的验证码记录(按场景)
recentCode, err := s.repo.GetValidByPhoneAndScene(ctx, phone, scene)
if err != nil {
// 如果没有记录,可以发送
// 如果没有该场景的记录,可以发送
return true, nil
}
@@ -144,8 +153,8 @@ func (s *SMSCodeService) CanResendCode(ctx context.Context, phone string, scene
// GetCodeStatus 获取验证码状态信息
func (s *SMSCodeService) GetCodeStatus(ctx context.Context, phone string, scene entities.SMSScene) (map[string]interface{}, error) {
// 1. 获取最近的验证码记录
recentCode, err := s.repo.GetRecentCode(ctx, phone, scene)
// 1. 获取最近的验证码记录(按场景)
recentCode, err := s.repo.GetValidByPhoneAndScene(ctx, phone, scene)
if err != nil {
return map[string]interface{}{
"has_code": false,
@@ -170,11 +179,11 @@ func (s *SMSCodeService) GetCodeStatus(ctx context.Context, phone string, scene
}
// checkRateLimit 检查发送频率限制
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
func (s *SMSCodeService) CheckRateLimit(ctx context.Context, phone string, scene entities.SMSScene) error {
now := time.Now()
// 检查最小发送间隔
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
lastSentKey := fmt.Sprintf("sms:last_sent:%s:%s", scene, phone)
var lastSent time.Time
if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil {
if now.Sub(lastSent) < s.config.RateLimit.MinInterval {
@@ -204,11 +213,11 @@ func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error
}
// updateSendRecord 更新发送记录
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) {
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string, scene entities.SMSScene) {
now := time.Now()
// 更新最后发送时间
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
lastSentKey := fmt.Sprintf("sms:last_sent:%s:%s", scene, phone)
s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval)
// 更新每小时计数
@@ -232,5 +241,5 @@ func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) {
// CleanExpiredCodes 清理过期验证码
func (s *SMSCodeService) CleanExpiredCodes(ctx context.Context) error {
return s.repo.CleanupExpired(ctx)
return s.repo.DeleteBatch(ctx, []string{})
}

View File

@@ -4,308 +4,126 @@ import (
"context"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
"tyapi-server/internal/domains/user/dto"
"tyapi-server/internal/domains/user/entities"
"tyapi-server/internal/domains/user/events"
"tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/interfaces"
)
// UserService 用户服务实现
// UserService 用户领域服务
type UserService struct {
repo *repositories.UserRepository
smsCodeService *SMSCodeService
eventBus interfaces.EventBus
logger *zap.Logger
userRepo repositories.UserRepository
enterpriseService *EnterpriseService
logger *zap.Logger
}
// NewUserService 创建用户服务
// NewUserService 创建用户领域服务
func NewUserService(
repo *repositories.UserRepository,
smsCodeService *SMSCodeService,
eventBus interfaces.EventBus,
userRepo repositories.UserRepository,
enterpriseService *EnterpriseService,
logger *zap.Logger,
) *UserService {
return &UserService{
repo: repo,
smsCodeService: smsCodeService,
eventBus: eventBus,
logger: logger,
userRepo: userRepo,
enterpriseService: enterpriseService,
logger: logger,
}
}
// Name 返回服务名称
func (s *UserService) Name() string {
return "user-service"
}
// Initialize 初始化服务
func (s *UserService) Initialize(ctx context.Context) error {
s.logger.Info("用户服务已初始化")
return nil
}
// HealthCheck 健康检查
func (s *UserService) HealthCheck(ctx context.Context) error {
// 简单的健康检查
return nil
}
// Shutdown 关闭服务
func (s *UserService) Shutdown(ctx context.Context) error {
s.logger.Info("用户服务已关闭")
return nil
}
// Register 用户注册
func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) {
// 1. 验证短信验证码
if err := s.smsCodeService.VerifyCode(ctx, registerReq.Phone, registerReq.Code, entities.SMSSceneRegister); err != nil {
return nil, fmt.Errorf("验证码验证失败: %w", err)
}
// 2. 检查手机号是否已存在
if err := s.checkPhoneDuplicate(ctx, registerReq.Phone); err != nil {
return nil, err
}
// 3. 使用工厂方法创建用户实体(业务规则验证在实体中完成)
user, err := entities.NewUser(registerReq.Phone, registerReq.Password)
// IsPhoneRegistered 检查手机号是否已注册
func (s *UserService) IsPhoneRegistered(ctx context.Context, phone string) (bool, error) {
_, err := s.userRepo.GetByPhone(ctx, phone)
if err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
return false, err
}
return true, nil
}
// 4. 设置用户ID
user.ID = uuid.New().String()
// GetUserWithEnterpriseInfo 获取用户信息(包含企业信息)
func (s *UserService) GetUserWithEnterpriseInfo(ctx context.Context, userID string) (*entities.User, error) {
// 通过企业服务获取用户信息(包含企业信息)
return s.enterpriseService.GetUserWithEnterpriseInfo(ctx, userID)
}
// 5. 保存用户
if err := s.repo.Create(ctx, user); err != nil {
s.logger.Error("创建用户失败", zap.Error(err))
return nil, fmt.Errorf("创建用户失败: %w", err)
// GetUserByID 根据ID获取用户信息
func (s *UserService) GetUserByID(ctx context.Context, userID string) (*entities.User, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
return &user, nil
}
// 6. 发布用户注册事件
event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx))
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
// GetUserByPhone 根据手机号获取用户信息
func (s *UserService) GetUserByPhone(ctx context.Context, phone string) (*entities.User, error) {
user, err := s.userRepo.GetByPhone(ctx, phone)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
s.logger.Info("用户注册成功",
zap.String("user_id", user.ID),
zap.String("phone", user.Phone))
return user, nil
}
// LoginWithPassword 密码登录
func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.LoginWithPasswordRequest) (*entities.User, error) {
// 1. 根据手机号查找用户
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
if err != nil {
return nil, fmt.Errorf("用户名或密码错误")
// UpdateUser 更新用户信息
func (s *UserService) UpdateUser(ctx context.Context, user *entities.User) error {
if err := s.userRepo.Update(ctx, *user); err != nil {
s.logger.Error("更新用户信息失败", zap.Error(err))
return fmt.Errorf("更新用户信息失败: %w", err)
}
// 2. 检查用户是否可以登录(委托给实体)
if !user.CanLogin() {
return nil, fmt.Errorf("用户状态异常,无法登录")
}
// 3. 验证密码(委托给实体)
if !user.CheckPassword(loginReq.Password) {
return nil, fmt.Errorf("用户名或密码错误")
}
// 4. 发布用户登录事件
event := events.NewUserLoggedInEvent(
user.ID, user.Phone,
s.getClientIP(ctx), s.getUserAgent(ctx),
s.getCorrelationID(ctx))
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
}
s.logger.Info("用户密码登录成功",
s.logger.Info("用户信息更新成功",
zap.String("user_id", user.ID),
zap.String("phone", user.Phone))
zap.String("phone", user.Phone),
)
return user, nil
return nil
}
// LoginWithSMS 短信验证码登录
func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithSMSRequest) (*entities.User, error) {
// 1. 验证短信验证码
if err := s.smsCodeService.VerifyCode(ctx, loginReq.Phone, loginReq.Code, entities.SMSSceneLogin); err != nil {
return nil, fmt.Errorf("验证码验证失败: %w", err)
}
// 2. 根据手机号查找用户
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
if err != nil {
return nil, fmt.Errorf("用户不存在")
}
// 3. 检查用户是否可以登录(委托给实体)
if !user.CanLogin() {
return nil, fmt.Errorf("用户状态异常,无法登录")
}
// 4. 发布用户登录事件
event := events.NewUserLoggedInEvent(
user.ID, user.Phone,
s.getClientIP(ctx), s.getUserAgent(ctx),
s.getCorrelationID(ctx))
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
}
s.logger.Info("用户短信登录成功",
zap.String("user_id", user.ID),
zap.String("phone", user.Phone))
return user, nil
}
// ChangePassword 修改密码
func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
// 1. 获取用户信息
user, err := s.repo.GetByID(ctx, userID)
// ChangePassword 修改用户密码
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("用户不存在: %w", err)
}
// 2. 验证短信验证码
if err := s.smsCodeService.VerifyCode(ctx, user.Phone, req.Code, entities.SMSSceneChangePassword); err != nil {
return fmt.Errorf("验证码验证失败: %w", err)
}
// 3. 执行业务逻辑(委托给实体)
if err := user.ChangePassword(req.OldPassword, req.NewPassword, req.ConfirmNewPassword); err != nil {
if err := user.ChangePassword(oldPassword, newPassword, newPassword); err != nil {
return err
}
// 4. 保存用户
if err := s.repo.Update(ctx, user); err != nil {
return fmt.Errorf("密码更新失败: %w", err)
if err := s.userRepo.Update(ctx, user); err != nil {
s.logger.Error("密码修改失败", zap.Error(err))
return fmt.Errorf("密码修改失败: %w", err)
}
// 5. 发布密码修改事件
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, s.getCorrelationID(ctx))
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布密码修改事件失败", zap.Error(err))
}
s.logger.Info("密码修改成功", zap.String("user_id", userID))
s.logger.Info("密码修改成功",
zap.String("user_id", userID),
)
return nil
}
// GetByID 根据ID获取用户
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
if id == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
return user, nil
}
// UpdateUserProfile 更新用户信息
func (s *UserService) UpdateUserProfile(ctx context.Context, userID string, req *dto.UpdateProfileRequest) (*entities.User, error) {
// 1. 获取用户信息
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 2. 更新手机号(如果需要)
if req.Phone != "" && req.Phone != user.Phone {
// 检查新手机号是否已存在
if err := s.checkPhoneDuplicate(ctx, req.Phone); err != nil {
return nil, err
}
// 使用实体的方法设置手机号
if err := user.SetPhone(req.Phone); err != nil {
return nil, err
}
}
// 3. 保存用户
if err := s.repo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("更新用户信息失败: %w", err)
}
s.logger.Info("用户信息更新成功", zap.String("user_id", userID))
return user, nil
}
// DeactivateUser 停用用户
func (s *UserService) DeactivateUser(ctx context.Context, userID string) error {
// 1. 获取用户信息
user, err := s.repo.GetByID(ctx, userID)
// ValidateUser 验证用户信息
func (s *UserService) ValidateUser(ctx context.Context, userID string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("用户不存在: %w", err)
}
// 2. 检查用户状态
if user.IsDeleted() {
return fmt.Errorf("用户已被停用")
// 这里可以添加更多的用户验证逻辑
if user.Phone == "" {
return fmt.Errorf("用户手机号不能为空")
}
// 3. 软删除用户(这里需要调用仓储的软删除方法)
if err := s.repo.SoftDelete(ctx, userID); err != nil {
return fmt.Errorf("停用用户失败: %w", err)
}
s.logger.Info("用户停用成功", zap.String("user_id", userID))
return nil
}
// ================ 工具方法 ================
// checkPhoneDuplicate 检查手机号重复
func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) error {
if _, err := s.repo.FindByPhone(ctx, phone); err == nil {
return fmt.Errorf("手机号已存在")
// GetUserStats 获取用户统计信息
func (s *UserService) GetUserStats(ctx context.Context) (map[string]interface{}, error) {
// 这里可以添加用户统计逻辑
stats := map[string]interface{}{
"total_users": 0, // 需要实现具体的统计逻辑
"active_users": 0,
"new_users_today": 0,
}
return nil
}
// getCorrelationID 获取关联ID
func (s *UserService) getCorrelationID(ctx context.Context) string {
if id := ctx.Value("correlation_id"); id != nil {
if strID, ok := id.(string); ok {
return strID
}
}
return uuid.New().String()
}
// getClientIP 获取客户端IP
func (s *UserService) getClientIP(ctx context.Context) string {
if ip := ctx.Value("client_ip"); ip != nil {
if strIP, ok := ip.(string); ok {
return strIP
}
}
return ""
}
// getUserAgent 获取用户代理
func (s *UserService) getUserAgent(ctx context.Context) string {
if ua := ctx.Value("user_agent"); ua != nil {
if strUA, ok := ua.(string); ok {
return strUA
}
}
return ""
return stats, nil
}