This commit is contained in:
2025-07-11 21:05:58 +08:00
parent 5b4392894f
commit e3d64e7485
74 changed files with 14379 additions and 697 deletions

View File

@@ -0,0 +1,178 @@
package dto
import (
"time"
"tyapi-server/internal/domains/admin/entities"
)
// AdminLoginRequest 管理员登录请求
type AdminLoginRequest struct {
Username string `json:"username" binding:"required"` // 用户名
Password string `json:"password" binding:"required"` // 密码
}
// AdminLoginResponse 管理员登录响应
type AdminLoginResponse struct {
Token string `json:"token"` // JWT令牌
ExpiresAt time.Time `json:"expires_at"` // 过期时间
Admin AdminInfo `json:"admin"` // 管理员信息
}
// AdminInfo 管理员信息
type AdminInfo struct {
ID string `json:"id"` // 管理员ID
Username string `json:"username"` // 用户名
Email string `json:"email"` // 邮箱
Phone string `json:"phone"` // 手机号
RealName string `json:"real_name"` // 真实姓名
Role entities.AdminRole `json:"role"` // 角色
IsActive bool `json:"is_active"` // 是否激活
LastLoginAt *time.Time `json:"last_login_at"` // 最后登录时间
LoginCount int `json:"login_count"` // 登录次数
Permissions []string `json:"permissions"` // 权限列表
CreatedAt time.Time `json:"created_at"` // 创建时间
}
// AdminCreateRequest 创建管理员请求
type AdminCreateRequest struct {
Username string `json:"username" binding:"required"` // 用户名
Password string `json:"password" binding:"required"` // 密码
Email string `json:"email" binding:"required,email"` // 邮箱
Phone string `json:"phone"` // 手机号
RealName string `json:"real_name" binding:"required"` // 真实姓名
Role entities.AdminRole `json:"role" binding:"required"` // 角色
Permissions []string `json:"permissions"` // 权限列表
}
// AdminUpdateRequest 更新管理员请求
type AdminUpdateRequest struct {
Email string `json:"email" binding:"email"` // 邮箱
Phone string `json:"phone"` // 手机号
RealName string `json:"real_name"` // 真实姓名
Role entities.AdminRole `json:"role"` // 角色
IsActive *bool `json:"is_active"` // 是否激活
Permissions []string `json:"permissions"` // 权限列表
}
// AdminPasswordChangeRequest 修改密码请求
type AdminPasswordChangeRequest struct {
OldPassword string `json:"old_password" binding:"required"` // 旧密码
NewPassword string `json:"new_password" binding:"required"` // 新密码
}
// AdminListRequest 管理员列表请求
type AdminListRequest struct {
Page int `form:"page" binding:"min=1"` // 页码
PageSize int `form:"page_size" binding:"min=1,max=100"` // 每页数量
Username string `form:"username"` // 用户名搜索
Email string `form:"email"` // 邮箱搜索
Role string `form:"role"` // 角色筛选
IsActive *bool `form:"is_active"` // 状态筛选
}
// AdminListResponse 管理员列表响应
type AdminListResponse struct {
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
Size int `json:"size"` // 每页数量
Admins []AdminInfo `json:"admins"` // 管理员列表
}
// AdminStatsResponse 管理员统计响应
type AdminStatsResponse struct {
TotalAdmins int64 `json:"total_admins"` // 总管理员数
ActiveAdmins int64 `json:"active_admins"` // 激活管理员数
TodayLogins int64 `json:"today_logins"` // 今日登录数
TotalOperations int64 `json:"total_operations"` // 总操作数
}
// AdminOperationLogRequest 操作日志请求
type AdminOperationLogRequest struct {
Page int `form:"page" binding:"min=1"` // 页码
PageSize int `form:"page_size" binding:"min=1,max=100"` // 每页数量
AdminID string `form:"admin_id"` // 管理员ID
Action string `form:"action"` // 操作类型
Resource string `form:"resource"` // 操作资源
Status string `form:"status"` // 操作状态
StartTime time.Time `form:"start_time"` // 开始时间
EndTime time.Time `form:"end_time"` // 结束时间
}
// AdminOperationLogResponse 操作日志响应
type AdminOperationLogResponse struct {
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
Size int `json:"size"` // 每页数量
Logs []AdminOperationLogInfo `json:"logs"` // 日志列表
}
// AdminOperationLogInfo 操作日志信息
type AdminOperationLogInfo struct {
ID string `json:"id"` // 日志ID
AdminID string `json:"admin_id"` // 管理员ID
Username string `json:"username"` // 用户名
Action string `json:"action"` // 操作类型
Resource string `json:"resource"` // 操作资源
ResourceID string `json:"resource_id"` // 资源ID
Details string `json:"details"` // 操作详情
IP string `json:"ip"` // IP地址
UserAgent string `json:"user_agent"` // 用户代理
Status string `json:"status"` // 操作状态
Message string `json:"message"` // 操作消息
CreatedAt time.Time `json:"created_at"` // 创建时间
}
// AdminLoginLogRequest 登录日志请求
type AdminLoginLogRequest struct {
Page int `form:"page" binding:"min=1"` // 页码
PageSize int `form:"page_size" binding:"min=1,max=100"` // 每页数量
AdminID string `form:"admin_id"` // 管理员ID
Username string `form:"username"` // 用户名
Status string `form:"status"` // 登录状态
StartTime time.Time `form:"start_time"` // 开始时间
EndTime time.Time `form:"end_time"` // 结束时间
}
// AdminLoginLogResponse 登录日志响应
type AdminLoginLogResponse struct {
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
Size int `json:"size"` // 每页数量
Logs []AdminLoginLogInfo `json:"logs"` // 日志列表
}
// AdminLoginLogInfo 登录日志信息
type AdminLoginLogInfo struct {
ID string `json:"id"` // 日志ID
AdminID string `json:"admin_id"` // 管理员ID
Username string `json:"username"` // 用户名
IP string `json:"ip"` // IP地址
UserAgent string `json:"user_agent"` // 用户代理
Status string `json:"status"` // 登录状态
Message string `json:"message"` // 登录消息
CreatedAt time.Time `json:"created_at"` // 创建时间
}
// PermissionInfo 权限信息
type PermissionInfo struct {
ID string `json:"id"` // 权限ID
Name string `json:"name"` // 权限名称
Code string `json:"code"` // 权限代码
Description string `json:"description"` // 权限描述
Module string `json:"module"` // 所属模块
IsActive bool `json:"is_active"` // 是否激活
CreatedAt time.Time `json:"created_at"` // 创建时间
}
// RolePermissionRequest 角色权限请求
type RolePermissionRequest struct {
Role entities.AdminRole `json:"role" binding:"required"` // 角色
PermissionIDs []string `json:"permission_ids" binding:"required"` // 权限ID列表
}
// RolePermissionResponse 角色权限响应
type RolePermissionResponse struct {
Role entities.AdminRole `json:"role"` // 角色
Permissions []PermissionInfo `json:"permissions"` // 权限列表
}

View File

@@ -0,0 +1,147 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// AdminRole 管理员角色枚举
// 定义系统中不同级别的管理员角色,用于权限控制和功能分配
type AdminRole string
const (
RoleSuperAdmin AdminRole = "super_admin" // 超级管理员 - 拥有所有权限
RoleAdmin AdminRole = "admin" // 普通管理员 - 拥有大部分管理权限
RoleReviewer AdminRole = "reviewer" // 审核员 - 仅拥有审核相关权限
)
// Admin 管理员实体
// 系统管理员的核心信息,包括账户信息、权限配置、操作统计等
// 支持多角色管理,提供完整的权限控制和操作审计功能
type Admin struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"管理员唯一标识"`
Username string `gorm:"type:varchar(100);not null;uniqueIndex" comment:"登录用户名"`
Password string `gorm:"type:varchar(255);not null" comment:"登录密码(加密存储)"`
Email string `gorm:"type:varchar(255);not null;uniqueIndex" comment:"邮箱地址"`
Phone string `gorm:"type:varchar(20)" comment:"手机号码"`
RealName string `gorm:"type:varchar(100);not null" comment:"真实姓名"`
Role AdminRole `gorm:"type:varchar(50);not null;default:'reviewer'" comment:"管理员角色"`
// 状态信息 - 账户状态和登录统计
IsActive bool `gorm:"default:true" comment:"账户是否激活"`
LastLoginAt *time.Time `comment:"最后登录时间"`
LoginCount int `gorm:"default:0" comment:"登录次数统计"`
// 权限信息 - 细粒度权限控制
Permissions string `gorm:"type:text" comment:"权限列表(JSON格式存储)"`
// 审核统计 - 管理员的工作绩效统计
ReviewCount int `gorm:"default:0" comment:"审核总数"`
ApprovedCount int `gorm:"default:0" comment:"通过数量"`
RejectedCount int `gorm:"default:0" comment:"拒绝数量"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// AdminLoginLog 管理员登录日志实体
// 记录管理员的所有登录尝试,包括成功和失败的登录记录
// 用于安全审计和异常登录检测
type AdminLoginLog struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"日志记录唯一标识"`
AdminID string `gorm:"type:varchar(36);not null;index" comment:"管理员ID"`
Username string `gorm:"type:varchar(100);not null" comment:"登录用户名"`
IP string `gorm:"type:varchar(45);not null" comment:"登录IP地址"`
UserAgent string `gorm:"type:varchar(500)" comment:"客户端信息"`
Status string `gorm:"type:varchar(20);not null" comment:"登录状态(success/failed)"`
Message string `gorm:"type:varchar(500)" comment:"登录结果消息"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
}
// AdminOperationLog 管理员操作日志实体
// 记录管理员在系统中的所有重要操作,用于操作审计和问题追踪
// 支持操作类型、资源、详情等完整信息的记录
type AdminOperationLog struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"操作日志唯一标识"`
AdminID string `gorm:"type:varchar(36);not null;index" comment:"操作管理员ID"`
Username string `gorm:"type:varchar(100);not null" comment:"操作管理员用户名"`
Action string `gorm:"type:varchar(100);not null" comment:"操作类型"`
Resource string `gorm:"type:varchar(100);not null" comment:"操作资源"`
ResourceID string `gorm:"type:varchar(36)" comment:"资源ID"`
Details string `gorm:"type:text" comment:"操作详情(JSON格式)"`
IP string `gorm:"type:varchar(45);not null" comment:"操作IP地址"`
UserAgent string `gorm:"type:varchar(500)" comment:"客户端信息"`
Status string `gorm:"type:varchar(20);not null" comment:"操作状态(success/failed)"`
Message string `gorm:"type:varchar(500)" comment:"操作结果消息"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
}
// AdminPermission 管理员权限实体
// 定义系统中的所有权限项,支持模块化权限管理
// 每个权限都有唯一的代码标识,便于程序中的权限检查
type AdminPermission struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"权限唯一标识"`
Name string `gorm:"type:varchar(100);not null;uniqueIndex" comment:"权限名称"`
Code string `gorm:"type:varchar(100);not null;uniqueIndex" comment:"权限代码"`
Description string `gorm:"type:varchar(500)" comment:"权限描述"`
Module string `gorm:"type:varchar(50);not null" comment:"所属模块"`
IsActive bool `gorm:"default:true" comment:"权限是否启用"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// AdminRolePermission 角色权限关联实体
// 建立角色和权限之间的多对多关系,实现基于角色的权限控制(RBAC)
type AdminRolePermission struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"关联记录唯一标识"`
Role AdminRole `gorm:"type:varchar(50);not null;index" comment:"角色"`
PermissionID string `gorm:"type:varchar(36);not null;index" comment:"权限ID"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
}
// TableName 指定数据库表名
func (Admin) TableName() string {
return "admins"
}
// IsValid 检查管理员账户是否有效
// 判断管理员账户是否处于可用状态,包括激活状态和软删除状态检查
func (a *Admin) IsValid() bool {
return a.IsActive && a.DeletedAt.Time.IsZero()
}
// UpdateLastLoginAt 更新最后登录时间
// 在管理员成功登录后调用,记录最新的登录时间
func (a *Admin) UpdateLastLoginAt() {
now := time.Now()
a.LastLoginAt = &now
}
// Deactivate 停用管理员账户
// 将管理员账户设置为非激活状态,禁止登录和操作
func (a *Admin) Deactivate() {
a.IsActive = false
}
// Activate 激活管理员账户
// 重新启用管理员账户,允许正常登录和操作
func (a *Admin) Activate() {
a.IsActive = true
}

View File

