This commit is contained in:
Mrx
2026-03-17 17:18:54 +08:00
parent 6f0a8e0519
commit 12ed1c81e3
16 changed files with 763 additions and 123 deletions

View File

@@ -37,6 +37,15 @@ type CertificationApplicationService interface {
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
// AdminApproveSubmitRecord 管理端审核通过
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminRejectSubmitRecord 管理端审核拒绝
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// ================ e签宝回调处理 ================
// 处理e签宝回调

View File

@@ -2,6 +2,7 @@ package certification
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -10,6 +11,7 @@ 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/config"
api_service "tyapi-server/internal/domains/api/services"
"tyapi-server/internal/domains/certification/entities"
certification_value_objects "tyapi-server/internal/domains/certification/entities/value_objects"
@@ -18,8 +20,7 @@ import (
"tyapi-server/internal/domains/certification/services"
finance_service "tyapi-server/internal/domains/finance/services"
user_entities "tyapi-server/internal/domains/user/entities"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/config"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/external/notification"
"tyapi-server/internal/infrastructure/external/storage"
"tyapi-server/internal/shared/database"
@@ -51,6 +52,7 @@ type CertificationApplicationServiceImpl struct {
wechatWorkService *notification.WeChatWorkService
logger *zap.Logger
config *config.Config
}
// NewCertificationApplicationService 创建认证应用服务
@@ -93,6 +95,7 @@ func NewCertificationApplicationService(
txManager: txManager,
wechatWorkService: wechatSvc,
logger: logger,
config: cfg,
}
}
@@ -106,7 +109,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
s.logger.Info("开始提交企业信息",
zap.String("user_id", cmd.UserID))
// 1.5 插入企业信息提交记录
// 1.5 插入企业信息提交记录(包含扩展字段)
record := entities.NewEnterpriseInfoSubmitRecord(
cmd.UserID,
cmd.CompanyName,
@@ -117,6 +120,36 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
cmd.EnterpriseAddress,
)
// 扩展字段赋值
record.BusinessLicenseImageURL = cmd.BusinessLicenseImageURL
if len(cmd.OfficePlaceImageURLs) > 0 {
if data, mErr := json.Marshal(cmd.OfficePlaceImageURLs); mErr == nil {
record.OfficePlaceImageURLs = string(data)
} else {
s.logger.Warn("序列化办公场地图片URL失败", zap.Error(mErr))
}
}
record.APIUsage = cmd.APIUsage
if len(cmd.ScenarioAttachmentURLs) > 0 {
if data, mErr := json.Marshal(cmd.ScenarioAttachmentURLs); mErr == nil {
record.ScenarioAttachmentURLs = string(data)
} else {
s.logger.Warn("序列化场景附件图片URL失败", zap.Error(mErr))
}
}
// 授权代表信息落库
record.AuthorizedRepName = cmd.AuthorizedRepName
record.AuthorizedRepID = cmd.AuthorizedRepID
record.AuthorizedRepPhone = cmd.AuthorizedRepPhone
if len(cmd.AuthorizedRepIDImageURLs) > 0 {
if data, mErr := json.Marshal(cmd.AuthorizedRepIDImageURLs); mErr == nil {
record.AuthorizedRepIDImageURLs = string(data)
} else {
s.logger.Warn("序列化授权代表身份证图片URL失败", zap.Error(mErr))
}
}
// 验证验证码
// 特殊验证码"768005"直接跳过验证环节
if cmd.VerificationCode != "768005" {
@@ -141,6 +174,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
}
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
}
if exists {
record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确")
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
@@ -148,6 +182,7 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确")
}
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
@@ -205,56 +240,24 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
return fmt.Errorf("加载认证信息失败: %s", err.Error())
}
// 3. 调用e签宝看是否进行过认证
respMeta := map[string]interface{}{}
// 已是「已提交企业信息」:说明已通过人工审核,直接返回当前认证数据,前端可刷新到企业认证步骤
if cert.Status == enums.StatusInfoSubmitted {
response = s.convertToResponse(cert)
response.Metadata = map[string]interface{}{
"next_action": "您已通过审核,请完成企业认证步骤",
}
return nil
}
identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
OrgName: cmd.CompanyName,
})
if identity != nil && identity.Data.RealnameStatus == 1 {
// 已提交
err = cert.SubmitEnterpriseInfo(enterpriseInfo, "", "")
if err != nil {
return fmt.Errorf("提交企业认证信息失败: %s", err.Error())
}
s.logger.Info("企业认证成功", zap.Any("identity", identity))
// 4. 提交企业信息进入人工审核(不调用 e签宝不生成认证链接
err = cert.SubmitEnterpriseInfoForReview(enterpriseInfo)
if err != nil {
return fmt.Errorf("提交企业信息失败: %s", err.Error())
}
// 完成企业认证流程
err = s.completeEnterpriseVerification(txCtx, cert, cmd.UserID, cmd.CompanyName, cmd.LegalPersonName)
if err != nil {
return err
}
respMeta = map[string]interface{}{
"enterprise_info": enterpriseInfo,
"next_action": "企业已认证,可进行后续操作",
}
} else {
if err != nil {
s.logger.Error("e签宝查询企业认证信息失败或未进行企业认证", zap.Error(err))
}
authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{
CompanyName: enterpriseInfo.CompanyName,
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
LegalPersonName: enterpriseInfo.LegalPersonName,
LegalPersonID: enterpriseInfo.LegalPersonID,
TransactorName: enterpriseInfo.LegalPersonName,
TransactorMobile: enterpriseInfo.LegalPersonPhone,
TransactorID: enterpriseInfo.LegalPersonID,
})
if err != nil {
s.logger.Error("生成企业认证链接失败", zap.Error(err))
return fmt.Errorf("生成企业认证链接失败: %s", err.Error())
}
err = cert.SubmitEnterpriseInfo(enterpriseInfo, authURL.AuthShortURL, authURL.AuthFlowID)
if err != nil {
return fmt.Errorf("提交企业认证信息失败: %s", err.Error())
}
respMeta = map[string]interface{}{
"enterprise_info": enterpriseInfo,
"authUrl": authURL.AuthURL,
"next_action": "请完成企业认证",
}
respMeta := map[string]interface{}{
"enterprise_info": enterpriseInfo,
"next_action": "请等待管理员审核企业信息",
}
err = s.aggregateService.SaveCertification(txCtx, cert)
@@ -694,6 +697,183 @@ func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutC
return response, nil
}
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
ctx context.Context,
query *queries.AdminListSubmitRecordsQuery,
) (*responses.AdminSubmitRecordsListResponse, error) {
if query.PageSize <= 0 {
query.PageSize = 10
}
if query.Page <= 0 {
query.Page = 1
}
filter := repositories.ListSubmitRecordsFilter{
Page: query.Page,
PageSize: query.PageSize,
ManualReviewStatus: query.ManualReviewStatus,
}
result, err := s.enterpriseInfoSubmitRecordRepo.List(ctx, filter)
if err != nil {
return nil, fmt.Errorf("查询提交记录失败: %w", err)
}
items := make([]*responses.AdminSubmitRecordItem, 0, len(result.Records))
for _, r := range result.Records {
certStatus := ""
if cert, err := s.aggregateService.LoadCertificationByUserID(ctx, r.UserID); err == nil && cert != nil {
certStatus = string(cert.Status)
}
items = append(items, &responses.AdminSubmitRecordItem{
ID: r.ID,
UserID: r.UserID,
CompanyName: r.CompanyName,
UnifiedSocialCode: r.UnifiedSocialCode,
LegalPersonName: r.LegalPersonName,
SubmitAt: r.SubmitAt,
Status: r.Status,
ManualReviewStatus: r.ManualReviewStatus,
ManualReviewedAt: r.ManualReviewedAt,
CertificationStatus: certStatus,
})
}
totalPages := int((result.Total + int64(query.PageSize) - 1) / int64(query.PageSize))
if totalPages == 0 {
totalPages = 1
}
return &responses.AdminSubmitRecordsListResponse{
Items: items,
Total: result.Total,
Page: query.Page,
PageSize: query.PageSize,
TotalPages: totalPages,
}, nil
}
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error) {
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
if err != nil {
return nil, fmt.Errorf("获取提交记录失败: %w", err)
}
certStatus := ""
if cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID); loadErr == nil && cert != nil {
certStatus = string(cert.Status)
}
return &responses.AdminSubmitRecordDetail{
ID: record.ID,
UserID: record.UserID,
CompanyName: record.CompanyName,
UnifiedSocialCode: record.UnifiedSocialCode,
LegalPersonName: record.LegalPersonName,
LegalPersonID: record.LegalPersonID,
LegalPersonPhone: record.LegalPersonPhone,
EnterpriseAddress: record.EnterpriseAddress,
AuthorizedRepName: record.AuthorizedRepName,
AuthorizedRepID: record.AuthorizedRepID,
AuthorizedRepPhone: record.AuthorizedRepPhone,
AuthorizedRepIDImageURLs: record.AuthorizedRepIDImageURLs,
BusinessLicenseImageURL: record.BusinessLicenseImageURL,
OfficePlaceImageURLs: record.OfficePlaceImageURLs,
APIUsage: record.APIUsage,
ScenarioAttachmentURLs: record.ScenarioAttachmentURLs,
Status: record.Status,
SubmitAt: record.SubmitAt,
VerifiedAt: record.VerifiedAt,
FailedAt: record.FailedAt,
FailureReason: record.FailureReason,
ManualReviewStatus: record.ManualReviewStatus,
ManualReviewRemark: record.ManualReviewRemark,
ManualReviewedAt: record.ManualReviewedAt,
ManualReviewerID: record.ManualReviewerID,
CertificationStatus: certStatus,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
}, nil
}
// AdminApproveSubmitRecord 管理端审核通过
func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error {
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
if err != nil {
return fmt.Errorf("获取提交记录失败: %w", err)
}
if record.ManualReviewStatus != "pending" {
return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus)
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
if cert.Status != enums.StatusInfoPendingReview {
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
}
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
CompanyName: record.CompanyName,
UnifiedSocialCode: record.UnifiedSocialCode,
LegalPersonName: record.LegalPersonName,
LegalPersonID: record.LegalPersonID,
LegalPersonPhone: record.LegalPersonPhone,
EnterpriseAddress: record.EnterpriseAddress,
}
authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{
CompanyName: enterpriseInfo.CompanyName,
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
LegalPersonName: enterpriseInfo.LegalPersonName,
LegalPersonID: enterpriseInfo.LegalPersonID,
TransactorName: enterpriseInfo.LegalPersonName,
TransactorMobile: enterpriseInfo.LegalPersonPhone,
TransactorID: enterpriseInfo.LegalPersonID,
})
if err != nil {
return fmt.Errorf("生成企业认证链接失败: %w", err)
}
record.MarkManualApproved(adminID, remark)
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
return fmt.Errorf("保存提交记录失败: %w", err)
}
if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, adminID); err != nil {
return fmt.Errorf("更新认证状态失败: %w", err)
}
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
return fmt.Errorf("保存认证信息失败: %w", err)
}
s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
return nil
}
// AdminRejectSubmitRecord 管理端审核拒绝
func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error {
if remark == "" {
return fmt.Errorf("拒绝时必须填写审核备注")
}
record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID)
if err != nil {
return fmt.Errorf("获取提交记录失败: %w", err)
}
if record.ManualReviewStatus != "pending" {
return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus)
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
if cert.Status != enums.StatusInfoPendingReview {
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
}
record.MarkManualRejected(adminID, remark)
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
return fmt.Errorf("保存提交记录失败: %w", err)
}
if err := cert.RejectEnterpriseInfoReview(adminID, remark); err != nil {
return fmt.Errorf("更新认证状态失败: %w", err)
}
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
return fmt.Errorf("保存认证信息失败: %w", err)
}
s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
return nil
}
// ================ 辅助方法 ================
// convertToResponse 转换实体为响应DTO

