This commit is contained in:
2026-03-19 17:47:11 +08:00
42 changed files with 1918 additions and 1394 deletions

View File

@@ -3,6 +3,7 @@ package certification
import (
"context"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/repositories"
"tyapi-server/internal/shared/database"
"go.uber.org/zap"
@@ -39,6 +40,15 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) Exists(ctx context.Context, I
return r.ExistsEntity(ctx, ID, &entities.EnterpriseInfoSubmitRecord{})
}
func (r *GormEnterpriseInfoSubmitRecordRepository) FindByID(ctx context.Context, id string) (*entities.EnterpriseInfoSubmitRecord, error) {
var record entities.EnterpriseInfoSubmitRecord
err := r.GetDB(ctx).Where("id = ?", id).First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) {
var record entities.EnterpriseInfoSubmitRecord
err := r.GetDB(ctx).
@@ -61,4 +71,69 @@ func (r *GormEnterpriseInfoSubmitRecordRepository) FindLatestVerifiedByUserID(ct
return nil, err
}
return &record, nil
}
// ExistsByUnifiedSocialCodeExcludeUser 检查该统一社会信用代码是否已被其他用户占用(已提交或已通过验证的记录)
func (r *GormEnterpriseInfoSubmitRecordRepository) ExistsByUnifiedSocialCodeExcludeUser(ctx context.Context, unifiedSocialCode string, excludeUserID string) (bool, error) {
if unifiedSocialCode == "" {
return false, nil
}
var count int64
query := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}).
Where("unified_social_code = ? AND status IN (?, ?)", unifiedSocialCode, "submitted", "verified")
if excludeUserID != "" {
query = query.Where("user_id != ?", excludeUserID)
}
if err := query.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *GormEnterpriseInfoSubmitRecordRepository) List(ctx context.Context, filter repositories.ListSubmitRecordsFilter) (*repositories.ListSubmitRecordsResult, error) {
base := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{})
if filter.CertificationStatus != "" {
base = base.Joins("JOIN certifications ON certifications.user_id = enterprise_info_submit_records.user_id AND certifications.deleted_at IS NULL").
Where("certifications.status = ?", filter.CertificationStatus)
}
if filter.CompanyName != "" {
base = base.Where("enterprise_info_submit_records.company_name LIKE ?", "%"+filter.CompanyName+"%")
}
if filter.LegalPersonPhone != "" {
base = base.Where("enterprise_info_submit_records.legal_person_phone = ?", filter.LegalPersonPhone)
}
if filter.LegalPersonName != "" {
base = base.Where("enterprise_info_submit_records.legal_person_name LIKE ?", "%"+filter.LegalPersonName+"%")
}
var total int64
if err := base.Count(&total).Error; err != nil {
return nil, err
}
if filter.PageSize <= 0 {
filter.PageSize = 10
}
if filter.Page <= 0 {
filter.Page = 1
}
offset := (filter.Page - 1) * filter.PageSize
var records []*entities.EnterpriseInfoSubmitRecord
q := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{})
if filter.CertificationStatus != "" {
q = q.Joins("JOIN certifications ON certifications.user_id = enterprise_info_submit_records.user_id AND certifications.deleted_at IS NULL").
Where("certifications.status = ?", filter.CertificationStatus)
}
if filter.CompanyName != "" {
q = q.Where("enterprise_info_submit_records.company_name LIKE ?", "%"+filter.CompanyName+"%")
}
if filter.LegalPersonPhone != "" {
q = q.Where("enterprise_info_submit_records.legal_person_phone = ?", filter.LegalPersonPhone)
}
if filter.LegalPersonName != "" {
q = q.Where("enterprise_info_submit_records.legal_person_name LIKE ?", "%"+filter.LegalPersonName+"%")
}
err := q.Order("enterprise_info_submit_records.submit_at DESC").Offset(offset).Limit(filter.PageSize).Find(&records).Error
if err != nil {
return nil, err
}
return &repositories.ListSubmitRecordsResult{Records: records, Total: total}, nil
}

View File