@@ -0,0 +1,313 @@
package handlers
import (
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/domains/admin/dto"
"tyapi-server/internal/domains/admin/services"
"tyapi-server/internal/shared/interfaces"
)
// AdminHandler 管理员HTTP处理器
type AdminHandler struct {
adminService *services.AdminService
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
}
// NewAdminHandler 创建管理员HTTP处理器
func NewAdminHandler(
adminService *services.AdminService,
responseBuilder interfaces.ResponseBuilder,
logger *zap.Logger,
) *AdminHandler {
return &AdminHandler{
adminService: adminService,
responseBuilder: responseBuilder,
logger: logger,
}
}
// Login 管理员登录
// @Summary 管理员登录
// @Description 管理员登录接口
// @Tags 管理员认证
// @Accept json
// @Produce json
// @Param request body dto.AdminLoginRequest true "登录请求"
// @Success 200 {object} dto.AdminLoginResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 401 {object} interfaces.ErrorResponse
// @Router /admin/login [post]
func (h *AdminHandler) Login(c *gin.Context) {
var req dto.AdminLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("管理员登录参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 获取客户端信息
clientIP := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 调用服务
response, err := h.adminService.Login(c.Request.Context(), &req, clientIP, userAgent)
if err != nil {
h.logger.Error("管理员登录失败", zap.Error(err))
h.responseBuilder.Unauthorized(c, err.Error())
return
}
h.responseBuilder.Success(c, response, "登录成功")
}
// CreateAdmin 创建管理员
// @Summary 创建管理员
// @Description 创建新管理员账户
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param request body dto.AdminCreateRequest true "创建管理员请求"
// @Success 201 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 403 {object} interfaces.ErrorResponse
// @Router /admin [post]
func (h *AdminHandler) CreateAdmin(c *gin.Context) {
var req dto.AdminCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("创建管理员参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 获取当前操作的管理员ID从JWT中解析
operatorID := h.getCurrentAdminID(c)
// 调用服务
err := h.adminService.CreateAdmin(c.Request.Context(), &req, operatorID)
if err != nil {
h.logger.Error("创建管理员失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, nil, "管理员创建成功")
}
// UpdateAdmin 更新管理员
// @Summary 更新管理员
// @Description 更新管理员信息
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param id path string true "管理员ID"
// @Param request body dto.AdminUpdateRequest true "更新管理员请求"
// @Success 200 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /admin/{id} [put]
func (h *AdminHandler) UpdateAdmin(c *gin.Context) {
adminID := c.Param("id")
if adminID == "" {
h.responseBuilder.BadRequest(c, "管理员ID不能为空")
return
}
var req dto.AdminUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("更新管理员参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 获取当前操作的管理员ID
operatorID := h.getCurrentAdminID(c)
// 调用服务
err := h.adminService.UpdateAdmin(c.Request.Context(), adminID, &req, operatorID)
if err != nil {
h.logger.Error("更新管理员失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "管理员更新成功")
}
// ChangePassword 修改密码
// @Summary 修改密码
// @Description 管理员修改自己的密码
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param request body dto.AdminPasswordChangeRequest true "修改密码请求"
// @Success 200 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /admin/change-password [post]
func (h *AdminHandler) ChangePassword(c *gin.Context) {
var req dto.AdminPasswordChangeRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("修改密码参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 获取当前管理员ID
adminID := h.getCurrentAdminID(c)
// 调用服务
err := h.adminService.ChangePassword(c.Request.Context(), adminID, &req)
if err != nil {
h.logger.Error("修改密码失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "密码修改成功")
}
// ListAdmins 获取管理员列表
// @Summary 获取管理员列表
// @Description 分页获取管理员列表
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param username query string false "用户名搜索"
// @Param email query string false "邮箱搜索"
// @Param role query string false "角色筛选"
// @Param is_active query bool false "状态筛选"
// @Success 200 {object} dto.AdminListResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /admin [get]
func (h *AdminHandler) ListAdmins(c *gin.Context) {
var req dto.AdminListRequest
// 解析查询参数
if page, err := strconv.Atoi(c.DefaultQuery("page", "1")); err == nil {
req.Page = page
} else {
req.Page = 1
}
if pageSize, err := strconv.Atoi(c.DefaultQuery("page_size", "10")); err == nil {
req.PageSize = pageSize
} else {
req.PageSize = 10
}
req.Username = c.Query("username")
req.Email = c.Query("email")
req.Role = c.Query("role")
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
req.IsActive = &isActive
}
}
// 调用服务
response, err := h.adminService.ListAdmins(c.Request.Context(), &req)
if err != nil {
h.logger.Error("获取管理员列表失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取管理员列表失败")
return
}
h.responseBuilder.Success(c, response, "获取管理员列表成功")
}
// GetAdminByID 根据ID获取管理员
// @Summary 获取管理员详情
// @Description 根据ID获取管理员详细信息
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param id path string true "管理员ID"
// @Success 200 {object} dto.AdminInfo
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /admin/{id} [get]
func (h *AdminHandler) GetAdminByID(c *gin.Context) {
adminID := c.Param("id")
if adminID == "" {
h.responseBuilder.BadRequest(c, "管理员ID不能为空")
return
}
// 调用服务
admin, err := h.adminService.GetAdminByID(c.Request.Context(), adminID)
if err != nil {
h.logger.Error("获取管理员详情失败", zap.Error(err))
h.responseBuilder.NotFound(c, err.Error())
return
}
h.responseBuilder.Success(c, admin, "获取管理员详情成功")
}
// DeleteAdmin 删除管理员
// @Summary 删除管理员
// @Description 软删除管理员账户
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Param id path string true "管理员ID"
// @Success 200 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /admin/{id} [delete]
func (h *AdminHandler) DeleteAdmin(c *gin.Context) {
adminID := c.Param("id")
if adminID == "" {
h.responseBuilder.BadRequest(c, "管理员ID不能为空")
return
}
// 获取当前操作的管理员ID
operatorID := h.getCurrentAdminID(c)
// 调用服务
err := h.adminService.DeleteAdmin(c.Request.Context(), adminID, operatorID)
if err != nil {
h.logger.Error("删除管理员失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "管理员删除成功")
}
// GetAdminStats 获取管理员统计信息
// @Summary 获取管理员统计
// @Description 获取管理员相关的统计信息
// @Tags 管理员管理
// @Accept json
// @Produce json
// @Success 200 {object} dto.AdminStatsResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /admin/stats [get]
func (h *AdminHandler) GetAdminStats(c *gin.Context) {
// 调用服务
stats, err := h.adminService.GetAdminStats(c.Request.Context())
if err != nil {
h.logger.Error("获取管理员统计失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取统计信息失败")
return
}
h.responseBuilder.Success(c, stats, "获取统计信息成功")
}
// getCurrentAdminID 获取当前管理员ID
func (h *AdminHandler) getCurrentAdminID(c *gin.Context) string {
// 这里应该从JWT令牌中解析出管理员ID
// 为了简化这里返回一个模拟的ID
// 实际实现中应该从中间件中获取
return "current_admin_id"
}

View File

@@ -0,0 +1,72 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/admin/dto"
"tyapi-server/internal/domains/admin/entities"
"tyapi-server/internal/shared/interfaces"
)
// AdminRepository 管理员仓储接口
type AdminRepository interface {
interfaces.Repository[entities.Admin]
// 管理员认证
FindByUsername(ctx context.Context, username string) (*entities.Admin, error)
FindByEmail(ctx context.Context, email string) (*entities.Admin, error)
// 管理员管理
ListAdmins(ctx context.Context, req *dto.AdminListRequest) (*dto.AdminListResponse, error)
GetStats(ctx context.Context) (*dto.AdminStatsResponse, error)
// 权限管理
GetPermissionsByRole(ctx context.Context, role entities.AdminRole) ([]entities.AdminPermission, error)
UpdatePermissions(ctx context.Context, adminID string, permissions []string) error
// 统计信息
UpdateLoginStats(ctx context.Context, adminID string) error
UpdateReviewStats(ctx context.Context, adminID string, approved bool) error
}
// AdminLoginLogRepository 管理员登录日志仓储接口
type AdminLoginLogRepository interface {
interfaces.Repository[entities.AdminLoginLog]
// 日志查询
ListLogs(ctx context.Context, req *dto.AdminLoginLogRequest) (*dto.AdminLoginLogResponse, error)
// 统计查询
GetTodayLoginCount(ctx context.Context) (int64, error)
GetLoginCountByAdmin(ctx context.Context, adminID string, days int) (int64, error)
}
// AdminOperationLogRepository 管理员操作日志仓储接口
type AdminOperationLogRepository interface {
interfaces.Repository[entities.AdminOperationLog]
// 日志查询
ListLogs(ctx context.Context, req *dto.AdminOperationLogRequest) (*dto.AdminOperationLogResponse, error)
// 统计查询
GetTotalOperations(ctx context.Context) (int64, error)
GetOperationsByAdmin(ctx context.Context, adminID string, days int) (int64, error)
// 批量操作
BatchCreate(ctx context.Context, logs []entities.AdminOperationLog) error
}
// AdminPermissionRepository 管理员权限仓储接口
type AdminPermissionRepository interface {
interfaces.Repository[entities.AdminPermission]
// 权限查询
FindByCode(ctx context.Context, code string) (*entities.AdminPermission, error)
FindByModule(ctx context.Context, module string) ([]entities.AdminPermission, error)
ListActive(ctx context.Context) ([]entities.AdminPermission, error)
// 角色权限管理
GetPermissionsByRole(ctx context.Context, role entities.AdminRole) ([]entities.AdminPermission, error)
AssignPermissionsToRole(ctx context.Context, role entities.AdminRole, permissionIDs []string) error
RemovePermissionsFromRole(ctx context.Context, role entities.AdminRole, permissionIDs []string) error
}

View File

@@ -0,0 +1,341 @@
package repositories
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/admin/dto"
"tyapi-server/internal/domains/admin/entities"
"tyapi-server/internal/shared/interfaces"
)
// GormAdminRepository 管理员GORM仓储实现
type GormAdminRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormAdminRepository 创建管理员GORM仓储
func NewGormAdminRepository(db *gorm.DB, logger *zap.Logger) *GormAdminRepository {
return &GormAdminRepository{
db: db,
logger: logger,
}
}
// Create 创建管理员
func (r *GormAdminRepository) Create(ctx context.Context, admin entities.Admin) error {
r.logger.Info("创建管理员", zap.String("username", admin.Username))
return r.db.WithContext(ctx).Create(&admin).Error
}
// GetByID 根据ID获取管理员
func (r *GormAdminRepository) GetByID(ctx context.Context, id string) (entities.Admin, error) {
var admin entities.Admin
err := r.db.WithContext(ctx).Where("id = ?", id).First(&admin).Error
return admin, err
}
// Update 更新管理员
func (r *GormAdminRepository) Update(ctx context.Context, admin entities.Admin) error {
r.logger.Info("更新管理员", zap.String("id", admin.ID))
return r.db.WithContext(ctx).Save(&admin).Error
}
// Delete 删除管理员
func (r *GormAdminRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除管理员", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Admin{}, "id = ?", id).Error
}
// SoftDelete 软删除管理员
func (r *GormAdminRepository) SoftDelete(ctx context.Context, id string) error {
r.logger.Info("软删除管理员", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Admin{}, "id = ?", id).Error
}
// Restore 恢复管理员
func (r *GormAdminRepository) Restore(ctx context.Context, id string) error {
r.logger.Info("恢复管理员", zap.String("id", id))
return r.db.WithContext(ctx).Unscoped().Model(&entities.Admin{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// Count 统计管理员数量
func (r *GormAdminRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.Admin{})
// 应用过滤条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
}
return count, query.Count(&count).Error
}
// Exists 检查管理员是否存在
func (r *GormAdminRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Admin{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// CreateBatch 批量创建管理员
func (r *GormAdminRepository) CreateBatch(ctx context.Context, admins []entities.Admin) error {
r.logger.Info("批量创建管理员", zap.Int("count", len(admins)))
return r.db.WithContext(ctx).Create(&admins).Error
}
// GetByIDs 根据ID列表获取管理员
func (r *GormAdminRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Admin, error) {
var admins []entities.Admin
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&admins).Error
return admins, err
}
// UpdateBatch 批量更新管理员
func (r *GormAdminRepository) UpdateBatch(ctx context.Context, admins []entities.Admin) error {
r.logger.Info("批量更新管理员", zap.Int("count", len(admins)))
return r.db.WithContext(ctx).Save(&admins).Error
}
// DeleteBatch 批量删除管理员
func (r *GormAdminRepository) DeleteBatch(ctx context.Context, ids []string) error {
r.logger.Info("批量删除管理员", zap.Strings("ids", ids))
return r.db.WithContext(ctx).Delete(&entities.Admin{}, "id IN ?", ids).Error
}
// List 获取管理员列表
func (r *GormAdminRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Admin, error) {
var admins []entities.Admin
query := r.db.WithContext(ctx).Model(&entities.Admin{})
// 应用过滤条件
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
// 应用搜索条件
if options.Search != "" {
query = query.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
}
// 应用排序
if options.Sort != "" {
order := "ASC"
if options.Order != "" {
order = options.Order
}
query = query.Order(options.Sort + " " + order)
}
// 应用分页
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
return admins, query.Find(&admins).Error
}
// WithTx 使用事务
func (r *GormAdminRepository) WithTx(tx interface{}) interfaces.Repository[entities.Admin] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormAdminRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}
// FindByUsername 根据用户名查找管理员
func (r *GormAdminRepository) FindByUsername(ctx context.Context, username string) (*entities.Admin, error) {
var admin entities.Admin
err := r.db.WithContext(ctx).Where("username = ?", username).First(&admin).Error
if err != nil {
return nil, err
}
return &admin, nil
}
// FindByEmail 根据邮箱查找管理员
func (r *GormAdminRepository) FindByEmail(ctx context.Context, email string) (*entities.Admin, error) {
var admin entities.Admin
err := r.db.WithContext(ctx).Where("email = ?", email).First(&admin).Error
if err != nil {
return nil, err
}
return &admin, nil
}
// ListAdmins 获取管理员列表(带分页和筛选)
func (r *GormAdminRepository) ListAdmins(ctx context.Context, req *dto.AdminListRequest) (*dto.AdminListResponse, error) {
var admins []entities.Admin
var total int64
query := r.db.WithContext(ctx).Model(&entities.Admin{})
// 应用筛选条件
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.Email != "" {
query = query.Where("email LIKE ?", "%"+req.Email+"%")
}
if req.Role != "" {
query = query.Where("role = ?", req.Role)
}
if req.IsActive != nil {
query = query.Where("is_active = ?", *req.IsActive)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
return nil, err
}
// 应用分页
offset := (req.Page - 1) * req.PageSize
query = query.Offset(offset).Limit(req.PageSize)
// 默认排序
query = query.Order("created_at DESC")
// 查询数据
if err := query.Find(&admins).Error; err != nil {
return nil, err
}
// 转换为DTO
adminInfos := make([]dto.AdminInfo, len(admins))
for i, admin := range admins {
adminInfos[i] = r.convertToAdminInfo(admin)
}
return &dto.AdminListResponse{
Total: total,
Page: req.Page,
Size: req.PageSize,
Admins: adminInfos,
}, nil
}
// GetStats 获取管理员统计信息
func (r *GormAdminRepository) GetStats(ctx context.Context) (*dto.AdminStatsResponse, error) {
var stats dto.AdminStatsResponse
// 总管理员数
if err := r.db.WithContext(ctx).Model(&entities.Admin{}).Count(&stats.TotalAdmins).Error; err != nil {
return nil, err
}
// 激活管理员数
if err := r.db.WithContext(ctx).Model(&entities.Admin{}).Where("is_active = ?", true).Count(&stats.ActiveAdmins).Error; err != nil {
return nil, err
}
// 今日登录数
today := time.Now().Truncate(24 * time.Hour)
if err := r.db.WithContext(ctx).Model(&entities.AdminLoginLog{}).Where("created_at >= ?", today).Count(&stats.TodayLogins).Error; err != nil {
return nil, err
}
// 总操作数
if err := r.db.WithContext(ctx).Model(&entities.AdminOperationLog{}).Count(&stats.TotalOperations).Error; err != nil {
return nil, err
}
return &stats, nil
}
// GetPermissionsByRole 根据角色获取权限
func (r *GormAdminRepository) GetPermissionsByRole(ctx context.Context, role entities.AdminRole) ([]entities.AdminPermission, error) {
var permissions []entities.AdminPermission
query := r.db.WithContext(ctx).
Joins("JOIN admin_role_permissions ON admin_permissions.id = admin_role_permissions.permission_id").
Where("admin_role_permissions.role = ? AND admin_permissions.is_active = ?", role, true)
return permissions, query.Find(&permissions).Error
}
// UpdatePermissions 更新管理员权限
func (r *GormAdminRepository) UpdatePermissions(ctx context.Context, adminID string, permissions []string) error {
permissionsJSON, err := json.Marshal(permissions)
if err != nil {
return fmt.Errorf("序列化权限失败: %w", err)
}
return r.db.WithContext(ctx).
Model(&entities.Admin{}).
Where("id = ?", adminID).
Update("permissions", string(permissionsJSON)).Error
}
// UpdateLoginStats 更新登录统计
func (r *GormAdminRepository) UpdateLoginStats(ctx context.Context, adminID string) error {
return r.db.WithContext(ctx).
Model(&entities.Admin{}).
Where("id = ?", adminID).
Updates(map[string]interface{}{
"last_login_at": time.Now(),
"login_count": gorm.Expr("login_count + 1"),
}).Error
}
// UpdateReviewStats 更新审核统计
func (r *GormAdminRepository) UpdateReviewStats(ctx context.Context, adminID string, approved bool) error {
updates := map[string]interface{}{
"review_count": gorm.Expr("review_count + 1"),
}
if approved {
updates["approved_count"] = gorm.Expr("approved_count + 1")
} else {
updates["rejected_count"] = gorm.Expr("rejected_count + 1")
}
return r.db.WithContext(ctx).
Model(&entities.Admin{}).
Where("id = ?", adminID).
Updates(updates).Error
}
// convertToAdminInfo 转换为管理员信息DTO
func (r *GormAdminRepository) convertToAdminInfo(admin entities.Admin) dto.AdminInfo {
var permissions []string
if admin.Permissions != "" {
json.Unmarshal([]byte(admin.Permissions), &permissions)
}
return dto.AdminInfo{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
Phone: admin.Phone,
RealName: admin.RealName,
Role: admin.Role,
IsActive: admin.IsActive,
LastLoginAt: admin.LastLoginAt,
LoginCount: admin.LoginCount,
Permissions: permissions,
CreatedAt: admin.CreatedAt,
}
}

View File

@@ -0,0 +1,29 @@
package routes
import (
"github.com/gin-gonic/gin"
"tyapi-server/internal/domains/admin/handlers"
)
// RegisterAdminRoutes 注册管理员路由
func RegisterAdminRoutes(router *gin.Engine, adminHandler *handlers.AdminHandler) {
// 管理员路由组
adminGroup := router.Group("/api/admin")
{
// 认证相关路由(无需认证)
authGroup := adminGroup.Group("/auth")
{
authGroup.POST("/login", adminHandler.Login)
}
// 管理员管理路由(需要认证)
adminGroup.POST("", adminHandler.CreateAdmin) // 创建管理员
adminGroup.GET("", adminHandler.ListAdmins) // 获取管理员列表
adminGroup.GET("/stats", adminHandler.GetAdminStats) // 获取统计信息
adminGroup.GET("/:id", adminHandler.GetAdminByID) // 获取管理员详情
adminGroup.PUT("/:id", adminHandler.UpdateAdmin) // 更新管理员
adminGroup.DELETE("/:id", adminHandler.DeleteAdmin) // 删除管理员
adminGroup.POST("/change-password", adminHandler.ChangePassword) // 修改密码
}
}

View File

@@ -0,0 +1,431 @@
package services
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"tyapi-server/internal/domains/admin/dto"
"tyapi-server/internal/domains/admin/entities"
"tyapi-server/internal/domains/admin/repositories"
"tyapi-server/internal/shared/interfaces"
)
// AdminService 管理员服务
type AdminService struct {
adminRepo repositories.AdminRepository
loginLogRepo repositories.AdminLoginLogRepository
operationLogRepo repositories.AdminOperationLogRepository
permissionRepo repositories.AdminPermissionRepository
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
}
// NewAdminService 创建管理员服务
func NewAdminService(
adminRepo repositories.AdminRepository,
loginLogRepo repositories.AdminLoginLogRepository,
operationLogRepo repositories.AdminOperationLogRepository,
permissionRepo repositories.AdminPermissionRepository,
responseBuilder interfaces.ResponseBuilder,
logger *zap.Logger,
) *AdminService {
return &AdminService{
adminRepo: adminRepo,
loginLogRepo: loginLogRepo,
operationLogRepo: operationLogRepo,
permissionRepo: permissionRepo,
responseBuilder: responseBuilder,
logger: logger,
}
}
// Login 管理员登录
func (s *AdminService) Login(ctx context.Context, req *dto.AdminLoginRequest, clientIP, userAgent string) (*dto.AdminLoginResponse, error) {
s.logger.Info("管理员登录", zap.String("username", req.Username))
// 查找管理员
admin, err := s.adminRepo.FindByUsername(ctx, req.Username)
if err != nil {
s.logger.Warn("管理员登录失败:用户不存在", zap.String("username", req.Username))
s.recordLoginLog(ctx, req.Username, clientIP, userAgent, "failed", "用户不存在")
return nil, fmt.Errorf("用户名或密码错误")
}
// 检查管理员状态
if !admin.IsActive {
s.logger.Warn("管理员登录失败:账户已禁用", zap.String("username", req.Username))
s.recordLoginLog(ctx, req.Username, clientIP, userAgent, "failed", "账户已禁用")
return nil, fmt.Errorf("账户已被禁用,请联系管理员")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(req.Password)); err != nil {
s.logger.Warn("管理员登录失败:密码错误", zap.String("username", req.Username))
s.recordLoginLog(ctx, req.Username, clientIP, userAgent, "failed", "密码错误")
return nil, fmt.Errorf("用户名或密码错误")
}
// 更新登录统计
if err := s.adminRepo.UpdateLoginStats(ctx, admin.ID); err != nil {
s.logger.Error("更新登录统计失败", zap.Error(err))
}
// 记录登录日志
s.recordLoginLog(ctx, req.Username, clientIP, userAgent, "success", "登录成功")
// 生成JWT令牌
token, expiresAt, err := s.generateJWTToken(admin)
if err != nil {
return nil, fmt.Errorf("生成令牌失败: %w", err)
}
// 获取权限列表
permissions, err := s.getAdminPermissions(ctx, admin)
if err != nil {
s.logger.Error("获取管理员权限失败", zap.Error(err))
permissions = []string{}
}
// 构建响应
adminInfo := dto.AdminInfo{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
Phone: admin.Phone,
RealName: admin.RealName,
Role: admin.Role,
IsActive: admin.IsActive,
LastLoginAt: admin.LastLoginAt,
LoginCount: admin.LoginCount,
Permissions: permissions,
CreatedAt: admin.CreatedAt,
}
s.logger.Info("管理员登录成功", zap.String("username", req.Username))
return &dto.AdminLoginResponse{
Token: token,
ExpiresAt: expiresAt,
Admin: adminInfo,
}, nil
}
// CreateAdmin 创建管理员
func (s *AdminService) CreateAdmin(ctx context.Context, req *dto.AdminCreateRequest, operatorID string) error {
s.logger.Info("创建管理员", zap.String("username", req.Username))
// 检查用户名是否已存在
if _, err := s.adminRepo.FindByUsername(ctx, req.Username); err == nil {
return fmt.Errorf("用户名已存在")
}
// 检查邮箱是否已存在
if _, err := s.adminRepo.FindByEmail(ctx, req.Email); err == nil {
return fmt.Errorf("邮箱已存在")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
// 序列化权限
permissionsJSON := "[]"
if len(req.Permissions) > 0 {
permissionsBytes, err := json.Marshal(req.Permissions)
if err != nil {
return fmt.Errorf("权限序列化失败: %w", err)
}
permissionsJSON = string(permissionsBytes)
}
// 创建管理员
admin := entities.Admin{
ID: s.generateID(),
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
Role: req.Role,
IsActive: true,
Permissions: permissionsJSON,
}
if err := s.adminRepo.Create(ctx, admin); err != nil {
return fmt.Errorf("创建管理员失败: %w", err)
}
// 记录操作日志
s.recordOperationLog(ctx, operatorID, "create", "admin", admin.ID, map[string]interface{}{
"username": req.Username,
"email": req.Email,
"role": req.Role,
}, "success", "创建管理员成功")
s.logger.Info("管理员创建成功", zap.String("username", req.Username))
return nil
}
// UpdateAdmin 更新管理员
func (s *AdminService) UpdateAdmin(ctx context.Context, adminID string, req *dto.AdminUpdateRequest, operatorID string) error {
s.logger.Info("更新管理员", zap.String("admin_id", adminID))
// 获取管理员
admin, err := s.adminRepo.GetByID(ctx, adminID)
if err != nil {
return fmt.Errorf("管理员不存在")
}
// 更新字段
if req.Email != "" {
// 检查邮箱是否被其他管理员使用
if existingAdmin, err := s.adminRepo.FindByEmail(ctx, req.Email); err == nil && existingAdmin.ID != adminID {
return fmt.Errorf("邮箱已被其他管理员使用")
}
admin.Email = req.Email
}
if req.Phone != "" {
admin.Phone = req.Phone
}
if req.RealName != "" {
admin.RealName = req.RealName
}
if req.Role != "" {
admin.Role = req.Role
}
if req.IsActive != nil {
admin.IsActive = *req.IsActive
}
if len(req.Permissions) > 0 {
permissionsJSON, err := json.Marshal(req.Permissions)
if err != nil {
return fmt.Errorf("权限序列化失败: %w", err)
}
admin.Permissions = string(permissionsJSON)
}
// 保存更新
if err := s.adminRepo.Update(ctx, admin); err != nil {
return fmt.Errorf("更新管理员失败: %w", err)
}
// 记录操作日志
s.recordOperationLog(ctx, operatorID, "update", "admin", adminID, map[string]interface{}{
"email": req.Email,
"phone": req.Phone,
"real_name": req.RealName,
"role": req.Role,
"is_active": req.IsActive,
}, "success", "更新管理员成功")
s.logger.Info("管理员更新成功", zap.String("admin_id", adminID))
return nil
}
// ChangePassword 修改密码
func (s *AdminService) ChangePassword(ctx context.Context, adminID string, req *dto.AdminPasswordChangeRequest) error {
s.logger.Info("修改管理员密码", zap.String("admin_id", adminID))
// 获取管理员
admin, err := s.adminRepo.GetByID(ctx, adminID)
if err != nil {
return fmt.Errorf("管理员不存在")
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(req.OldPassword)); err != nil {
return fmt.Errorf("旧密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
// 更新密码
admin.Password = string(hashedPassword)
if err := s.adminRepo.Update(ctx, admin); err != nil {
return fmt.Errorf("更新密码失败: %w", err)
}
// 记录操作日志
s.recordOperationLog(ctx, adminID, "change_password", "admin", adminID, nil, "success", "修改密码成功")
s.logger.Info("管理员密码修改成功", zap.String("admin_id", adminID))
return nil
}
// ListAdmins 获取管理员列表
func (s *AdminService) ListAdmins(ctx context.Context, req *dto.AdminListRequest) (*dto.AdminListResponse, error) {
s.logger.Info("获取管理员列表", zap.Int("page", req.Page), zap.Int("page_size", req.PageSize))
response, err := s.adminRepo.ListAdmins(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取管理员列表失败: %w", err)
}
return response, nil
}
// GetAdminStats 获取管理员统计信息
func (s *AdminService) GetAdminStats(ctx context.Context) (*dto.AdminStatsResponse, error) {
s.logger.Info("获取管理员统计信息")
stats, err := s.adminRepo.GetStats(ctx)
if err != nil {
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
return stats, nil
}
// GetAdminByID 根据ID获取管理员
func (s *AdminService) GetAdminByID(ctx context.Context, adminID string) (*dto.AdminInfo, error) {
s.logger.Info("获取管理员信息", zap.String("admin_id", adminID))
admin, err := s.adminRepo.GetByID(ctx, adminID)
if err != nil {
return nil, fmt.Errorf("管理员不存在")
}
// 获取权限列表
permissions, err := s.getAdminPermissions(ctx, &admin)
if err != nil {
s.logger.Error("获取管理员权限失败", zap.Error(err))
permissions = []string{}
}
adminInfo := dto.AdminInfo{
ID: admin.ID,
Username: admin.Username,
Email: admin.Email,
Phone: admin.Phone,
RealName: admin.RealName,
Role: admin.Role,
IsActive: admin.IsActive,
LastLoginAt: admin.LastLoginAt,
LoginCount: admin.LoginCount,
Permissions: permissions,
CreatedAt: admin.CreatedAt,
}
return &adminInfo, nil
}
// DeleteAdmin 删除管理员
func (s *AdminService) DeleteAdmin(ctx context.Context, adminID string, operatorID string) error {
s.logger.Info("删除管理员", zap.String("admin_id", adminID))
// 检查管理员是否存在
if _, err := s.adminRepo.GetByID(ctx, adminID); err != nil {
return fmt.Errorf("管理员不存在")
}
// 软删除管理员
if err := s.adminRepo.SoftDelete(ctx, adminID); err != nil {
return fmt.Errorf("删除管理员失败: %w", err)
}
// 记录操作日志
s.recordOperationLog(ctx, operatorID, "delete", "admin", adminID, nil, "success", "删除管理员成功")
s.logger.Info("管理员删除成功", zap.String("admin_id", adminID))
return nil
}
// getAdminPermissions 获取管理员权限
func (s *AdminService) getAdminPermissions(ctx context.Context, admin *entities.Admin) ([]string, error) {
// 首先从角色获取权限
rolePermissions, err := s.adminRepo.GetPermissionsByRole(ctx, admin.Role)
if err != nil {
return nil, err
}
// 从角色权限中提取权限代码
permissions := make([]string, 0, len(rolePermissions))
for _, perm := range rolePermissions {
permissions = append(permissions, perm.Code)
}
// 如果有自定义权限,也添加进去
if admin.Permissions != "" {
var customPermissions []string
if err := json.Unmarshal([]byte(admin.Permissions), &customPermissions); err == nil {
permissions = append(permissions, customPermissions...)
}
}
return permissions, nil
}
// generateJWTToken 生成JWT令牌
func (s *AdminService) generateJWTToken(admin *entities.Admin) (string, time.Time, error) {
// 这里应该使用JWT库生成令牌
// 为了简化,这里返回一个模拟的令牌
token := fmt.Sprintf("admin_token_%s_%d", admin.ID, time.Now().Unix())
expiresAt := time.Now().Add(24 * time.Hour)
return token, expiresAt, nil
}
// generateID 生成ID
func (s *AdminService) generateID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// recordLoginLog 记录登录日志
func (s *AdminService) recordLoginLog(ctx context.Context, username, ip, userAgent, status, message string) {
log := entities.AdminLoginLog{
ID: s.generateID(),
Username: username,
IP: ip,
UserAgent: userAgent,
Status: status,
Message: message,
}
if err := s.loginLogRepo.Create(ctx, log); err != nil {
s.logger.Error("记录登录日志失败", zap.Error(err))
}
}
// recordOperationLog 记录操作日志
func (s *AdminService) recordOperationLog(ctx context.Context, adminID, action, resource, resourceID string, details map[string]interface{}, status, message string) {
detailsJSON := "{}"
if details != nil {
if bytes, err := json.Marshal(details); err == nil {
detailsJSON = string(bytes)
}
}
log := entities.AdminOperationLog{
ID: s.generateID(),
AdminID: adminID,
Action: action,
Resource: resource,
ResourceID: resourceID,
Details: detailsJSON,
Status: status,
Message: message,
}
if err := s.operationLogRepo.Create(ctx, log); err != nil {
s.logger.Error("记录操作日志失败", zap.Error(err))
}
}

View File

@@ -0,0 +1,110 @@
package dto
import (
"time"
"tyapi-server/internal/domains/certification/enums"
)
// CertificationCreateRequest 创建认证申请请求
type CertificationCreateRequest struct {
UserID string `json:"user_id" binding:"required"`
}
// CertificationCreateResponse 创建认证申请响应
type CertificationCreateResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Status enums.CertificationStatus `json:"status"`
}
// CertificationStatusResponse 认证状态响应
type CertificationStatusResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Status enums.CertificationStatus `json:"status"`
StatusName string `json:"status_name"`
Progress int `json:"progress"`
IsUserActionRequired bool `json:"is_user_action_required"`
IsAdminActionRequired bool `json:"is_admin_action_required"`
// 时间节点
InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"`
FaceVerifiedAt *time.Time `json:"face_verified_at,omitempty"`
ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"`
ContractApprovedAt *time.Time `json:"contract_approved_at,omitempty"`
ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
// 关联信息
Enterprise *EnterpriseInfoResponse `json:"enterprise,omitempty"`
ContractURL string `json:"contract_url,omitempty"`
SigningURL string `json:"signing_url,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SubmitEnterpriseInfoRequest 提交企业信息请求
type SubmitEnterpriseInfoRequest struct {
CompanyName string `json:"company_name" binding:"required"`
UnifiedSocialCode string `json:"unified_social_code" binding:"required"`
LegalPersonName string `json:"legal_person_name" binding:"required"`
LegalPersonID string `json:"legal_person_id" binding:"required"`
LicenseUploadRecordID string `json:"license_upload_record_id" binding:"required"`
}
// SubmitEnterpriseInfoResponse 提交企业信息响应
type SubmitEnterpriseInfoResponse struct {
ID string `json:"id"`
Status enums.CertificationStatus `json:"status"`
Enterprise *EnterpriseInfoResponse `json:"enterprise"`
}
// FaceVerifyRequest 人脸识别请求
type FaceVerifyRequest struct {
RealName string `json:"real_name" binding:"required"`
IDCardNumber string `json:"id_card_number" binding:"required"`
ReturnURL string `json:"return_url" binding:"required"`
}
// FaceVerifyResponse 人脸识别响应
type FaceVerifyResponse struct {
CertifyID string `json:"certify_id"`
VerifyURL string `json:"verify_url"`
ExpiresAt time.Time `json:"expires_at"`
}
// ApplyContractRequest 申请合同请求(无需额外参数)
type ApplyContractRequest struct{}
// ApplyContractResponse 申请合同响应
type ApplyContractResponse struct {
ID string `json:"id"`
Status enums.CertificationStatus `json:"status"`
ContractAppliedAt time.Time `json:"contract_applied_at"`
}
// SignContractRequest 签署合同请求
type SignContractRequest struct {
SignatureData string `json:"signature_data,omitempty"`
}
// SignContractResponse 签署合同响应
type SignContractResponse struct {
ID string `json:"id"`
Status enums.CertificationStatus `json:"status"`
ContractSignedAt time.Time `json:"contract_signed_at"`
}
// CertificationDetailResponse 认证详情响应
type CertificationDetailResponse struct {
*CertificationStatusResponse
// 详细记录
LicenseUploadRecord *LicenseUploadRecordResponse `json:"license_upload_record,omitempty"`
FaceVerifyRecords []FaceVerifyRecordResponse `json:"face_verify_records,omitempty"`
ContractRecords []ContractRecordResponse `json:"contract_records,omitempty"`
NotificationRecords []NotificationRecordResponse `json:"notification_records,omitempty"`
}

View File

@@ -0,0 +1,108 @@
package dto
import "time"
// EnterpriseInfoResponse 企业信息响应
type EnterpriseInfoResponse struct {
ID string `json:"id"`
CertificationID string `json:"certification_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
LegalPersonID string `json:"legal_person_id"`
LicenseUploadRecordID string `json:"license_upload_record_id"`
OCRRawData string `json:"ocr_raw_data,omitempty"`
OCRConfidence float64 `json:"ocr_confidence,omitempty"`
IsOCRVerified bool `json:"is_ocr_verified"`
IsFaceVerified bool `json:"is_face_verified"`
VerificationData string `json:"verification_data,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// LicenseUploadRecordResponse 营业执照上传记录响应
type LicenseUploadRecordResponse struct {
ID string `json:"id"`
CertificationID *string `json:"certification_id,omitempty"`
UserID string `json:"user_id"`
OriginalFileName string `json:"original_file_name"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
FileURL string `json:"file_url"`
QiNiuKey string `json:"qiniu_key"`
OCRProcessed bool `json:"ocr_processed"`
OCRSuccess bool `json:"ocr_success"`
OCRConfidence float64 `json:"ocr_confidence,omitempty"`
OCRRawData string `json:"ocr_raw_data,omitempty"`
OCRErrorMessage string `json:"ocr_error_message,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// FaceVerifyRecordResponse 人脸识别记录响应
type FaceVerifyRecordResponse struct {
ID string `json:"id"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
CertifyID string `json:"certify_id"`
VerifyURL string `json:"verify_url,omitempty"`
ReturnURL string `json:"return_url,omitempty"`
RealName string `json:"real_name"`
IDCardNumber string `json:"id_card_number"`
Status string `json:"status"`
StatusName string `json:"status_name"`
ResultCode string `json:"result_code,omitempty"`
ResultMessage string `json:"result_message,omitempty"`
VerifyScore float64 `json:"verify_score,omitempty"`
InitiatedAt time.Time `json:"initiated_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ContractRecordResponse 合同记录响应
type ContractRecordResponse struct {
ID string `json:"id"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
AdminID *string `json:"admin_id,omitempty"`
ContractType string `json:"contract_type"`
ContractURL string `json:"contract_url,omitempty"`
SigningURL string `json:"signing_url,omitempty"`
SignatureData string `json:"signature_data,omitempty"`
SignedAt *time.Time `json:"signed_at,omitempty"`
ClientIP string `json:"client_ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Status string `json:"status"`
StatusName string `json:"status_name"`
ApprovalNotes string `json:"approval_notes,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NotificationRecordResponse 通知记录响应
type NotificationRecordResponse struct {
ID string `json:"id"`
CertificationID *string `json:"certification_id,omitempty"`
UserID *string `json:"user_id,omitempty"`
NotificationType string `json:"notification_type"`
NotificationTypeName string `json:"notification_type_name"`
NotificationScene string `json:"notification_scene"`
NotificationSceneName string `json:"notification_scene_name"`
Recipient string `json:"recipient"`
Title string `json:"title,omitempty"`
Content string `json:"content"`
TemplateID string `json:"template_id,omitempty"`
TemplateParams string `json:"template_params,omitempty"`
Status string `json:"status"`
StatusName string `json:"status_name"`
ErrorMessage string `json:"error_message,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
RetryCount int `json:"retry_count"`
MaxRetryCount int `json:"max_retry_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,77 @@
package dto
// BusinessLicenseResult 营业执照识别结果
type BusinessLicenseResult struct {
CompanyName string `json:"company_name"` // 公司名称
LegalRepresentative string `json:"legal_representative"` // 法定代表人
RegisteredCapital string `json:"registered_capital"` // 注册资本
RegisteredAddress string `json:"registered_address"` // 注册地址
RegistrationNumber string `json:"registration_number"` // 统一社会信用代码
BusinessScope string `json:"business_scope"` // 经营范围
RegistrationDate string `json:"registration_date"` // 成立日期
ValidDate string `json:"valid_date"` // 营业期限
Confidence float64 `json:"confidence"` // 识别置信度
Words []string `json:"words"` // 识别的所有文字
}
// IDCardResult 身份证识别结果
type IDCardResult struct {
Side string `json:"side"` // 身份证面front/back
Name string `json:"name"` // 姓名(正面)
Sex string `json:"sex"` // 性别(正面)
Nation string `json:"nation"` // 民族(正面)
BirthDate string `json:"birth_date"` // 出生日期(正面)
Address string `json:"address"` // 住址(正面)
IDNumber string `json:"id_number"` // 身份证号码(正面)
IssuingAuthority string `json:"issuing_authority"` // 签发机关(背面)
ValidDate string `json:"valid_date"` // 有效期限(背面)
Confidence float64 `json:"confidence"` // 识别置信度
Words []string `json:"words"` // 识别的所有文字
}
// GeneralTextResult 通用文字识别结果
type GeneralTextResult struct {
Words []string `json:"words"` // 识别的文字列表
Confidence float64 `json:"confidence"` // 识别置信度
}
// OCREnterpriseInfo OCR识别的企业信息
type OCREnterpriseInfo struct {
CompanyName string `json:"company_name"` // 企业名称
UnifiedSocialCode string `json:"unified_social_code"` // 统一社会信用代码
LegalPersonName string `json:"legal_person_name"` // 法人姓名
LegalPersonID string `json:"legal_person_id"` // 法人身份证号
Confidence float64 `json:"confidence"` // 识别置信度
}
// LicenseProcessResult 营业执照处理结果
type LicenseProcessResult struct {
LicenseURL string `json:"license_url"` // 营业执照文件URL
EnterpriseInfo *OCREnterpriseInfo `json:"enterprise_info"` // OCR识别的企业信息
OCRSuccess bool `json:"ocr_success"` // OCR是否成功
OCRError string `json:"ocr_error,omitempty"` // OCR错误信息
}
// UploadLicenseRequest 上传营业执照请求
type UploadLicenseRequest struct {
// 文件通过multipart/form-data上传这里定义验证规则
}
// UploadLicenseResponse 上传营业执照响应
type UploadLicenseResponse struct {
UploadRecordID string `json:"upload_record_id"` // 上传记录ID
FileURL string `json:"file_url"` // 文件URL
OCRProcessed bool `json:"ocr_processed"` // OCR是否已处理
OCRSuccess bool `json:"ocr_success"` // OCR是否成功
EnterpriseInfo *OCREnterpriseInfo `json:"enterprise_info"` // OCR识别的企业信息如果成功
OCRErrorMessage string `json:"ocr_error_message,omitempty"` // OCR错误信息如果失败
}
// UploadResult 上传结果
type UploadResult struct {
Key string `json:"key"` // 文件key
URL string `json:"url"` // 文件访问URL
MimeType string `json:"mime_type"` // MIME类型
Size int64 `json:"size"` // 文件大小
Hash string `json:"hash"` // 文件哈希值
}

View File

@@ -0,0 +1,179 @@
package entities
import (
"time"
"tyapi-server/internal/domains/certification/enums"
"gorm.io/gorm"
)
// Certification 认证申请实体
// 这是企业认证流程的核心实体,负责管理整个认证申请的生命周期
// 包含认证状态、时间节点、审核信息、合同信息等核心数据
type Certification struct {
// 基础信息
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"认证申请唯一标识"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"申请用户ID"`
EnterpriseID *string `gorm:"type:varchar(36);index" json:"enterprise_id" comment:"关联的企业信息ID"`
Status enums.CertificationStatus `gorm:"type:varchar(50);not null;index" json:"status" comment:"当前认证状态"`
// 流程节点时间戳 - 记录每个关键步骤的完成时间
InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty" comment:"企业信息提交时间"`
FaceVerifiedAt *time.Time `json:"face_verified_at,omitempty" comment:"人脸识别完成时间"`
ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty" comment:"合同申请时间"`
ContractApprovedAt *time.Time `json:"contract_approved_at,omitempty" comment:"合同审核通过时间"`
ContractSignedAt *time.Time `json:"contract_signed_at,omitempty" comment:"合同签署完成时间"`
CompletedAt *time.Time `json:"completed_at,omitempty" comment:"认证完成时间"`
// 审核信息 - 管理员审核相关数据
AdminID *string `gorm:"type:varchar(36)" json:"admin_id,omitempty" comment:"审核管理员ID"`
ApprovalNotes string `gorm:"type:text" json:"approval_notes,omitempty" comment:"审核备注信息"`
RejectReason string `gorm:"type:text" json:"reject_reason,omitempty" comment:"拒绝原因说明"`
// 合同信息 - 电子合同相关链接
ContractURL string `gorm:"type:varchar(500)" json:"contract_url,omitempty" comment:"合同文件访问链接"`
SigningURL string `gorm:"type:varchar(500)" json:"signing_url,omitempty" comment:"电子签署链接"`
// OCR识别信息 - 营业执照OCR识别结果
OCRRequestID string `gorm:"type:varchar(100)" json:"ocr_request_id,omitempty" comment:"OCR识别请求ID"`
OCRConfidence float64 `gorm:"type:decimal(5,2)" json:"ocr_confidence,omitempty" comment:"OCR识别置信度(0-1)"`
// 时间戳字段
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:"软删除时间"`
// 关联关系 - 与其他实体的关联
Enterprise *Enterprise `gorm:"foreignKey:EnterpriseID" json:"enterprise,omitempty" comment:"关联的企业信息"`
LicenseUploadRecord *LicenseUploadRecord `gorm:"foreignKey:CertificationID" json:"license_upload_record,omitempty" comment:"关联的营业执照上传记录"`
FaceVerifyRecords []FaceVerifyRecord `gorm:"foreignKey:CertificationID" json:"face_verify_records,omitempty" comment:"关联的人脸识别记录列表"`
ContractRecords []ContractRecord `gorm:"foreignKey:CertificationID" json:"contract_records,omitempty" comment:"关联的合同记录列表"`
NotificationRecords []NotificationRecord `gorm:"foreignKey:CertificationID" json:"notification_records,omitempty" comment:"关联的通知记录列表"`
}
// TableName 指定数据库表名
func (Certification) TableName() string {
return "certifications"
}
// IsStatusChangeable 检查状态是否可以变更
// 只有非最终状态(完成/拒绝)的认证申请才能进行状态变更
func (c *Certification) IsStatusChangeable() bool {
return !enums.IsFinalStatus(c.Status)
}
// CanRetryFaceVerify 检查是否可以重试人脸识别
// 只有人脸识别失败状态的申请才能重试
func (c *Certification) CanRetryFaceVerify() bool {
return c.Status == enums.StatusFaceFailed
}
// CanRetrySign 检查是否可以重试签署
// 只有签署失败状态的申请才能重试
func (c *Certification) CanRetrySign() bool {
return c.Status == enums.StatusSignFailed
}
// CanRestart 检查是否可以重新开始流程
// 只有被拒绝的申请才能重新开始认证流程
func (c *Certification) CanRestart() bool {
return c.Status == enums.StatusRejected
}
// GetNextValidStatuses 获取当前状态可以转换到的下一个状态列表
// 根据状态机规则,返回所有合法的下一个状态
func (c *Certification) GetNextValidStatuses() []enums.CertificationStatus {
switch c.Status {
case enums.StatusPending:
return []enums.CertificationStatus{enums.StatusInfoSubmitted}
case enums.StatusInfoSubmitted:
return []enums.CertificationStatus{enums.StatusFaceVerified, enums.StatusFaceFailed}
case enums.StatusFaceVerified:
return []enums.CertificationStatus{enums.StatusContractApplied}
case enums.StatusContractApplied:
return []enums.CertificationStatus{enums.StatusContractPending}
case enums.StatusContractPending:
return []enums.CertificationStatus{enums.StatusContractApproved, enums.StatusRejected}
case enums.StatusContractApproved:
return []enums.CertificationStatus{enums.StatusContractSigned, enums.StatusSignFailed}
case enums.StatusContractSigned:
return []enums.CertificationStatus{enums.StatusCompleted}
case enums.StatusFaceFailed:
return []enums.CertificationStatus{enums.StatusFaceVerified}
case enums.StatusSignFailed:
return []enums.CertificationStatus{enums.StatusContractSigned}
case enums.StatusRejected:
return []enums.CertificationStatus{enums.StatusInfoSubmitted}
default:
return []enums.CertificationStatus{}
}
}
// CanTransitionTo 检查是否可以转换到指定状态
// 验证状态转换的合法性,确保状态机规则得到遵守
func (c *Certification) CanTransitionTo(targetStatus enums.CertificationStatus) bool {
validStatuses := c.GetNextValidStatuses()
for _, status := range validStatuses {
if status == targetStatus {
return true
}
}
return false
}
// GetProgressPercentage 获取认证进度百分比
// 根据当前状态计算认证流程的完成进度,用于前端进度条显示
func (c *Certification) GetProgressPercentage() int {
switch c.Status {
case enums.StatusPending:
return 0
case enums.StatusInfoSubmitted:
return 12
case enums.StatusFaceVerified:
return 25
case enums.StatusContractApplied:
return 37
case enums.StatusContractPending:
return 50
case enums.StatusContractApproved:
return 75
case enums.StatusContractSigned:
return 87
case enums.StatusCompleted:
return 100
case enums.StatusFaceFailed, enums.StatusSignFailed:
return c.GetProgressPercentage() // 失败状态保持原进度
case enums.StatusRejected:
return 0
default:
return 0
}
}
// IsUserActionRequired 检查是否需要用户操作
// 判断当前状态是否需要用户进行下一步操作,用于前端提示
func (c *Certification) IsUserActionRequired() bool {
userActionStatuses := []enums.CertificationStatus{
enums.StatusPending,
enums.StatusInfoSubmitted,
enums.StatusFaceVerified,
enums.StatusContractApproved,
enums.StatusFaceFailed,
enums.StatusSignFailed,
enums.StatusRejected,
}
for _, status := range userActionStatuses {
if c.Status == status {
return true
}
}
return false
}
// IsAdminActionRequired 检查是否需要管理员操作
// 判断当前状态是否需要管理员审核,用于后台管理界面
func (c *Certification) IsAdminActionRequired() bool {
return c.Status == enums.StatusContractPending
}

View File

@@ -0,0 +1,98 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// ContractRecord 合同记录实体
// 记录电子合同的详细信息,包括合同生成、审核、签署的完整流程
// 支持合同状态跟踪、签署信息记录、审核流程管理等功能
type ContractRecord struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"合同记录唯一标识"`
CertificationID string `gorm:"type:varchar(36);not null;index" json:"certification_id" comment:"关联的认证申请ID"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"合同申请人ID"`
AdminID *string `gorm:"type:varchar(36);index" json:"admin_id,omitempty" comment:"审核管理员ID"`
// 合同信息 - 电子合同的基本信息
ContractType string `gorm:"type:varchar(50);not null" json:"contract_type" comment:"合同类型(ENTERPRISE_CERTIFICATION)"`
ContractURL string `gorm:"type:varchar(500)" json:"contract_url,omitempty" comment:"合同文件访问链接"`
SigningURL string `gorm:"type:varchar(500)" json:"signing_url,omitempty" comment:"电子签署链接"`
// 签署信息 - 记录用户签署的详细信息
SignatureData string `gorm:"type:text" json:"signature_data,omitempty" comment:"签署数据(JSON格式)"`
SignedAt *time.Time `json:"signed_at,omitempty" comment:"签署完成时间"`
ClientIP string `gorm:"type:varchar(50)" json:"client_ip,omitempty" comment:"签署客户端IP"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent,omitempty" comment:"签署客户端信息"`
// 状态信息 - 合同的生命周期状态
Status string `gorm:"type:varchar(50);not null;index" json:"status" comment:"合同状态(PENDING/APPROVED/SIGNED/EXPIRED)"`
ApprovalNotes string `gorm:"type:text" json:"approval_notes,omitempty" comment:"审核备注信息"`
RejectReason string `gorm:"type:text" json:"reject_reason,omitempty" comment:"拒绝原因说明"`
ExpiresAt *time.Time `json:"expires_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:"软删除时间"`
// 关联关系
Certification *Certification `gorm:"foreignKey:CertificationID" json:"certification,omitempty" comment:"关联的认证申请"`
}
// TableName 指定数据库表名
func (ContractRecord) TableName() string {
return "contract_records"
}
// IsPending 检查合同是否待审核
// 判断合同是否处于等待管理员审核的状态
func (c *ContractRecord) IsPending() bool {
return c.Status == "PENDING"
}
// IsApproved 检查合同是否已审核通过
// 判断合同是否已通过管理员审核,可以进入签署阶段
func (c *ContractRecord) IsApproved() bool {
return c.Status == "APPROVED"
}
// IsSigned 检查合同是否已签署
// 判断合同是否已完成电子签署,认证流程即将完成
func (c *ContractRecord) IsSigned() bool {
return c.Status == "SIGNED"
}
// IsExpired 检查合同是否已过期
// 判断合同是否已超过有效期,过期后需要重新申请
func (c *ContractRecord) IsExpired() bool {
if c.ExpiresAt == nil {
return false
}
return time.Now().After(*c.ExpiresAt)
}
// HasSigningURL 检查是否有签署链接
// 判断是否已生成电子签署链接,用于前端判断是否显示签署按钮
func (c *ContractRecord) HasSigningURL() bool {
return c.SigningURL != ""
}
// GetStatusName 获取状态的中文名称
// 将英文状态码转换为中文显示名称,用于前端展示和用户理解
func (c *ContractRecord) GetStatusName() string {
statusNames := map[string]string{
"PENDING": "待审核",
"APPROVED": "已审核",
"SIGNED": "已签署",
"EXPIRED": "已过期",
"REJECTED": "已拒绝",
}
if name, exists := statusNames[c.Status]; exists {
return name
}
return c.Status
}

View File

@@ -0,0 +1,66 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// Enterprise 企业信息实体
// 存储企业认证的核心信息,包括企业四要素和验证状态
// 与认证申请是一对一关系,每个认证申请对应一个企业信息
type Enterprise struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"企业信息唯一标识"`
CertificationID string `gorm:"type:varchar(36);not null;index" json:"certification_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:"法定代表人身份证号"`
// 关联的营业执照上传记录
LicenseUploadRecordID string `gorm:"type:varchar(36);not null;index" json:"license_upload_record_id" comment:"关联的营业执照上传记录ID"`
// 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)"`
// 验证状态 - 各环节的验证结果
IsOCRVerified bool `gorm:"default:false" json:"is_ocr_verified" comment:"OCR验证是否通过"`
IsFaceVerified bool `gorm:"default:false" json:"is_face_verified" comment:"人脸识别是否通过"`
VerificationData string `gorm:"type:text" json:"verification_data,omitempty" comment:"验证数据(JSON格式)"`
// 时间戳字段
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:"软删除时间"`
// 关联关系
Certification *Certification `gorm:"foreignKey:CertificationID" json:"certification,omitempty" comment:"关联的认证申请"`
LicenseUploadRecord *LicenseUploadRecord `gorm:"foreignKey:LicenseUploadRecordID" json:"license_upload_record,omitempty" comment:"关联的营业执照上传记录"`
}
// TableName 指定数据库表名
func (Enterprise) TableName() string {
return "enterprises"
}
// IsComplete 检查企业四要素是否完整
// 验证企业名称、统一社会信用代码、法定代表人姓名、身份证号是否都已填写
func (e *Enterprise) IsComplete() bool {
return e.CompanyName != "" &&
e.UnifiedSocialCode != "" &&
e.LegalPersonName != "" &&
e.LegalPersonID != ""
}
// Validate 验证企业信息是否有效
// 这里可以添加企业信息的业务验证逻辑
// 比如统一社会信用代码格式验证、身份证号格式验证等
func (e *Enterprise) Validate() error {
// 这里可以添加企业信息的业务验证逻辑
// 比如统一社会信用代码格式验证、身份证号格式验证等
return nil
}

View File

@@ -0,0 +1,89 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// FaceVerifyRecord 人脸识别记录实体
// 记录用户进行人脸识别验证的详细信息,包括验证状态、结果和身份信息
// 支持多次验证尝试,每次验证都会生成独立的记录,便于追踪和重试
type FaceVerifyRecord struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"人脸识别记录唯一标识"`
CertificationID string `gorm:"type:varchar(36);not null;index" json:"certification_id" comment:"关联的认证申请ID"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"进行验证的用户ID"`
// 阿里云人脸识别信息 - 第三方服务的相关数据
CertifyID string `gorm:"type:varchar(100);not null;index" json:"certify_id" comment:"阿里云人脸识别任务ID"`
VerifyURL string `gorm:"type:varchar(500)" json:"verify_url,omitempty" comment:"人脸识别验证页面URL"`
ReturnURL string `gorm:"type:varchar(500)" json:"return_url,omitempty" comment:"验证完成后的回调URL"`
// 身份信息 - 用于人脸识别的身份验证数据
RealName string `gorm:"type:varchar(100);not null" json:"real_name" comment:"真实姓名"`
IDCardNumber string `gorm:"type:varchar(50);not null" json:"id_card_number" comment:"身份证号码"`
// 验证结果 - 记录验证的详细结果信息
Status string `gorm:"type:varchar(50);not null;index" json:"status" comment:"验证状态(PROCESSING/SUCCESS/FAIL)"`
ResultCode string `gorm:"type:varchar(50)" json:"result_code,omitempty" comment:"结果代码"`
ResultMessage string `gorm:"type:varchar(500)" json:"result_message,omitempty" comment:"结果描述信息"`
VerifyScore float64 `gorm:"type:decimal(5,2)" json:"verify_score,omitempty" comment:"验证分数(0-1)"`
// 时间信息 - 验证流程的时间节点
InitiatedAt time.Time `gorm:"autoCreateTime" json:"initiated_at" comment:"验证发起时间"`
CompletedAt *time.Time `json:"completed_at,omitempty" comment:"验证完成时间"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at" 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:"软删除时间"`
// 关联关系
Certification *Certification `gorm:"foreignKey:CertificationID" json:"certification,omitempty" comment:"关联的认证申请"`
}
// TableName 指定数据库表名
func (FaceVerifyRecord) TableName() string {
return "face_verify_records"
}
// IsSuccess 检查人脸识别是否成功
// 判断验证状态是否为成功状态
func (f *FaceVerifyRecord) IsSuccess() bool {
return f.Status == "SUCCESS"
}
// IsProcessing 检查是否正在处理中
// 判断验证是否正在进行中,等待用户完成验证
func (f *FaceVerifyRecord) IsProcessing() bool {
return f.Status == "PROCESSING"
}
// IsFailed 检查是否失败
// 判断验证是否失败,包括超时、验证不通过等情况
func (f *FaceVerifyRecord) IsFailed() bool {
return f.Status == "FAIL"
}
// IsExpired 检查是否已过期
// 判断验证链接是否已超过有效期,过期后需要重新发起验证
func (f *FaceVerifyRecord) IsExpired() bool {
return time.Now().After(f.ExpiresAt)
}
// GetStatusName 获取状态的中文名称
// 将英文状态码转换为中文显示名称,用于前端展示
func (f *FaceVerifyRecord) GetStatusName() string {
statusNames := map[string]string{
"PROCESSING": "处理中",
"SUCCESS": "成功",
"FAIL": "失败",
}
if name, exists := statusNames[f.Status]; exists {
return name
}
return f.Status
}

View File

@@ -0,0 +1,70 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// LicenseUploadRecord 营业执照上传记录实体
// 记录用户上传营业执照文件的详细信息包括文件元数据和OCR处理结果
// 支持多种文件格式自动进行OCR识别为后续企业信息验证提供数据支持
type LicenseUploadRecord struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"上传记录唯一标识"`
CertificationID *string `gorm:"type:varchar(36);index" json:"certification_id,omitempty" comment:"关联的认证申请ID(可为空,表示独立上传)"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"上传用户ID"`
// 文件信息 - 存储文件的元数据信息
OriginalFileName string `gorm:"type:varchar(255);not null" json:"original_file_name" comment:"原始文件名"`
FileSize int64 `gorm:"not null" json:"file_size" comment:"文件大小(字节)"`
FileType string `gorm:"type:varchar(50);not null" json:"file_type" comment:"文件MIME类型"`
FileURL string `gorm:"type:varchar(500);not null" json:"file_url" comment:"文件访问URL"`
QiNiuKey string `gorm:"type:varchar(255);not null;index" json:"qiniu_key" comment:"七牛云存储的Key"`
// OCR处理结果 - 记录OCR识别的详细结果
OCRProcessed bool `gorm:"default:false" json:"ocr_processed" comment:"是否已进行OCR处理"`
OCRSuccess bool `gorm:"default:false" json:"ocr_success" comment:"OCR识别是否成功"`
OCRConfidence float64 `gorm:"type:decimal(5,2)" json:"ocr_confidence,omitempty" comment:"OCR识别置信度(0-1)"`
OCRRawData string `gorm:"type:text" json:"ocr_raw_data,omitempty" comment:"OCR原始返回数据(JSON格式)"`
OCRErrorMessage string `gorm:"type:varchar(500)" json:"ocr_error_message,omitempty" comment:"OCR处理错误信息"`
// 时间戳字段
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:"软删除时间"`
// 关联关系
Certification *Certification `gorm:"foreignKey:CertificationID" json:"certification,omitempty" comment:"关联的认证申请"`
}
// TableName 指定数据库表名
func (LicenseUploadRecord) TableName() string {
return "license_upload_records"
}
// IsOCRSuccess 检查OCR是否成功
// 判断OCR处理已完成且识别成功
func (l *LicenseUploadRecord) IsOCRSuccess() bool {
return l.OCRProcessed && l.OCRSuccess
}
// GetFileExtension 获取文件扩展名
// 从原始文件名中提取文件扩展名,用于文件类型判断
func (l *LicenseUploadRecord) GetFileExtension() string {
// 从OriginalFileName提取扩展名的逻辑
// 这里简化处理实际使用时可以用path.Ext()
return l.FileType
}
// IsValidForOCR 检查文件是否适合OCR处理
// 验证文件类型是否支持OCR识别目前支持JPEG、PNG格式
func (l *LicenseUploadRecord) IsValidForOCR() bool {
validTypes := []string{"image/jpeg", "image/png", "image/jpg"}
for _, validType := range validTypes {
if l.FileType == validType {
return true
}
}
return false
}

View File

@@ -0,0 +1,127 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// NotificationRecord 通知记录实体
// 记录系统发送的所有通知信息,包括短信、企业微信、邮件等多种通知渠道
// 支持通知状态跟踪、重试机制、模板化消息等功能,确保通知的可靠送达
type NotificationRecord struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"通知记录唯一标识"`
CertificationID *string `gorm:"type:varchar(36);index" json:"certification_id,omitempty" comment:"关联的认证申请ID(可为空)"`
UserID *string `gorm:"type:varchar(36);index" json:"user_id,omitempty" comment:"接收用户ID(可为空)"`
// 通知类型和渠道 - 定义通知的发送方式和业务场景
NotificationType string `gorm:"type:varchar(50);not null;index" json:"notification_type" comment:"通知类型(SMS/WECHAT_WORK/EMAIL)"`
NotificationScene string `gorm:"type:varchar(50);not null;index" json:"notification_scene" comment:"通知场景(ADMIN_NEW_APPLICATION/USER_CONTRACT_READY等)"`
// 接收方信息 - 通知的目标接收者
Recipient string `gorm:"type:varchar(255);not null" json:"recipient" comment:"接收方标识(手机号/邮箱/用户ID)"`
// 消息内容 - 通知的具体内容信息
Title string `gorm:"type:varchar(255)" json:"title,omitempty" comment:"通知标题"`
Content string `gorm:"type:text;not null" json:"content" comment:"通知内容"`
TemplateID string `gorm:"type:varchar(100)" json:"template_id,omitempty" comment:"消息模板ID"`
TemplateParams string `gorm:"type:text" json:"template_params,omitempty" comment:"模板参数(JSON格式)"`
// 发送状态 - 记录通知的发送过程和结果
Status string `gorm:"type:varchar(50);not null;index" json:"status" comment:"发送状态(PENDING/SENT/FAILED)"`
ErrorMessage string `gorm:"type:varchar(500)" json:"error_message,omitempty" comment:"发送失败的错误信息"`
SentAt *time.Time `json:"sent_at,omitempty" comment:"发送成功时间"`
RetryCount int `gorm:"default:0" json:"retry_count" comment:"当前重试次数"`
MaxRetryCount int `gorm:"default:3" json:"max_retry_count" 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:"软删除时间"`
// 关联关系
Certification *Certification `gorm:"foreignKey:CertificationID" json:"certification,omitempty" comment:"关联的认证申请"`
}
// TableName 指定数据库表名
func (NotificationRecord) TableName() string {
return "notification_records"
}
// IsPending 检查通知是否待发送
// 判断通知是否处于等待发送的状态
func (n *NotificationRecord) IsPending() bool {
return n.Status == "PENDING"
}
// IsSent 检查通知是否已发送
// 判断通知是否已成功发送到接收方
func (n *NotificationRecord) IsSent() bool {
return n.Status == "SENT"
}
// IsFailed 检查通知是否发送失败
// 判断通知是否发送失败,包括网络错误、接收方无效等情况
func (n *NotificationRecord) IsFailed() bool {
return n.Status == "FAILED"
}
// CanRetry 检查是否可以重试
// 判断失败的通知是否还可以进行重试发送
func (n *NotificationRecord) CanRetry() bool {
return n.IsFailed() && n.RetryCount < n.MaxRetryCount
}
// IncrementRetryCount 增加重试次数
// 在重试发送时增加重试计数器
func (n *NotificationRecord) IncrementRetryCount() {
n.RetryCount++
}
// GetStatusName 获取状态的中文名称
// 将英文状态码转换为中文显示名称,用于前端展示
func (n *NotificationRecord) GetStatusName() string {
statusNames := map[string]string{
"PENDING": "待发送",
"SENT": "已发送",
"FAILED": "发送失败",
}
if name, exists := statusNames[n.Status]; exists {
return name
}
return n.Status
}
// GetNotificationTypeName 获取通知类型的中文名称
// 将通知类型转换为中文显示名称,便于用户理解
func (n *NotificationRecord) GetNotificationTypeName() string {
typeNames := map[string]string{
"SMS": "短信",
"WECHAT_WORK": "企业微信",
"EMAIL": "邮件",
}
if name, exists := typeNames[n.NotificationType]; exists {
return name
}
return n.NotificationType
}
// GetNotificationSceneName 获取通知场景的中文名称
// 将通知场景转换为中文显示名称,便于业务人员理解通知的触发原因
func (n *NotificationRecord) GetNotificationSceneName() string {
sceneNames := map[string]string{
"ADMIN_NEW_APPLICATION": "管理员新申请通知",
"USER_CONTRACT_READY": "用户合同就绪通知",
"USER_CERTIFICATION_COMPLETED": "用户认证完成通知",
"USER_FACE_VERIFY_FAILED": "用户人脸识别失败通知",
"USER_CONTRACT_REJECTED": "用户合同被拒绝通知",
}
if name, exists := sceneNames[n.NotificationScene]; exists {
return name
}
return n.NotificationScene
}

View File

@@ -0,0 +1,88 @@
package enums
// CertificationStatus 认证状态枚举
type CertificationStatus string
const (
// 主流程状态
StatusPending CertificationStatus = "pending" // 待开始
StatusInfoSubmitted CertificationStatus = "info_submitted" // 企业信息已提交
StatusFaceVerified CertificationStatus = "face_verified" // 人脸识别完成
StatusContractApplied CertificationStatus = "contract_applied" // 已申请合同
StatusContractPending CertificationStatus = "contract_pending" // 合同待审核
StatusContractApproved CertificationStatus = "contract_approved" // 合同已审核(有链接)
StatusContractSigned CertificationStatus = "contract_signed" // 合同已签署
StatusCompleted CertificationStatus = "completed" // 认证完成
// 失败和重试状态
StatusFaceFailed CertificationStatus = "face_failed" // 人脸识别失败
StatusSignFailed CertificationStatus = "sign_failed" // 签署失败
StatusRejected CertificationStatus = "rejected" // 已拒绝
)
// IsValidStatus 检查状态是否有效
func IsValidStatus(status CertificationStatus) bool {
validStatuses := []CertificationStatus{
StatusPending, StatusInfoSubmitted, StatusFaceVerified,
StatusContractApplied, StatusContractPending, StatusContractApproved,
StatusContractSigned, StatusCompleted, StatusFaceFailed,
StatusSignFailed, StatusRejected,
}
for _, validStatus := range validStatuses {
if status == validStatus {
return true
}
}
return false
}
// GetStatusName 获取状态的中文名称
func GetStatusName(status CertificationStatus) string {
statusNames := map[CertificationStatus]string{
StatusPending: "待开始",
StatusInfoSubmitted: "企业信息已提交",
StatusFaceVerified: "人脸识别完成",
StatusContractApplied: "已申请合同",
StatusContractPending: "合同待审核",
StatusContractApproved: "合同已审核",
StatusContractSigned: "合同已签署",
StatusCompleted: "认证完成",
StatusFaceFailed: "人脸识别失败",
StatusSignFailed: "签署失败",
StatusRejected: "已拒绝",
}
if name, exists := statusNames[status]; exists {
return name
}
return string(status)
}
// IsFinalStatus 判断是否为最终状态
func IsFinalStatus(status CertificationStatus) bool {
finalStatuses := []CertificationStatus{
StatusCompleted, StatusRejected,
}
for _, finalStatus := range finalStatuses {
if status == finalStatus {
return true
}
}
return false
}
// IsFailedStatus 判断是否为失败状态
func IsFailedStatus(status CertificationStatus) bool {
failedStatuses := []CertificationStatus{
StatusFaceFailed, StatusSignFailed, StatusRejected,
}
for _, failedStatus := range failedStatuses {
if status == failedStatus {
return true
}
}
return false
}

View File

@@ -0,0 +1,526 @@
package events
import (
"encoding/json"
"time"
"tyapi-server/internal/domains/certification/entities"
)
// 认证事件类型常量
const (
EventTypeCertificationCreated = "certification.created"
EventTypeCertificationSubmitted = "certification.submitted"
EventTypeLicenseUploaded = "certification.license.uploaded"
EventTypeOCRCompleted = "certification.ocr.completed"
EventTypeEnterpriseInfoConfirmed = "certification.enterprise.confirmed"
EventTypeFaceVerifyInitiated = "certification.face_verify.initiated"
EventTypeFaceVerifyCompleted = "certification.face_verify.completed"
EventTypeContractRequested = "certification.contract.requested"
EventTypeContractGenerated = "certification.contract.generated"
EventTypeContractSigned = "certification.contract.signed"
EventTypeCertificationApproved = "certification.approved"
EventTypeCertificationRejected = "certification.rejected"
EventTypeWalletCreated = "certification.wallet.created"
EventTypeCertificationCompleted = "certification.completed"
EventTypeCertificationFailed = "certification.failed"
)
// BaseCertificationEvent 认证事件基础结构
type BaseCertificationEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Version string `json:"version"`
Timestamp time.Time `json:"timestamp"`
Source string `json:"source"`
AggregateID string `json:"aggregate_id"`
AggregateType string `json:"aggregate_type"`
Metadata map[string]interface{} `json:"metadata"`
Payload interface{} `json:"payload"`
}
// 实现 Event 接口
func (e *BaseCertificationEvent) GetID() string { return e.ID }
func (e *BaseCertificationEvent) GetType() string { return e.Type }
func (e *BaseCertificationEvent) GetVersion() string { return e.Version }
func (e *BaseCertificationEvent) GetTimestamp() time.Time { return e.Timestamp }
func (e *BaseCertificationEvent) GetSource() string { return e.Source }
func (e *BaseCertificationEvent) GetAggregateID() string { return e.AggregateID }
func (e *BaseCertificationEvent) GetAggregateType() string { return e.AggregateType }
func (e *BaseCertificationEvent) GetPayload() interface{} { return e.Payload }
func (e *BaseCertificationEvent) GetMetadata() map[string]interface{} { return e.Metadata }
func (e *BaseCertificationEvent) Marshal() ([]byte, error) { return json.Marshal(e) }
func (e *BaseCertificationEvent) Unmarshal(data []byte) error { return json.Unmarshal(data, e) }
func (e *BaseCertificationEvent) GetDomainVersion() string { return e.Version }
func (e *BaseCertificationEvent) GetCausationID() string { return e.ID }
func (e *BaseCertificationEvent) GetCorrelationID() string { return e.ID }
// NewBaseCertificationEvent 创建基础认证事件
func NewBaseCertificationEvent(eventType, aggregateID string, payload interface{}) *BaseCertificationEvent {
return &BaseCertificationEvent{
ID: generateEventID(),
Type: eventType,
Version: "1.0",
Timestamp: time.Now(),
Source: "certification-domain",
AggregateID: aggregateID,
AggregateType: "certification",
Metadata: make(map[string]interface{}),
Payload: payload,
}
}
// CertificationCreatedEvent 认证创建事件
type CertificationCreatedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationCreatedEvent 创建认证创建事件
func NewCertificationCreatedEvent(certification *entities.Certification) *CertificationCreatedEvent {
event := &CertificationCreatedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationCreated,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// CertificationSubmittedEvent 认证提交事件
type CertificationSubmittedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationSubmittedEvent 创建认证提交事件
func NewCertificationSubmittedEvent(certification *entities.Certification) *CertificationSubmittedEvent {
event := &CertificationSubmittedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationSubmitted,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// LicenseUploadedEvent 营业执照上传事件
type LicenseUploadedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
Status string `json:"status"`
} `json:"data"`
}
// NewLicenseUploadedEvent 创建营业执照上传事件
func NewLicenseUploadedEvent(certification *entities.Certification, record *entities.LicenseUploadRecord) *LicenseUploadedEvent {
event := &LicenseUploadedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeLicenseUploaded,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.FileURL = record.FileURL
event.Data.FileName = record.OriginalFileName
event.Data.FileSize = record.FileSize
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// OCRCompletedEvent OCR识别完成事件
type OCRCompletedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
OCRResult map[string]interface{} `json:"ocr_result"`
Confidence float64 `json:"confidence"`
Status string `json:"status"`
} `json:"data"`
}
// NewOCRCompletedEvent 创建OCR识别完成事件
func NewOCRCompletedEvent(certification *entities.Certification, ocrResult map[string]interface{}, confidence float64) *OCRCompletedEvent {
event := &OCRCompletedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeOCRCompleted,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.OCRResult = ocrResult
event.Data.Confidence = confidence
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// EnterpriseInfoConfirmedEvent 企业信息确认事件
type EnterpriseInfoConfirmedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
EnterpriseInfo map[string]interface{} `json:"enterprise_info"`
Status string `json:"status"`
} `json:"data"`
}
// NewEnterpriseInfoConfirmedEvent 创建企业信息确认事件
func NewEnterpriseInfoConfirmedEvent(certification *entities.Certification, enterpriseInfo map[string]interface{}) *EnterpriseInfoConfirmedEvent {
event := &EnterpriseInfoConfirmedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeEnterpriseInfoConfirmed,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.EnterpriseInfo = enterpriseInfo
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// FaceVerifyInitiatedEvent 人脸识别初始化事件
type FaceVerifyInitiatedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
VerifyToken string `json:"verify_token"`
Status string `json:"status"`
} `json:"data"`
}
// NewFaceVerifyInitiatedEvent 创建人脸识别初始化事件
func NewFaceVerifyInitiatedEvent(certification *entities.Certification, verifyToken string) *FaceVerifyInitiatedEvent {
event := &FaceVerifyInitiatedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeFaceVerifyInitiated,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.VerifyToken = verifyToken
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// FaceVerifyCompletedEvent 人脸识别完成事件
type FaceVerifyCompletedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
VerifyToken string `json:"verify_token"`
Success bool `json:"success"`
Score float64 `json:"score"`
Status string `json:"status"`
} `json:"data"`
}
// NewFaceVerifyCompletedEvent 创建人脸识别完成事件
func NewFaceVerifyCompletedEvent(certification *entities.Certification, record *entities.FaceVerifyRecord) *FaceVerifyCompletedEvent {
event := &FaceVerifyCompletedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeFaceVerifyCompleted,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.VerifyToken = record.CertifyID
event.Data.Success = record.IsSuccess()
event.Data.Score = record.VerifyScore
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// ContractRequestedEvent 合同申请事件
type ContractRequestedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
} `json:"data"`
}
// NewContractRequestedEvent 创建合同申请事件
func NewContractRequestedEvent(certification *entities.Certification) *ContractRequestedEvent {
event := &ContractRequestedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeContractRequested,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// ContractGeneratedEvent 合同生成事件
type ContractGeneratedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
ContractURL string `json:"contract_url"`
ContractID string `json:"contract_id"`
Status string `json:"status"`
} `json:"data"`
}
// NewContractGeneratedEvent 创建合同生成事件
func NewContractGeneratedEvent(certification *entities.Certification, record *entities.ContractRecord) *ContractGeneratedEvent {
event := &ContractGeneratedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeContractGenerated,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.ContractURL = record.ContractURL
event.Data.ContractID = record.ID
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// ContractSignedEvent 合同签署事件
type ContractSignedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
ContractID string `json:"contract_id"`
SignedAt string `json:"signed_at"`
Status string `json:"status"`
} `json:"data"`
}
// NewContractSignedEvent 创建合同签署事件
func NewContractSignedEvent(certification *entities.Certification, record *entities.ContractRecord) *ContractSignedEvent {
event := &ContractSignedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeContractSigned,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.ContractID = record.ID
event.Data.SignedAt = record.SignedAt.Format(time.RFC3339)
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// CertificationApprovedEvent 认证审核通过事件
type CertificationApprovedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
AdminID string `json:"admin_id"`
ApprovedAt string `json:"approved_at"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationApprovedEvent 创建认证审核通过事件
func NewCertificationApprovedEvent(certification *entities.Certification, adminID string) *CertificationApprovedEvent {
event := &CertificationApprovedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationApproved,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.AdminID = adminID
event.Data.ApprovedAt = time.Now().Format(time.RFC3339)
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// CertificationRejectedEvent 认证审核拒绝事件
type CertificationRejectedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
AdminID string `json:"admin_id"`
RejectReason string `json:"reject_reason"`
RejectedAt string `json:"rejected_at"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationRejectedEvent 创建认证审核拒绝事件
func NewCertificationRejectedEvent(certification *entities.Certification, adminID, rejectReason string) *CertificationRejectedEvent {
event := &CertificationRejectedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationRejected,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.AdminID = adminID
event.Data.RejectReason = rejectReason
event.Data.RejectedAt = time.Now().Format(time.RFC3339)
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// WalletCreatedEvent 钱包创建事件
type WalletCreatedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
WalletID string `json:"wallet_id"`
AccessID string `json:"access_id"`
Status string `json:"status"`
} `json:"data"`
}
// NewWalletCreatedEvent 创建钱包创建事件
func NewWalletCreatedEvent(certification *entities.Certification, walletID, accessID string) *WalletCreatedEvent {
event := &WalletCreatedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeWalletCreated,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.WalletID = walletID
event.Data.AccessID = accessID
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// CertificationCompletedEvent 认证完成事件
type CertificationCompletedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
WalletID string `json:"wallet_id"`
CompletedAt string `json:"completed_at"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationCompletedEvent 创建认证完成事件
func NewCertificationCompletedEvent(certification *entities.Certification, walletID string) *CertificationCompletedEvent {
event := &CertificationCompletedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationCompleted,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.WalletID = walletID
event.Data.CompletedAt = time.Now().Format(time.RFC3339)
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// CertificationFailedEvent 认证失败事件
type CertificationFailedEvent struct {
*BaseCertificationEvent
Data struct {
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
FailedAt string `json:"failed_at"`
FailureReason string `json:"failure_reason"`
Status string `json:"status"`
} `json:"data"`
}
// NewCertificationFailedEvent 创建认证失败事件
func NewCertificationFailedEvent(certification *entities.Certification, failureReason string) *CertificationFailedEvent {
event := &CertificationFailedEvent{
BaseCertificationEvent: NewBaseCertificationEvent(
EventTypeCertificationFailed,
certification.ID,
nil,
),
}
event.Data.CertificationID = certification.ID
event.Data.UserID = certification.UserID
event.Data.FailedAt = time.Now().Format(time.RFC3339)
event.Data.FailureReason = failureReason
event.Data.Status = string(certification.Status)
event.Payload = event.Data
return event
}
// generateEventID 生成事件ID
func generateEventID() string {
return time.Now().Format("20060102150405") + "-" + generateRandomString(8)
}
// generateRandomString 生成随机字符串
func generateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(b)
}

View File

@@ -0,0 +1,489 @@
package events
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/notification"
)
// CertificationEventHandler 认证事件处理器
type CertificationEventHandler struct {
logger *zap.Logger
notification notification.WeChatWorkService
name string
eventTypes []string
isAsync bool
}
// NewCertificationEventHandler 创建认证事件处理器
func NewCertificationEventHandler(logger *zap.Logger, notification notification.WeChatWorkService) *CertificationEventHandler {
return &CertificationEventHandler{
logger: logger,
notification: notification,
name: "certification-event-handler",
eventTypes: []string{
EventTypeCertificationCreated,
EventTypeCertificationSubmitted,
EventTypeLicenseUploaded,
EventTypeOCRCompleted,
EventTypeEnterpriseInfoConfirmed,
EventTypeFaceVerifyInitiated,
EventTypeFaceVerifyCompleted,
EventTypeContractRequested,
EventTypeContractGenerated,
EventTypeContractSigned,
EventTypeCertificationApproved,
EventTypeCertificationRejected,
EventTypeWalletCreated,
EventTypeCertificationCompleted,
EventTypeCertificationFailed,
},
isAsync: true,
}
}
// GetName 获取处理器名称
func (h *CertificationEventHandler) GetName() string {
return h.name
}
// GetEventTypes 获取支持的事件类型
func (h *CertificationEventHandler) GetEventTypes() []string {
return h.eventTypes
}
// IsAsync 是否为异步处理器
func (h *CertificationEventHandler) IsAsync() bool {
return h.isAsync
}
// GetRetryConfig 获取重试配置
func (h *CertificationEventHandler) GetRetryConfig() interfaces.RetryConfig {
return interfaces.RetryConfig{
MaxRetries: 3,
RetryDelay: 5 * time.Second,
BackoffFactor: 2.0,
MaxDelay: 30 * time.Second,
}
}
// Handle 处理事件
func (h *CertificationEventHandler) Handle(ctx context.Context, event interfaces.Event) error {
h.logger.Info("处理认证事件",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.String("aggregate_id", event.GetAggregateID()),
)
switch event.GetType() {
case EventTypeCertificationCreated:
return h.handleCertificationCreated(ctx, event)
case EventTypeCertificationSubmitted:
return h.handleCertificationSubmitted(ctx, event)
case EventTypeLicenseUploaded:
return h.handleLicenseUploaded(ctx, event)
case EventTypeOCRCompleted:
return h.handleOCRCompleted(ctx, event)
case EventTypeEnterpriseInfoConfirmed:
return h.handleEnterpriseInfoConfirmed(ctx, event)
case EventTypeFaceVerifyInitiated:
return h.handleFaceVerifyInitiated(ctx, event)
case EventTypeFaceVerifyCompleted:
return h.handleFaceVerifyCompleted(ctx, event)
case EventTypeContractRequested:
return h.handleContractRequested(ctx, event)
case EventTypeContractGenerated:
return h.handleContractGenerated(ctx, event)
case EventTypeContractSigned:
return h.handleContractSigned(ctx, event)
case EventTypeCertificationApproved:
return h.handleCertificationApproved(ctx, event)
case EventTypeCertificationRejected:
return h.handleCertificationRejected(ctx, event)
case EventTypeWalletCreated:
return h.handleWalletCreated(ctx, event)
case EventTypeCertificationCompleted:
return h.handleCertificationCompleted(ctx, event)
case EventTypeCertificationFailed:
return h.handleCertificationFailed(ctx, event)
default:
h.logger.Warn("未知的事件类型", zap.String("event_type", event.GetType()))
return nil
}
}
// handleCertificationCreated 处理认证创建事件
func (h *CertificationEventHandler) handleCertificationCreated(ctx context.Context, event interfaces.Event) error {
h.logger.Info("认证申请已创建",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("🎉 您的企业认证申请已创建成功!\n\n认证ID: %s\n创建时间: %s\n\n请按照指引完成后续认证步骤。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "认证申请创建成功", message)
}
// handleCertificationSubmitted 处理认证提交事件
func (h *CertificationEventHandler) handleCertificationSubmitted(ctx context.Context, event interfaces.Event) error {
h.logger.Info("认证申请已提交",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给管理员
adminMessage := fmt.Sprintf("📋 新的企业认证申请待审核\n\n认证ID: %s\n用户ID: %s\n提交时间: %s\n\n请及时处理审核。",
event.GetAggregateID(),
h.extractUserID(event),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendAdminNotification(ctx, event, "新认证申请待审核", adminMessage)
}
// handleLicenseUploaded 处理营业执照上传事件
func (h *CertificationEventHandler) handleLicenseUploaded(ctx context.Context, event interfaces.Event) error {
h.logger.Info("营业执照已上传",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("📄 营业执照上传成功!\n\n认证ID: %s\n上传时间: %s\n\n系统正在识别营业执照信息请稍候...",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "营业执照上传成功", message)
}
// handleOCRCompleted 处理OCR识别完成事件
func (h *CertificationEventHandler) handleOCRCompleted(ctx context.Context, event interfaces.Event) error {
h.logger.Info("OCR识别已完成",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("✅ OCR识别完成\n\n认证ID: %s\n识别时间: %s\n\n请确认企业信息是否正确如有问题请及时联系客服。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "OCR识别完成", message)
}
// handleEnterpriseInfoConfirmed 处理企业信息确认事件
func (h *CertificationEventHandler) handleEnterpriseInfoConfirmed(ctx context.Context, event interfaces.Event) error {
h.logger.Info("企业信息已确认",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("✅ 企业信息确认成功!\n\n认证ID: %s\n确认时间: %s\n\n下一步请完成人脸识别验证。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "企业信息确认成功", message)
}
// handleFaceVerifyInitiated 处理人脸识别初始化事件
func (h *CertificationEventHandler) handleFaceVerifyInitiated(ctx context.Context, event interfaces.Event) error {
h.logger.Info("人脸识别已初始化",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("👤 人脸识别验证已开始!\n\n认证ID: %s\n开始时间: %s\n\n请按照指引完成人脸识别验证。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "人脸识别验证开始", message)
}
// handleFaceVerifyCompleted 处理人脸识别完成事件
func (h *CertificationEventHandler) handleFaceVerifyCompleted(ctx context.Context, event interfaces.Event) error {
h.logger.Info("人脸识别已完成",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("✅ 人脸识别验证完成!\n\n认证ID: %s\n完成时间: %s\n\n下一步系统将为您申请电子合同。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "人脸识别验证完成", message)
}
// handleContractRequested 处理合同申请事件
func (h *CertificationEventHandler) handleContractRequested(ctx context.Context, event interfaces.Event) error {
h.logger.Info("电子合同申请已提交",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给管理员
adminMessage := fmt.Sprintf("📋 新的电子合同申请待审核\n\n认证ID: %s\n用户ID: %s\n申请时间: %s\n\n请及时处理合同审核。",
event.GetAggregateID(),
h.extractUserID(event),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendAdminNotification(ctx, event, "新合同申请待审核", adminMessage)
}
// handleContractGenerated 处理合同生成事件
func (h *CertificationEventHandler) handleContractGenerated(ctx context.Context, event interfaces.Event) error {
h.logger.Info("电子合同已生成",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("📄 电子合同已生成!\n\n认证ID: %s\n生成时间: %s\n\n请及时签署电子合同以完成认证流程。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "电子合同已生成", message)
}
// handleContractSigned 处理合同签署事件
func (h *CertificationEventHandler) handleContractSigned(ctx context.Context, event interfaces.Event) error {
h.logger.Info("电子合同已签署",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("✅ 电子合同签署成功!\n\n认证ID: %s\n签署时间: %s\n\n您的企业认证申请已进入最终审核阶段。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "电子合同签署成功", message)
}
// handleCertificationApproved 处理认证审核通过事件
func (h *CertificationEventHandler) handleCertificationApproved(ctx context.Context, event interfaces.Event) error {
h.logger.Info("认证申请已审核通过",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("🎉 恭喜!您的企业认证申请已审核通过!\n\n认证ID: %s\n审核时间: %s\n\n系统正在为您创建钱包和访问密钥...",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "认证申请审核通过", message)
}
// handleCertificationRejected 处理认证审核拒绝事件
func (h *CertificationEventHandler) handleCertificationRejected(ctx context.Context, event interfaces.Event) error {
h.logger.Info("认证申请已被拒绝",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("❌ 很抱歉,您的企业认证申请未通过审核\n\n认证ID: %s\n拒绝时间: %s\n\n请根据拒绝原因修改后重新提交申请。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "认证申请审核未通过", message)
}
// handleWalletCreated 处理钱包创建事件
func (h *CertificationEventHandler) handleWalletCreated(ctx context.Context, event interfaces.Event) error {
h.logger.Info("钱包已创建",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("💰 钱包创建成功!\n\n认证ID: %s\n创建时间: %s\n\n您的企业钱包已激活可以开始使用相关服务。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "钱包创建成功", message)
}
// handleCertificationCompleted 处理认证完成事件
func (h *CertificationEventHandler) handleCertificationCompleted(ctx context.Context, event interfaces.Event) error {
h.logger.Info("企业认证已完成",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("🎉 恭喜!您的企业认证已全部完成!\n\n认证ID: %s\n完成时间: %s\n\n您现在可以享受完整的企业级服务功能。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "企业认证完成", message)
}
// handleCertificationFailed 处理认证失败事件
func (h *CertificationEventHandler) handleCertificationFailed(ctx context.Context, event interfaces.Event) error {
h.logger.Error("企业认证失败",
zap.String("certification_id", event.GetAggregateID()),
zap.String("user_id", h.extractUserID(event)),
)
// 发送通知给用户
message := fmt.Sprintf("❌ 企业认证流程遇到问题\n\n认证ID: %s\n失败时间: %s\n\n请联系客服获取帮助。",
event.GetAggregateID(),
event.GetTimestamp().Format("2006-01-02 15:04:05"))
return h.sendUserNotification(ctx, event, "企业认证失败", message)
}
// sendUserNotification 发送用户通知
func (h *CertificationEventHandler) sendUserNotification(ctx context.Context, event interfaces.Event, title, message string) error {
url := fmt.Sprintf("https://example.com/certification/%s", event.GetAggregateID())
btnText := "查看详情"
if err := h.notification.SendCardMessage(ctx, title, message, url, btnText); err != nil {
h.logger.Error("发送用户通知失败",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.Error(err),
)
return err
}
h.logger.Info("用户通知发送成功",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
return nil
}
// sendAdminNotification 发送管理员通知
func (h *CertificationEventHandler) sendAdminNotification(ctx context.Context, event interfaces.Event, title, message string) error {
url := fmt.Sprintf("https://admin.example.com/certification/%s", event.GetAggregateID())
btnText := "立即处理"
if err := h.notification.SendCardMessage(ctx, title, message, url, btnText); err != nil {
h.logger.Error("发送管理员通知失败",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.Error(err),
)
return err
}
h.logger.Info("管理员通知发送成功",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
return nil
}
// extractUserID 从事件中提取用户ID
func (h *CertificationEventHandler) extractUserID(event interfaces.Event) string {
if payload, ok := event.GetPayload().(map[string]interface{}); ok {
if userID, exists := payload["user_id"]; exists {
if id, ok := userID.(string); ok {
return id
}
}
}
// 尝试从事件数据中提取
if eventData, ok := event.(*BaseCertificationEvent); ok {
if data, ok := eventData.Payload.(map[string]interface{}); ok {
if userID, exists := data["user_id"]; exists {
if id, ok := userID.(string); ok {
return id
}
}
}
}
return "unknown"
}
// LoggingEventHandler 日志记录事件处理器
type LoggingEventHandler struct {
logger *zap.Logger
name string
eventTypes []string
isAsync bool
}
// NewLoggingEventHandler 创建日志记录事件处理器
func NewLoggingEventHandler(logger *zap.Logger) *LoggingEventHandler {
return &LoggingEventHandler{
logger: logger,
name: "logging-event-handler",
eventTypes: []string{
EventTypeCertificationCreated,
EventTypeCertificationSubmitted,
EventTypeLicenseUploaded,
EventTypeOCRCompleted,
EventTypeEnterpriseInfoConfirmed,
EventTypeFaceVerifyInitiated,
EventTypeFaceVerifyCompleted,
EventTypeContractRequested,
EventTypeContractGenerated,
EventTypeContractSigned,
EventTypeCertificationApproved,
EventTypeCertificationRejected,
EventTypeWalletCreated,
EventTypeCertificationCompleted,
EventTypeCertificationFailed,
},
isAsync: false, // 同步处理,确保日志及时记录
}
}
// GetName 获取处理器名称
func (l *LoggingEventHandler) GetName() string {
return l.name
}
// GetEventTypes 获取支持的事件类型
func (l *LoggingEventHandler) GetEventTypes() []string {
return l.eventTypes
}
// IsAsync 是否为异步处理器
func (l *LoggingEventHandler) IsAsync() bool {
return l.isAsync
}
// GetRetryConfig 获取重试配置
func (l *LoggingEventHandler) GetRetryConfig() interfaces.RetryConfig {
return interfaces.RetryConfig{
MaxRetries: 1,
RetryDelay: 1 * time.Second,
BackoffFactor: 1.0,
MaxDelay: 1 * time.Second,
}
}
// Handle 处理事件
func (l *LoggingEventHandler) Handle(ctx context.Context, event interfaces.Event) error {
// 记录结构化日志
eventData, _ := json.Marshal(event.GetPayload())
l.logger.Info("认证事件记录",
zap.String("event_id", event.GetID()),
zap.String("event_type", event.GetType()),
zap.String("aggregate_id", event.GetAggregateID()),
zap.String("aggregate_type", event.GetAggregateType()),
zap.Time("timestamp", event.GetTimestamp()),
zap.String("source", event.GetSource()),
zap.String("payload", string(eventData)),
)
return nil
}

View File

@@ -0,0 +1,536 @@
package handlers
import (
"io"
"path/filepath"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/domains/certification/dto"
"tyapi-server/internal/domains/certification/services"
"tyapi-server/internal/shared/interfaces"
)
// CertificationHandler 认证处理器
type CertificationHandler struct {
certificationService *services.CertificationService
response interfaces.ResponseBuilder
logger *zap.Logger
}
// NewCertificationHandler 创建认证处理器
func NewCertificationHandler(
certificationService *services.CertificationService,
response interfaces.ResponseBuilder,
logger *zap.Logger,
) *CertificationHandler {
return &CertificationHandler{
certificationService: certificationService,
response: response,
logger: logger,
}
}
// CreateCertification 创建认证申请
// @Summary 创建认证申请
// @Description 用户创建企业认证申请
// @Tags 认证
// @Accept json
// @Produce json
// @Success 200 {object} dto.CertificationCreateResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/create [post]
func (h *CertificationHandler) CreateCertification(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
result, err := h.certificationService.CreateCertification(c.Request.Context(), userID)
if err != nil {
h.logger.Error("创建认证申请失败",
zap.String("user_id", userID),
zap.Error(err),
)
h.response.InternalError(c, "创建认证申请失败")
return
}
h.response.Success(c, result, "认证申请创建成功")
}
// UploadLicense 上传营业执照
// @Summary 上传营业执照
// @Description 上传营业执照文件并进行OCR识别
// @Tags 认证
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "营业执照文件"
// @Success 200 {object} dto.UploadLicenseResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/upload-license [post]
func (h *CertificationHandler) UploadLicense(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
h.logger.Error("获取上传文件失败", zap.Error(err))
h.response.BadRequest(c, "请选择要上传的文件")
return
}
defer file.Close()
// 检查文件类型
fileName := header.Filename
ext := strings.ToLower(filepath.Ext(fileName))
allowedExts := []string{".jpg", ".jpeg", ".png", ".pdf"}
isAllowed := false
for _, allowedExt := range allowedExts {
if ext == allowedExt {
isAllowed = true
break
}
}
if !isAllowed {
h.response.BadRequest(c, "文件格式不支持,仅支持 JPG、PNG、PDF 格式")
return
}
// 检查文件大小限制为10MB
const maxFileSize = 10 * 1024 * 1024 // 10MB
if header.Size > maxFileSize {
h.response.BadRequest(c, "文件大小不能超过10MB")
return
}
// 读取文件内容
fileBytes, err := io.ReadAll(file)
if err != nil {
h.logger.Error("读取文件内容失败", zap.Error(err))
h.response.InternalError(c, "文件读取失败")
return
}
// 调用服务上传文件
result, err := h.certificationService.UploadLicense(c.Request.Context(), userID, fileBytes, fileName)
if err != nil {
h.logger.Error("上传营业执照失败",
zap.String("user_id", userID),
zap.String("file_name", fileName),
zap.Error(err),
)
h.response.InternalError(c, "上传失败,请稍后重试")
return
}
h.response.Success(c, result, "营业执照上传成功")
}
// SubmitEnterpriseInfo 提交企业信息
// @Summary 提交企业信息
// @Description 确认并提交企业四要素信息
// @Tags 认证
// @Accept json
// @Produce json
// @Param id path string true "认证申请ID"
// @Param request body dto.SubmitEnterpriseInfoRequest true "企业信息"
// @Success 200 {object} dto.SubmitEnterpriseInfoResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/{id}/submit-info [put]
func (h *CertificationHandler) SubmitEnterpriseInfo(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
certificationID := c.Param("id")
if certificationID == "" {
h.response.BadRequest(c, "认证申请ID不能为空")
return
}
var req dto.SubmitEnterpriseInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.BadRequest(c, "请求参数格式错误")
return
}
// 验证企业信息
if req.CompanyName == "" {
h.response.BadRequest(c, "企业名称不能为空")
return
}
if req.UnifiedSocialCode == "" {
h.response.BadRequest(c, "统一社会信用代码不能为空")
return
}
if req.LegalPersonName == "" {
h.response.BadRequest(c, "法定代表人姓名不能为空")
return
}
if req.LegalPersonID == "" {
h.response.BadRequest(c, "法定代表人身份证号不能为空")
return
}
if req.LicenseUploadRecordID == "" {
h.response.BadRequest(c, "营业执照上传记录ID不能为空")
return
}
result, err := h.certificationService.SubmitEnterpriseInfo(c.Request.Context(), certificationID, &req)
if err != nil {
h.logger.Error("提交企业信息失败",
zap.String("certification_id", certificationID),
zap.String("user_id", userID),
zap.Error(err),
)
if strings.Contains(err.Error(), "已被使用") || strings.Contains(err.Error(), "不允许") {
h.response.BadRequest(c, err.Error())
} else {
h.response.InternalError(c, "提交失败,请稍后重试")
}
return
}
h.response.Success(c, result, "企业信息提交成功")
}
// InitiateFaceVerify 初始化人脸识别
// @Summary 初始化人脸识别
// @Description 开始人脸识别认证流程
// @Tags 认证
// @Accept json
// @Produce json
// @Param id path string true "认证申请ID"
// @Param request body dto.FaceVerifyRequest true "人脸识别请求"
// @Success 200 {object} dto.FaceVerifyResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/{id}/face-verify [post]
func (h *CertificationHandler) InitiateFaceVerify(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
certificationID := c.Param("id")
if certificationID == "" {
h.response.BadRequest(c, "认证申请ID不能为空")
return
}
var req dto.FaceVerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("参数绑定失败", zap.Error(err))
h.response.BadRequest(c, "请求参数格式错误")
return
}
// 验证请求参数
if req.RealName == "" {
h.response.BadRequest(c, "真实姓名不能为空")
return
}
if req.IDCardNumber == "" {
h.response.BadRequest(c, "身份证号不能为空")
return
}
result, err := h.certificationService.InitiateFaceVerify(c.Request.Context(), certificationID, &req)
if err != nil {
h.logger.Error("初始化人脸识别失败",
zap.String("certification_id", certificationID),
zap.String("user_id", userID),
zap.Error(err),
)
if strings.Contains(err.Error(), "不允许") {
h.response.BadRequest(c, err.Error())
} else {
h.response.InternalError(c, "初始化失败,请稍后重试")
}
return
}
h.response.Success(c, result, "人脸识别初始化成功")
}
// ApplyContract 申请电子合同
// @Summary 申请电子合同
// @Description 申请生成企业认证电子合同
// @Tags 认证
// @Accept json
// @Produce json
// @Param id path string true "认证申请ID"
// @Success 200 {object} dto.ApplyContractResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/{id}/apply-contract [post]
func (h *CertificationHandler) ApplyContract(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
certificationID := c.Param("id")
if certificationID == "" {
h.response.BadRequest(c, "认证申请ID不能为空")
return
}
result, err := h.certificationService.ApplyContract(c.Request.Context(), certificationID)
if err != nil {
h.logger.Error("申请电子合同失败",
zap.String("certification_id", certificationID),
zap.String("user_id", userID),
zap.Error(err),
)
if strings.Contains(err.Error(), "不允许") {
h.response.BadRequest(c, err.Error())
} else {
h.response.InternalError(c, "申请失败,请稍后重试")
}
return
}
h.response.Success(c, result, "合同申请提交成功,请等待管理员审核")
}
// GetCertificationStatus 获取认证状态
// @Summary 获取认证状态
// @Description 查询当前用户的认证申请状态和进度
// @Tags 认证
// @Accept json
// @Produce json
// @Success 200 {object} dto.CertificationStatusResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/status [get]
func (h *CertificationHandler) GetCertificationStatus(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
result, err := h.certificationService.GetCertificationStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("获取认证状态失败",
zap.String("user_id", userID),
zap.Error(err),
)
if strings.Contains(err.Error(), "不存在") {
h.response.NotFound(c, "未找到认证申请记录")
} else {
h.response.InternalError(c, "查询失败,请稍后重试")
}
return
}
h.response.Success(c, result, "查询成功")
}
// GetCertificationDetails 获取认证详情
// @Summary 获取认证详情
// @Description 获取指定认证申请的详细信息
// @Tags 认证
// @Accept json
// @Produce json
// @Param id path string true "认证申请ID"
// @Success 200 {object} dto.CertificationStatusResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/{id} [get]
func (h *CertificationHandler) GetCertificationDetails(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
certificationID := c.Param("id")
if certificationID == "" {
h.response.BadRequest(c, "认证申请ID不能为空")
return
}
// 通过用户ID获取状态来确保用户只能查看自己的认证记录
result, err := h.certificationService.GetCertificationStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("获取认证详情失败",
zap.String("certification_id", certificationID),
zap.String("user_id", userID),
zap.Error(err),
)
if strings.Contains(err.Error(), "不存在") {
h.response.NotFound(c, "未找到认证申请记录")
} else {
h.response.InternalError(c, "查询失败,请稍后重试")
}
return
}
// 检查是否是用户自己的认证记录
if result.ID != certificationID {
h.response.Forbidden(c, "无权访问此认证记录")
return
}
h.response.Success(c, result, "查询成功")
}
// RetryStep 重试认证步骤
// @Summary 重试认证步骤
// @Description 重试失败的认证步骤(如人脸识别失败、签署失败等)
// @Tags 认证
// @Accept json
// @Produce json
// @Param id path string true "认证申请ID"
// @Param step query string true "重试步骤face_verify, sign_contract"
// @Success 200 {object} interfaces.APIResponse
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/{id}/retry [post]
func (h *CertificationHandler) RetryStep(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
certificationID := c.Param("id")
if certificationID == "" {
h.response.BadRequest(c, "认证申请ID不能为空")
return
}
step := c.Query("step")
if step == "" {
h.response.BadRequest(c, "重试步骤不能为空")
return
}
// TODO: 实现重试逻辑
// 这里需要根据不同的步骤调用状态机进行状态重置
h.logger.Info("重试认证步骤",
zap.String("certification_id", certificationID),
zap.String("user_id", userID),
zap.String("step", step),
)
h.response.Success(c, gin.H{
"certification_id": certificationID,
"step": step,
"message": "重试操作已提交",
}, "重试操作成功")
}
// GetProgressStats 获取进度统计
// @Summary 获取进度统计
// @Description 获取用户认证申请的进度统计信息
// @Tags 认证
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} interfaces.APIResponse
// @Failure 500 {object} interfaces.APIResponse
// @Router /api/v1/certification/progress [get]
func (h *CertificationHandler) GetProgressStats(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.response.Unauthorized(c, "用户未认证")
return
}
// 获取认证状态
status, err := h.certificationService.GetCertificationStatus(c.Request.Context(), userID)
if err != nil {
if strings.Contains(err.Error(), "不存在") {
h.response.Success(c, gin.H{
"has_certification": false,
"progress": 0,
"status": "",
"next_steps": []string{"开始企业认证"},
}, "查询成功")
return
}
h.response.InternalError(c, "查询失败")
return
}
// 构建进度统计
nextSteps := []string{}
if status.IsUserActionRequired {
switch status.Status {
case "pending":
nextSteps = append(nextSteps, "上传营业执照")
case "info_submitted":
nextSteps = append(nextSteps, "进行人脸识别")
case "face_verified":
nextSteps = append(nextSteps, "申请电子合同")
case "contract_approved":
nextSteps = append(nextSteps, "签署电子合同")
case "face_failed":
nextSteps = append(nextSteps, "重新进行人脸识别")
case "sign_failed":
nextSteps = append(nextSteps, "重新签署合同")
}
} else if status.IsAdminActionRequired {
nextSteps = append(nextSteps, "等待管理员审核")
} else {
nextSteps = append(nextSteps, "认证流程已完成")
}
result := gin.H{
"has_certification": true,
"certification_id": status.ID,
"progress": status.Progress,
"status": status.Status,
"status_name": status.StatusName,
"is_user_action_required": status.IsUserActionRequired,
"is_admin_action_required": status.IsAdminActionRequired,
"next_steps": nextSteps,
"created_at": status.CreatedAt,
"updated_at": status.UpdatedAt,
}
h.response.Success(c, result, "查询成功")
}
// parsePageParams 解析分页参数
func (h *CertificationHandler) parsePageParams(c *gin.Context) (int, int) {
page := 1
pageSize := 20
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
if sizeStr := c.Query("page_size"); sizeStr != "" {
if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 {
pageSize = s
}
}
return page, pageSize
}

View File

@@ -0,0 +1,223 @@
package repositories
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
)
// GormCertificationRepository GORM认证仓储实现
type GormCertificationRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormCertificationRepository 创建GORM认证仓储
func NewGormCertificationRepository(db *gorm.DB, logger *zap.Logger) CertificationRepository {
return &GormCertificationRepository{
db: db,
logger: logger,
}
}
// Create 创建认证记录
func (r *GormCertificationRepository) Create(ctx context.Context, cert *entities.Certification) error {
if err := r.db.WithContext(ctx).Create(cert).Error; err != nil {
r.logger.Error("创建认证记录失败",
zap.String("user_id", cert.UserID),
zap.Error(err),
)
return fmt.Errorf("创建认证记录失败: %w", err)
}
r.logger.Info("认证记录创建成功",
zap.String("id", cert.ID),
zap.String("user_id", cert.UserID),
zap.String("status", string(cert.Status)),
)
return nil
}
// GetByID 根据ID获取认证记录
func (r *GormCertificationRepository) GetByID(ctx context.Context, id string) (*entities.Certification, error) {
var cert entities.Certification
if err := r.db.WithContext(ctx).First(&cert, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("认证记录不存在")
}
r.logger.Error("获取认证记录失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
return &cert, nil
}
// GetByUserID 根据用户ID获取认证记录
func (r *GormCertificationRepository) GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) {
var cert entities.Certification
if err := r.db.WithContext(ctx).First(&cert, "user_id = ?", userID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("用户认证记录不存在")
}
r.logger.Error("获取用户认证记录失败",
zap.String("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("获取用户认证记录失败: %w", err)
}
return &cert, nil
}
// Update 更新认证记录
func (r *GormCertificationRepository) Update(ctx context.Context, cert *entities.Certification) error {
if err := r.db.WithContext(ctx).Save(cert).Error; err != nil {
r.logger.Error("更新认证记录失败",
zap.String("id", cert.ID),
zap.Error(err),
)
return fmt.Errorf("更新认证记录失败: %w", err)
}
r.logger.Info("认证记录更新成功",
zap.String("id", cert.ID),
zap.String("status", string(cert.Status)),
)
return nil
}
// Delete 删除认证记录(软删除)
func (r *GormCertificationRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.Certification{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除认证记录失败",
zap.String("id", id),
zap.Error(err),
)
return fmt.Errorf("删除认证记录失败: %w", err)
}
r.logger.Info("认证记录删除成功", zap.String("id", id))
return nil
}
// List 获取认证记录列表
func (r *GormCertificationRepository) List(ctx context.Context, page, pageSize int, status enums.CertificationStatus) ([]*entities.Certification, int, error) {
var certs []*entities.Certification
var total int64
query := r.db.WithContext(ctx).Model(&entities.Certification{})
// 如果指定了状态,添加状态过滤
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
r.logger.Error("获取认证记录总数失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取认证记录总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&certs).Error; err != nil {
r.logger.Error("获取认证记录列表失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取认证记录列表失败: %w", err)
}
return certs, int(total), nil
}
// GetByStatus 根据状态获取认证记录
func (r *GormCertificationRepository) GetByStatus(ctx context.Context, status enums.CertificationStatus, page, pageSize int) ([]*entities.Certification, int, error) {
return r.List(ctx, page, pageSize, status)
}
// GetPendingApprovals 获取待审核的认证申请
func (r *GormCertificationRepository) GetPendingApprovals(ctx context.Context, page, pageSize int) ([]*entities.Certification, int, error) {
return r.GetByStatus(ctx, enums.StatusContractPending, page, pageSize)
}
// GetWithEnterprise 获取包含企业信息的认证记录
func (r *GormCertificationRepository) GetWithEnterprise(ctx context.Context, id string) (*entities.Certification, error) {
var cert entities.Certification
if err := r.db.WithContext(ctx).Preload("Enterprise").First(&cert, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("认证记录不存在")
}
r.logger.Error("获取认证记录(含企业信息)失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
return &cert, nil
}
// GetWithAllRelations 获取包含所有关联关系的认证记录
func (r *GormCertificationRepository) GetWithAllRelations(ctx context.Context, id string) (*entities.Certification, error) {
var cert entities.Certification
if err := r.db.WithContext(ctx).
Preload("Enterprise").
Preload("LicenseUploadRecord").
Preload("FaceVerifyRecords").
Preload("ContractRecords").
Preload("NotificationRecords").
First(&cert, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("认证记录不存在")
}
r.logger.Error("获取认证记录(含所有关联)失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
return &cert, nil
}
// CountByStatus 根据状态统计认证记录数量
func (r *GormCertificationRepository) CountByStatus(ctx context.Context, status enums.CertificationStatus) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&entities.Certification{}).Where("status = ?", status).Count(&count).Error; err != nil {
r.logger.Error("统计认证记录数量失败",
zap.String("status", string(status)),
zap.Error(err),
)
return 0, fmt.Errorf("统计认证记录数量失败: %w", err)
}
return count, nil
}
// CountByUserID 根据用户ID统计认证记录数量
func (r *GormCertificationRepository) CountByUserID(ctx context.Context, userID string) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&entities.Certification{}).Where("user_id = ?", userID).Count(&count).Error; err != nil {
r.logger.Error("统计用户认证记录数量失败",
zap.String("user_id", userID),
zap.Error(err),
)
return 0, fmt.Errorf("统计用户认证记录数量失败: %w", err)
}
return count, nil
}

View File

@@ -0,0 +1,175 @@
package repositories
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/certification/entities"
)
// GormContractRecordRepository GORM合同记录仓储实现
type GormContractRecordRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormContractRecordRepository 创建GORM合同记录仓储
func NewGormContractRecordRepository(db *gorm.DB, logger *zap.Logger) ContractRecordRepository {
return &GormContractRecordRepository{
db: db,
logger: logger,
}
}
// Create 创建合同记录
func (r *GormContractRecordRepository) Create(ctx context.Context, record *entities.ContractRecord) error {
if err := r.db.WithContext(ctx).Create(record).Error; err != nil {
r.logger.Error("创建合同记录失败",
zap.String("certification_id", record.CertificationID),
zap.String("contract_type", record.ContractType),
zap.Error(err),
)
return fmt.Errorf("创建合同记录失败: %w", err)
}
r.logger.Info("合同记录创建成功",
zap.String("id", record.ID),
zap.String("contract_type", record.ContractType),
)
return nil
}
// GetByID 根据ID获取合同记录
func (r *GormContractRecordRepository) GetByID(ctx context.Context, id string) (*entities.ContractRecord, error) {
var record entities.ContractRecord
if err := r.db.WithContext(ctx).First(&record, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("合同记录不存在")
}
r.logger.Error("获取合同记录失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取合同记录失败: %w", err)
}
return &record, nil
}
// GetByCertificationID 根据认证申请ID获取合同记录列表
func (r *GormContractRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.ContractRecord, error) {
var records []*entities.ContractRecord
if err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("根据认证申请ID获取合同记录失败",
zap.String("certification_id", certificationID),
zap.Error(err),
)
return nil, fmt.Errorf("获取合同记录失败: %w", err)
}
return records, nil
}
// Update 更新合同记录
func (r *GormContractRecordRepository) Update(ctx context.Context, record *entities.ContractRecord) error {
if err := r.db.WithContext(ctx).Save(record).Error; err != nil {
r.logger.Error("更新合同记录失败",
zap.String("id", record.ID),
zap.Error(err),
)
return fmt.Errorf("更新合同记录失败: %w", err)
}
return nil
}
// Delete 删除合同记录
func (r *GormContractRecordRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.ContractRecord{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除合同记录失败",
zap.String("id", id),
zap.Error(err),
)
return fmt.Errorf("删除合同记录失败: %w", err)
}
return nil
}
// GetByUserID 根据用户ID获取合同记录列表
func (r *GormContractRecordRepository) GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.ContractRecord, int, error) {
var records []*entities.ContractRecord
var total int64
query := r.db.WithContext(ctx).Model(&entities.ContractRecord{}).Where("user_id = ?", userID)
// 获取总数
if err := query.Count(&total).Error; err != nil {
r.logger.Error("获取用户合同记录总数失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取合同记录总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("获取用户合同记录列表失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取合同记录列表失败: %w", err)
}
return records, int(total), nil
}
// GetByStatus 根据状态获取合同记录列表
func (r *GormContractRecordRepository) GetByStatus(ctx context.Context, status string, page, pageSize int) ([]*entities.ContractRecord, int, error) {
var records []*entities.ContractRecord
var total int64
query := r.db.WithContext(ctx).Model(&entities.ContractRecord{}).Where("status = ?", status)
// 获取总数
if err := query.Count(&total).Error; err != nil {
r.logger.Error("根据状态获取合同记录总数失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取合同记录总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("根据状态获取合同记录列表失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取合同记录列表失败: %w", err)
}
return records, int(total), nil
}
// GetPendingContracts 获取待审核的合同记录
func (r *GormContractRecordRepository) GetPendingContracts(ctx context.Context, page, pageSize int) ([]*entities.ContractRecord, int, error) {
return r.GetByStatus(ctx, "PENDING", page, pageSize)
}
// GetExpiredSigningContracts 获取签署链接已过期的合同记录
func (r *GormContractRecordRepository) GetExpiredSigningContracts(ctx context.Context, limit int) ([]*entities.ContractRecord, error) {
var records []*entities.ContractRecord
if err := r.db.WithContext(ctx).
Where("expires_at < NOW() AND status = ?", "APPROVED").
Limit(limit).
Order("expires_at ASC").
Find(&records).Error; err != nil {
r.logger.Error("获取过期签署合同记录失败", zap.Error(err))
return nil, fmt.Errorf("获取过期签署合同记录失败: %w", err)
}
return records, nil
}
// GetExpiredContracts 获取已过期的合同记录(通用方法)
func (r *GormContractRecordRepository) GetExpiredContracts(ctx context.Context, limit int) ([]*entities.ContractRecord, error) {
return r.GetExpiredSigningContracts(ctx, limit)
}

View File

@@ -0,0 +1,148 @@
package repositories
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/certification/entities"
)
// GormEnterpriseRepository GORM企业信息仓储实现
type GormEnterpriseRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormEnterpriseRepository 创建GORM企业信息仓储
func NewGormEnterpriseRepository(db *gorm.DB, logger *zap.Logger) EnterpriseRepository {
return &GormEnterpriseRepository{
db: db,
logger: logger,
}
}
// Create 创建企业信息
func (r *GormEnterpriseRepository) Create(ctx context.Context, enterprise *entities.Enterprise) error {
if err := r.db.WithContext(ctx).Create(enterprise).Error; err != nil {
r.logger.Error("创建企业信息失败",
zap.String("certification_id", enterprise.CertificationID),
zap.String("company_name", enterprise.CompanyName),
zap.Error(err),
)
return fmt.Errorf("创建企业信息失败: %w", err)
}
r.logger.Info("企业信息创建成功",
zap.String("id", enterprise.ID),
zap.String("company_name", enterprise.CompanyName),
zap.String("unified_social_code", enterprise.UnifiedSocialCode),
)
return nil
}
// GetByID 根据ID获取企业信息
func (r *GormEnterpriseRepository) GetByID(ctx context.Context, id string) (*entities.Enterprise, error) {
var enterprise entities.Enterprise
if err := r.db.WithContext(ctx).First(&enterprise, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("企业信息不存在")
}
r.logger.Error("获取企业信息失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取企业信息失败: %w", err)
}
return &enterprise, nil
}
// GetByCertificationID 根据认证ID获取企业信息
func (r *GormEnterpriseRepository) GetByCertificationID(ctx context.Context, certificationID string) (*entities.Enterprise, error) {
var enterprise entities.Enterprise
if err := r.db.WithContext(ctx).First(&enterprise, "certification_id = ?", certificationID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("企业信息不存在")
}
r.logger.Error("根据认证ID获取企业信息失败",
zap.String("certification_id", certificationID),
zap.Error(err),
)
return nil, fmt.Errorf("获取企业信息失败: %w", err)
}
return &enterprise, nil
}
// Update 更新企业信息
func (r *GormEnterpriseRepository) Update(ctx context.Context, enterprise *entities.Enterprise) error {
if err := r.db.WithContext(ctx).Save(enterprise).Error; err != nil {
r.logger.Error("更新企业信息失败",
zap.String("id", enterprise.ID),
zap.String("company_name", enterprise.CompanyName),
zap.Error(err),
)
return fmt.Errorf("更新企业信息失败: %w", err)
}
r.logger.Info("企业信息更新成功",
zap.String("id", enterprise.ID),
zap.String("company_name", enterprise.CompanyName),
)
return nil
}
// Delete 删除企业信息(软删除)
func (r *GormEnterpriseRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.Enterprise{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除企业信息失败",
zap.String("id", id),
zap.Error(err),
)
return fmt.Errorf("删除企业信息失败: %w", err)
}
r.logger.Info("企业信息删除成功", zap.String("id", id))
return nil
}
// GetByUnifiedSocialCode 根据统一社会信用代码获取企业信息
func (r *GormEnterpriseRepository) GetByUnifiedSocialCode(ctx context.Context, code string) (*entities.Enterprise, error) {
var enterprise entities.Enterprise
if err := r.db.WithContext(ctx).First(&enterprise, "unified_social_code = ?", code).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("企业信息不存在")
}
r.logger.Error("根据统一社会信用代码获取企业信息失败",
zap.String("unified_social_code", code),
zap.Error(err),
)
return nil, fmt.Errorf("获取企业信息失败: %w", err)
}
return &enterprise, nil
}
// ExistsByUnifiedSocialCode 检查统一社会信用代码是否已存在
func (r *GormEnterpriseRepository) ExistsByUnifiedSocialCode(ctx context.Context, code string) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&entities.Enterprise{}).
Where("unified_social_code = ?", code).Count(&count).Error; err != nil {
r.logger.Error("检查统一社会信用代码是否存在失败",
zap.String("unified_social_code", code),
zap.Error(err),
)
return false, fmt.Errorf("检查统一社会信用代码失败: %w", err)
}
return count > 0, nil
}

View File

@@ -0,0 +1,160 @@
package repositories
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/certification/entities"
)
// GormFaceVerifyRecordRepository GORM人脸识别记录仓储实现
type GormFaceVerifyRecordRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormFaceVerifyRecordRepository 创建GORM人脸识别记录仓储
func NewGormFaceVerifyRecordRepository(db *gorm.DB, logger *zap.Logger) FaceVerifyRecordRepository {
return &GormFaceVerifyRecordRepository{
db: db,
logger: logger,
}
}
// Create 创建人脸识别记录
func (r *GormFaceVerifyRecordRepository) Create(ctx context.Context, record *entities.FaceVerifyRecord) error {
if err := r.db.WithContext(ctx).Create(record).Error; err != nil {
r.logger.Error("创建人脸识别记录失败",
zap.String("certification_id", record.CertificationID),
zap.String("certify_id", record.CertifyID),
zap.Error(err),
)
return fmt.Errorf("创建人脸识别记录失败: %w", err)
}
r.logger.Info("人脸识别记录创建成功",
zap.String("id", record.ID),
zap.String("certify_id", record.CertifyID),
)
return nil
}
// GetByID 根据ID获取人脸识别记录
func (r *GormFaceVerifyRecordRepository) GetByID(ctx context.Context, id string) (*entities.FaceVerifyRecord, error) {
var record entities.FaceVerifyRecord
if err := r.db.WithContext(ctx).First(&record, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("人脸识别记录不存在")
}
r.logger.Error("获取人脸识别记录失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取人脸识别记录失败: %w", err)
}
return &record, nil
}
// GetByCertifyID 根据认证ID获取人脸识别记录
func (r *GormFaceVerifyRecordRepository) GetByCertifyID(ctx context.Context, certifyID string) (*entities.FaceVerifyRecord, error) {
var record entities.FaceVerifyRecord
if err := r.db.WithContext(ctx).First(&record, "certify_id = ?", certifyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("人脸识别记录不存在")
}
r.logger.Error("根据认证ID获取人脸识别记录失败",
zap.String("certify_id", certifyID),
zap.Error(err),
)
return nil, fmt.Errorf("获取人脸识别记录失败: %w", err)
}
return &record, nil
}
// GetByCertificationID 根据认证申请ID获取人脸识别记录列表
func (r *GormFaceVerifyRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.FaceVerifyRecord, error) {
var records []*entities.FaceVerifyRecord
if err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("根据认证申请ID获取人脸识别记录失败",
zap.String("certification_id", certificationID),
zap.Error(err),
)
return nil, fmt.Errorf("获取人脸识别记录失败: %w", err)
}
return records, nil
}
// Update 更新人脸识别记录
func (r *GormFaceVerifyRecordRepository) Update(ctx context.Context, record *entities.FaceVerifyRecord) error {
if err := r.db.WithContext(ctx).Save(record).Error; err != nil {
r.logger.Error("更新人脸识别记录失败",
zap.String("id", record.ID),
zap.Error(err),
)
return fmt.Errorf("更新人脸识别记录失败: %w", err)
}
return nil
}
// Delete 删除人脸识别记录
func (r *GormFaceVerifyRecordRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.FaceVerifyRecord{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除人脸识别记录失败",
zap.String("id", id),
zap.Error(err),
)
return fmt.Errorf("删除人脸识别记录失败: %w", err)
}
return nil
}
// GetByUserID 根据用户ID获取人脸识别记录列表
func (r *GormFaceVerifyRecordRepository) GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.FaceVerifyRecord, int, error) {
var records []*entities.FaceVerifyRecord
var total int64
query := r.db.WithContext(ctx).Model(&entities.FaceVerifyRecord{}).Where("user_id = ?", userID)
// 获取总数
if err := query.Count(&total).Error; err != nil {
r.logger.Error("获取用户人脸识别记录总数失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取人脸识别记录总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("获取用户人脸识别记录列表失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取人脸识别记录列表失败: %w", err)
}
return records, int(total), nil
}
// GetExpiredRecords 获取已过期的人脸识别记录
func (r *GormFaceVerifyRecordRepository) GetExpiredRecords(ctx context.Context, limit int) ([]*entities.FaceVerifyRecord, error) {
var records []*entities.FaceVerifyRecord
if err := r.db.WithContext(ctx).
Where("expires_at < NOW() AND status = ?", "PROCESSING").
Limit(limit).
Order("expires_at ASC").
Find(&records).Error; err != nil {
r.logger.Error("获取过期人脸识别记录失败", zap.Error(err))
return nil, fmt.Errorf("获取过期人脸识别记录失败: %w", err)
}
return records, nil
}

View File

@@ -0,0 +1,163 @@
package repositories
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/certification/entities"
)
// GormLicenseUploadRecordRepository GORM营业执照上传记录仓储实现
type GormLicenseUploadRecordRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormLicenseUploadRecordRepository 创建GORM营业执照上传记录仓储
func NewGormLicenseUploadRecordRepository(db *gorm.DB, logger *zap.Logger) LicenseUploadRecordRepository {
return &GormLicenseUploadRecordRepository{
db: db,
logger: logger,
}
}
// Create 创建上传记录
func (r *GormLicenseUploadRecordRepository) Create(ctx context.Context, record *entities.LicenseUploadRecord) error {
if err := r.db.WithContext(ctx).Create(record).Error; err != nil {
r.logger.Error("创建上传记录失败",
zap.String("user_id", record.UserID),
zap.String("file_name", record.OriginalFileName),
zap.Error(err),
)
return fmt.Errorf("创建上传记录失败: %w", err)
}
r.logger.Info("上传记录创建成功",
zap.String("id", record.ID),
zap.String("file_name", record.OriginalFileName),
)
return nil
}
// GetByID 根据ID获取上传记录
func (r *GormLicenseUploadRecordRepository) GetByID(ctx context.Context, id string) (*entities.LicenseUploadRecord, error) {
var record entities.LicenseUploadRecord
if err := r.db.WithContext(ctx).First(&record, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("上传记录不存在")
}
r.logger.Error("获取上传记录失败",
zap.String("id", id),
zap.Error(err),
)
return nil, fmt.Errorf("获取上传记录失败: %w", err)
}
return &record, nil
}
// GetByUserID 根据用户ID获取上传记录列表
func (r *GormLicenseUploadRecordRepository) GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.LicenseUploadRecord, int, error) {
var records []*entities.LicenseUploadRecord
var total int64
query := r.db.WithContext(ctx).Model(&entities.LicenseUploadRecord{}).Where("user_id = ?", userID)
// 获取总数
if err := query.Count(&total).Error; err != nil {
r.logger.Error("获取用户上传记录总数失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取上传记录总数失败: %w", err)
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil {
r.logger.Error("获取用户上传记录列表失败", zap.Error(err))
return nil, 0, fmt.Errorf("获取上传记录列表失败: %w", err)
}
return records, int(total), nil
}
// GetByCertificationID 根据认证ID获取上传记录
func (r *GormLicenseUploadRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) (*entities.LicenseUploadRecord, error) {
var record entities.LicenseUploadRecord
if err := r.db.WithContext(ctx).First(&record, "certification_id = ?", certificationID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("上传记录不存在")
}
r.logger.Error("根据认证ID获取上传记录失败",
zap.String("certification_id", certificationID),
zap.Error(err),
)
return nil, fmt.Errorf("获取上传记录失败: %w", err)
}
return &record, nil
}
// Update 更新上传记录
func (r *GormLicenseUploadRecordRepository) Update(ctx context.Context, record *entities.LicenseUploadRecord) error {
if err := r.db.WithContext(ctx).Save(record).Error; err != nil {
r.logger.Error("更新上传记录失败",
zap.String("id", record.ID),
zap.Error(err),
)
return fmt.Errorf("更新上传记录失败: %w", err)
}
return nil
}
// Delete 删除上传记录
func (r *GormLicenseUploadRecordRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.LicenseUploadRecord{}, "id = ?", id).Error; err != nil {
r.logger.Error("删除上传记录失败",
zap.String("id", id),
zap.Error(err),
)
return fmt.Errorf("删除上传记录失败: %w", err)
}
return nil
}
// GetByQiNiuKey 根据七牛云Key获取上传记录
func (r *GormLicenseUploadRecordRepository) GetByQiNiuKey(ctx context.Context, key string) (*entities.LicenseUploadRecord, error) {
var record entities.LicenseUploadRecord
if err := r.db.WithContext(ctx).First(&record, "qiniu_key = ?", key).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("上传记录不存在")
}
r.logger.Error("根据七牛云Key获取上传记录失败",
zap.String("qiniu_key", key),
zap.Error(err),
)
return nil, fmt.Errorf("获取上传记录失败: %w", err)
}
return &record, nil
}
// GetPendingOCR 获取待OCR处理的上传记录
func (r *GormLicenseUploadRecordRepository) GetPendingOCR(ctx context.Context, limit int) ([]*entities.LicenseUploadRecord, error) {
var records []*entities.LicenseUploadRecord
if err := r.db.WithContext(ctx).
Where("ocr_processed = ? OR (ocr_processed = ? AND ocr_success = ?)", false, true, false).
Limit(limit).
Order("created_at ASC").
Find(&records).Error; err != nil {
r.logger.Error("获取待OCR处理记录失败", zap.Error(err))
return nil, fmt.Errorf("获取待OCR处理记录失败: %w", err)
}
return records, nil
}

View File

@@ -0,0 +1,105 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
)
// CertificationRepository 认证仓储接口
type CertificationRepository interface {
// 基础CRUD操作
Create(ctx context.Context, cert *entities.Certification) error
GetByID(ctx context.Context, id string) (*entities.Certification, error)
GetByUserID(ctx context.Context, userID string) (*entities.Certification, error)
Update(ctx context.Context, cert *entities.Certification) error
Delete(ctx context.Context, id string) error
// 查询操作
List(ctx context.Context, page, pageSize int, status enums.CertificationStatus) ([]*entities.Certification, int, error)
GetByStatus(ctx context.Context, status enums.CertificationStatus, page, pageSize int) ([]*entities.Certification, int, error)
GetPendingApprovals(ctx context.Context, page, pageSize int) ([]*entities.Certification, int, error)
// 关联查询
GetWithEnterprise(ctx context.Context, id string) (*entities.Certification, error)
GetWithAllRelations(ctx context.Context, id string) (*entities.Certification, error)
// 统计操作
CountByStatus(ctx context.Context, status enums.CertificationStatus) (int64, error)
CountByUserID(ctx context.Context, userID string) (int64, error)
}
// EnterpriseRepository 企业信息仓储接口
type EnterpriseRepository interface {
// 基础CRUD操作
Create(ctx context.Context, enterprise *entities.Enterprise) error
GetByID(ctx context.Context, id string) (*entities.Enterprise, error)
GetByCertificationID(ctx context.Context, certificationID string) (*entities.Enterprise, error)
Update(ctx context.Context, enterprise *entities.Enterprise) error
Delete(ctx context.Context, id string) error
// 查询操作
GetByUnifiedSocialCode(ctx context.Context, code string) (*entities.Enterprise, error)
ExistsByUnifiedSocialCode(ctx context.Context, code string) (bool, error)
}
// LicenseUploadRecordRepository 营业执照上传记录仓储接口
type LicenseUploadRecordRepository interface {
// 基础CRUD操作
Create(ctx context.Context, record *entities.LicenseUploadRecord) error
GetByID(ctx context.Context, id string) (*entities.LicenseUploadRecord, error)
GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.LicenseUploadRecord, int, error)
GetByCertificationID(ctx context.Context, certificationID string) (*entities.LicenseUploadRecord, error)
Update(ctx context.Context, record *entities.LicenseUploadRecord) error
Delete(ctx context.Context, id string) error
// 查询操作
GetByQiNiuKey(ctx context.Context, key string) (*entities.LicenseUploadRecord, error)
GetPendingOCR(ctx context.Context, limit int) ([]*entities.LicenseUploadRecord, error)
}
// FaceVerifyRecordRepository 人脸识别记录仓储接口
type FaceVerifyRecordRepository interface {
// 基础CRUD操作
Create(ctx context.Context, record *entities.FaceVerifyRecord) error
GetByID(ctx context.Context, id string) (*entities.FaceVerifyRecord, error)
GetByCertifyID(ctx context.Context, certifyID string) (*entities.FaceVerifyRecord, error)
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.FaceVerifyRecord, error)
Update(ctx context.Context, record *entities.FaceVerifyRecord) error
Delete(ctx context.Context, id string) error
// 查询操作
GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.FaceVerifyRecord, int, error)
GetExpiredRecords(ctx context.Context, limit int) ([]*entities.FaceVerifyRecord, error)
}
// ContractRecordRepository 合同记录仓储接口
type ContractRecordRepository interface {
// 基础CRUD操作
Create(ctx context.Context, record *entities.ContractRecord) error
GetByID(ctx context.Context, id string) (*entities.ContractRecord, error)
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.ContractRecord, error)
Update(ctx context.Context, record *entities.ContractRecord) error
Delete(ctx context.Context, id string) error
// 查询操作
GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.ContractRecord, int, error)
GetByStatus(ctx context.Context, status string, page, pageSize int) ([]*entities.ContractRecord, int, error)
GetExpiredContracts(ctx context.Context, limit int) ([]*entities.ContractRecord, error)
}
// NotificationRecordRepository 通知记录仓储接口
type NotificationRecordRepository interface {
// 基础CRUD操作
Create(ctx context.Context, record *entities.NotificationRecord) error
GetByID(ctx context.Context, id string) (*entities.NotificationRecord, error)
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.NotificationRecord, error)
Update(ctx context.Context, record *entities.NotificationRecord) error
Delete(ctx context.Context, id string) error
// 查询操作
GetByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.NotificationRecord, int, error)
GetPendingNotifications(ctx context.Context, limit int) ([]*entities.NotificationRecord, error)
GetFailedNotifications(ctx context.Context, limit int) ([]*entities.NotificationRecord, error)
}