View File

@@ -104,4 +104,19 @@ type SubmitEnterpriseInfoCommand struct {
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号11位13800138000"`
EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"`
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
// 营业执照图片 URL单张
BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"`
// 办公场地图片 URL 列表(前端传 string 数组)
OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"`
// 授权代表信息(与前端 authorized_rep_* 及表字段一致)
AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"`
AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"`
AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"`
AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"`
// 应用场景
APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"`
ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"`
}

View File

@@ -192,3 +192,10 @@ func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
}
return false
}
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询
type AdminListSubmitRecordsQuery struct {
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
ManualReviewStatus string `json:"manual_review_status" form:"manual_review_status"` // pending, approved, rejected空为全部
}

View File

@@ -53,13 +53,13 @@ type CertificationResponse struct {
// ConfirmAuthResponse 确认认证状态响应
type ConfirmAuthResponse struct {
Status enums.CertificationStatus `json:"status"`
Reason string `json:"reason"`
Reason string `json:"reason"`
}
// ConfirmSignResponse 确认签署状态响应
type ConfirmSignResponse struct {
Status enums.CertificationStatus `json:"status"`
Reason string `json:"reason"`
Reason string `json:"reason"`
}
// CertificationListResponse 认证列表响应
@@ -81,7 +81,6 @@ type ContractSignUrlResponse struct {
Message string `json:"message"`
}
// SystemMonitoringResponse 系统监控响应
type SystemMonitoringResponse struct {
TimeRange string `json:"time_range"`
@@ -111,6 +110,61 @@ type SystemHealthStatus struct {
Details map[string]interface{} `json:"details,omitempty"`
}
// AdminSubmitRecordItem 管理端提交记录列表项
type AdminSubmitRecordItem struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
SubmitAt time.Time `json:"submit_at"`
Status string `json:"status"`
ManualReviewStatus string `json:"manual_review_status"`
ManualReviewedAt *time.Time `json:"manual_reviewed_at,omitempty"`
CertificationStatus string `json:"certification_status,omitempty"` // 该用户当前认证状态,用于前端判断是否已完成企业认证并显示「已审核」
}
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL
type AdminSubmitRecordDetail struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
LegalPersonID string `json:"legal_person_id"`
LegalPersonPhone string `json:"legal_person_phone"`
EnterpriseAddress string `json:"enterprise_address"`
AuthorizedRepName string `json:"authorized_rep_name"`
AuthorizedRepID string `json:"authorized_rep_id"`
AuthorizedRepPhone string `json:"authorized_rep_phone"`
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组
BusinessLicenseImageURL string `json:"business_license_image_url"`
OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串
APIUsage string `json:"api_usage"`
ScenarioAttachmentURLs string `json:"scenario_attachment_urls"`
Status string `json:"status"`
SubmitAt time.Time `json:"submit_at"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
FailedAt *time.Time `json:"failed_at,omitempty"`
FailureReason string `json:"failure_reason,omitempty"`
ManualReviewStatus string `json:"manual_review_status"`
ManualReviewRemark string `json:"manual_review_remark,omitempty"`
ManualReviewedAt *time.Time `json:"manual_reviewed_at,omitempty"`
ManualReviewerID string `json:"manual_reviewer_id,omitempty"`
CertificationStatus string `json:"certification_status,omitempty"` // 该用户当前认证状态,用于前端显示「已审核」
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AdminSubmitRecordsListResponse 管理端提交记录列表响应
type AdminSubmitRecordsListResponse struct {
Items []*AdminSubmitRecordItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ================ 响应构建辅助方法 ================
// NewCertificationListResponse 创建认证列表响应
@@ -146,7 +200,6 @@ func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextActio
return response
}
// NewSystemAlert 创建系统警告
func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert {
return &SystemAlert{
@@ -161,7 +214,6 @@ func NewSystemAlert(level, alertType, message, metric string, value, threshold i
}
}
// IsHealthy 检查系统是否健康
func (r *SystemMonitoringResponse) IsHealthy() bool {
return r.SystemHealth.Overall == "healthy"