基础架构

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

View File

@@ -1,178 +0,0 @@
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

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -145,3 +146,11 @@ func (a *Admin) Deactivate() {
func (a *Admin) Activate() {
a.IsActive = true
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (a *Admin) BeforeCreate(tx *gorm.DB) error {
if a.ID == "" {
a.ID = uuid.New().String()
}
return nil
}

View File

@@ -1,313 +0,0 @@
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

@@ -2,12 +2,19 @@ package repositories
import (
"context"
"tyapi-server/internal/domains/admin/dto"
"tyapi-server/internal/domains/admin/entities"
"tyapi-server/internal/domains/admin/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// AdminStats 管理员统计
type AdminStats struct {
TotalAdmins int64
ActiveAdmins int64
TodayLogins int64
TotalOperations int64
}
// AdminRepository 管理员仓储接口
type AdminRepository interface {
interfaces.Repository[entities.Admin]
@@ -17,8 +24,8 @@ type AdminRepository interface {
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)
ListAdmins(ctx context.Context, query *queries.ListAdminsQuery) ([]*entities.Admin, int64, error)
GetStats(ctx context.Context, query *queries.GetAdminInfoQuery) (*AdminStats, error)
// 权限管理
GetPermissionsByRole(ctx context.Context, role entities.AdminRole) ([]entities.AdminPermission, error)
@@ -34,7 +41,7 @@ type AdminLoginLogRepository interface {
interfaces.Repository[entities.AdminLoginLog]
// 日志查询
ListLogs(ctx context.Context, req *dto.AdminLoginLogRequest) (*dto.AdminLoginLogResponse, error)
ListLogs(ctx context.Context, query *queries.ListAdminLoginLogQuery) ([]*entities.AdminLoginLog, int64, error)
// 统计查询
GetTodayLoginCount(ctx context.Context) (int64, error)
@@ -46,7 +53,7 @@ type AdminOperationLogRepository interface {
interfaces.Repository[entities.AdminOperationLog]
// 日志查询
ListLogs(ctx context.Context, req *dto.AdminOperationLogRequest) (*dto.AdminOperationLogResponse, error)
ListLogs(ctx context.Context, query *queries.ListAdminOperationLogQuery) ([]*entities.AdminOperationLog, int64, error)
// 统计查询
GetTotalOperations(ctx context.Context) (int64, error)

View File

@@ -1,341 +0,0 @@
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,9 @@
package queries
type ListAdminLoginLogQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
AdminID string `json:"admin_id"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}

View File

@@ -0,0 +1,11 @@
package queries
type ListAdminOperationLogQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
AdminID string `json:"admin_id"`
Module string `json:"module"`
Action string `json:"action"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}

View File

@@ -0,0 +1,16 @@
package queries
import "tyapi-server/internal/domains/admin/entities"
type ListAdminsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Username string `json:"username"`
Email string `json:"email"`
Role entities.AdminRole `json:"role"`
IsActive *bool `json:"is_active"`
}
type GetAdminInfoQuery struct {
AdminID string `json:"admin_id"`
}

View File

@@ -1,29 +0,0 @@
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

@@ -2,353 +2,36 @@ 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 管理员服务
// AdminService 管理员领域服务
type AdminService struct {
adminRepo repositories.AdminRepository
loginLogRepo repositories.AdminLoginLogRepository
operationLogRepo repositories.AdminOperationLogRepository
permissionRepo repositories.AdminPermissionRepository
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
adminRepo repositories.AdminRepository
permissionRepo repositories.AdminPermissionRepository
logger *zap.Logger
}
// NewAdminService 创建管理员服务
// 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,
adminRepo: adminRepo,
permissionRepo: permissionRepo,
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) {
// GetAdminPermissions 获取管理员权限
func (s *AdminService) GetAdminPermissions(ctx context.Context, admin *entities.Admin) ([]string, error) {
// 首先从角色获取权限
rolePermissions, err := s.adminRepo.GetPermissionsByRole(ctx, admin.Role)
if err != nil {
@@ -371,61 +54,3 @@ func (s *AdminService) getAdminPermissions(ctx context.Context, admin *entities.
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

@@ -5,6 +5,8 @@ import (
"tyapi-server/internal/domains/certification/enums"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -13,10 +15,9 @@ import (
// 包含认证状态、时间节点、审核信息、合同信息等核心数据
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:"当前认证状态"`
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"认证申请唯一标识"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_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:"企业信息提交时间"`
@@ -45,7 +46,6 @@ type Certification struct {
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:"关联的合同记录列表"`
@@ -57,6 +57,14 @@ func (Certification) TableName() string {
return "certifications"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (c *Certification) BeforeCreate(tx *gorm.DB) error {
if c.ID == "" {
c.ID = uuid.New().String()
}
return nil
}
// IsStatusChangeable 检查状态是否可以变更
// 只有非最终状态(完成/拒绝)的认证申请才能进行状态变更
func (c *Certification) IsStatusChangeable() bool {

View File

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -96,3 +97,11 @@ func (c *ContractRecord) GetStatusName() string {
}
return c.Status
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (c *ContractRecord) BeforeCreate(tx *gorm.DB) error {
if c.ID == "" {
c.ID = uuid.New().String()
}
return nil
}

View File

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -87,3 +88,11 @@ func (f *FaceVerifyRecord) GetStatusName() string {
}
return f.Status
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (f *FaceVerifyRecord) BeforeCreate(tx *gorm.DB) error {
if f.ID == "" {
f.ID = uuid.New().String()
}
return nil
}

View File

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -68,3 +69,11 @@ func (l *LicenseUploadRecord) IsValidForOCR() bool {
}
return false
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (l *LicenseUploadRecord) BeforeCreate(tx *gorm.DB) error {
if l.ID == "" {
l.ID = uuid.New().String()
}
return nil
}

View File

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -49,6 +50,14 @@ func (NotificationRecord) TableName() string {
return "notification_records"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (n *NotificationRecord) BeforeCreate(tx *gorm.DB) error {
if n.ID == "" {
n.ID = uuid.New().String()
}
return nil
}
// IsPending 检查通知是否待发送
// 判断通知是否处于等待发送的状态
func (n *NotificationRecord) IsPending() bool {

View File

@@ -5,6 +5,7 @@ type CertificationStatus string
const (
// 主流程状态
StatusNotStarted CertificationStatus = "not_started" // 未开始认证
StatusPending CertificationStatus = "pending" // 待开始
StatusInfoSubmitted CertificationStatus = "info_submitted" // 企业信息已提交
StatusFaceVerified CertificationStatus = "face_verified" // 人脸识别完成
@@ -23,7 +24,7 @@ const (
// IsValidStatus 检查状态是否有效
func IsValidStatus(status CertificationStatus) bool {
validStatuses := []CertificationStatus{
StatusPending, StatusInfoSubmitted, StatusFaceVerified,
StatusNotStarted, StatusPending, StatusInfoSubmitted, StatusFaceVerified,
StatusContractApplied, StatusContractPending, StatusContractApproved,
StatusContractSigned, StatusCompleted, StatusFaceFailed,
StatusSignFailed, StatusRejected,
@@ -40,6 +41,7 @@ func IsValidStatus(status CertificationStatus) bool {
// GetStatusName 获取状态的中文名称
func GetStatusName(status CertificationStatus) string {
statusNames := map[CertificationStatus]string{
StatusNotStarted: "未开始认证",
StatusPending: "待开始",
StatusInfoSubmitted: "企业信息已提交",
StatusFaceVerified: "人脸识别完成",

View File

@@ -8,8 +8,8 @@ import (
"go.uber.org/zap"
"tyapi-server/internal/infrastructure/external/notification"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/notification"
)
// CertificationEventHandler 认证事件处理器

View File

@@ -1,536 +0,0 @@
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,98 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// CertificationStats 认证统计信息
type CertificationStats struct {
TotalCertifications int64
PendingCertifications int64
CompletedCertifications int64
RejectedCertifications int64
TodaySubmissions int64
}
// CertificationRepository 认证申请仓储接口
type CertificationRepository interface {
interfaces.Repository[entities.Certification]
// 基础查询 - 直接使用实体
GetByUserID(ctx context.Context, userID string) (*entities.Certification, error)
GetByStatus(ctx context.Context, status string) ([]*entities.Certification, error)
GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error)
// 复杂查询 - 使用查询参数
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error)
// 业务操作
UpdateStatus(ctx context.Context, certificationID string, status string, adminID *string, notes string) error
// 统计信息
GetStats(ctx context.Context) (*CertificationStats, error)
GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*CertificationStats, error)
}
// FaceVerifyRecordRepository 人脸识别记录仓储接口
type FaceVerifyRecordRepository interface {
interfaces.Repository[entities.FaceVerifyRecord]
// 基础查询 - 直接使用实体
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.FaceVerifyRecord, error)
GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.FaceVerifyRecord, error)
// 复杂查询 - 使用查询参数
ListRecords(ctx context.Context, query *queries.ListFaceVerifyRecordsQuery) ([]*entities.FaceVerifyRecord, int64, error)
// 统计信息
GetSuccessRate(ctx context.Context, days int) (float64, error)
}
// ContractRecordRepository 合同记录仓储接口
type ContractRecordRepository interface {
interfaces.Repository[entities.ContractRecord]
// 基础查询 - 直接使用实体
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.ContractRecord, error)
GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.ContractRecord, error)
// 复杂查询 - 使用查询参数
ListRecords(ctx context.Context, query *queries.ListContractRecordsQuery) ([]*entities.ContractRecord, int64, error)
// 业务操作
UpdateContractStatus(ctx context.Context, recordID string, status string, adminID *string, notes string) error
}
// LicenseUploadRecordRepository 营业执照上传记录仓储接口
type LicenseUploadRecordRepository interface {
interfaces.Repository[entities.LicenseUploadRecord]
// 基础查询 - 直接使用实体
GetByCertificationID(ctx context.Context, certificationID string) (*entities.LicenseUploadRecord, error)
// 复杂查询 - 使用查询参数
ListRecords(ctx context.Context, query *queries.ListLicenseUploadRecordsQuery) ([]*entities.LicenseUploadRecord, int64, error)
// 业务操作
UpdateOCRResult(ctx context.Context, recordID string, ocrResult string, confidence float64) error
}
// NotificationRecordRepository 通知记录仓储接口
type NotificationRecordRepository interface {
interfaces.Repository[entities.NotificationRecord]
// 基础查询 - 直接使用实体
GetByCertificationID(ctx context.Context, certificationID string) ([]*entities.NotificationRecord, error)
GetUnreadByUserID(ctx context.Context, userID string) ([]*entities.NotificationRecord, error)
// 复杂查询 - 使用查询参数
ListRecords(ctx context.Context, query *queries.ListNotificationRecordsQuery) ([]*entities.NotificationRecord, int64, error)
// 批量操作
BatchCreate(ctx context.Context, records []entities.NotificationRecord) error
MarkAsRead(ctx context.Context, recordIDs []string) error
MarkAllAsReadByUser(ctx context.Context, userID string) error
}

View File

@@ -1,223 +0,0 @@
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

@@ -1,175 +0,0 @@
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

@@ -1,148 +0,0 @@
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

@@ -1,160 +0,0 @@
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

@@ -1,163 +0,0 @@
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

@@ -1,105 +0,0 @@
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,72 @@
package queries
import "tyapi-server/internal/domains/certification/enums"
// ListCertificationsQuery 认证申请列表查询参数
type ListCertificationsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
UserID string `json:"user_id"`
Status enums.CertificationStatus `json:"status"`
AdminID string `json:"admin_id"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
EnterpriseName string `json:"enterprise_name"`
}
// ListEnterprisesQuery 企业信息列表查询参数
type ListEnterprisesQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
UserID string `json:"user_id"`
EnterpriseName string `json:"enterprise_name"`
LicenseNumber string `json:"license_number"`
LegalPersonName string `json:"legal_person_name"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListFaceVerifyRecordsQuery 人脸识别记录列表查询参数
type ListFaceVerifyRecordsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListContractRecordsQuery 合同记录列表查询参数
type ListContractRecordsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListLicenseUploadRecordsQuery 营业执照上传记录列表查询参数
type ListLicenseUploadRecordsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListNotificationRecordsQuery 通知记录列表查询参数
type ListNotificationRecordsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
CertificationID string `json:"certification_id"`
UserID string `json:"user_id"`
Type string `json:"type"`
IsRead *bool `json:"is_read"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}

View File

@@ -1,62 +0,0 @@
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

@@ -124,17 +124,17 @@ func (sm *CertificationStateMachine) TransitionTo(
}
// 执行状态转换前的验证
if err := sm.validateTransition(ctx, cert, targetStatus, metadata); err != nil {
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.updateTimestamp(&cert, targetStatus)
// 更新其他字段
sm.updateCertificationFields(cert, targetStatus, metadata)
sm.updateCertificationFields(&cert, targetStatus, metadata)
// 保存到数据库
if err := sm.certRepo.Update(ctx, cert); err != nil {
@@ -215,9 +215,9 @@ func (sm *CertificationStateMachine) validateTransition(
switch targetStatus {
case enums.StatusInfoSubmitted:
// 验证企业信息是否完整
if cert.EnterpriseID == nil {
return fmt.Errorf("企业信息未提交")
}
// 这里应该检查用户是否有企业信息,通过用户域的企业服务验证
// 暂时跳过验证,由应用服务层协调
break
case enums.StatusFaceVerified:
// 验证人脸识别是否成功
@@ -285,3 +285,164 @@ func (sm *CertificationStateMachine) GetTransitionAction(
return ""
}
// GetTransitionHistory 获取状态转换历史
func (sm *CertificationStateMachine) GetTransitionHistory(ctx context.Context, certificationID string) ([]map[string]interface{}, error) {
cert, err := sm.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
history := []map[string]interface{}{}
// 添加创建时间
history = append(history, map[string]interface{}{
"status": "CREATED",
"timestamp": cert.CreatedAt,
"action": "create",
"performer": "system",
"metadata": map[string]interface{}{},
})
// 添加各个时间节点的状态转换
if cert.InfoSubmittedAt != nil {
history = append(history, map[string]interface{}{
"status": string(enums.StatusInfoSubmitted),
"timestamp": *cert.InfoSubmittedAt,
"action": "submit_info",
"performer": "user",
"metadata": map[string]interface{}{},
})
}
if cert.FaceVerifiedAt != nil {
history = append(history, map[string]interface{}{
"status": string(enums.StatusFaceVerified),
"timestamp": *cert.FaceVerifiedAt,
"action": "face_verify",
"performer": "system",
"metadata": map[string]interface{}{},
})
}
if cert.ContractAppliedAt != nil {
history = append(history, map[string]interface{}{
"status": string(enums.StatusContractApplied),
"timestamp": *cert.ContractAppliedAt,
"action": "apply_contract",
"performer": "user",
"metadata": map[string]interface{}{},
})
}
if cert.ContractApprovedAt != nil {
metadata := map[string]interface{}{}
if cert.AdminID != nil {
metadata["admin_id"] = *cert.AdminID
}
if cert.ApprovalNotes != "" {
metadata["approval_notes"] = cert.ApprovalNotes
}
if cert.SigningURL != "" {
metadata["signing_url"] = cert.SigningURL
}
history = append(history, map[string]interface{}{
"status": string(enums.StatusContractApproved),
"timestamp": *cert.ContractApprovedAt,
"action": "admin_approve",
"performer": "admin",
"metadata": metadata,
})
}
if cert.ContractSignedAt != nil {
metadata := map[string]interface{}{}
if cert.ContractURL != "" {
metadata["contract_url"] = cert.ContractURL
}
history = append(history, map[string]interface{}{
"status": string(enums.StatusContractSigned),
"timestamp": *cert.ContractSignedAt,
"action": "user_sign",
"performer": "user",
"metadata": metadata,
})
}
if cert.CompletedAt != nil {
history = append(history, map[string]interface{}{
"status": string(enums.StatusCompleted),
"timestamp": *cert.CompletedAt,
"action": "system_complete",
"performer": "system",
"metadata": map[string]interface{}{},
})
}
return history, nil
}
// ValidateCertificationFlow 验证认证流程的完整性
func (sm *CertificationStateMachine) ValidateCertificationFlow(ctx context.Context, certificationID string) (map[string]interface{}, error) {
cert, err := sm.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
validation := map[string]interface{}{
"certification_id": certificationID,
"current_status": cert.Status,
"is_valid": true,
"issues": []string{},
"warnings": []string{},
}
// 检查必要的时间节点
if cert.Status != enums.StatusPending {
if cert.InfoSubmittedAt == nil {
validation["is_valid"] = false
validation["issues"] = append(validation["issues"].([]string), "缺少企业信息提交时间")
}
}
if cert.Status == enums.StatusFaceVerified || cert.Status == enums.StatusContractApplied ||
cert.Status == enums.StatusContractPending || cert.Status == enums.StatusContractApproved ||
cert.Status == enums.StatusContractSigned || cert.Status == enums.StatusCompleted {
if cert.FaceVerifiedAt == nil {
validation["is_valid"] = false
validation["issues"] = append(validation["issues"].([]string), "缺少人脸识别完成时间")
}
}
if cert.Status == enums.StatusContractApproved || cert.Status == enums.StatusContractSigned ||
cert.Status == enums.StatusCompleted {
if cert.ContractApprovedAt == nil {
validation["is_valid"] = false
validation["issues"] = append(validation["issues"].([]string), "缺少合同审核时间")
}
if cert.SigningURL == "" {
validation["warnings"] = append(validation["warnings"].([]string), "缺少合同签署链接")
}
}
if cert.Status == enums.StatusContractSigned || cert.Status == enums.StatusCompleted {
if cert.ContractSignedAt == nil {
validation["is_valid"] = false
validation["issues"] = append(validation["issues"].([]string), "缺少合同签署时间")
}
if cert.ContractURL == "" {
validation["warnings"] = append(validation["warnings"].([]string), "缺少合同文件链接")
}
}
if cert.Status == enums.StatusCompleted {
if cert.CompletedAt == nil {
validation["is_valid"] = false
validation["issues"] = append(validation["issues"].([]string), "缺少认证完成时间")
}
}
return validation, nil
}

View File

@@ -3,6 +3,7 @@ package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -65,3 +66,11 @@ func (u *UserSecrets) Deactivate() {
func (u *UserSecrets) Activate() {
u.IsActive = true
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (u *UserSecrets) BeforeCreate(tx *gorm.DB) error {
if u.ID == "" {
u.ID = uuid.New().String()
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@@ -69,3 +70,11 @@ func (w *Wallet) SubtractBalance(amount decimal.Decimal) error {
func (w *Wallet) GetFormattedBalance() string {
return w.Balance.String()
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (w *Wallet) BeforeCreate(tx *gorm.DB) error {
if w.ID == "" {
w.ID = uuid.New().String()
}
return nil
}

View File

@@ -1,336 +0,0 @@
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

@@ -1,46 +0,0 @@
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,57 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories/queries"
"tyapi-server/internal/shared/interfaces"
)
// FinanceStats 财务统计信息
type FinanceStats struct {
TotalWallets int64
ActiveWallets int64
TotalBalance string
TodayTransactions int64
}
// WalletRepository 钱包仓储接口
type WalletRepository interface {
interfaces.Repository[entities.Wallet]
// 基础查询 - 直接使用实体
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
GetByWalletAddress(ctx context.Context, walletAddress string) (*entities.Wallet, error)
GetByWalletType(ctx context.Context, userID string, walletType string) (*entities.Wallet, error)
// 复杂查询 - 使用查询参数
ListWallets(ctx context.Context, query *queries.ListWalletsQuery) ([]*entities.Wallet, int64, error)
// 业务操作
UpdateBalance(ctx context.Context, walletID string, balance string) error
AddBalance(ctx context.Context, walletID string, amount string) error
SubtractBalance(ctx context.Context, walletID string, amount string) error
ActivateWallet(ctx context.Context, walletID string) error
DeactivateWallet(ctx context.Context, walletID string) error
// 统计信息
GetStats(ctx context.Context) (*FinanceStats, error)
GetUserWalletStats(ctx context.Context, userID string) (*FinanceStats, error)
}
// UserSecretsRepository 用户密钥仓储接口
type UserSecretsRepository interface {
interfaces.Repository[entities.UserSecrets]
// 基础查询 - 直接使用实体
GetByUserID(ctx context.Context, userID string) (*entities.UserSecrets, error)
GetBySecretType(ctx context.Context, userID string, secretType string) (*entities.UserSecrets, error)
// 复杂查询 - 使用查询参数
ListUserSecrets(ctx context.Context, query *queries.ListUserSecretsQuery) ([]*entities.UserSecrets, int64, error)
// 业务操作
UpdateSecret(ctx context.Context, userID string, secretType string, secretValue string) error
DeleteSecret(ctx context.Context, userID string, secretType string) error
ValidateSecret(ctx context.Context, userID string, secretType string, secretValue string) (bool, error)
}

View File

@@ -1,410 +0,0 @@
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,24 @@
package queries
// ListWalletsQuery 钱包列表查询参数
type ListWalletsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
UserID string `json:"user_id"`
WalletType string `json:"wallet_type"`
WalletAddress string `json:"wallet_address"`
IsActive *bool `json:"is_active"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
// ListUserSecretsQuery 用户密钥列表查询参数
type ListUserSecretsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
UserID string `json:"user_id"`
SecretType string `json:"secret_type"`
IsActive *bool `json:"is_active"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}

View File

@@ -1,35 +0,0 @@
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

@@ -1,470 +1,24 @@
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 财务服务
// FinanceService 财务领域服务
type FinanceService struct {
walletRepo repositories.WalletRepository
userSecretsRepo repositories.UserSecretsRepository
responseBuilder interfaces.ResponseBuilder
logger *zap.Logger
walletRepo repositories.WalletRepository
logger *zap.Logger
}
// NewFinanceService 创建财务服务
// 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,
walletRepo: walletRepo,
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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