View File

@@ -0,0 +1,62 @@
package routes
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/domains/certification/handlers"
"tyapi-server/internal/shared/middleware"
)
// CertificationRoutes 认证路由组
type CertificationRoutes struct {
certificationHandler *handlers.CertificationHandler
authMiddleware *middleware.JWTAuthMiddleware
logger *zap.Logger
}
// NewCertificationRoutes 创建认证路由
func NewCertificationRoutes(
certificationHandler *handlers.CertificationHandler,
authMiddleware *middleware.JWTAuthMiddleware,
logger *zap.Logger,
) *CertificationRoutes {
return &CertificationRoutes{
certificationHandler: certificationHandler,
authMiddleware: authMiddleware,
logger: logger,
}
}
// RegisterRoutes 注册认证相关路由
func (r *CertificationRoutes) RegisterRoutes(router *gin.Engine) {
// 认证相关路由组,需要用户认证
certificationGroup := router.Group("/api/v1/certification")
certificationGroup.Use(r.authMiddleware.Handle())
{
// 创建认证申请
certificationGroup.POST("/create", r.certificationHandler.CreateCertification)
// 上传营业执照
certificationGroup.POST("/upload-license", r.certificationHandler.UploadLicense)
// 获取认证状态
certificationGroup.GET("/status", r.certificationHandler.GetCertificationStatus)
// 获取进度统计
certificationGroup.GET("/progress", r.certificationHandler.GetProgressStats)
// 提交企业信息
certificationGroup.PUT("/:id/submit-info", r.certificationHandler.SubmitEnterpriseInfo)
// 发起人脸识别验证
certificationGroup.POST("/:id/face-verify", r.certificationHandler.InitiateFaceVerify)
// 申请合同签署
certificationGroup.POST("/:id/apply-contract", r.certificationHandler.ApplyContract)
// 获取认证详情
certificationGroup.GET("/:id", r.certificationHandler.GetCertificationDetails)
// 重试认证步骤
certificationGroup.POST("/:id/retry", r.certificationHandler.RetryStep)
}
r.logger.Info("认证路由注册完成")
}