@@ -14,17 +14,19 @@ import (
"tyapi-server/internal/application/certification/dto/commands"
"tyapi-server/internal/application/certification/dto/queries"
_ "tyapi-server/internal/application/certification/dto/responses"
"tyapi-server/internal/infrastructure/external/storage"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/middleware"
)
// CertificationHandler 认证HTTP处理器
type CertificationHandler struct {
appService certification.CertificationApplicationService
response interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
jwtAuth *middleware.JWTAuthMiddleware
appService certification.CertificationApplicationService
response interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
jwtAuth *middleware.JWTAuthMiddleware
storageService *storage.QiNiuStorageService
}
// NewCertificationHandler 创建认证处理器
@@ -34,13 +36,15 @@ func NewCertificationHandler(
validator interfaces.RequestValidator,
logger *zap.Logger,
jwtAuth *middleware.JWTAuthMiddleware,
storageService *storage.QiNiuStorageService,
) *CertificationHandler {
return &CertificationHandler{
appService: appService,
response: response,
validator: validator,
logger: logger,
jwtAuth: jwtAuth,
appService: appService,
response: response,
validator: validator,
logger: logger,
jwtAuth: jwtAuth,
storageService: storageService,
}
}
@@ -295,6 +299,78 @@ func (h *CertificationHandler) RecognizeBusinessLicense(c *gin.Context) {
h.response.Success(c, result, "营业执照识别成功")
}
// UploadCertificationFile 上传认证相关图片到七牛云(企业信息中的营业执照、办公场地、场景附件、授权代表身份证等)
// @Summary 上传认证图片
// @Description 上传企业信息中使用的图片到七牛云,返回可访问的 URL
// @Tags 认证管理
// @Accept multipart/form-data
// @Produce json
// @Security Bearer
// @Param file formData file true "图片文件"
// @Success 200 {object} map[string]string "上传成功,返回 url 与 key"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/certifications/upload [post]
func (h *CertificationHandler) UploadCertificationFile(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未登录")
return
}
file, err := c.FormFile("file")
if err != nil {
h.logger.Error("获取上传文件失败", zap.Error(err), zap.String("user_id", userID))
h.response.BadRequest(c, "请选择要上传的图片文件")
return
}
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/webp": true,
}
contentType := file.Header.Get("Content-Type")
if !allowedTypes[contentType] {
h.response.BadRequest(c, "只支持 JPG、PNG、WEBP 格式的图片")
return
}
if file.Size > 5*1024*1024 {
h.response.BadRequest(c, "图片大小不能超过 5MB")
return
}
src, err := file.Open()
if err != nil {
h.logger.Error("打开上传文件失败", zap.Error(err), zap.String("user_id", userID))
h.response.BadRequest(c, "文件读取失败")
return
}
defer src.Close()
fileBytes, err := io.ReadAll(src)
if err != nil {
h.logger.Error("读取文件内容失败", zap.Error(err), zap.String("user_id", userID))
h.response.BadRequest(c, "文件读取失败")
return
}
uploadResult, err := h.storageService.UploadFile(c.Request.Context(), fileBytes, file.Filename)
if err != nil {
h.logger.Error("上传文件到七牛云失败", zap.Error(err), zap.String("user_id", userID), zap.String("file_name", file.Filename))
h.response.BadRequest(c, "图片上传失败,请稍后重试")
return
}
h.response.Success(c, map[string]string{
"url": uploadResult.URL,
"key": uploadResult.Key,
}, "上传成功")
}
// ListCertifications 获取认证列表(管理员)
// @Summary 获取认证列表
// @Description 管理员获取认证申请列表
@@ -375,6 +451,147 @@ func (h *CertificationHandler) AdminCompleteCertificationWithoutContract(c *gin.
h.response.Success(c, result, "代用户完成认证成功")
}
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
// @Summary 管理端企业审核列表
// @Tags 认证管理
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页条数"
// @Param certification_status query string false "按状态机筛选info_pending_review/info_submitted/info_rejected空为全部"
// @Success 200 {object} responses.AdminSubmitRecordsListResponse
// @Router /api/v1/certifications/admin/submit-records [get]
func (h *CertificationHandler) AdminListSubmitRecords(c *gin.Context) {
query := &queries.AdminListSubmitRecordsQuery{}
if err := c.ShouldBindQuery(query); err != nil {
h.response.BadRequest(c, "参数错误")
return
}
result, err := h.appService.AdminListSubmitRecords(c.Request.Context(), query)
if err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "获取成功")
}
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
// @Summary 管理端企业审核详情
// @Tags 认证管理
// @Produce json
// @Security Bearer
// @Param id path string true "记录ID"
// @Success 200 {object} responses.AdminSubmitRecordDetail
// @Router /api/v1/certifications/admin/submit-records/{id} [get]
func (h *CertificationHandler) AdminGetSubmitRecordByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
h.response.BadRequest(c, "记录ID不能为空")
return
}
result, err := h.appService.AdminGetSubmitRecordByID(c.Request.Context(), id)
if err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "获取成功")
}
// AdminApproveSubmitRecord 管理端审核通过
// @Summary 管理端企业审核通过
// @Tags 认证管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "记录ID"
// @Param request body object true "可选 remark"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/certifications/admin/submit-records/{id}/approve [post]
func (h *CertificationHandler) AdminApproveSubmitRecord(c *gin.Context) {
adminID := h.getCurrentUserID(c)
if adminID == "" {
h.response.Unauthorized(c, "未登录")
return
}
id := c.Param("id")
if id == "" {
h.response.BadRequest(c, "记录ID不能为空")
return
}
var body struct {
Remark string `json:"remark"`
}
_ = c.ShouldBindJSON(&body)
if err := h.appService.AdminApproveSubmitRecord(c.Request.Context(), id, adminID, body.Remark); err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, nil, "审核通过")
}
// AdminRejectSubmitRecord 管理端审核拒绝
// @Summary 管理端企业审核拒绝
// @Tags 认证管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "记录ID"
// @Param request body object true "remark 必填"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/certifications/admin/submit-records/{id}/reject [post]
func (h *CertificationHandler) AdminRejectSubmitRecord(c *gin.Context) {
adminID := h.getCurrentUserID(c)
if adminID == "" {
h.response.Unauthorized(c, "未登录")
return
}
id := c.Param("id")
if id == "" {
h.response.BadRequest(c, "记录ID不能为空")
return
}
var body struct {
Remark string `json:"remark" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
h.response.BadRequest(c, "请填写拒绝原因(remark)")
return
}
if err := h.appService.AdminRejectSubmitRecord(c.Request.Context(), id, adminID, body.Remark); err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, nil, "已拒绝")
}
// AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准)
// @Summary 管理端变更认证状态
// @Tags 认证管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body commands.AdminTransitionCertificationStatusCommand true "user_id, target_status(info_submitted/info_rejected), remark"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/certifications/admin/transition-status [post]
func (h *CertificationHandler) AdminTransitionCertificationStatus(c *gin.Context) {
adminID := h.getCurrentUserID(c)
if adminID == "" {
h.response.Unauthorized(c, "未登录")
return
}
var cmd commands.AdminTransitionCertificationStatusCommand
if err := c.ShouldBindJSON(&cmd); err != nil {
h.response.BadRequest(c, "参数错误")
return
}
cmd.AdminID = adminID
if err := h.appService.AdminTransitionCertificationStatus(c.Request.Context(), &cmd); err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, nil, "状态已更新")
}
// ================ 回调处理 ================
// HandleEsignCallback 处理e签宝回调

View File

@@ -14,6 +14,7 @@ type CertificationRoutes struct {
router *http.GinRouter
logger *zap.Logger
auth *middleware.JWTAuthMiddleware
admin *middleware.AdminAuthMiddleware
optional *middleware.OptionalAuthMiddleware
dailyRateLimit *middleware.DailyRateLimitMiddleware
}
@@ -24,6 +25,7 @@ func NewCertificationRoutes(
router *http.GinRouter,
logger *zap.Logger,
auth *middleware.JWTAuthMiddleware,
admin *middleware.AdminAuthMiddleware,
optional *middleware.OptionalAuthMiddleware,
dailyRateLimit *middleware.DailyRateLimitMiddleware,
) *CertificationRoutes {
@@ -32,6 +34,7 @@ func NewCertificationRoutes(
router: router,
logger: logger,
auth: auth,
admin: admin,
optional: optional,
dailyRateLimit: dailyRateLimit,
}
@@ -57,6 +60,9 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
// OCR营业执照识别接口
authGroup.POST("/ocr/business-license", r.handler.RecognizeBusinessLicense)
// 认证图片上传(七牛云,用于企业信息中的各类图片)
authGroup.POST("/upload", r.handler.UploadCertificationFile)
// 3. 申请合同签署
authGroup.POST("/apply-contract", r.handler.ApplyContract)
@@ -71,6 +77,21 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
}
// 管理端企业审核(需管理员权限,以状态机状态为准)
adminGroup := certificationGroup.Group("/admin")
adminGroup.Use(r.auth.Handle())
adminGroup.Use(r.admin.Handle())
{
adminGroup.POST("/transition-status", r.handler.AdminTransitionCertificationStatus)
}
adminCertGroup := adminGroup.Group("/submit-records")
{
adminCertGroup.GET("", r.handler.AdminListSubmitRecords)
adminCertGroup.GET("/:id", r.handler.AdminGetSubmitRecordByID)
adminCertGroup.POST("/:id/approve", r.handler.AdminApproveSubmitRecord)
adminCertGroup.POST("/:id/reject", r.handler.AdminRejectSubmitRecord)
}
// 回调路由(不需要认证,但需要验证签名)
callbackGroup := certificationGroup.Group("/callbacks")
{