View File

@@ -0,0 +1,404 @@
package services
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"tyapi-server/internal/domains/certification/dto"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
"tyapi-server/internal/domains/certification/repositories"
"tyapi-server/internal/shared/ocr"
"tyapi-server/internal/shared/storage"
)
// CertificationService 认证服务
type CertificationService struct {
certRepo repositories.CertificationRepository
enterpriseRepo repositories.EnterpriseRepository
licenseRepo repositories.LicenseUploadRecordRepository
faceVerifyRepo repositories.FaceVerifyRecordRepository
contractRepo repositories.ContractRecordRepository
stateMachine *CertificationStateMachine
storageService storage.StorageService
ocrService ocr.OCRService
logger *zap.Logger
}
// NewCertificationService 创建认证服务
func NewCertificationService(
certRepo repositories.CertificationRepository,
enterpriseRepo repositories.EnterpriseRepository,
licenseRepo repositories.LicenseUploadRecordRepository,
faceVerifyRepo repositories.FaceVerifyRecordRepository,
contractRepo repositories.ContractRecordRepository,
stateMachine *CertificationStateMachine,
storageService storage.StorageService,
ocrService ocr.OCRService,
logger *zap.Logger,
) *CertificationService {
return &CertificationService{
certRepo: certRepo,
enterpriseRepo: enterpriseRepo,
licenseRepo: licenseRepo,
faceVerifyRepo: faceVerifyRepo,
contractRepo: contractRepo,
stateMachine: stateMachine,
storageService: storageService,
ocrService: ocrService,
logger: logger,
}
}
// CreateCertification 创建认证申请
func (s *CertificationService) CreateCertification(ctx context.Context, userID string) (*dto.CertificationCreateResponse, error) {
s.logger.Info("创建认证申请", zap.String("user_id", userID))
// 检查用户是否已有认证申请
existingCert, err := s.certRepo.GetByUserID(ctx, userID)
if err == nil && existingCert != nil {
// 如果已存在且不是最终状态,返回现有申请
if !enums.IsFinalStatus(existingCert.Status) {
return &dto.CertificationCreateResponse{
ID: existingCert.ID,
UserID: existingCert.UserID,
Status: existingCert.Status,
}, nil
}
}
// 创建新的认证申请
certification := &entities.Certification{
ID: uuid.New().String(),
UserID: userID,
Status: enums.StatusPending,
}
if err := s.certRepo.Create(ctx, certification); err != nil {
return nil, fmt.Errorf("创建认证申请失败: %w", err)
}
s.logger.Info("认证申请创建成功",
zap.String("certification_id", certification.ID),
zap.String("user_id", userID),
)
return &dto.CertificationCreateResponse{
ID: certification.ID,
UserID: certification.UserID,
Status: certification.Status,
}, nil
}
// UploadLicense 上传营业执照并进行OCR识别
func (s *CertificationService) UploadLicense(ctx context.Context, userID string, fileBytes []byte, fileName string) (*dto.UploadLicenseResponse, error) {
s.logger.Info("上传营业执照",
zap.String("user_id", userID),
zap.String("file_name", fileName),
zap.Int("file_size", len(fileBytes)),
)
// 1. 上传文件到存储服务
uploadResult, err := s.storageService.UploadFile(ctx, fileBytes, fileName)
if err != nil {
s.logger.Error("文件上传失败", zap.Error(err))
return nil, fmt.Errorf("文件上传失败: %w", err)
}
// 2. 创建上传记录
uploadRecord := &entities.LicenseUploadRecord{
ID: uuid.New().String(),
UserID: userID,
OriginalFileName: fileName,
FileSize: int64(len(fileBytes)),
FileType: uploadResult.MimeType,
FileURL: uploadResult.URL,
QiNiuKey: uploadResult.Key,
OCRProcessed: false,
OCRSuccess: false,
}
if err := s.licenseRepo.Create(ctx, uploadRecord); err != nil {
s.logger.Error("创建上传记录失败", zap.Error(err))
return nil, fmt.Errorf("创建上传记录失败: %w", err)
}
// 3. 尝试OCR识别
var enterpriseInfo *dto.OCREnterpriseInfo
var ocrError string
ocrResult, err := s.ocrService.RecognizeBusinessLicense(ctx, uploadResult.URL)
if err != nil {
s.logger.Warn("OCR识别失败", zap.Error(err))
ocrError = err.Error()
uploadRecord.OCRProcessed = true
uploadRecord.OCRSuccess = false
uploadRecord.OCRErrorMessage = ocrError
} else {
s.logger.Info("OCR识别成功",
zap.String("company_name", ocrResult.CompanyName),
zap.Float64("confidence", ocrResult.Confidence),
)
enterpriseInfo = ocrResult
uploadRecord.OCRProcessed = true
uploadRecord.OCRSuccess = true
uploadRecord.OCRConfidence = ocrResult.Confidence
// 存储OCR原始数据
if rawData, err := s.serializeOCRResult(ocrResult); err == nil {
uploadRecord.OCRRawData = rawData
}
}
// 更新上传记录
if err := s.licenseRepo.Update(ctx, uploadRecord); err != nil {
s.logger.Warn("更新上传记录失败", zap.Error(err))
}
return &dto.UploadLicenseResponse{
UploadRecordID: uploadRecord.ID,
FileURL: uploadResult.URL,
OCRProcessed: uploadRecord.OCRProcessed,
OCRSuccess: uploadRecord.OCRSuccess,
EnterpriseInfo: enterpriseInfo,
OCRErrorMessage: ocrError,
}, nil
}
// SubmitEnterpriseInfo 提交企业信息
func (s *CertificationService) SubmitEnterpriseInfo(ctx context.Context, certificationID string, req *dto.SubmitEnterpriseInfoRequest) (*dto.SubmitEnterpriseInfoResponse, error) {
s.logger.Info("提交企业信息",
zap.String("certification_id", certificationID),
zap.String("company_name", req.CompanyName),
)
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态是否允许提交企业信息
if !cert.CanTransitionTo(enums.StatusInfoSubmitted) {
return nil, fmt.Errorf("当前状态不允许提交企业信息,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 检查统一社会信用代码是否已存在
exists, err := s.enterpriseRepo.ExistsByUnifiedSocialCode(ctx, req.UnifiedSocialCode)
if err != nil {
return nil, fmt.Errorf("检查企业信息失败: %w", err)
}
if exists {
return nil, fmt.Errorf("该统一社会信用代码已被使用")
}
// 4. 创建企业信息
enterprise := &entities.Enterprise{
ID: uuid.New().String(),
CertificationID: certificationID,
CompanyName: req.CompanyName,
UnifiedSocialCode: req.UnifiedSocialCode,
LegalPersonName: req.LegalPersonName,
LegalPersonID: req.LegalPersonID,
LicenseUploadRecordID: req.LicenseUploadRecordID,
IsOCRVerified: false,
IsFaceVerified: false,
}
if err := s.enterpriseRepo.Create(ctx, enterprise); err != nil {
return nil, fmt.Errorf("创建企业信息失败: %w", err)
}
// 5. 更新认证记录状态
cert.EnterpriseID = &enterprise.ID
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusInfoSubmitted, true, false, nil); err != nil {
return nil, fmt.Errorf("更新认证状态失败: %w", err)
}
s.logger.Info("企业信息提交成功",
zap.String("certification_id", certificationID),
zap.String("enterprise_id", enterprise.ID),
)
return &dto.SubmitEnterpriseInfoResponse{
ID: certificationID,
Status: enums.StatusInfoSubmitted,
Enterprise: &dto.EnterpriseInfoResponse{
ID: enterprise.ID,
CertificationID: enterprise.CertificationID,
CompanyName: enterprise.CompanyName,
UnifiedSocialCode: enterprise.UnifiedSocialCode,
LegalPersonName: enterprise.LegalPersonName,
LegalPersonID: enterprise.LegalPersonID,
LicenseUploadRecordID: enterprise.LicenseUploadRecordID,
IsOCRVerified: enterprise.IsOCRVerified,
IsFaceVerified: enterprise.IsFaceVerified,
CreatedAt: enterprise.CreatedAt,
UpdatedAt: enterprise.UpdatedAt,
},
}, nil
}
// GetCertificationStatus 获取认证状态
func (s *CertificationService) GetCertificationStatus(ctx context.Context, userID string) (*dto.CertificationStatusResponse, error) {
s.logger.Info("获取认证状态", zap.String("user_id", userID))
// 获取用户的认证记录
cert, err := s.certRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 获取企业信息
var enterprise *dto.EnterpriseInfoResponse
if cert.EnterpriseID != nil {
ent, err := s.enterpriseRepo.GetByID(ctx, *cert.EnterpriseID)
if err == nil {
enterprise = &dto.EnterpriseInfoResponse{
ID: ent.ID,
CertificationID: ent.CertificationID,
CompanyName: ent.CompanyName,
UnifiedSocialCode: ent.UnifiedSocialCode,
LegalPersonName: ent.LegalPersonName,
LegalPersonID: ent.LegalPersonID,
LicenseUploadRecordID: ent.LicenseUploadRecordID,
IsOCRVerified: ent.IsOCRVerified,
IsFaceVerified: ent.IsFaceVerified,
CreatedAt: ent.CreatedAt,
UpdatedAt: ent.UpdatedAt,
}
}
}
return &dto.CertificationStatusResponse{
ID: cert.ID,
UserID: cert.UserID,
Status: cert.Status,
StatusName: enums.GetStatusName(cert.Status),
Progress: cert.GetProgressPercentage(),
IsUserActionRequired: cert.IsUserActionRequired(),
IsAdminActionRequired: cert.IsAdminActionRequired(),
InfoSubmittedAt: cert.InfoSubmittedAt,
FaceVerifiedAt: cert.FaceVerifiedAt,
ContractAppliedAt: cert.ContractAppliedAt,
ContractApprovedAt: cert.ContractApprovedAt,
ContractSignedAt: cert.ContractSignedAt,
CompletedAt: cert.CompletedAt,
Enterprise: enterprise,
ContractURL: cert.ContractURL,
SigningURL: cert.SigningURL,
RejectReason: cert.RejectReason,
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
}, nil
}
// InitiateFaceVerify 初始化人脸识别
func (s *CertificationService) InitiateFaceVerify(ctx context.Context, certificationID string, req *dto.FaceVerifyRequest) (*dto.FaceVerifyResponse, error) {
s.logger.Info("初始化人脸识别",
zap.String("certification_id", certificationID),
zap.String("real_name", req.RealName),
)
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态
if cert.Status != enums.StatusInfoSubmitted && cert.Status != enums.StatusFaceFailed {
return nil, fmt.Errorf("当前状态不允许进行人脸识别,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 创建人脸识别记录
verifyRecord := &entities.FaceVerifyRecord{
ID: uuid.New().String(),
CertificationID: certificationID,
UserID: cert.UserID,
CertifyID: fmt.Sprintf("cert_%s_%d", certificationID, time.Now().Unix()),
RealName: req.RealName,
IDCardNumber: req.IDCardNumber,
ReturnURL: req.ReturnURL,
Status: "PROCESSING",
ExpiresAt: time.Now().Add(24 * time.Hour), // 24小时过期
}
// TODO: 实际调用阿里云人脸识别API
// 这里是模拟实现
verifyRecord.VerifyURL = fmt.Sprintf("https://face-verify.aliyun.com/verify?certifyId=%s", verifyRecord.CertifyID)
if err := s.faceVerifyRepo.Create(ctx, verifyRecord); err != nil {
return nil, fmt.Errorf("创建人脸识别记录失败: %w", err)
}
s.logger.Info("人脸识别初始化成功",
zap.String("certification_id", certificationID),
zap.String("certify_id", verifyRecord.CertifyID),
)
return &dto.FaceVerifyResponse{
CertifyID: verifyRecord.CertifyID,
VerifyURL: verifyRecord.VerifyURL,
ExpiresAt: verifyRecord.ExpiresAt,
}, nil
}
// ApplyContract 申请电子合同
func (s *CertificationService) ApplyContract(ctx context.Context, certificationID string) (*dto.ApplyContractResponse, error) {
s.logger.Info("申请电子合同", zap.String("certification_id", certificationID))
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态
if !cert.CanTransitionTo(enums.StatusContractApplied) {
return nil, fmt.Errorf("当前状态不允许申请合同,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 转换状态
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractApplied, true, false, nil); err != nil {
return nil, fmt.Errorf("更新认证状态失败: %w", err)
}
// 4. 自动转换到待审核状态
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractPending, false, false, nil); err != nil {
s.logger.Warn("自动转换到待审核状态失败", zap.Error(err))
}
// 5. 创建合同记录
contractRecord := &entities.ContractRecord{
ID: uuid.New().String(),
CertificationID: certificationID,
UserID: cert.UserID,
ContractType: "ENTERPRISE_CERTIFICATION",
Status: "PENDING",
}
if err := s.contractRepo.Create(ctx, contractRecord); err != nil {
s.logger.Warn("创建合同记录失败", zap.Error(err))
}
// TODO: 发送通知给管理员
s.logger.Info("合同申请成功", zap.String("certification_id", certificationID))
return &dto.ApplyContractResponse{
ID: certificationID,
Status: enums.StatusContractPending,
ContractAppliedAt: time.Now(),
}, nil
}
// serializeOCRResult 序列化OCR结果
func (s *CertificationService) serializeOCRResult(result *dto.OCREnterpriseInfo) (string, error) {
// 简单的JSON序列化
return fmt.Sprintf(`{"company_name":"%s","unified_social_code":"%s","legal_person_name":"%s","legal_person_id":"%s","confidence":%f}`,
result.CompanyName, result.UnifiedSocialCode, result.LegalPersonName, result.LegalPersonID, result.Confidence), nil
}

View File

@@ -0,0 +1,287 @@
package services
import (
"context"
"fmt"
"time"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
"tyapi-server/internal/domains/certification/repositories"
"go.uber.org/zap"
)
// StateTransition 状态转换规则
type StateTransition struct {
From enums.CertificationStatus
To enums.CertificationStatus
Action string
AllowUser bool // 是否允许用户操作
AllowAdmin bool // 是否允许管理员操作
RequiresValidation bool // 是否需要额外验证
}
// CertificationStateMachine 认证状态机
type CertificationStateMachine struct {
transitions map[enums.CertificationStatus][]StateTransition
certRepo repositories.CertificationRepository
logger *zap.Logger
}
// NewCertificationStateMachine 创建认证状态机
func NewCertificationStateMachine(
certRepo repositories.CertificationRepository,
logger *zap.Logger,
) *CertificationStateMachine {
sm := &CertificationStateMachine{
transitions: make(map[enums.CertificationStatus][]StateTransition),
certRepo: certRepo,
logger: logger,
}
// 初始化状态转换规则
sm.initializeTransitions()
return sm
}
// initializeTransitions 初始化状态转换规则
func (sm *CertificationStateMachine) initializeTransitions() {
transitions := []StateTransition{
// 正常流程转换
{enums.StatusPending, enums.StatusInfoSubmitted, "submit_info", true, false, true},
{enums.StatusInfoSubmitted, enums.StatusFaceVerified, "face_verify", true, false, true},
{enums.StatusFaceVerified, enums.StatusContractApplied, "apply_contract", true, false, false},
{enums.StatusContractApplied, enums.StatusContractPending, "system_process", false, false, false},
{enums.StatusContractPending, enums.StatusContractApproved, "admin_approve", false, true, true},
{enums.StatusContractApproved, enums.StatusContractSigned, "user_sign", true, false, true},
{enums.StatusContractSigned, enums.StatusCompleted, "system_complete", false, false, false},
// 失败和重试转换
{enums.StatusInfoSubmitted, enums.StatusFaceFailed, "face_fail", false, false, false},
{enums.StatusFaceFailed, enums.StatusFaceVerified, "retry_face", true, false, true},
{enums.StatusContractPending, enums.StatusRejected, "admin_reject", false, true, true},
{enums.StatusRejected, enums.StatusInfoSubmitted, "restart_process", true, false, false},
{enums.StatusContractApproved, enums.StatusSignFailed, "sign_fail", false, false, false},
{enums.StatusSignFailed, enums.StatusContractSigned, "retry_sign", true, false, true},
}
// 构建状态转换映射
for _, transition := range transitions {
sm.transitions[transition.From] = append(sm.transitions[transition.From], transition)
}
}
// CanTransition 检查是否可以转换到指定状态
func (sm *CertificationStateMachine) CanTransition(
from enums.CertificationStatus,
to enums.CertificationStatus,
isUser bool,
isAdmin bool,
) (bool, string) {
validTransitions, exists := sm.transitions[from]
if !exists {
return false, "当前状态不支持任何转换"
}
for _, transition := range validTransitions {
if transition.To == to {
if isUser && !transition.AllowUser {
return false, "用户不允许执行此操作"
}
if isAdmin && !transition.AllowAdmin {
return false, "管理员不允许执行此操作"
}
if !isUser && !isAdmin && (transition.AllowUser || transition.AllowAdmin) {
return false, "此操作需要用户或管理员权限"
}
return true, ""
}
}
return false, "不支持的状态转换"
}
// TransitionTo 执行状态转换
func (sm *CertificationStateMachine) TransitionTo(
ctx context.Context,
certificationID string,
targetStatus enums.CertificationStatus,
isUser bool,
isAdmin bool,
metadata map[string]interface{},
) error {
// 获取当前认证记录
cert, err := sm.certRepo.GetByID(ctx, certificationID)
if err != nil {
return fmt.Errorf("获取认证记录失败: %w", err)
}
// 检查是否可以转换
canTransition, reason := sm.CanTransition(cert.Status, targetStatus, isUser, isAdmin)
if !canTransition {
return fmt.Errorf("状态转换失败: %s", reason)
}
// 执行状态转换前的验证
if err := sm.validateTransition(ctx, cert, targetStatus, metadata); err != nil {
return fmt.Errorf("状态转换验证失败: %w", err)
}
// 更新状态和时间戳
oldStatus := cert.Status
cert.Status = targetStatus
sm.updateTimestamp(cert, targetStatus)
// 更新其他字段
sm.updateCertificationFields(cert, targetStatus, metadata)
// 保存到数据库
if err := sm.certRepo.Update(ctx, cert); err != nil {
return fmt.Errorf("保存状态转换失败: %w", err)
}
sm.logger.Info("认证状态转换成功",
zap.String("certification_id", certificationID),
zap.String("from_status", string(oldStatus)),
zap.String("to_status", string(targetStatus)),
zap.Bool("is_user", isUser),
zap.Bool("is_admin", isAdmin),
)
return nil
}
// updateTimestamp 更新对应的时间戳字段
func (sm *CertificationStateMachine) updateTimestamp(cert *entities.Certification, status enums.CertificationStatus) {
now := time.Now()
switch status {
case enums.StatusInfoSubmitted:
cert.InfoSubmittedAt = &now
case enums.StatusFaceVerified:
cert.FaceVerifiedAt = &now
case enums.StatusContractApplied:
cert.ContractAppliedAt = &now
case enums.StatusContractApproved:
cert.ContractApprovedAt = &now
case enums.StatusContractSigned:
cert.ContractSignedAt = &now
case enums.StatusCompleted:
cert.CompletedAt = &now
}
}
// updateCertificationFields 根据状态更新认证记录的其他字段
func (sm *CertificationStateMachine) updateCertificationFields(
cert *entities.Certification,
status enums.CertificationStatus,
metadata map[string]interface{},
) {
switch status {
case enums.StatusContractApproved:
if adminID, ok := metadata["admin_id"].(string); ok {
cert.AdminID = &adminID
}
if approvalNotes, ok := metadata["approval_notes"].(string); ok {
cert.ApprovalNotes = approvalNotes
}
if signingURL, ok := metadata["signing_url"].(string); ok {
cert.SigningURL = signingURL
}
case enums.StatusRejected:
if adminID, ok := metadata["admin_id"].(string); ok {
cert.AdminID = &adminID
}
if rejectReason, ok := metadata["reject_reason"].(string); ok {
cert.RejectReason = rejectReason
}
case enums.StatusContractSigned:
if contractURL, ok := metadata["contract_url"].(string); ok {
cert.ContractURL = contractURL
}
}
}
// validateTransition 验证状态转换的有效性
func (sm *CertificationStateMachine) validateTransition(
ctx context.Context,
cert *entities.Certification,
targetStatus enums.CertificationStatus,
metadata map[string]interface{},
) error {
switch targetStatus {
case enums.StatusInfoSubmitted:
// 验证企业信息是否完整
if cert.EnterpriseID == nil {
return fmt.Errorf("企业信息未提交")
}
case enums.StatusFaceVerified:
// 验证人脸识别是否成功
// 这里可以添加人脸识别结果的验证逻辑
case enums.StatusContractApproved:
// 验证管理员审核信息
if metadata["signing_url"] == nil || metadata["signing_url"].(string) == "" {
return fmt.Errorf("缺少合同签署链接")
}
case enums.StatusRejected:
// 验证拒绝原因
if metadata["reject_reason"] == nil || metadata["reject_reason"].(string) == "" {
return fmt.Errorf("缺少拒绝原因")
}
case enums.StatusContractSigned:
// 验证合同签署信息
if cert.SigningURL == "" {
return fmt.Errorf("缺少合同签署链接")
}
}
return nil
}
// GetValidNextStatuses 获取当前状态可以转换到的下一个状态列表
func (sm *CertificationStateMachine) GetValidNextStatuses(
currentStatus enums.CertificationStatus,
isUser bool,
isAdmin bool,
) []enums.CertificationStatus {
var validStatuses []enums.CertificationStatus
transitions, exists := sm.transitions[currentStatus]
if !exists {
return validStatuses
}
for _, transition := range transitions {
if (isUser && transition.AllowUser) || (isAdmin && transition.AllowAdmin) {
validStatuses = append(validStatuses, transition.To)
}
}
return validStatuses
}
// GetTransitionAction 获取状态转换对应的操作名称
func (sm *CertificationStateMachine) GetTransitionAction(
from enums.CertificationStatus,
to enums.CertificationStatus,
) string {
transitions, exists := sm.transitions[from]
if !exists {
return ""
}
for _, transition := range transitions {
if transition.To == to {
return transition.Action
}
}
return ""
}

View File

@@ -0,0 +1,140 @@
package dto
import (
"time"
"github.com/shopspring/decimal"
)
// WalletInfo 钱包信息
type WalletInfo struct {
ID string `json:"id"` // 钱包ID
UserID string `json:"user_id"` // 用户ID
IsActive bool `json:"is_active"` // 是否激活
Balance decimal.Decimal `json:"balance"` // 余额
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
}
// UserSecretsInfo 用户密钥信息
type UserSecretsInfo struct {
ID string `json:"id"` // 密钥ID
UserID string `json:"user_id"` // 用户ID
AccessID string `json:"access_id"` // 访问ID
AccessKey string `json:"access_key"` // 访问密钥
IsActive bool `json:"is_active"` // 是否激活
LastUsedAt *time.Time `json:"last_used_at"` // 最后使用时间
ExpiresAt *time.Time `json:"expires_at"` // 过期时间
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
}
// CreateWalletRequest 创建钱包请求
type CreateWalletRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
}
// CreateWalletResponse 创建钱包响应
type CreateWalletResponse struct {
Wallet WalletInfo `json:"wallet"` // 钱包信息
}
// GetWalletRequest 获取钱包请求
type GetWalletRequest struct {
UserID string `form:"user_id" binding:"required"` // 用户ID
}
// UpdateWalletRequest 更新钱包请求
type UpdateWalletRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
Balance decimal.Decimal `json:"balance"` // 余额
IsActive *bool `json:"is_active"` // 是否激活
}
// RechargeRequest 充值请求
type RechargeRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
Amount decimal.Decimal `json:"amount" binding:"required"` // 充值金额
}
// RechargeResponse 充值响应
type RechargeResponse struct {
WalletID string `json:"wallet_id"` // 钱包ID
Amount decimal.Decimal `json:"amount"` // 充值金额
Balance decimal.Decimal `json:"balance"` // 充值后余额
}
// WithdrawRequest 提现请求
type WithdrawRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
Amount decimal.Decimal `json:"amount" binding:"required"` // 提现金额
}
// WithdrawResponse 提现响应
type WithdrawResponse struct {
WalletID string `json:"wallet_id"` // 钱包ID
Amount decimal.Decimal `json:"amount"` // 提现金额
Balance decimal.Decimal `json:"balance"` // 提现后余额
}
// CreateUserSecretsRequest 创建用户密钥请求
type CreateUserSecretsRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
ExpiresAt *time.Time `json:"expires_at"` // 过期时间
}
// CreateUserSecretsResponse 创建用户密钥响应
type CreateUserSecretsResponse struct {
Secrets UserSecretsInfo `json:"secrets"` // 密钥信息
}
// GetUserSecretsRequest 获取用户密钥请求
type GetUserSecretsRequest struct {
UserID string `form:"user_id" binding:"required"` // 用户ID
}
// RegenerateAccessKeyRequest 重新生成访问密钥请求
type RegenerateAccessKeyRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
ExpiresAt *time.Time `json:"expires_at"` // 过期时间
}
// RegenerateAccessKeyResponse 重新生成访问密钥响应
type RegenerateAccessKeyResponse struct {
AccessID string `json:"access_id"` // 新的访问ID
AccessKey string `json:"access_key"` // 新的访问密钥
}
// DeactivateUserSecretsRequest 停用用户密钥请求
type DeactivateUserSecretsRequest struct {
UserID string `json:"user_id" binding:"required"` // 用户ID
}
// WalletTransactionRequest 钱包交易请求
type WalletTransactionRequest struct {
FromUserID string `json:"from_user_id" binding:"required"` // 转出用户ID
ToUserID string `json:"to_user_id" binding:"required"` // 转入用户ID
Amount decimal.Decimal `json:"amount" binding:"required"` // 交易金额
Notes string `json:"notes"` // 交易备注
}
// WalletTransactionResponse 钱包交易响应
type WalletTransactionResponse struct {
TransactionID string `json:"transaction_id"` // 交易ID
FromUserID string `json:"from_user_id"` // 转出用户ID
ToUserID string `json:"to_user_id"` // 转入用户ID
Amount decimal.Decimal `json:"amount"` // 交易金额
FromBalance decimal.Decimal `json:"from_balance"` // 转出后余额
ToBalance decimal.Decimal `json:"to_balance"` // 转入后余额
Notes string `json:"notes"` // 交易备注
CreatedAt time.Time `json:"created_at"` // 交易时间
}
// WalletStatsResponse 钱包统计响应
type WalletStatsResponse struct {
TotalWallets int64 `json:"total_wallets"` // 总钱包数
ActiveWallets int64 `json:"active_wallets"` // 激活钱包数
TotalBalance decimal.Decimal `json:"total_balance"` // 总余额
TodayTransactions int64 `json:"today_transactions"` // 今日交易数
TodayVolume decimal.Decimal `json:"today_volume"` // 今日交易量
}

View File

@@ -0,0 +1,67 @@
package entities
import (
"time"
"gorm.io/gorm"
)
// UserSecrets 用户密钥实体
// 存储用户的API访问密钥信息用于第三方服务集成和API调用
// 支持密钥的生命周期管理,包括激活状态、过期时间、使用统计等
type UserSecrets struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" comment:"密钥记录唯一标识"`
UserID string `gorm:"type:varchar(36);not null;uniqueIndex" comment:"关联用户ID"`
AccessID string `gorm:"type:varchar(100);not null;uniqueIndex" comment:"访问ID(用于API认证)"`
AccessKey string `gorm:"type:varchar(255);not null" comment:"访问密钥(加密存储)"`
// 密钥状态 - 密钥的生命周期管理
IsActive bool `gorm:"default:true" comment:"密钥是否激活"`
LastUsedAt *time.Time `comment:"最后使用时间"`
ExpiresAt *time.Time `comment:"密钥过期时间"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
}
// TableName 指定数据库表名
func (UserSecrets) TableName() string {
return "user_secrets"
}
// IsExpired 检查密钥是否已过期
// 判断密钥是否超过有效期,过期后需要重新生成或续期
func (u *UserSecrets) IsExpired() bool {
if u.ExpiresAt == nil {
return false // 没有过期时间表示永不过期
}
return time.Now().After(*u.ExpiresAt)
}
// IsValid 检查密钥是否有效
// 综合判断密钥是否可用,包括激活状态和过期状态检查
func (u *UserSecrets) IsValid() bool {
return u.IsActive && !u.IsExpired()
}
// UpdateLastUsedAt 更新最后使用时间
// 在密钥被使用时调用,记录最新的使用时间,用于使用统计和监控
func (u *UserSecrets) UpdateLastUsedAt() {
now := time.Now()
u.LastUsedAt = &now
}
// Deactivate 停用密钥
// 将密钥设置为非激活状态禁止使用该密钥进行API调用
func (u *UserSecrets) Deactivate() {
u.IsActive = false
}
// Activate 激活密钥
// 重新启用密钥允许使用该密钥进行API调用
func (u *UserSecrets) Activate() {
u.IsActive = true
}

View File

@@ -0,0 +1,71 @@
package entities
import (
"fmt"
"time"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
// Wallet 钱包实体
// 用户数字钱包的核心信息,支持多种钱包类型和精确的余额管理
// 使用decimal类型确保金额计算的精确性避免浮点数精度问题
type Wallet 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"`
// 钱包状态 - 钱包的基本状态信息
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
// 钱包信息 - 钱包的详细配置信息
WalletAddress string `gorm:"type:varchar(255)" json:"wallet_address,omitempty" comment:"钱包地址"`
WalletType string `gorm:"type:varchar(50);default:'MAIN'" json:"wallet_type" comment:"钱包类型(MAIN/DEPOSIT/WITHDRAWAL)"` // MAIN, DEPOSIT, WITHDRAWAL
// 时间戳字段
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:"软删除时间"`
}
// TableName 指定数据库表名
func (Wallet) TableName() string {
return "wallets"
}
// IsZeroBalance 检查余额是否为零
// 判断钱包余额是否为零,用于业务逻辑判断
func (w *Wallet) IsZeroBalance() bool {
return w.Balance.IsZero()
}
// HasSufficientBalance 检查是否有足够余额
// 判断钱包余额是否足够支付指定金额,用于交易前的余额验证
func (w *Wallet) HasSufficientBalance(amount decimal.Decimal) bool {
return w.Balance.GreaterThanOrEqual(amount)
}
// AddBalance 增加余额
// 向钱包增加指定金额,用于充值、收入等场景
func (w *Wallet) AddBalance(amount decimal.Decimal) {
w.Balance = w.Balance.Add(amount)
}
// SubtractBalance 减少余额
// 从钱包扣除指定金额,用于消费、转账等场景
// 如果余额不足会返回错误,确保资金安全
func (w *Wallet) SubtractBalance(amount decimal.Decimal) error {
if !w.HasSufficientBalance(amount) {
return fmt.Errorf("余额不足")
}
w.Balance = w.Balance.Sub(amount)
return nil
}
// GetFormattedBalance 获取格式化的余额字符串
// 将decimal类型的余额转换为字符串格式便于显示和传输
func (w *Wallet) GetFormattedBalance() string {
return w.Balance.String()
}

View File

@@ -0,0 +1,336 @@
package handlers
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/domains/finance/dto"
"tyapi-server/internal/domains/finance/services"
"tyapi-server/internal/shared/interfaces"
)
// FinanceHandler 财务HTTP处理器
type FinanceHandler struct {
financeService *services.FinanceService
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
}
// NewFinanceHandler 创建财务HTTP处理器
func NewFinanceHandler(
financeService *services.FinanceService,
responseBuilder interfaces.ResponseBuilder,
logger *zap.Logger,
) *FinanceHandler {
return &FinanceHandler{
financeService: financeService,
responseBuilder: responseBuilder,
logger: logger,
}
}
// CreateWallet 创建钱包
// @Summary 创建钱包
// @Description 为用户创建钱包
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.CreateWalletRequest true "创建钱包请求"
// @Success 201 {object} dto.CreateWalletResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /finance/wallet [post]
func (h *FinanceHandler) CreateWallet(c *gin.Context) {
var req dto.CreateWalletRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("创建钱包参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.CreateWallet(c.Request.Context(), &req)
if err != nil {
h.logger.Error("创建钱包失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, response, "钱包创建成功")
}
// GetWallet 获取钱包信息
// @Summary 获取钱包信息
// @Description 获取用户钱包信息
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param user_id query string true "用户ID"
// @Success 200 {object} dto.WalletInfo
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/wallet [get]
func (h *FinanceHandler) GetWallet(c *gin.Context) {
userID := c.Query("user_id")
if userID == "" {
h.responseBuilder.BadRequest(c, "用户ID不能为空")
return
}
wallet, err := h.financeService.GetWallet(c.Request.Context(), userID)
if err != nil {
h.logger.Error("获取钱包信息失败", zap.Error(err))
h.responseBuilder.NotFound(c, err.Error())
return
}
h.responseBuilder.Success(c, wallet, "获取钱包信息成功")
}
// UpdateWallet 更新钱包
// @Summary 更新钱包
// @Description 更新用户钱包信息
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.UpdateWalletRequest true "更新钱包请求"
// @Success 200 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/wallet [put]
func (h *FinanceHandler) UpdateWallet(c *gin.Context) {
var req dto.UpdateWalletRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("更新钱包参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
err := h.financeService.UpdateWallet(c.Request.Context(), &req)
if err != nil {
h.logger.Error("更新钱包失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "钱包更新成功")
}
// Recharge 充值
// @Summary 钱包充值
// @Description 为用户钱包充值
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.RechargeRequest true "充值请求"
// @Success 200 {object} dto.RechargeResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/wallet/recharge [post]
func (h *FinanceHandler) Recharge(c *gin.Context) {
var req dto.RechargeRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("充值参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.Recharge(c.Request.Context(), &req)
if err != nil {
h.logger.Error("充值失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, response, "充值成功")
}
// Withdraw 提现
// @Summary 钱包提现
// @Description 从用户钱包提现
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.WithdrawRequest true "提现请求"
// @Success 200 {object} dto.WithdrawResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/wallet/withdraw [post]
func (h *FinanceHandler) Withdraw(c *gin.Context) {
var req dto.WithdrawRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("提现参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.Withdraw(c.Request.Context(), &req)
if err != nil {
h.logger.Error("提现失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, response, "提现成功")
}
// CreateUserSecrets 创建用户密钥
// @Summary 创建用户密钥
// @Description 为用户创建访问密钥
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.CreateUserSecretsRequest true "创建密钥请求"
// @Success 201 {object} dto.CreateUserSecretsResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /finance/secrets [post]
func (h *FinanceHandler) CreateUserSecrets(c *gin.Context) {
var req dto.CreateUserSecretsRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("创建密钥参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.CreateUserSecrets(c.Request.Context(), &req)
if err != nil {
h.logger.Error("创建密钥失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Created(c, response, "密钥创建成功")
}
// GetUserSecrets 获取用户密钥
// @Summary 获取用户密钥
// @Description 获取用户访问密钥信息
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param user_id query string true "用户ID"
// @Success 200 {object} dto.UserSecretsInfo
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/secrets [get]
func (h *FinanceHandler) GetUserSecrets(c *gin.Context) {
userID := c.Query("user_id")
if userID == "" {
h.responseBuilder.BadRequest(c, "用户ID不能为空")
return
}
secrets, err := h.financeService.GetUserSecrets(c.Request.Context(), userID)
if err != nil {
h.logger.Error("获取密钥失败", zap.Error(err))
h.responseBuilder.NotFound(c, err.Error())
return
}
h.responseBuilder.Success(c, secrets, "获取密钥成功")
}
// RegenerateAccessKey 重新生成访问密钥
// @Summary 重新生成访问密钥
// @Description 重新生成用户的访问密钥
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.RegenerateAccessKeyRequest true "重新生成密钥请求"
// @Success 200 {object} dto.RegenerateAccessKeyResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/secrets/regenerate [post]
func (h *FinanceHandler) RegenerateAccessKey(c *gin.Context) {
var req dto.RegenerateAccessKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("重新生成密钥参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.RegenerateAccessKey(c.Request.Context(), &req)
if err != nil {
h.logger.Error("重新生成密钥失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, response, "密钥重新生成成功")
}
// DeactivateUserSecrets 停用用户密钥
// @Summary 停用用户密钥
// @Description 停用用户的访问密钥
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.DeactivateUserSecretsRequest true "停用密钥请求"
// @Success 200 {object} interfaces.SuccessResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/secrets/deactivate [post]
func (h *FinanceHandler) DeactivateUserSecrets(c *gin.Context) {
var req dto.DeactivateUserSecretsRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("停用密钥参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
err := h.financeService.DeactivateUserSecrets(c.Request.Context(), &req)
if err != nil {
h.logger.Error("停用密钥失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "密钥停用成功")
}
// WalletTransaction 钱包交易
// @Summary 钱包交易
// @Description 用户间钱包转账
// @Tags 财务系统
// @Accept json
// @Produce json
// @Param request body dto.WalletTransactionRequest true "交易请求"
// @Success 200 {object} dto.WalletTransactionResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Failure 404 {object} interfaces.ErrorResponse
// @Router /finance/wallet/transaction [post]
func (h *FinanceHandler) WalletTransaction(c *gin.Context) {
var req dto.WalletTransactionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("交易参数验证失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
response, err := h.financeService.WalletTransaction(c.Request.Context(), &req)
if err != nil {
h.logger.Error("交易失败", zap.Error(err))
h.responseBuilder.BadRequest(c, err.Error())
return
}
h.responseBuilder.Success(c, response, "交易成功")
}
// GetWalletStats 获取钱包统计
// @Summary 获取钱包统计
// @Description 获取钱包系统统计信息
// @Tags 财务系统
// @Accept json
// @Produce json
// @Success 200 {object} dto.WalletStatsResponse
// @Failure 400 {object} interfaces.ErrorResponse
// @Router /finance/wallet/stats [get]
func (h *FinanceHandler) GetWalletStats(c *gin.Context) {
stats, err := h.financeService.GetWalletStats(c.Request.Context())
if err != nil {
h.logger.Error("获取钱包统计失败", zap.Error(err))
h.responseBuilder.InternalError(c, "获取统计信息失败")
return
}
h.responseBuilder.Success(c, stats, "获取统计信息成功")
}

View File

@@ -0,0 +1,46 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
)
// WalletRepository 钱包仓储接口
type WalletRepository interface {
interfaces.Repository[entities.Wallet]
// 钱包管理
FindByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
ExistsByUserID(ctx context.Context, userID string) (bool, error)
// 余额操作
UpdateBalance(ctx context.Context, userID string, balance interface{}) error
AddBalance(ctx context.Context, userID string, amount interface{}) error
SubtractBalance(ctx context.Context, userID string, amount interface{}) error
// 统计查询
GetTotalBalance(ctx context.Context) (interface{}, error)
GetActiveWalletCount(ctx context.Context) (int64, error)
}
// UserSecretsRepository 用户密钥仓储接口
type UserSecretsRepository interface {
interfaces.Repository[entities.UserSecrets]
// 密钥管理
FindByUserID(ctx context.Context, userID string) (*entities.UserSecrets, error)
FindByAccessID(ctx context.Context, accessID string) (*entities.UserSecrets, error)
ExistsByUserID(ctx context.Context, userID string) (bool, error)
ExistsByAccessID(ctx context.Context, accessID string) (bool, error)
// 密钥操作
UpdateLastUsedAt(ctx context.Context, accessID string) error
DeactivateByUserID(ctx context.Context, userID string) error
RegenerateAccessKey(ctx context.Context, userID string, accessID, accessKey string) error
// 过期密钥清理
GetExpiredSecrets(ctx context.Context) ([]entities.UserSecrets, error)
DeleteExpiredSecrets(ctx context.Context) error
}

View File

@@ -0,0 +1,410 @@
package repositories
import (
"context"
"time"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"gorm.io/gorm"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
)
// GormWalletRepository 钱包GORM仓储实现
type GormWalletRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormWalletRepository 创建钱包GORM仓储
func NewGormWalletRepository(db *gorm.DB, logger *zap.Logger) *GormWalletRepository {
return &GormWalletRepository{
db: db,
logger: logger,
}
}
// Create 创建钱包
func (r *GormWalletRepository) Create(ctx context.Context, wallet entities.Wallet) error {
r.logger.Info("创建钱包", zap.String("user_id", wallet.UserID))
return r.db.WithContext(ctx).Create(&wallet).Error
}
// GetByID 根据ID获取钱包
func (r *GormWalletRepository) GetByID(ctx context.Context, id string) (entities.Wallet, error) {
var wallet entities.Wallet
err := r.db.WithContext(ctx).Where("id = ?", id).First(&wallet).Error
return wallet, err
}
// Update 更新钱包
func (r *GormWalletRepository) Update(ctx context.Context, wallet entities.Wallet) error {
r.logger.Info("更新钱包", zap.String("id", wallet.ID))
return r.db.WithContext(ctx).Save(&wallet).Error
}
// Delete 删除钱包
func (r *GormWalletRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除钱包", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Wallet{}, "id = ?", id).Error
}
// SoftDelete 软删除钱包
func (r *GormWalletRepository) SoftDelete(ctx context.Context, id string) error {
r.logger.Info("软删除钱包", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.Wallet{}, "id = ?", id).Error
}
// Restore 恢复钱包
func (r *GormWalletRepository) Restore(ctx context.Context, id string) error {
r.logger.Info("恢复钱包", zap.String("id", id))
return r.db.WithContext(ctx).Unscoped().Model(&entities.Wallet{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// Count 统计钱包数量
func (r *GormWalletRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.Wallet{})
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ?", "%"+options.Search+"%")
}
return count, query.Count(&count).Error
}
// Exists 检查钱包是否存在
func (r *GormWalletRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// CreateBatch 批量创建钱包
func (r *GormWalletRepository) CreateBatch(ctx context.Context, wallets []entities.Wallet) error {
r.logger.Info("批量创建钱包", zap.Int("count", len(wallets)))
return r.db.WithContext(ctx).Create(&wallets).Error
}
// GetByIDs 根据ID列表获取钱包
func (r *GormWalletRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Wallet, error) {
var wallets []entities.Wallet
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&wallets).Error
return wallets, err
}
// UpdateBatch 批量更新钱包
func (r *GormWalletRepository) UpdateBatch(ctx context.Context, wallets []entities.Wallet) error {
r.logger.Info("批量更新钱包", zap.Int("count", len(wallets)))
return r.db.WithContext(ctx).Save(&wallets).Error
}
// DeleteBatch 批量删除钱包
func (r *GormWalletRepository) DeleteBatch(ctx context.Context, ids []string) error {
r.logger.Info("批量删除钱包", zap.Strings("ids", ids))
return r.db.WithContext(ctx).Delete(&entities.Wallet{}, "id IN ?", ids).Error
}
// List 获取钱包列表
func (r *GormWalletRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Wallet, error) {
var wallets []entities.Wallet
query := r.db.WithContext(ctx).Model(&entities.Wallet{})
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ?", "%"+options.Search+"%")
}
if options.Sort != "" {
order := "ASC"
if options.Order != "" {
order = options.Order
}
query = query.Order(options.Sort + " " + order)
}
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
return wallets, query.Find(&wallets).Error
}
// WithTx 使用事务
func (r *GormWalletRepository) WithTx(tx interface{}) interfaces.Repository[entities.Wallet] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormWalletRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}
// FindByUserID 根据用户ID查找钱包
func (r *GormWalletRepository) FindByUserID(ctx context.Context, userID string) (*entities.Wallet, error) {
var wallet entities.Wallet
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&wallet).Error
if err != nil {
return nil, err
}
return &wallet, nil
}
// ExistsByUserID 检查用户钱包是否存在
func (r *GormWalletRepository) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("user_id = ?", userID).Count(&count).Error
return count > 0, err
}
// UpdateBalance 更新余额
func (r *GormWalletRepository) UpdateBalance(ctx context.Context, userID string, balance interface{}) error {
return r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("user_id = ?", userID).Update("balance", balance).Error
}
// AddBalance 增加余额
func (r *GormWalletRepository) AddBalance(ctx context.Context, userID string, amount interface{}) error {
return r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("user_id = ?", userID).Update("balance", gorm.Expr("balance + ?", amount)).Error
}
// SubtractBalance 减少余额
func (r *GormWalletRepository) SubtractBalance(ctx context.Context, userID string, amount interface{}) error {
return r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("user_id = ?", userID).Update("balance", gorm.Expr("balance - ?", amount)).Error
}
// GetTotalBalance 获取总余额
func (r *GormWalletRepository) GetTotalBalance(ctx context.Context) (interface{}, error) {
var total decimal.Decimal
err := r.db.WithContext(ctx).Model(&entities.Wallet{}).Select("COALESCE(SUM(balance), 0)").Scan(&total).Error
return total, err
}
// GetActiveWalletCount 获取激活钱包数量
func (r *GormWalletRepository) GetActiveWalletCount(ctx context.Context) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.Wallet{}).Where("is_active = ?", true).Count(&count).Error
return count, err
}
// GormUserSecretsRepository 用户密钥GORM仓储实现
type GormUserSecretsRepository struct {
db *gorm.DB
logger *zap.Logger
}
// NewGormUserSecretsRepository 创建用户密钥GORM仓储
func NewGormUserSecretsRepository(db *gorm.DB, logger *zap.Logger) *GormUserSecretsRepository {
return &GormUserSecretsRepository{
db: db,
logger: logger,
}
}
// Create 创建用户密钥
func (r *GormUserSecretsRepository) Create(ctx context.Context, secrets entities.UserSecrets) error {
r.logger.Info("创建用户密钥", zap.String("user_id", secrets.UserID))
return r.db.WithContext(ctx).Create(&secrets).Error
}
// GetByID 根据ID获取用户密钥
func (r *GormUserSecretsRepository) GetByID(ctx context.Context, id string) (entities.UserSecrets, error) {
var secrets entities.UserSecrets
err := r.db.WithContext(ctx).Where("id = ?", id).First(&secrets).Error
return secrets, err
}
// Update 更新用户密钥
func (r *GormUserSecretsRepository) Update(ctx context.Context, secrets entities.UserSecrets) error {
r.logger.Info("更新用户密钥", zap.String("id", secrets.ID))
return r.db.WithContext(ctx).Save(&secrets).Error
}
// Delete 删除用户密钥
func (r *GormUserSecretsRepository) Delete(ctx context.Context, id string) error {
r.logger.Info("删除用户密钥", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.UserSecrets{}, "id = ?", id).Error
}
// SoftDelete 软删除用户密钥
func (r *GormUserSecretsRepository) SoftDelete(ctx context.Context, id string) error {
r.logger.Info("软删除用户密钥", zap.String("id", id))
return r.db.WithContext(ctx).Delete(&entities.UserSecrets{}, "id = ?", id).Error
}
// Restore 恢复用户密钥
func (r *GormUserSecretsRepository) Restore(ctx context.Context, id string) error {
r.logger.Info("恢复用户密钥", zap.String("id", id))
return r.db.WithContext(ctx).Unscoped().Model(&entities.UserSecrets{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
// Count 统计用户密钥数量
func (r *GormUserSecretsRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.UserSecrets{})
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ? OR access_id LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
return count, query.Count(&count).Error
}
// Exists 检查用户密钥是否存在
func (r *GormUserSecretsRepository) Exists(ctx context.Context, id string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("id = ?", id).Count(&count).Error
return count > 0, err
}
// CreateBatch 批量创建用户密钥
func (r *GormUserSecretsRepository) CreateBatch(ctx context.Context, secrets []entities.UserSecrets) error {
r.logger.Info("批量创建用户密钥", zap.Int("count", len(secrets)))
return r.db.WithContext(ctx).Create(&secrets).Error
}
// GetByIDs 根据ID列表获取用户密钥
func (r *GormUserSecretsRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.UserSecrets, error) {
var secrets []entities.UserSecrets
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&secrets).Error
return secrets, err
}
// UpdateBatch 批量更新用户密钥
func (r *GormUserSecretsRepository) UpdateBatch(ctx context.Context, secrets []entities.UserSecrets) error {
r.logger.Info("批量更新用户密钥", zap.Int("count", len(secrets)))
return r.db.WithContext(ctx).Save(&secrets).Error
}
// DeleteBatch 批量删除用户密钥
func (r *GormUserSecretsRepository) DeleteBatch(ctx context.Context, ids []string) error {
r.logger.Info("批量删除用户密钥", zap.Strings("ids", ids))
return r.db.WithContext(ctx).Delete(&entities.UserSecrets{}, "id IN ?", ids).Error
}
// List 获取用户密钥列表
func (r *GormUserSecretsRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.UserSecrets, error) {
var secrets []entities.UserSecrets
query := r.db.WithContext(ctx).Model(&entities.UserSecrets{})
if options.Filters != nil {
for key, value := range options.Filters {
query = query.Where(key+" = ?", value)
}
}
if options.Search != "" {
query = query.Where("user_id LIKE ? OR access_id LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
}
if options.Sort != "" {
order := "ASC"
if options.Order != "" {
order = options.Order
}
query = query.Order(options.Sort + " " + order)
}
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
return secrets, query.Find(&secrets).Error
}
// WithTx 使用事务
func (r *GormUserSecretsRepository) WithTx(tx interface{}) interfaces.Repository[entities.UserSecrets] {
if gormTx, ok := tx.(*gorm.DB); ok {
return &GormUserSecretsRepository{
db: gormTx,
logger: r.logger,
}
}
return r
}
// FindByUserID 根据用户ID查找密钥
func (r *GormUserSecretsRepository) FindByUserID(ctx context.Context, userID string) (*entities.UserSecrets, error) {
var secrets entities.UserSecrets
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&secrets).Error
if err != nil {
return nil, err
}
return &secrets, nil
}
// FindByAccessID 根据访问ID查找密钥
func (r *GormUserSecretsRepository) FindByAccessID(ctx context.Context, accessID string) (*entities.UserSecrets, error) {
var secrets entities.UserSecrets
err := r.db.WithContext(ctx).Where("access_id = ?", accessID).First(&secrets).Error
if err != nil {
return nil, err
}
return &secrets, nil
}
// ExistsByUserID 检查用户密钥是否存在
func (r *GormUserSecretsRepository) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("user_id = ?", userID).Count(&count).Error
return count > 0, err
}
// ExistsByAccessID 检查访问ID是否存在
func (r *GormUserSecretsRepository) ExistsByAccessID(ctx context.Context, accessID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("access_id = ?", accessID).Count(&count).Error
return count > 0, err
}
// UpdateLastUsedAt 更新最后使用时间
func (r *GormUserSecretsRepository) UpdateLastUsedAt(ctx context.Context, accessID string) error {
return r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("access_id = ?", accessID).Update("last_used_at", time.Now()).Error
}
// DeactivateByUserID 停用用户密钥
func (r *GormUserSecretsRepository) DeactivateByUserID(ctx context.Context, userID string) error {
return r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("user_id = ?", userID).Update("is_active", false).Error
}
// RegenerateAccessKey 重新生成访问密钥
func (r *GormUserSecretsRepository) RegenerateAccessKey(ctx context.Context, userID string, accessID, accessKey string) error {
return r.db.WithContext(ctx).Model(&entities.UserSecrets{}).Where("user_id = ?", userID).Updates(map[string]interface{}{
"access_id": accessID,
"access_key": accessKey,
"updated_at": time.Now(),
}).Error
}
// GetExpiredSecrets 获取过期的密钥
func (r *GormUserSecretsRepository) GetExpiredSecrets(ctx context.Context) ([]entities.UserSecrets, error) {
var secrets []entities.UserSecrets
err := r.db.WithContext(ctx).Where("expires_at IS NOT NULL AND expires_at < ?", time.Now()).Find(&secrets).Error
return secrets, err
}
// DeleteExpiredSecrets 删除过期的密钥
func (r *GormUserSecretsRepository) DeleteExpiredSecrets(ctx context.Context) error {
return r.db.WithContext(ctx).Where("expires_at IS NOT NULL AND expires_at < ?", time.Now()).Delete(&entities.UserSecrets{}).Error
}

View File

@@ -0,0 +1,35 @@
package routes
import (
"github.com/gin-gonic/gin"
"tyapi-server/internal/domains/finance/handlers"
)
// RegisterFinanceRoutes 注册财务路由
func RegisterFinanceRoutes(router *gin.Engine, financeHandler *handlers.FinanceHandler) {
// 财务路由组
financeGroup := router.Group("/api/finance")
{
// 钱包相关路由
walletGroup := financeGroup.Group("/wallet")
{
walletGroup.POST("", financeHandler.CreateWallet) // 创建钱包
walletGroup.GET("", financeHandler.GetWallet) // 获取钱包信息
walletGroup.PUT("", financeHandler.UpdateWallet) // 更新钱包
walletGroup.POST("/recharge", financeHandler.Recharge) // 充值
walletGroup.POST("/withdraw", financeHandler.Withdraw) // 提现
walletGroup.POST("/transaction", financeHandler.WalletTransaction) // 钱包交易
walletGroup.GET("/stats", financeHandler.GetWalletStats) // 获取钱包统计
}
// 用户密钥相关路由
secretsGroup := financeGroup.Group("/secrets")
{
secretsGroup.POST("", financeHandler.CreateUserSecrets) // 创建用户密钥
secretsGroup.GET("", financeHandler.GetUserSecrets) // 获取用户密钥
secretsGroup.POST("/regenerate", financeHandler.RegenerateAccessKey) // 重新生成访问密钥
secretsGroup.POST("/deactivate", financeHandler.DeactivateUserSecrets) // 停用用户密钥
}
}
}

View File

@@ -0,0 +1,470 @@
package services
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"tyapi-server/internal/domains/finance/dto"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/shared/interfaces"
)
// FinanceService 财务服务
type FinanceService struct {
walletRepo repositories.WalletRepository
userSecretsRepo repositories.UserSecretsRepository
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
}
// NewFinanceService 创建财务服务
func NewFinanceService(
walletRepo repositories.WalletRepository,
userSecretsRepo repositories.UserSecretsRepository,
responseBuilder interfaces.ResponseBuilder,
logger *zap.Logger,
) *FinanceService {
return &FinanceService{
walletRepo: walletRepo,
userSecretsRepo: userSecretsRepo,
responseBuilder: responseBuilder,
logger: logger,
}
}
// CreateWallet 创建钱包
func (s *FinanceService) CreateWallet(ctx context.Context, req *dto.CreateWalletRequest) (*dto.CreateWalletResponse, error) {
s.logger.Info("创建钱包", zap.String("user_id", req.UserID))
// 检查用户是否已有钱包
exists, err := s.walletRepo.ExistsByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("检查钱包存在性失败: %w", err)
}
if exists {
return nil, fmt.Errorf("用户已存在钱包")
}
// 创建钱包
wallet := entities.Wallet{
ID: s.generateID(),
UserID: req.UserID,
IsActive: true,
Balance: decimal.Zero,
}
if err := s.walletRepo.Create(ctx, wallet); err != nil {
return nil, fmt.Errorf("创建钱包失败: %w", err)
}
// 构建响应
walletInfo := dto.WalletInfo{
ID: wallet.ID,
UserID: wallet.UserID,
IsActive: wallet.IsActive,
Balance: wallet.Balance,
CreatedAt: wallet.CreatedAt,
UpdatedAt: wallet.UpdatedAt,
}
s.logger.Info("钱包创建成功", zap.String("wallet_id", wallet.ID))
return &dto.CreateWalletResponse{Wallet: walletInfo}, nil
}
// GetWallet 获取钱包信息
func (s *FinanceService) GetWallet(ctx context.Context, userID string) (*dto.WalletInfo, error) {
s.logger.Info("获取钱包信息", zap.String("user_id", userID))
wallet, err := s.walletRepo.FindByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("钱包不存在")
}
walletInfo := dto.WalletInfo{
ID: wallet.ID,
UserID: wallet.UserID,
IsActive: wallet.IsActive,
Balance: wallet.Balance,
CreatedAt: wallet.CreatedAt,
UpdatedAt: wallet.UpdatedAt,
}
return &walletInfo, nil
}
// UpdateWallet 更新钱包
func (s *FinanceService) UpdateWallet(ctx context.Context, req *dto.UpdateWalletRequest) error {
s.logger.Info("更新钱包", zap.String("user_id", req.UserID))
wallet, err := s.walletRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return fmt.Errorf("钱包不存在")
}
// 更新字段
if !req.Balance.IsZero() {
wallet.Balance = req.Balance
}
if req.IsActive != nil {
wallet.IsActive = *req.IsActive
}
if err := s.walletRepo.Update(ctx, *wallet); err != nil {
return fmt.Errorf("更新钱包失败: %w", err)
}
s.logger.Info("钱包更新成功", zap.String("user_id", req.UserID))
return nil
}
// Recharge 充值
func (s *FinanceService) Recharge(ctx context.Context, req *dto.RechargeRequest) (*dto.RechargeResponse, error) {
s.logger.Info("钱包充值", zap.String("user_id", req.UserID), zap.String("amount", req.Amount.String()))
// 验证金额
if req.Amount.LessThanOrEqual(decimal.Zero) {
return nil, fmt.Errorf("充值金额必须大于0")
}
// 获取钱包
wallet, err := s.walletRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("钱包不存在")
}
// 检查钱包状态
if !wallet.IsActive {
return nil, fmt.Errorf("钱包已被禁用")
}
// 增加余额
if err := s.walletRepo.AddBalance(ctx, req.UserID, req.Amount); err != nil {
return nil, fmt.Errorf("充值失败: %w", err)
}
// 获取更新后的余额
updatedWallet, err := s.walletRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("获取更新后余额失败: %w", err)
}
s.logger.Info("充值成功", zap.String("user_id", req.UserID), zap.String("amount", req.Amount.String()))
return &dto.RechargeResponse{
WalletID: updatedWallet.ID,
Amount: req.Amount,
Balance: updatedWallet.Balance,
}, nil
}
// Withdraw 提现
func (s *FinanceService) Withdraw(ctx context.Context, req *dto.WithdrawRequest) (*dto.WithdrawResponse, error) {
s.logger.Info("钱包提现", zap.String("user_id", req.UserID), zap.String("amount", req.Amount.String()))
// 验证金额
if req.Amount.LessThanOrEqual(decimal.Zero) {
return nil, fmt.Errorf("提现金额必须大于0")
}
// 获取钱包
wallet, err := s.walletRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("钱包不存在")
}
// 检查钱包状态
if !wallet.IsActive {
return nil, fmt.Errorf("钱包已被禁用")
}
// 检查余额是否足够
if wallet.Balance.LessThan(req.Amount) {
return nil, fmt.Errorf("余额不足")
}
// 减少余额
if err := s.walletRepo.SubtractBalance(ctx, req.UserID, req.Amount); err != nil {
return nil, fmt.Errorf("提现失败: %w", err)
}
// 获取更新后的余额
updatedWallet, err := s.walletRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("获取更新后余额失败: %w", err)
}
s.logger.Info("提现成功", zap.String("user_id", req.UserID), zap.String("amount", req.Amount.String()))
return &dto.WithdrawResponse{
WalletID: updatedWallet.ID,
Amount: req.Amount,
Balance: updatedWallet.Balance,
}, nil
}
// CreateUserSecrets 创建用户密钥
func (s *FinanceService) CreateUserSecrets(ctx context.Context, req *dto.CreateUserSecretsRequest) (*dto.CreateUserSecretsResponse, error) {
s.logger.Info("创建用户密钥", zap.String("user_id", req.UserID))
// 检查用户是否已有密钥
exists, err := s.userSecretsRepo.ExistsByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("检查密钥存在性失败: %w", err)
}
if exists {
return nil, fmt.Errorf("用户已存在密钥")
}
// 生成访问ID和密钥
accessID := s.generateAccessID()
accessKey := s.generateAccessKey()
// 创建密钥
secrets := entities.UserSecrets{
ID: s.generateID(),
UserID: req.UserID,
AccessID: accessID,
AccessKey: accessKey,
IsActive: true,
ExpiresAt: req.ExpiresAt,
}
if err := s.userSecretsRepo.Create(ctx, secrets); err != nil {
return nil, fmt.Errorf("创建密钥失败: %w", err)
}
// 构建响应
secretsInfo := dto.UserSecretsInfo{
ID: secrets.ID,
UserID: secrets.UserID,
AccessID: secrets.AccessID,
AccessKey: secrets.AccessKey,
IsActive: secrets.IsActive,
LastUsedAt: secrets.LastUsedAt,
ExpiresAt: secrets.ExpiresAt,
CreatedAt: secrets.CreatedAt,
UpdatedAt: secrets.UpdatedAt,
}
s.logger.Info("用户密钥创建成功", zap.String("user_id", req.UserID))
return &dto.CreateUserSecretsResponse{Secrets: secretsInfo}, nil
}
// GetUserSecrets 获取用户密钥
func (s *FinanceService) GetUserSecrets(ctx context.Context, userID string) (*dto.UserSecretsInfo, error) {
s.logger.Info("获取用户密钥", zap.String("user_id", userID))
secrets, err := s.userSecretsRepo.FindByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("密钥不存在")
}
secretsInfo := dto.UserSecretsInfo{
ID: secrets.ID,
UserID: secrets.UserID,
AccessID: secrets.AccessID,
AccessKey: secrets.AccessKey,
IsActive: secrets.IsActive,
LastUsedAt: secrets.LastUsedAt,
ExpiresAt: secrets.ExpiresAt,
CreatedAt: secrets.CreatedAt,
UpdatedAt: secrets.UpdatedAt,
}
return &secretsInfo, nil
}
// RegenerateAccessKey 重新生成访问密钥
func (s *FinanceService) RegenerateAccessKey(ctx context.Context, req *dto.RegenerateAccessKeyRequest) (*dto.RegenerateAccessKeyResponse, error) {
s.logger.Info("重新生成访问密钥", zap.String("user_id", req.UserID))
// 检查密钥是否存在
secrets, err := s.userSecretsRepo.FindByUserID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("密钥不存在")
}
// 生成新的访问ID和密钥
newAccessID := s.generateAccessID()
newAccessKey := s.generateAccessKey()
// 更新密钥
if err := s.userSecretsRepo.RegenerateAccessKey(ctx, req.UserID, newAccessID, newAccessKey); err != nil {
return nil, fmt.Errorf("重新生成密钥失败: %w", err)
}
// 更新过期时间
if req.ExpiresAt != nil {
secrets.ExpiresAt = req.ExpiresAt
if err := s.userSecretsRepo.Update(ctx, *secrets); err != nil {
s.logger.Error("更新密钥过期时间失败", zap.Error(err))
}
}
s.logger.Info("访问密钥重新生成成功", zap.String("user_id", req.UserID))
return &dto.RegenerateAccessKeyResponse{
AccessID: newAccessID,
AccessKey: newAccessKey,
}, nil
}
// DeactivateUserSecrets 停用用户密钥
func (s *FinanceService) DeactivateUserSecrets(ctx context.Context, req *dto.DeactivateUserSecretsRequest) error {
s.logger.Info("停用用户密钥", zap.String("user_id", req.UserID))
// 检查密钥是否存在
if _, err := s.userSecretsRepo.FindByUserID(ctx, req.UserID); err != nil {
return fmt.Errorf("密钥不存在")
}
// 停用密钥
if err := s.userSecretsRepo.DeactivateByUserID(ctx, req.UserID); err != nil {
return fmt.Errorf("停用密钥失败: %w", err)
}
s.logger.Info("用户密钥停用成功", zap.String("user_id", req.UserID))
return nil
}
// WalletTransaction 钱包交易
func (s *FinanceService) WalletTransaction(ctx context.Context, req *dto.WalletTransactionRequest) (*dto.WalletTransactionResponse, error) {
s.logger.Info("钱包交易",
zap.String("from_user_id", req.FromUserID),
zap.String("to_user_id", req.ToUserID),
zap.String("amount", req.Amount.String()))
// 验证金额
if req.Amount.LessThanOrEqual(decimal.Zero) {
return nil, fmt.Errorf("交易金额必须大于0")
}
// 验证用户不能给自己转账
if req.FromUserID == req.ToUserID {
return nil, fmt.Errorf("不能给自己转账")
}
// 获取转出钱包
fromWallet, err := s.walletRepo.FindByUserID(ctx, req.FromUserID)
if err != nil {
return nil, fmt.Errorf("转出钱包不存在")
}
// 获取转入钱包
toWallet, err := s.walletRepo.FindByUserID(ctx, req.ToUserID)
if err != nil {
return nil, fmt.Errorf("转入钱包不存在")
}
// 检查钱包状态
if !fromWallet.IsActive {
return nil, fmt.Errorf("转出钱包已被禁用")
}
if !toWallet.IsActive {
return nil, fmt.Errorf("转入钱包已被禁用")
}
// 检查余额是否足够
if fromWallet.Balance.LessThan(req.Amount) {
return nil, fmt.Errorf("余额不足")
}
// 执行交易(使用事务)
// 这里简化处理,实际应该使用数据库事务
if err := s.walletRepo.SubtractBalance(ctx, req.FromUserID, req.Amount); err != nil {
return nil, fmt.Errorf("扣款失败: %w", err)
}
if err := s.walletRepo.AddBalance(ctx, req.ToUserID, req.Amount); err != nil {
return nil, fmt.Errorf("入账失败: %w", err)
}
// 获取更新后的余额
updatedFromWallet, err := s.walletRepo.FindByUserID(ctx, req.FromUserID)
if err != nil {
return nil, fmt.Errorf("获取转出后余额失败: %w", err)
}
updatedToWallet, err := s.walletRepo.FindByUserID(ctx, req.ToUserID)
if err != nil {
return nil, fmt.Errorf("获取转入后余额失败: %w", err)
}
s.logger.Info("钱包交易成功",
zap.String("from_user_id", req.FromUserID),
zap.String("to_user_id", req.ToUserID),
zap.String("amount", req.Amount.String()))
return &dto.WalletTransactionResponse{
TransactionID: s.generateID(),
FromUserID: req.FromUserID,
ToUserID: req.ToUserID,
Amount: req.Amount,
FromBalance: updatedFromWallet.Balance,
ToBalance: updatedToWallet.Balance,
Notes: req.Notes,
CreatedAt: time.Now(),
}, nil
}
// GetWalletStats 获取钱包统计
func (s *FinanceService) GetWalletStats(ctx context.Context) (*dto.WalletStatsResponse, error) {
s.logger.Info("获取钱包统计")
// 获取总钱包数
totalWallets, err := s.walletRepo.Count(ctx, interfaces.CountOptions{})
if err != nil {
return nil, fmt.Errorf("获取总钱包数失败: %w", err)
}
// 获取激活钱包数
activeWallets, err := s.walletRepo.GetActiveWalletCount(ctx)
if err != nil {
return nil, fmt.Errorf("获取激活钱包数失败: %w", err)
}
// 获取总余额
totalBalance, err := s.walletRepo.GetTotalBalance(ctx)
if err != nil {
return nil, fmt.Errorf("获取总余额失败: %w", err)
}
// 这里简化处理,实际应该查询交易记录表
todayTransactions := int64(0)
todayVolume := decimal.Zero
return &dto.WalletStatsResponse{
TotalWallets: totalWallets,
ActiveWallets: activeWallets,
TotalBalance: totalBalance.(decimal.Decimal),
TodayTransactions: todayTransactions,
TodayVolume: todayVolume,
}, nil
}
// generateID 生成ID
func (s *FinanceService) generateID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// generateAccessID 生成访问ID
func (s *FinanceService) generateAccessID() string {
bytes := make([]byte, 20)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// generateAccessKey 生成访问密钥
func (s *FinanceService) generateAccessKey() string {
bytes := make([]byte, 32)
rand.Read(bytes)
hash := sha256.Sum256(bytes)
return hex.EncodeToString(hash[:])
}

View File

@@ -34,6 +34,12 @@ type ChangePasswordRequest struct {
Code string `json:"code" binding:"required,len=6" example:"123456"`
}
// UpdateProfileRequest 更新用户信息请求
type UpdateProfileRequest struct {
Phone string `json:"phone" binding:"omitempty,len=11" example:"13800138000"`
// 可以在这里添加更多用户信息字段,如昵称、头像等
}
// UserResponse 用户响应
type UserResponse struct {
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`

View File

@@ -6,50 +6,58 @@ import (
"gorm.io/gorm"
)
// SMSCode 短信验证码记录
// SMSCode 短信验证码记录实体
// 记录用户发送的所有短信验证码,支持多种使用场景
// 包含验证码的有效期管理、使用状态跟踪、安全审计等功能
type SMSCode struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Phone string `gorm:"type:varchar(20);not null;index" json:"phone"`
Code string `gorm:"type:varchar(10);not null" json:"-"` // 不返回给前端
Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene"`
Used bool `gorm:"default:false" json:"used"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
UsedAt *time.Time `json:"used_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"短信验证码记录唯一标识"`
Phone string `gorm:"type:varchar(20);not null;index" json:"phone" comment:"接收手机号"`
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:"过期时间"`
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:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
// 额外信息
IP string `gorm:"type:varchar(45)" json:"ip"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent"`
// 额外信息 - 安全审计相关数据
IP string `gorm:"type:varchar(45)" json:"ip" comment:"发送IP地址"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent" comment:"客户端信息"`
}
// SMSScene 短信验证码使用场景
// SMSScene 短信验证码使用场景枚举
// 定义系统中所有需要使用短信验证码的业务场景
type SMSScene string
const (
SMSSceneRegister SMSScene = "register" // 注册
SMSSceneLogin SMSScene = "login" // 登录
SMSSceneChangePassword SMSScene = "change_password" // 修改密码
SMSSceneResetPassword SMSScene = "reset_password" // 重置密码
SMSSceneBind SMSScene = "bind" // 绑定手机号
SMSSceneUnbind SMSScene = "unbind" // 解绑手机号
SMSSceneRegister SMSScene = "register" // 注册 - 新用户注册验证
SMSSceneLogin SMSScene = "login" // 登录 - 手机号登录验证
SMSSceneChangePassword SMSScene = "change_password" // 修改密码 - 修改密码验证
SMSSceneResetPassword SMSScene = "reset_password" // 重置密码 - 忘记密码重置
SMSSceneBind SMSScene = "bind" // 绑定手机号 - 绑定新手机号
SMSSceneUnbind SMSScene = "unbind" // 解绑手机号 - 解绑当前手机号
)
// 实现 Entity 接口
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (s *SMSCode) GetID() string {
return s.ID
}
// GetCreatedAt 获取创建时间
func (s *SMSCode) GetCreatedAt() time.Time {
return s.CreatedAt
}
// GetUpdatedAt 获取更新时间
func (s *SMSCode) GetUpdatedAt() time.Time {
return s.UpdatedAt
}
// Validate 验证短信验证码
// 检查短信验证码记录的必填字段是否完整,确保数据的有效性
func (s *SMSCode) Validate() error {
if s.Phone == "" {
return &ValidationError{Message: "手机号不能为空"}
@@ -64,24 +72,253 @@ func (s *SMSCode) Validate() error {
return &ValidationError{Message: "过期时间不能为空"}
}
// 验证手机号格式
if !IsValidPhoneFormat(s.Phone) {
return &ValidationError{Message: "手机号格式无效"}
}
// 验证验证码格式
if err := s.validateCodeFormat(); err != nil {
return err
}
return nil
}
// 业务方法
func (s *SMSCode) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
// ================ 业务方法 ================
// VerifyCode 验证验证码
// 检查输入的验证码是否匹配且有效
func (s *SMSCode) VerifyCode(inputCode string) error {
// 1. 检查验证码是否已使用
if s.Used {
return &ValidationError{Message: "验证码已被使用"}
}
// 2. 检查验证码是否已过期
if s.IsExpired() {
return &ValidationError{Message: "验证码已过期"}
}
// 3. 检查验证码是否匹配
if s.Code != inputCode {
return &ValidationError{Message: "验证码错误"}
}
// 4. 标记为已使用
s.MarkAsUsed()
return nil
}
// IsExpired 检查验证码是否已过期
// 判断当前时间是否超过验证码的有效期
func (s *SMSCode) IsExpired() bool {
return time.Now().After(s.ExpiresAt) || time.Now().Equal(s.ExpiresAt)
}
// IsValid 检查验证码是否有效
// 综合判断验证码是否可用,包括未使用和未过期两个条件
func (s *SMSCode) IsValid() bool {
return !s.Used && !s.IsExpired()
}
// MarkAsUsed 标记验证码为已使用
// 在验证码被成功使用后调用,记录使用时间并标记状态
func (s *SMSCode) MarkAsUsed() {
s.Used = true
now := time.Now()
s.UsedAt = &now
}
// CanResend 检查是否可以重新发送验证码
// 基于时间间隔和场景判断是否允许重新发送
func (s *SMSCode) CanResend(minInterval time.Duration) bool {
// 如果验证码已使用或已过期,可以重新发送
if s.Used || s.IsExpired() {
return true
}
// 检查距离上次发送的时间间隔
timeSinceCreated := time.Since(s.CreatedAt)
return timeSinceCreated >= minInterval
}
// GetRemainingTime 获取验证码剩余有效时间
func (s *SMSCode) GetRemainingTime() time.Duration {
if s.IsExpired() {
return 0
}
return s.ExpiresAt.Sub(time.Now())
}
// IsRecentlySent 检查是否最近发送过验证码
func (s *SMSCode) IsRecentlySent(within time.Duration) bool {
return time.Since(s.CreatedAt) < within
}
// GetMaskedCode 获取脱敏的验证码(用于日志记录)
func (s *SMSCode) GetMaskedCode() string {
if len(s.Code) < 3 {
return "***"
}
return s.Code[:1] + "***" + s.Code[len(s.Code)-1:]
}
// GetMaskedPhone 获取脱敏的手机号
func (s *SMSCode) GetMaskedPhone() string {
if len(s.Phone) < 7 {
return s.Phone
}
return s.Phone[:3] + "****" + s.Phone[len(s.Phone)-4:]
}
// ================ 场景相关方法 ================
// IsSceneValid 检查场景是否有效
func (s *SMSCode) IsSceneValid() bool {
validScenes := []SMSScene{
SMSSceneRegister,
SMSSceneLogin,
SMSSceneChangePassword,
SMSSceneResetPassword,
SMSSceneBind,
SMSSceneUnbind,
}
for _, scene := range validScenes {
if s.Scene == scene {
return true
}
}
return false
}
// GetSceneName 获取场景的中文名称
func (s *SMSCode) GetSceneName() string {
sceneNames := map[SMSScene]string{
SMSSceneRegister: "用户注册",
SMSSceneLogin: "用户登录",
SMSSceneChangePassword: "修改密码",
SMSSceneResetPassword: "重置密码",
SMSSceneBind: "绑定手机号",
SMSSceneUnbind: "解绑手机号",
}
if name, exists := sceneNames[s.Scene]; exists {
return name
}
return string(s.Scene)
}
// ================ 安全相关方法 ================
// IsSuspicious 检查是否存在可疑行为
func (s *SMSCode) IsSuspicious() bool {
// 检查IP地址是否为空可能表示异常
if s.IP == "" {
return true
}
// 检查UserAgent是否为空可能表示异常
if s.UserAgent == "" {
return true
}
// 可以添加更多安全检查逻辑
// 例如检查IP是否来自异常地区、UserAgent是否异常等
return false
}
// GetSecurityInfo 获取安全信息摘要
func (s *SMSCode) GetSecurityInfo() map[string]interface{} {
return map[string]interface{}{
"ip": s.IP,
"user_agent": s.UserAgent,
"suspicious": s.IsSuspicious(),
"scene": s.GetSceneName(),
"created_at": s.CreatedAt,
}
}
// ================ 私有辅助方法 ================
// validateCodeFormat 验证验证码格式
func (s *SMSCode) validateCodeFormat() error {
// 检查验证码长度
if len(s.Code) < 4 || len(s.Code) > 10 {
return &ValidationError{Message: "验证码长度必须在4-10位之间"}
}
// 检查验证码是否只包含数字
for _, char := range s.Code {
if char < '0' || char > '9' {
return &ValidationError{Message: "验证码只能包含数字"}
}
}
return nil
}
// ================ 静态工具方法 ================
// IsValidScene 检查场景是否有效(静态方法)
func IsValidScene(scene SMSScene) bool {
validScenes := []SMSScene{
SMSSceneRegister,
SMSSceneLogin,
SMSSceneChangePassword,
SMSSceneResetPassword,
SMSSceneBind,
SMSSceneUnbind,
}
for _, validScene := range validScenes {
if scene == validScene {
return true
}
}
return false
}
// GetSceneName 获取场景的中文名称(静态方法)
func GetSceneName(scene SMSScene) string {
sceneNames := map[SMSScene]string{
SMSSceneRegister: "用户注册",
SMSSceneLogin: "用户登录",
SMSSceneChangePassword: "修改密码",
SMSSceneResetPassword: "重置密码",
SMSSceneBind: "绑定手机号",
SMSSceneUnbind: "解绑手机号",
}
if name, exists := sceneNames[scene]; exists {
return name
}
return string(scene)
}
// NewSMSCode 创建新的短信验证码(工厂方法)
func NewSMSCode(phone, code string, scene SMSScene, expireTime time.Duration, clientIP, userAgent string) (*SMSCode, error) {
smsCode := &SMSCode{
Phone: phone,
Code: code,
Scene: scene,
Used: false,
ExpiresAt: time.Now().Add(expireTime),
IP: clientIP,
UserAgent: userAgent,
}
// 验证实体
if err := smsCode.Validate(); err != nil {
return nil, err
}
return smsCode, nil
}
// TableName 指定表名
func (SMSCode) TableName() string {
return "sms_codes"

View File

@@ -0,0 +1,681 @@
package entities
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSMSCode_Validate(t *testing.T) {
tests := []struct {
name string
smsCode *SMSCode
wantErr bool
}{
{
name: "有效验证码",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "123456",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: false,
},
{
name: "手机号为空",
smsCode: &SMSCode{
Phone: "",
Code: "123456",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
{
name: "验证码为空",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
{
name: "场景为空",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "123456",
Scene: "",
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
{
name: "过期时间为零",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "123456",
Scene: SMSSceneRegister,
ExpiresAt: time.Time{},
},
wantErr: true,
},
{
name: "手机号格式无效",
smsCode: &SMSCode{
Phone: "123",
Code: "123456",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
{
name: "验证码格式无效-包含字母",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "12345a",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
{
name: "验证码长度过短",
smsCode: &SMSCode{
Phone: "13800138000",
Code: "123",
Scene: SMSSceneRegister,
ExpiresAt: time.Now().Add(time.Hour),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.smsCode.Validate()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSMSCode_VerifyCode(t *testing.T) {
now := time.Now()
expiresAt := now.Add(time.Hour)
tests := []struct {
name string
smsCode *SMSCode
inputCode string
wantErr bool
}{
{
name: "验证码正确",
smsCode: &SMSCode{
Code: "123456",
Used: false,
ExpiresAt: expiresAt,
},
inputCode: "123456",
wantErr: false,
},
{
name: "验证码错误",
smsCode: &SMSCode{
Code: "123456",
Used: false,
ExpiresAt: expiresAt,
},
inputCode: "654321",
wantErr: true,
},
{
name: "验证码已使用",
smsCode: &SMSCode{
Code: "123456",
Used: true,
ExpiresAt: expiresAt,
},
inputCode: "123456",
wantErr: true,
},
{
name: "验证码已过期",
smsCode: &SMSCode{
Code: "123456",
Used: false,
ExpiresAt: now.Add(-time.Hour),
},
inputCode: "123456",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.smsCode.VerifyCode(tt.inputCode)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// 验证码正确时应该被标记为已使用
assert.True(t, tt.smsCode.Used)
assert.NotNil(t, tt.smsCode.UsedAt)
}
})
}
}
func TestSMSCode_IsExpired(t *testing.T) {
now := time.Now()
tests := []struct {
name string
smsCode *SMSCode
expected bool
}{
{
name: "未过期",
smsCode: &SMSCode{
ExpiresAt: now.Add(time.Hour),
},
expected: false,
},
{
name: "已过期",
smsCode: &SMSCode{
ExpiresAt: now.Add(-time.Hour),
},
expected: true,
},
{
name: "刚好过期",
smsCode: &SMSCode{
ExpiresAt: now,
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.smsCode.IsExpired()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_IsValid(t *testing.T) {
now := time.Now()
tests := []struct {
name string
smsCode *SMSCode
expected bool
}{
{
name: "有效验证码",
smsCode: &SMSCode{
Used: false,
ExpiresAt: now.Add(time.Hour),
},
expected: true,
},
{
name: "已使用",
smsCode: &SMSCode{
Used: true,
ExpiresAt: now.Add(time.Hour),
},
expected: false,
},
{
name: "已过期",
smsCode: &SMSCode{
Used: false,
ExpiresAt: now.Add(-time.Hour),
},
expected: false,
},
{
name: "已使用且已过期",
smsCode: &SMSCode{
Used: true,
ExpiresAt: now.Add(-time.Hour),
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.smsCode.IsValid()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_CanResend(t *testing.T) {
now := time.Now()
minInterval := 60 * time.Second
tests := []struct {
name string
smsCode *SMSCode
expected bool
}{
{
name: "已使用-可以重发",
smsCode: &SMSCode{
Used: true,
CreatedAt: now.Add(-30 * time.Second),
},
expected: true,
},
{
name: "已过期-可以重发",
smsCode: &SMSCode{
Used: false,
ExpiresAt: now.Add(-time.Hour),
CreatedAt: now.Add(-30 * time.Second),
},
expected: true,
},
{
name: "未过期且未使用-间隔足够-可以重发",
smsCode: &SMSCode{
Used: false,
ExpiresAt: now.Add(time.Hour),
CreatedAt: now.Add(-2 * time.Minute),
},
expected: true,
},
{
name: "未过期且未使用-间隔不足-不能重发",
smsCode: &SMSCode{
Used: false,
ExpiresAt: now.Add(time.Hour),
CreatedAt: now.Add(-30 * time.Second),
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.smsCode.CanResend(minInterval)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_GetRemainingTime(t *testing.T) {
now := time.Now()
tests := []struct {
name string
smsCode *SMSCode
expected time.Duration
}{
{
name: "未过期",
smsCode: &SMSCode{
ExpiresAt: now.Add(time.Hour),
},
expected: time.Hour,
},
{
name: "已过期",
smsCode: &SMSCode{
ExpiresAt: now.Add(-time.Hour),
},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.smsCode.GetRemainingTime()
// 由于时间计算可能有微小差异,我们检查是否在合理范围内
if tt.expected > 0 {
assert.True(t, result > 0)
assert.True(t, result <= tt.expected)
} else {
assert.Equal(t, tt.expected, result)
}
})
}
}
func TestSMSCode_GetMaskedCode(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "6位验证码",
code: "123456",
expected: "1***6",
},
{
name: "4位验证码",
code: "1234",
expected: "1***4",
},
{
name: "短验证码",
code: "12",
expected: "***",
},
{
name: "单字符",
code: "1",
expected: "***",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
smsCode := &SMSCode{Code: tt.code}
result := smsCode.GetMaskedCode()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_GetMaskedPhone(t *testing.T) {
tests := []struct {
name string
phone string
expected string
}{
{
name: "标准手机号",
phone: "13800138000",
expected: "138****8000",
},
{
name: "短手机号",
phone: "138001",
expected: "138001",
},
{
name: "空手机号",
phone: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
smsCode := &SMSCode{Phone: tt.phone}
result := smsCode.GetMaskedPhone()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_IsSceneValid(t *testing.T) {
tests := []struct {
name string
scene SMSScene
expected bool
}{
{
name: "注册场景",
scene: SMSSceneRegister,
expected: true,
},
{
name: "登录场景",
scene: SMSSceneLogin,
expected: true,
},
{
name: "无效场景",
scene: "invalid",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
smsCode := &SMSCode{Scene: tt.scene}
result := smsCode.IsSceneValid()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_GetSceneName(t *testing.T) {
tests := []struct {
name string
scene SMSScene
expected string
}{
{
name: "注册场景",
scene: SMSSceneRegister,
expected: "用户注册",
},
{
name: "登录场景",
scene: SMSSceneLogin,
expected: "用户登录",
},
{
name: "无效场景",
scene: "invalid",
expected: "invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
smsCode := &SMSCode{Scene: tt.scene}
result := smsCode.GetSceneName()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_IsSuspicious(t *testing.T) {
tests := []struct {
name string
smsCode *SMSCode
expected bool
}{
{
name: "正常记录",
smsCode: &SMSCode{
IP: "192.168.1.1",
UserAgent: "Mozilla/5.0",
},
expected: false,
},
{
name: "IP为空-可疑",
smsCode: &SMSCode{
IP: "",
UserAgent: "Mozilla/5.0",
},
expected: true,
},
{
name: "UserAgent为空-可疑",
smsCode: &SMSCode{
IP: "192.168.1.1",
UserAgent: "",
},
expected: true,
},
{
name: "IP和UserAgent都为空-可疑",
smsCode: &SMSCode{
IP: "",
UserAgent: "",
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.smsCode.IsSuspicious()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSMSCode_GetSecurityInfo(t *testing.T) {
now := time.Now()
smsCode := &SMSCode{
IP: "192.168.1.1",
UserAgent: "Mozilla/5.0",
Scene: SMSSceneRegister,
CreatedAt: now,
}
securityInfo := smsCode.GetSecurityInfo()
assert.Equal(t, "192.168.1.1", securityInfo["ip"])
assert.Equal(t, "Mozilla/5.0", securityInfo["user_agent"])
assert.Equal(t, false, securityInfo["suspicious"])
assert.Equal(t, "用户注册", securityInfo["scene"])
assert.Equal(t, now, securityInfo["created_at"])
}
func TestIsValidScene(t *testing.T) {
tests := []struct {
name string
scene SMSScene
expected bool
}{
{
name: "注册场景",
scene: SMSSceneRegister,
expected: true,
},
{
name: "登录场景",
scene: SMSSceneLogin,
expected: true,
},
{
name: "无效场景",
scene: "invalid",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsValidScene(tt.scene)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetSceneName(t *testing.T) {
tests := []struct {
name string
scene SMSScene
expected string
}{
{
name: "注册场景",
scene: SMSSceneRegister,
expected: "用户注册",
},
{
name: "登录场景",
scene: SMSSceneLogin,
expected: "用户登录",
},
{
name: "无效场景",
scene: "invalid",
expected: "invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSceneName(tt.scene)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNewSMSCode(t *testing.T) {
tests := []struct {
name string
phone string
code string
scene SMSScene
expireTime time.Duration
clientIP string
userAgent string
expectError bool
}{
{
name: "有效参数",
phone: "13800138000",
code: "123456",
scene: SMSSceneRegister,
expireTime: time.Hour,
clientIP: "192.168.1.1",
userAgent: "Mozilla/5.0",
expectError: false,
},
{
name: "无效手机号",
phone: "123",
code: "123456",
scene: SMSSceneRegister,
expireTime: time.Hour,
clientIP: "192.168.1.1",
userAgent: "Mozilla/5.0",
expectError: true,
},
{
name: "无效验证码",
phone: "13800138000",
code: "123",
scene: SMSSceneRegister,
expireTime: time.Hour,
clientIP: "192.168.1.1",
userAgent: "Mozilla/5.0",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
smsCode, err := NewSMSCode(tt.phone, tt.code, tt.scene, tt.expireTime, tt.clientIP, tt.userAgent)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, smsCode)
} else {
assert.NoError(t, err)
assert.NotNil(t, smsCode)
assert.Equal(t, tt.phone, smsCode.Phone)
assert.Equal(t, tt.code, smsCode.Code)
assert.Equal(t, tt.scene, smsCode.Scene)
assert.Equal(t, tt.clientIP, smsCode.IP)
assert.Equal(t, tt.userAgent, smsCode.UserAgent)
assert.False(t, smsCode.Used)
assert.True(t, smsCode.ExpiresAt.After(time.Now()))
}
})
}
}

View File

@@ -1,35 +1,48 @@
package entities
import (
"errors"
"fmt"
"regexp"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// User 用户实体
// 系统用户的核心信息,提供基础的账户管理功能
// 支持手机号登录密码加密存储实现Entity接口便于统一管理
type User struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
Phone string `gorm:"uniqueIndex;type:varchar(20);not null" json:"phone"`
Password string `gorm:"type:varchar(255);not null" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"用户唯一标识"`
Phone string `gorm:"uniqueIndex;type:varchar(20);not null" json:"phone" comment:"手机号码(登录账号)"`
Password string `gorm:"type:varchar(255);not null" json:"-" 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:"软删除时间"`
}
// 实现 Entity 接口
// 实现 Entity 接口 - 提供统一的实体管理接口
// GetID 获取实体唯一标识
func (u *User) GetID() string {
return u.ID
}
// GetCreatedAt 获取创建时间
func (u *User) GetCreatedAt() time.Time {
return u.CreatedAt
}
// GetUpdatedAt 获取更新时间
func (u *User) GetUpdatedAt() time.Time {
return u.UpdatedAt
}
// 验证方法
// Validate 验证用户信息
// 检查用户必填字段是否完整,确保数据的有效性
func (u *User) Validate() error {
if u.Phone == "" {
return NewValidationError("手机号不能为空")
@@ -37,23 +50,226 @@ func (u *User) Validate() error {
if u.Password == "" {
return NewValidationError("密码不能为空")
}
// 验证手机号格式
if !u.IsValidPhone() {
return NewValidationError("手机号格式无效")
}
// 验证密码强度
if err := u.validatePasswordStrength(u.Password); err != nil {
return err
}
return nil
}
// ================ 业务方法 ================
// ChangePassword 修改密码
// 验证旧密码,检查新密码强度,更新密码
func (u *User) ChangePassword(oldPassword, newPassword, confirmPassword string) error {
// 1. 验证确认密码
if newPassword != confirmPassword {
return NewValidationError("新密码和确认新密码不匹配")
}
// 2. 验证旧密码
if !u.CheckPassword(oldPassword) {
return NewValidationError("当前密码错误")
}
// 3. 验证新密码强度
if err := u.validatePasswordStrength(newPassword); err != nil {
return err
}
// 4. 检查新密码不能与旧密码相同
if u.CheckPassword(newPassword) {
return NewValidationError("新密码不能与当前密码相同")
}
// 5. 更新密码
hashedPassword, err := u.hashPassword(newPassword)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
u.Password = hashedPassword
return nil
}
// CheckPassword 验证密码是否正确
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
// SetPassword 设置密码(用于注册或重置密码)
func (u *User) SetPassword(password string) error {
// 验证密码强度
if err := u.validatePasswordStrength(password); err != nil {
return err
}
// 加密密码
hashedPassword, err := u.hashPassword(password)
if err != nil {
return fmt.Errorf("密码加密失败: %w", err)
}
u.Password = hashedPassword
return nil
}
// CanLogin 检查用户是否可以登录
func (u *User) CanLogin() bool {
// 检查用户是否被删除
if !u.DeletedAt.Time.IsZero() {
return false
}
// 检查必要字段是否存在
if u.Phone == "" || u.Password == "" {
return false
}
return true
}
// IsActive 检查用户是否处于活跃状态
func (u *User) IsActive() bool {
return u.DeletedAt.Time.IsZero()
}
// IsDeleted 检查用户是否已被删除
func (u *User) IsDeleted() bool {
return !u.DeletedAt.Time.IsZero()
}
// ================ 手机号相关方法 ================
// IsValidPhone 验证手机号格式
func (u *User) IsValidPhone() bool {
return IsValidPhoneFormat(u.Phone)
}
// SetPhone 设置手机号
func (u *User) SetPhone(phone string) error {
if !IsValidPhoneFormat(phone) {
return NewValidationError("手机号格式无效")
}
u.Phone = phone
return nil
}
// GetMaskedPhone 获取脱敏的手机号
func (u *User) GetMaskedPhone() string {
if len(u.Phone) < 7 {
return u.Phone
}
return u.Phone[:3] + "****" + u.Phone[len(u.Phone)-4:]
}
// ================ 私有辅助方法 ================
// hashPassword 加密密码
func (u *User) hashPassword(password string) (string, error) {
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedBytes), nil
}
// validatePasswordStrength 验证密码强度
func (u *User) validatePasswordStrength(password string) error {
if len(password) < 8 {
return NewValidationError("密码长度至少8位")
}
if len(password) > 128 {
return NewValidationError("密码长度不能超过128位")
}
// 检查是否包含数字
hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
if !hasDigit {
return NewValidationError("密码必须包含数字")
}
// 检查是否包含字母
hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
if !hasLetter {
return NewValidationError("密码必须包含字母")
}
// 检查是否包含特殊字符(可选,可以根据需求调整)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
if !hasSpecial {
return NewValidationError("密码必须包含特殊字符")
}
return nil
}
// ================ 静态工具方法 ================
// IsValidPhoneFormat 验证手机号格式(静态方法)
func IsValidPhoneFormat(phone string) bool {
if phone == "" {
return false
}
// 中国手机号验证11位数字以1开头
pattern := `^1[3-9]\d{9}$`
matched, _ := regexp.MatchString(pattern, phone)
return matched
}
// NewUser 创建新用户(工厂方法)
func NewUser(phone, password string) (*User, error) {
user := &User{
ID: "", // 由数据库或调用方设置
Phone: phone,
}
// 验证手机号
if err := user.SetPhone(phone); err != nil {
return nil, err
}
// 设置密码
if err := user.SetPassword(password); err != nil {
return nil, err
}
return user, nil
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}
// ValidationError 验证错误
// 自定义验证错误类型,提供结构化的错误信息
type ValidationError struct {
Message string
}
// Error 实现error接口
func (e *ValidationError) Error() string {
return e.Message
}
// NewValidationError 创建新的验证错误
// 工厂方法,用于创建验证错误实例
func NewValidationError(message string) *ValidationError {
return &ValidationError{Message: message}
}
// IsValidationError 检查是否为验证错误
func IsValidationError(err error) bool {
var validationErr *ValidationError
return errors.As(err, &validationErr)
}

View File

@@ -0,0 +1,338 @@
package entities
import (
"testing"
)
func TestUser_ChangePassword(t *testing.T) {
// 创建测试用户
user, err := NewUser("13800138000", "OldPassword123!")
if err != nil {
t.Fatalf("创建用户失败: %v", err)
}
tests := []struct {
name string
oldPassword string
newPassword string
confirmPassword string
wantErr bool
errorContains string
}{
{
name: "正常修改密码",
oldPassword: "OldPassword123!",
newPassword: "NewPassword123!",
confirmPassword: "NewPassword123!",
wantErr: false,
},
{
name: "旧密码错误",
oldPassword: "WrongPassword123!",
newPassword: "NewPassword123!",
confirmPassword: "NewPassword123!",
wantErr: true,
errorContains: "当前密码错误",
},
{
name: "确认密码不匹配",
oldPassword: "OldPassword123!",
newPassword: "NewPassword123!",
confirmPassword: "DifferentPassword123!",
wantErr: true,
errorContains: "新密码和确认新密码不匹配",
},
{
name: "新密码与旧密码相同",
oldPassword: "OldPassword123!",
newPassword: "OldPassword123!",
confirmPassword: "OldPassword123!",
wantErr: true,
errorContains: "新密码不能与当前密码相同",
},
{
name: "新密码强度不足",
oldPassword: "OldPassword123!",
newPassword: "weak",
confirmPassword: "weak",
wantErr: true,
errorContains: "密码长度至少8位",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 重置用户密码为初始状态
user.SetPassword("OldPassword123!")
err := user.ChangePassword(tt.oldPassword, tt.newPassword, tt.confirmPassword)
if tt.wantErr {
if err == nil {
t.Errorf("期望错误但没有得到错误")
return
}
if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) {
t.Errorf("错误信息不包含期望的内容,期望包含: %s, 实际: %s", tt.errorContains, err.Error())
}
} else {
if err != nil {
t.Errorf("不期望错误但得到了错误: %v", err)
}
// 验证密码确实被修改了
if !user.CheckPassword(tt.newPassword) {
t.Errorf("密码修改后验证失败")
}
}
})
}
}
func TestUser_CheckPassword(t *testing.T) {
user, err := NewUser("13800138000", "TestPassword123!")
if err != nil {
t.Fatalf("创建用户失败: %v", err)
}
tests := []struct {
name string
password string
want bool
}{
{
name: "正确密码",
password: "TestPassword123!",
want: true,
},
{
name: "错误密码",
password: "WrongPassword123!",
want: false,
},
{
name: "空密码",
password: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := user.CheckPassword(tt.password)
if got != tt.want {
t.Errorf("CheckPassword() = %v, want %v", got, tt.want)
}
})
}
}
func TestUser_SetPhone(t *testing.T) {
user := &User{}
tests := []struct {
name string
phone string
wantErr bool
}{
{
name: "有效手机号",
phone: "13800138000",
wantErr: false,
},
{
name: "无效手机号-太短",
phone: "1380013800",
wantErr: true,
},
{
name: "无效手机号-太长",
phone: "138001380000",
wantErr: true,
},
{
name: "无效手机号-格式错误",
phone: "1380013800a",
wantErr: true,
},
{
name: "无效手机号-不以1开头",
phone: "23800138000",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := user.SetPhone(tt.phone)
if tt.wantErr {
if err == nil {
t.Errorf("期望错误但没有得到错误")
}
} else {
if err != nil {
t.Errorf("不期望错误但得到了错误: %v", err)
}
if user.Phone != tt.phone {
t.Errorf("手机号设置失败,期望: %s, 实际: %s", tt.phone, user.Phone)
}
}
})
}
}
func TestUser_GetMaskedPhone(t *testing.T) {
tests := []struct {
name string
phone string
expected string
}{
{
name: "正常手机号",
phone: "13800138000",
expected: "138****8000",
},
{
name: "短手机号",
phone: "138001",
expected: "138001",
},
{
name: "空手机号",
phone: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user := &User{Phone: tt.phone}
got := user.GetMaskedPhone()
if got != tt.expected {
t.Errorf("GetMaskedPhone() = %v, want %v", got, tt.expected)
}
})
}
}
func TestIsValidPhoneFormat(t *testing.T) {
tests := []struct {
name string
phone string
want bool
}{
{
name: "有效手机号-13开头",
phone: "13800138000",
want: true,
},
{
name: "有效手机号-15开头",
phone: "15800138000",
want: true,
},
{
name: "有效手机号-18开头",
phone: "18800138000",
want: true,
},
{
name: "无效手机号-12开头",
phone: "12800138000",
want: false,
},
{
name: "无效手机号-20开头",
phone: "20800138000",
want: false,
},
{
name: "无效手机号-太短",
phone: "1380013800",
want: false,
},
{
name: "无效手机号-太长",
phone: "138001380000",
want: false,
},
{
name: "无效手机号-包含字母",
phone: "1380013800a",
want: false,
},
{
name: "空手机号",
phone: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidPhoneFormat(tt.phone)
if got != tt.want {
t.Errorf("IsValidPhoneFormat() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewUser(t *testing.T) {
tests := []struct {
name string
phone string
password string
wantErr bool
}{
{
name: "有效用户信息",
phone: "13800138000",
password: "TestPassword123!",
wantErr: false,
},
{
name: "无效手机号",
phone: "1380013800",
password: "TestPassword123!",
wantErr: true,
},
{
name: "无效密码",
phone: "13800138000",
password: "weak",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := NewUser(tt.phone, tt.password)
if tt.wantErr {
if err == nil {
t.Errorf("期望错误但没有得到错误")
}
} else {
if err != nil {
t.Errorf("不期望错误但得到了错误: %v", err)
}
if user.Phone != tt.phone {
t.Errorf("手机号设置失败,期望: %s, 实际: %s", tt.phone, user.Phone)
}
if !user.CheckPassword(tt.password) {
t.Errorf("密码设置失败")
}
}
})
}
}
// 辅助函数
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || func() bool {
for i := 1; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}())))
}

View File

@@ -81,6 +81,34 @@ func (r *SMSCodeRepository) MarkAsUsed(ctx context.Context, id string) error {
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).

View File

@@ -133,6 +133,41 @@ func (r *UserRepository) Delete(ctx context.Context, id string) error {
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

View File

@@ -43,83 +43,132 @@ func NewSMSCodeService(
// 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. 生成验证码
code := s.smsClient.GenerateCode(s.config.CodeLength)
// 创建SMS验证码记录
smsCode := &entities.SMSCode{
ID: uuid.New().String(),
Phone: phone,
Code: code,
Scene: scene,
IP: clientIP,
UserAgent: userAgent,
Used: false,
ExpiresAt: time.Now().Add(s.config.ExpireTime),
// 3. 使用工厂方法创建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 {
s.logger.Error("保存短信验证码失败",
zap.String("phone", phone),
zap.String("scene", string(scene)),
zap.String("phone", smsCode.GetMaskedPhone()),
zap.String("scene", smsCode.GetSceneName()),
zap.Error(err))
return fmt.Errorf("保存验证码失败: %w", err)
}
// 发送短信
// 6. 发送短信
if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil {
// 记录发送失败但不删除验证码记录,让其自然过期
s.logger.Error("发送短信验证码失败",
zap.String("phone", phone),
zap.String("code", code),
zap.String("phone", smsCode.GetMaskedPhone()),
zap.String("code", smsCode.GetMaskedCode()),
zap.Error(err))
return fmt.Errorf("短信发送失败: %w", err)
}
// 更新发送记录缓存
// 7. 更新发送记录缓存
s.updateSendRecord(ctx, phone)
s.logger.Info("短信验证码发送成功",
zap.String("phone", phone),
zap.String("scene", string(scene)))
zap.String("phone", smsCode.GetMaskedPhone()),
zap.String("scene", smsCode.GetSceneName()),
zap.String("remaining_time", smsCode.GetRemainingTime().String()))
return nil
}
// VerifyCode 验证验证码
func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error {
// 根据手机号和场景获取有效的验证码记录
// 1. 根据手机号和场景获取有效的验证码记录
smsCode, err := s.repo.GetValidCode(ctx, phone, scene)
if err != nil {
return fmt.Errorf("验证码无效或已过期")
}
// 验证验证码是否匹配
if smsCode.Code != code {
return fmt.Errorf("验证码无效或已过期")
// 2. 使用实体的验证方法
if err := smsCode.VerifyCode(code); err != nil {
return err
}
// 标记验证码为已使用
if err := s.repo.MarkAsUsed(ctx, smsCode.ID); err != nil {
s.logger.Error("标记验证码为已使用失败",
// 3. 保存更新后的验证码状态
if err := s.repo.Update(ctx, smsCode); err != nil {
s.logger.Error("更新验证码状态失败",
zap.String("code_id", smsCode.ID),
zap.Error(err))
return fmt.Errorf("验证码状态更新失败")
}
s.logger.Info("短信验证码验证成功",
zap.String("phone", phone),
zap.String("scene", string(scene)))
zap.String("phone", smsCode.GetMaskedPhone()),
zap.String("scene", smsCode.GetSceneName()))
return nil
}
// CanResendCode 检查是否可以重新发送验证码
func (s *SMSCodeService) CanResendCode(ctx context.Context, phone string, scene entities.SMSScene) (bool, error) {
// 1. 获取最近的验证码记录
recentCode, err := s.repo.GetRecentCode(ctx, phone, scene)
if err != nil {
// 如果没有记录,可以发送
return true, nil
}
// 2. 使用实体的方法检查是否可以重新发送
canResend := recentCode.CanResend(s.config.RateLimit.MinInterval)
// 3. 记录检查结果
if !canResend {
remainingTime := s.config.RateLimit.MinInterval - time.Since(recentCode.CreatedAt)
s.logger.Info("验证码发送频率限制",
zap.String("phone", recentCode.GetMaskedPhone()),
zap.String("scene", recentCode.GetSceneName()),
zap.Duration("remaining_wait_time", remainingTime))
}
return canResend, nil
}
// 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)
if err != nil {
return map[string]interface{}{
"has_code": false,
"message": "没有找到验证码记录",
}, nil
}
// 2. 构建状态信息
status := map[string]interface{}{
"has_code": true,
"is_valid": recentCode.IsValid(),
"is_expired": recentCode.IsExpired(),
"is_used": recentCode.Used,
"remaining_time": recentCode.GetRemainingTime().String(),
"scene": recentCode.GetSceneName(),
"can_resend": recentCode.CanResend(s.config.RateLimit.MinInterval),
"created_at": recentCode.CreatedAt,
"security_info": recentCode.GetSecurityInfo(),
}
return status, nil
}
// checkRateLimit 检查发送频率限制
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
now := time.Now()

View File

@@ -3,11 +3,9 @@ package services
import (
"context"
"fmt"
"regexp"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"tyapi-server/internal/domains/user/dto"
"tyapi-server/internal/domains/user/entities"
@@ -64,44 +62,32 @@ func (s *UserService) Shutdown(ctx context.Context) error {
// Register 用户注册
func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) {
// 验证手机号格式
if !s.isValidPhone(registerReq.Phone) {
return nil, fmt.Errorf("手机号格式无效")
}
// 验证密码确认
if registerReq.Password != registerReq.ConfirmPassword {
return nil, fmt.Errorf("密码和确认密码不匹配")
}
// 验证短信验证码
// 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
}
// 创建用户实体
user := registerReq.ToEntity()
// 3. 使用工厂方法创建用户实体(业务规则验证在实体中完成)
user, err := entities.NewUser(registerReq.Phone, registerReq.Password)
if err != nil {
return nil, fmt.Errorf("创建用户失败: %w", err)
}
// 4. 设置用户ID
user.ID = uuid.New().String()
// 哈希密码
hashedPassword, err := s.hashPassword(registerReq.Password)
if err != nil {
return nil, fmt.Errorf("密码加密失败: %w", err)
}
user.Password = hashedPassword
// 保存用户
// 5. 保存用户
if err := s.repo.Create(ctx, user); err != nil {
s.logger.Error("创建用户失败", zap.Error(err))
return nil, fmt.Errorf("创建用户失败: %w", err)
}
// 发布用户注册事件
// 6. 发布用户注册事件
event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx))
if err := s.eventBus.Publish(ctx, event); err != nil {
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
@@ -116,18 +102,23 @@ func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterReq
// 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("用户名或密码错误")
}
// 验证密码
if !s.checkPassword(loginReq.Password, user.Password) {
// 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),
@@ -145,18 +136,23 @@ func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.Login
// 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),
@@ -174,40 +170,28 @@ func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithS
// ChangePassword 修改密码
func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
// 验证新密码确认
if req.NewPassword != req.ConfirmNewPassword {
return fmt.Errorf("新密码和确认新密码不匹配")
}
// 获取用户信息
// 1. 获取用户信息
user, err := s.repo.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)
}
// 验证当前密码
if !s.checkPassword(req.OldPassword, user.Password) {
return fmt.Errorf("当前密码错误")
// 3. 执行业务逻辑(委托给实体)
if err := user.ChangePassword(req.OldPassword, req.NewPassword, req.ConfirmNewPassword); err != nil {
return err
}
// 哈希新密码
hashedPassword, err := s.hashPassword(req.NewPassword)
if err != nil {
return fmt.Errorf("新密码加密失败: %w", err)
}
// 更新密码
user.Password = hashedPassword
// 4. 保存用户
if err := s.repo.Update(ctx, user); err != nil {
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))
@@ -232,7 +216,61 @@ func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, e
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)
if err != nil {
return fmt.Errorf("用户不存在: %w", err)
}
// 2. 检查用户状态
if user.IsDeleted() {
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 {
@@ -242,34 +280,6 @@ func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) err
return nil
}
// hashPassword 加密密码
func (s *UserService) hashPassword(password string) (string, error) {
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedBytes), nil
}
// checkPassword 验证密码
func (s *UserService) checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// isValidPhone 验证手机号格式
func (s *UserService) isValidPhone(phone string) bool {
// 简单的中国手机号验证11位数字以1开头
pattern := `^1[3-9]\d{9}$`
matched, _ := regexp.MatchString(pattern, phone)
return matched
}
// generateUserID 生成用户ID
func (s *UserService) generateUserID() string {
return uuid.New().String()
}
// getCorrelationID 获取关联ID
func (s *UserService) getCorrelationID(ctx context.Context) string {
if id := ctx.Value("correlation_id"); id != nil {