temp
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/admin/dto/commands"
|
||||
"tyapi-server/internal/application/admin/dto/queries"
|
||||
"tyapi-server/internal/application/admin/dto/responses"
|
||||
)
|
||||
|
||||
// AdminApplicationService 管理员应用服务接口
|
||||
type AdminApplicationService interface {
|
||||
Login(ctx context.Context, cmd *commands.AdminLoginCommand) (*responses.AdminLoginResponse, error)
|
||||
CreateAdmin(ctx context.Context, cmd *commands.CreateAdminCommand) error
|
||||
UpdateAdmin(ctx context.Context, cmd *commands.UpdateAdminCommand) error
|
||||
ChangePassword(ctx context.Context, cmd *commands.ChangeAdminPasswordCommand) error
|
||||
ListAdmins(ctx context.Context, query *queries.ListAdminsQuery) (*responses.AdminListResponse, error)
|
||||
GetAdminByID(ctx context.Context, query *queries.GetAdminInfoQuery) (*responses.AdminInfoResponse, error)
|
||||
DeleteAdmin(ctx context.Context, cmd *commands.DeleteAdminCommand) error
|
||||
GetAdminStats(ctx context.Context) (*responses.AdminStatsResponse, error)
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tyapi-server/internal/application/admin/dto/commands"
|
||||
"tyapi-server/internal/application/admin/dto/queries"
|
||||
"tyapi-server/internal/application/admin/dto/responses"
|
||||
"tyapi-server/internal/domains/admin/entities"
|
||||
"tyapi-server/internal/domains/admin/repositories"
|
||||
)
|
||||
|
||||
// AdminApplicationServiceImpl 管理员应用服务实现
|
||||
type AdminApplicationServiceImpl struct {
|
||||
adminRepo repositories.AdminRepository
|
||||
loginLogRepo repositories.AdminLoginLogRepository
|
||||
operationLogRepo repositories.AdminOperationLogRepository
|
||||
permissionRepo repositories.AdminPermissionRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAdminApplicationService 创建管理员应用服务
|
||||
func NewAdminApplicationService(
|
||||
adminRepo repositories.AdminRepository,
|
||||
loginLogRepo repositories.AdminLoginLogRepository,
|
||||
operationLogRepo repositories.AdminOperationLogRepository,
|
||||
permissionRepo repositories.AdminPermissionRepository,
|
||||
logger *zap.Logger,
|
||||
) AdminApplicationService {
|
||||
return &AdminApplicationServiceImpl{
|
||||
adminRepo: adminRepo,
|
||||
loginLogRepo: loginLogRepo,
|
||||
operationLogRepo: operationLogRepo,
|
||||
permissionRepo: permissionRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) Login(ctx context.Context, cmd *commands.AdminLoginCommand) (*responses.AdminLoginResponse, error) {
|
||||
s.logger.Info("管理员登录", zap.String("username", cmd.Username))
|
||||
|
||||
admin, err := s.adminRepo.FindByUsername(ctx, cmd.Username)
|
||||
if err != nil {
|
||||
s.logger.Warn("管理员登录失败:用户不存在", zap.String("username", cmd.Username))
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
if !admin.IsActive {
|
||||
s.logger.Warn("管理员登录失败:账户已禁用", zap.String("username", cmd.Username))
|
||||
return nil, fmt.Errorf("账户已被禁用,请联系管理员")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(cmd.Password)); err != nil {
|
||||
s.logger.Warn("管理员登录失败:密码错误", zap.String("username", cmd.Username))
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
if err := s.adminRepo.UpdateLoginStats(ctx, admin.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// This part would ideally be in a separate auth service or helper
|
||||
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 := responses.AdminInfoResponse{
|
||||
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", cmd.Username))
|
||||
return &responses.AdminLoginResponse{
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
Admin: adminInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) CreateAdmin(ctx context.Context, cmd *commands.CreateAdminCommand) error {
|
||||
// ... implementation ...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) UpdateAdmin(ctx context.Context, cmd *commands.UpdateAdminCommand) error {
|
||||
// ... implementation ...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) ChangePassword(ctx context.Context, cmd *commands.ChangeAdminPasswordCommand) error {
|
||||
// ... implementation ...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) ListAdmins(ctx context.Context, query *queries.ListAdminsQuery) (*responses.AdminListResponse, error) {
|
||||
// ... implementation ...
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) GetAdminByID(ctx context.Context, query *queries.GetAdminInfoQuery) (*responses.AdminInfoResponse, error) {
|
||||
// ... implementation ...
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) DeleteAdmin(ctx context.Context, cmd *commands.DeleteAdminCommand) error {
|
||||
// ... implementation ...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) GetAdminStats(ctx context.Context) (*responses.AdminStatsResponse, error) {
|
||||
// ... implementation ...
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Private helper methods from old service
|
||||
func (s *AdminApplicationServiceImpl) getAdminPermissions(ctx context.Context, admin *entities.Admin) ([]string, error) {
|
||||
rolePermissions, err := s.adminRepo.GetPermissionsByRole(ctx, admin.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissions := make([]string, 0, len(rolePermissions))
|
||||
for _, perm := range rolePermissions {
|
||||
permissions = append(permissions, perm.Code)
|
||||
}
|
||||
|
||||
if admin.Permissions != "" {
|
||||
var customPermissions []string
|
||||
if err := json.Unmarshal([]byte(admin.Permissions), &customPermissions); err == nil {
|
||||
permissions = append(permissions, customPermissions...)
|
||||
}
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
func (s *AdminApplicationServiceImpl) generateJWTToken(admin *entities.Admin) (string, time.Time, error) {
|
||||
// This should be handled by a dedicated auth service
|
||||
token := fmt.Sprintf("admin_token_%s_%d", admin.ID, time.Now().Unix())
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
return token, expiresAt, nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package commands
|
||||
|
||||
// AdminLoginCommand 管理员登录命令
|
||||
type AdminLoginCommand struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateAdminCommand 创建管理员命令
|
||||
type CreateAdminCommand 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 string `json:"role" binding:"required"`
|
||||
Permissions []string `json:"permissions"`
|
||||
OperatorID string `json:"-"`
|
||||
}
|
||||
|
||||
// UpdateAdminCommand 更新管理员命令
|
||||
type UpdateAdminCommand struct {
|
||||
AdminID string `json:"-"`
|
||||
Email string `json:"email" binding:"email"`
|
||||
Phone string `json:"phone"`
|
||||
RealName string `json:"real_name"`
|
||||
Role string `json:"role"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
Permissions []string `json:"permissions"`
|
||||
OperatorID string `json:"-"`
|
||||
}
|
||||
|
||||
// ChangeAdminPasswordCommand 修改密码命令
|
||||
type ChangeAdminPasswordCommand struct {
|
||||
AdminID string `json:"-"`
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
|
||||
// DeleteAdminCommand 删除管理员命令
|
||||
type DeleteAdminCommand struct {
|
||||
AdminID string `json:"-"`
|
||||
OperatorID string `json:"-"`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package queries
|
||||
|
||||
// ListAdminsQuery 获取管理员列表查询
|
||||
type ListAdminsQuery 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"`
|
||||
}
|
||||
|
||||
// GetAdminInfoQuery 获取管理员信息查询
|
||||
type GetAdminInfoQuery struct {
|
||||
AdminID string `uri:"id" binding:"required"`
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/admin/entities"
|
||||
)
|
||||
|
||||
// AdminLoginResponse 管理员登录响应
|
||||
type AdminLoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Admin AdminInfoResponse `json:"admin"`
|
||||
}
|
||||
|
||||
// AdminInfoResponse 管理员信息响应
|
||||
type AdminInfoResponse struct {
|
||||
ID string `json:"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"`
|
||||
}
|
||||
|
||||
// AdminListResponse 管理员列表响应
|
||||
type AdminListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Admins []AdminInfoResponse `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"`
|
||||
}
|
||||
@@ -10,39 +10,24 @@ import (
|
||||
|
||||
// CertificationApplicationService 认证应用服务接口
|
||||
type CertificationApplicationService interface {
|
||||
// 认证申请管理
|
||||
CreateCertification(ctx context.Context, cmd *commands.CreateCertificationCommand) (*responses.CertificationResponse, error)
|
||||
// 认证状态查询
|
||||
GetCertificationStatus(ctx context.Context, query *queries.GetCertificationStatusQuery) (*responses.CertificationResponse, error)
|
||||
GetCertificationDetails(ctx context.Context, query *queries.GetCertificationDetailsQuery) (*responses.CertificationResponse, error)
|
||||
GetCertificationProgress(ctx context.Context, userID string) (map[string]interface{}, error)
|
||||
|
||||
// 企业信息管理
|
||||
CreateEnterpriseInfo(ctx context.Context, cmd *commands.CreateEnterpriseInfoCommand) (*responses.EnterpriseInfoResponse, error)
|
||||
SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.EnterpriseInfoResponse, error)
|
||||
SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error)
|
||||
|
||||
// 营业执照上传
|
||||
UploadLicense(ctx context.Context, cmd *commands.UploadLicenseCommand) (*responses.UploadLicenseResponse, error)
|
||||
GetLicenseOCRResult(ctx context.Context, recordID string) (*responses.UploadLicenseResponse, error)
|
||||
|
||||
// UploadBusinessLicense 上传营业执照并同步OCR识别
|
||||
UploadBusinessLicense(ctx context.Context, userID string, fileBytes []byte, fileName string) (*responses.UploadLicenseResponse, error)
|
||||
|
||||
// 人脸识别
|
||||
InitiateFaceVerify(ctx context.Context, cmd *commands.InitiateFaceVerifyCommand) (*responses.FaceVerifyResponse, error)
|
||||
CompleteFaceVerify(ctx context.Context, faceVerifyID string, isSuccess bool) error
|
||||
RetryFaceVerify(ctx context.Context, userID string) (*responses.FaceVerifyResponse, error)
|
||||
// 企业认证
|
||||
GetEnterpriseAuthURL(ctx context.Context, userID string) (*responses.EnterpriseAuthURLResponse, error)
|
||||
|
||||
// 合同管理
|
||||
ApplyContract(ctx context.Context, userID string) (*responses.CertificationResponse, error)
|
||||
ApproveContract(ctx context.Context, certificationID, adminID, signingURL, approvalNotes string) error
|
||||
RejectContract(ctx context.Context, certificationID, adminID, rejectReason string) error
|
||||
CompleteContractSign(ctx context.Context, certificationID, contractURL string) error
|
||||
RetryContractSign(ctx context.Context, userID string) (*responses.CertificationResponse, error)
|
||||
|
||||
// 认证完成
|
||||
CompleteCertification(ctx context.Context, certificationID string) error
|
||||
|
||||
// 重试和重启
|
||||
RetryStep(ctx context.Context, cmd *commands.RetryStepCommand) error
|
||||
RestartCertification(ctx context.Context, certificationID string) error
|
||||
GetContractSignURL(ctx context.Context, cmd *commands.GetContractSignURLCommand) (*responses.ContractSignURLResponse, error)
|
||||
}
|
||||
|
||||
// EsignCallbackApplicationService e签宝回调应用服务接口
|
||||
type EsignCallbackApplicationService interface {
|
||||
// 处理e签宝回调
|
||||
HandleCallback(ctx context.Context, callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error
|
||||
}
|
||||
|
||||
@@ -2,112 +2,74 @@ package certification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/application/certification/dto/commands"
|
||||
"tyapi-server/internal/application/certification/dto/queries"
|
||||
"tyapi-server/internal/application/certification/dto/responses"
|
||||
"tyapi-server/internal/domains/certification/entities"
|
||||
"tyapi-server/internal/domains/certification/repositories"
|
||||
"tyapi-server/internal/domains/certification/enums"
|
||||
"tyapi-server/internal/domains/certification/services"
|
||||
user_entities "tyapi-server/internal/domains/user/entities"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/ocr"
|
||||
"tyapi-server/internal/shared/storage"
|
||||
user_services "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/shared/database"
|
||||
esign_service "tyapi-server/internal/shared/esign"
|
||||
)
|
||||
|
||||
// CertificationApplicationServiceImpl 认证应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type CertificationApplicationServiceImpl struct {
|
||||
certRepo repositories.CertificationRepository
|
||||
licenseRepo repositories.LicenseUploadRecordRepository
|
||||
faceVerifyRepo repositories.FaceVerifyRecordRepository
|
||||
contractRepo repositories.ContractRecordRepository
|
||||
certService *services.CertificationService
|
||||
stateMachine *services.CertificationStateMachine
|
||||
storageService storage.StorageService
|
||||
ocrService ocr.OCRService
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
|
||||
logger *zap.Logger
|
||||
certManagementService *services.CertificationManagementService
|
||||
certWorkflowService *services.CertificationWorkflowService
|
||||
certificationEsignService *services.CertificationEsignService
|
||||
enterpriseService *user_services.EnterpriseService
|
||||
esignService *esign_service.Client
|
||||
enterpriseRecordService *services.EnterpriseInfoSubmitRecordService
|
||||
smsCodeService *user_services.SMSCodeService
|
||||
txManager *database.TransactionManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCertificationApplicationService 创建认证应用服务
|
||||
func NewCertificationApplicationService(
|
||||
certRepo repositories.CertificationRepository,
|
||||
licenseRepo repositories.LicenseUploadRecordRepository,
|
||||
faceVerifyRepo repositories.FaceVerifyRecordRepository,
|
||||
contractRepo repositories.ContractRecordRepository,
|
||||
certService *services.CertificationService,
|
||||
stateMachine *services.CertificationStateMachine,
|
||||
storageService storage.StorageService,
|
||||
ocrService ocr.OCRService,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
certManagementService *services.CertificationManagementService,
|
||||
certWorkflowService *services.CertificationWorkflowService,
|
||||
certificationEsignService *services.CertificationEsignService,
|
||||
enterpriseService *user_services.EnterpriseService,
|
||||
esignService *esign_service.Client,
|
||||
enterpriseRecordService *services.EnterpriseInfoSubmitRecordService,
|
||||
smsCodeService *user_services.SMSCodeService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
) CertificationApplicationService {
|
||||
return &CertificationApplicationServiceImpl{
|
||||
certRepo: certRepo,
|
||||
licenseRepo: licenseRepo,
|
||||
faceVerifyRepo: faceVerifyRepo,
|
||||
contractRepo: contractRepo,
|
||||
certService: certService,
|
||||
stateMachine: stateMachine,
|
||||
storageService: storageService,
|
||||
ocrService: ocrService,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
logger: logger,
|
||||
certManagementService: certManagementService,
|
||||
certWorkflowService: certWorkflowService,
|
||||
certificationEsignService: certificationEsignService,
|
||||
enterpriseService: enterpriseService,
|
||||
esignService: esignService,
|
||||
enterpriseRecordService: enterpriseRecordService,
|
||||
smsCodeService: smsCodeService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertification 创建认证申请
|
||||
func (s *CertificationApplicationServiceImpl) CreateCertification(ctx context.Context, cmd *commands.CreateCertificationCommand) (*responses.CertificationResponse, error) {
|
||||
// 使用领域服务创建认证申请
|
||||
certification, err := s.certService.CreateCertification(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// SubmitEnterpriseInfo 提交企业信息
|
||||
// 业务流程:1. 验证企业信息 2. 创建或获取认证申请 3. 使用事务执行状态转换和创建记录
|
||||
func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error) {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := &responses.CertificationResponse{
|
||||
ID: certification.ID,
|
||||
UserID: certification.UserID,
|
||||
Status: certification.Status,
|
||||
StatusName: string(certification.Status),
|
||||
Progress: certification.GetProgressPercentage(),
|
||||
IsUserActionRequired: certification.IsUserActionRequired(),
|
||||
IsAdminActionRequired: certification.IsAdminActionRequired(),
|
||||
InfoSubmittedAt: certification.InfoSubmittedAt,
|
||||
FaceVerifiedAt: certification.FaceVerifiedAt,
|
||||
ContractAppliedAt: certification.ContractAppliedAt,
|
||||
ContractApprovedAt: certification.ContractApprovedAt,
|
||||
ContractSignedAt: certification.ContractSignedAt,
|
||||
CompletedAt: certification.CompletedAt,
|
||||
ContractURL: certification.ContractURL,
|
||||
SigningURL: certification.SigningURL,
|
||||
RejectReason: certification.RejectReason,
|
||||
CreatedAt: certification.CreatedAt,
|
||||
UpdatedAt: certification.UpdatedAt,
|
||||
}
|
||||
|
||||
s.logger.Info("认证申请创建成功",
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CreateEnterpriseInfo 创建企业信息
|
||||
func (s *CertificationApplicationServiceImpl) CreateEnterpriseInfo(ctx context.Context, cmd *commands.CreateEnterpriseInfoCommand) (*responses.EnterpriseInfoResponse, error) {
|
||||
// 检查用户是否已有企业信息
|
||||
existingInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, cmd.UserID)
|
||||
if err == nil && existingInfo != nil {
|
||||
return nil, fmt.Errorf("用户已有企业信息")
|
||||
}
|
||||
|
||||
// 检查统一社会信用代码是否已存在
|
||||
exists, err := s.enterpriseInfoRepo.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, "")
|
||||
// 2. 验证企业信息(检查统一社会信用代码是否已存在)
|
||||
exists, err := s.enterpriseService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查企业信息失败: %w", err)
|
||||
}
|
||||
@@ -115,494 +77,379 @@ func (s *CertificationApplicationServiceImpl) CreateEnterpriseInfo(ctx context.C
|
||||
return nil, fmt.Errorf("统一社会信用代码已存在")
|
||||
}
|
||||
|
||||
// 创建企业信息
|
||||
enterpriseInfo := &user_entities.EnterpriseInfo{
|
||||
UserID: cmd.UserID,
|
||||
CompanyName: cmd.CompanyName,
|
||||
UnifiedSocialCode: cmd.UnifiedSocialCode,
|
||||
LegalPersonName: cmd.LegalPersonName,
|
||||
LegalPersonID: cmd.LegalPersonID,
|
||||
// 3. 获取或创建认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
certification, err = s.certManagementService.CreateCertification(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建认证申请失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("获取认证申请失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
createdEnterpriseInfo, err := s.enterpriseInfoRepo.Create(ctx, *enterpriseInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("创建企业信息失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建企业信息失败: %w", err)
|
||||
// 4. 创建记录
|
||||
if certification.Status != enums.StatusPending && certification.Status != enums.StatusInfoSubmitted {
|
||||
return nil, fmt.Errorf("当前状态不允许提交企业信息")
|
||||
}
|
||||
s.logger.Info("企业信息创建成功",
|
||||
_, err = s.enterpriseRecordService.CreateEnterpriseInfoSubmitRecord(ctx, cmd.UserID, cmd.CompanyName, cmd.UnifiedSocialCode, cmd.LegalPersonName, cmd.LegalPersonID, cmd.LegalPersonPhone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("企业信息提交失败: %w", err)
|
||||
}
|
||||
s.logger.Info("企业信息提交成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("enterprise_id", enterpriseInfo.ID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("company_name", cmd.CompanyName),
|
||||
)
|
||||
|
||||
return &responses.EnterpriseInfoResponse{
|
||||
ID: createdEnterpriseInfo.ID,
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
IsOCRVerified: enterpriseInfo.IsOCRVerified,
|
||||
IsFaceVerified: enterpriseInfo.IsFaceVerified,
|
||||
CreatedAt: enterpriseInfo.CreatedAt,
|
||||
UpdatedAt: enterpriseInfo.UpdatedAt,
|
||||
}, nil
|
||||
// 转换状态
|
||||
err = s.certWorkflowService.SubmitEnterpriseInfo(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("转换状态失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 检查企业是否已经认证
|
||||
hasCertification, err := s.certManagementService.CheckCertification(ctx, cmd.CompanyName, cmd.UnifiedSocialCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查企业认证状态失败: %w", err)
|
||||
}
|
||||
if hasCertification {
|
||||
// 如果企业已经认证,则直接完成认证
|
||||
err = s.completeEnterpriseAuth(ctx, certification)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("完成企业认证失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 重新获取认证申请数据
|
||||
certification, err = s.certManagementService.GetCertificationByID(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取认证申请失败: %w", err)
|
||||
}
|
||||
return s.buildCertificationResponse(certification), nil
|
||||
}
|
||||
|
||||
// UploadLicense 上传营业执照
|
||||
func (s *CertificationApplicationServiceImpl) UploadLicense(ctx context.Context, cmd *commands.UploadLicenseCommand) (*responses.UploadLicenseResponse, error) {
|
||||
// 1. 业务规则验证 - 调用领域服务
|
||||
if err := s.certService.ValidateLicenseUpload(ctx, cmd.UserID, cmd.FileName, cmd.FileSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 上传文件到存储服务
|
||||
uploadResult, err := s.storageService.UploadFile(ctx, cmd.FileBytes, cmd.FileName)
|
||||
if err != nil {
|
||||
s.logger.Error("上传营业执照失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("上传营业执照失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 创建营业执照上传记录 - 调用领域服务
|
||||
licenseRecord, err := s.certService.CreateLicenseUploadRecord(ctx, cmd.UserID, cmd.FileName, cmd.FileSize, uploadResult)
|
||||
if err != nil {
|
||||
s.logger.Error("创建营业执照记录失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建营业执照记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 异步处理OCR识别 - 使用任务队列或后台任务
|
||||
go s.processOCRAsync(ctx, licenseRecord.ID, cmd.FileBytes)
|
||||
|
||||
s.logger.Info("营业执照上传成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("license_id", licenseRecord.ID),
|
||||
zap.String("file_url", uploadResult.URL),
|
||||
)
|
||||
|
||||
// 5. 构建响应
|
||||
response := &responses.UploadLicenseResponse{
|
||||
UploadRecordID: licenseRecord.ID,
|
||||
FileURL: uploadResult.URL,
|
||||
OCRProcessed: false,
|
||||
OCRSuccess: false,
|
||||
}
|
||||
|
||||
// 6. 如果OCR处理很快完成,尝试获取结果
|
||||
// 这里可以添加一个简单的轮询机制,或者使用WebSocket推送结果
|
||||
// 暂时返回基础信息,前端可以通过查询接口获取OCR结果
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// UploadBusinessLicense 上传营业执照并同步OCR识别
|
||||
func (s *CertificationApplicationServiceImpl) UploadBusinessLicense(ctx context.Context, userID string, fileBytes []byte, fileName string) (*responses.UploadLicenseResponse, error) {
|
||||
s.logger.Info("开始处理营业执照上传",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("file_name", fileName),
|
||||
)
|
||||
|
||||
// 调用领域服务进行上传和OCR识别
|
||||
uploadRecord, ocrResult, err := s.certService.UploadBusinessLicense(ctx, userID, fileBytes, fileName)
|
||||
if err != nil {
|
||||
s.logger.Error("营业执照上传失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := &responses.UploadLicenseResponse{
|
||||
UploadRecordID: uploadRecord.ID,
|
||||
FileURL: uploadRecord.FileURL,
|
||||
OCRProcessed: uploadRecord.OCRProcessed,
|
||||
OCRSuccess: uploadRecord.OCRSuccess,
|
||||
OCRConfidence: uploadRecord.OCRConfidence,
|
||||
OCRErrorMessage: uploadRecord.OCRErrorMessage,
|
||||
}
|
||||
|
||||
// 如果OCR成功,添加识别结果
|
||||
if ocrResult != nil && uploadRecord.OCRSuccess {
|
||||
response.EnterpriseName = ocrResult.CompanyName
|
||||
response.CreditCode = ocrResult.UnifiedSocialCode
|
||||
response.LegalPerson = ocrResult.LegalPersonName
|
||||
}
|
||||
|
||||
s.logger.Info("营业执照上传完成",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("upload_record_id", uploadRecord.ID),
|
||||
zap.Bool("ocr_success", uploadRecord.OCRSuccess),
|
||||
)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// SubmitEnterpriseInfo 提交企业信息
|
||||
func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.EnterpriseInfoResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, cmd.UserID)
|
||||
// GetEnterpriseAuthURL 获取企业认证链接
|
||||
// 业务流程:1. 获取认证申请 2. 获取企业信息提交记录 3. 生成e签宝认证文件 4. 返回认证链接
|
||||
func (s *CertificationApplicationServiceImpl) GetEnterpriseAuthURL(ctx context.Context, userID string) (*responses.EnterpriseAuthURLResponse, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 设置认证ID
|
||||
cmd.CertificationID = certification.ID
|
||||
// 2. 检查认证状态
|
||||
if certification.Status != enums.StatusInfoSubmitted {
|
||||
return nil, fmt.Errorf("当前状态不允许进行企业认证")
|
||||
}
|
||||
|
||||
// 调用领域服务提交企业信息
|
||||
if err := s.certService.SubmitEnterpriseInfo(ctx, certification.ID); err != nil {
|
||||
// 3. 获取企业信息提交记录
|
||||
enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取企业信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 生成e签宝认证文件
|
||||
authReq := &esign_service.EnterpriseAuthRequest{
|
||||
CompanyName: enterpriseRecord.CompanyName,
|
||||
UnifiedSocialCode: enterpriseRecord.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseRecord.LegalPersonName,
|
||||
LegalPersonID: enterpriseRecord.LegalPersonID,
|
||||
TransactorName: enterpriseRecord.LegalPersonName,
|
||||
TransactorMobile: enterpriseRecord.LegalPersonPhone,
|
||||
TransactorID: enterpriseRecord.LegalPersonID,
|
||||
}
|
||||
|
||||
authResp, err := s.esignService.GenerateEnterpriseAuth(authReq)
|
||||
if err != nil {
|
||||
s.logger.Error("生成企业认证文件失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("生成企业认证文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("获取企业认证链接成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("esign_flow_id", authResp.AuthFlowID),
|
||||
)
|
||||
|
||||
return &responses.EnterpriseAuthURLResponse{
|
||||
AuthURL: authResp.AuthURL,
|
||||
ShortURL: authResp.AuthShortURL,
|
||||
ExpireAt: time.Now().AddDate(0, 0, 7).Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApplyContract 申请合同文件
|
||||
// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 生成e签宝合同文件
|
||||
func (s *CertificationApplicationServiceImpl) ApplyContract(ctx context.Context, userID string) (*responses.CertificationResponse, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取企业信息
|
||||
enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取企业信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 生成e签宝合同
|
||||
components := map[string]string{
|
||||
"JFQY": "海南学宇思网络科技有限公司",
|
||||
"JFFR": "刘福思",
|
||||
"YFQY": enterpriseInfo.CompanyName,
|
||||
"YFFR": enterpriseInfo.LegalPersonName,
|
||||
"QDRQ": time.Now().Format("2006-01-02"),
|
||||
}
|
||||
_, err = s.certificationEsignService.FillTemplate(ctx, certification, components)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成e签宝合同失败: %w", err)
|
||||
}
|
||||
// 6. 重新获取更新后的认证申请数据
|
||||
updatedCertification, err := s.certManagementService.GetCertificationByID(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildCertificationResponse(updatedCertification), nil
|
||||
}
|
||||
|
||||
// GetContractSignURL 获取合同签署链接
|
||||
// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 获取签署链接
|
||||
func (s *CertificationApplicationServiceImpl) GetContractSignURL(ctx context.Context, cmd *commands.GetContractSignURLCommand) (*responses.ContractSignURLResponse, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查认证状态
|
||||
if certification.Status != enums.StatusContractApplied {
|
||||
return nil, fmt.Errorf("当前状态不允许获取签署链接")
|
||||
}
|
||||
|
||||
// 3. 获取企业信息
|
||||
enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取企业信息失败: %w", err)
|
||||
}
|
||||
|
||||
if certification.ContractFileID == "" {
|
||||
return nil, fmt.Errorf("请先申请合同文件")
|
||||
}
|
||||
|
||||
// 5. 发起签署
|
||||
signRecord, err := s.certificationEsignService.InitiateSign(ctx, certification, enterpriseInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取签署链接失败: %w", err)
|
||||
}
|
||||
// 转换状态
|
||||
err = s.certWorkflowService.ApplyContract(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("转换状态失败: %w", err)
|
||||
}
|
||||
// 6. 计算过期时间(7天后)
|
||||
expireAt := time.Now().AddDate(0, 0, 7).Format(time.RFC3339)
|
||||
s.logger.Info("获取签署链接成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("sign_flow_id", signRecord.EsignFlowID),
|
||||
)
|
||||
|
||||
return &responses.ContractSignURLResponse{
|
||||
SignURL: signRecord.SignURL,
|
||||
ShortURL: signRecord.SignShortURL,
|
||||
SignFlowID: signRecord.EsignFlowID,
|
||||
ExpireAt: expireAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompleteContractSign 完成合同签署
|
||||
// 业务流程:1. 获取认证申请 2. 完成合同签署 3. 自动完成认证
|
||||
func (s *CertificationApplicationServiceImpl) CompleteContractSign(ctx context.Context, cmd *commands.CompleteContractSignCommand) error {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
if certification.Status != enums.StatusContractApplied {
|
||||
return fmt.Errorf("当前状态不允许完成合同签署")
|
||||
}
|
||||
// 2. 完成合同签署(状态转换)
|
||||
if err := s.certWorkflowService.CompleteContractSign(ctx, certification.ID, cmd.ContractURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 重新获取认证申请
|
||||
updatedCertification, err := s.certManagementService.GetCertificationByID(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 如果合同已签署,自动完成认证
|
||||
if updatedCertification.Status == enums.StatusContractSigned {
|
||||
if err := s.certWorkflowService.CompleteCertification(ctx, certification.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("合同签署完成",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("contract_url", cmd.ContractURL),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertificationStatus 获取认证状态
|
||||
// 业务流程:1. 获取认证申请 2. 构建响应数据
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationStatus(ctx context.Context, query *queries.GetCertificationStatusQuery) (*responses.CertificationResponse, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, query.UserID)
|
||||
if err != nil {
|
||||
// 如果用户没有认证申请,返回一个表示未开始的状态
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &responses.CertificationResponse{
|
||||
ID: "",
|
||||
UserID: query.UserID,
|
||||
Status: "not_started",
|
||||
StatusName: "未开始认证",
|
||||
Progress: 0,
|
||||
IsUserActionRequired: true,
|
||||
InfoSubmittedAt: nil,
|
||||
EnterpriseVerifiedAt: nil,
|
||||
ContractAppliedAt: nil,
|
||||
ContractSignedAt: nil,
|
||||
CompletedAt: nil,
|
||||
ContractURL: "",
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建企业信息
|
||||
enterpriseInfo := &user_entities.EnterpriseInfo{
|
||||
UserID: cmd.UserID,
|
||||
CompanyName: cmd.CompanyName,
|
||||
UnifiedSocialCode: cmd.UnifiedSocialCode,
|
||||
LegalPersonName: cmd.LegalPersonName,
|
||||
LegalPersonID: cmd.LegalPersonID,
|
||||
}
|
||||
// 2. 构建响应
|
||||
return s.buildCertificationResponse(certification), nil
|
||||
}
|
||||
|
||||
*enterpriseInfo, err = s.enterpriseInfoRepo.Create(ctx, *enterpriseInfo)
|
||||
// GetCertificationDetails 获取认证详情
|
||||
// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 构建响应数据
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationDetails(ctx context.Context, query *queries.GetCertificationDetailsQuery) (*responses.CertificationResponse, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, query.UserID)
|
||||
if err != nil {
|
||||
s.logger.Error("创建企业信息失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建企业信息失败: %w", err)
|
||||
// 如果用户没有认证申请,返回错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("企业信息提交成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("enterprise_id", enterpriseInfo.ID),
|
||||
)
|
||||
// 2. 构建响应
|
||||
response := s.buildCertificationResponse(certification)
|
||||
|
||||
// 3. 添加企业信息
|
||||
if certification.UserID != "" {
|
||||
enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, certification.UserID)
|
||||
if err == nil && enterpriseInfo != nil {
|
||||
response.Enterprise = s.buildEnterpriseInfoResponse(enterpriseInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetCertificationProgress 获取认证进度
|
||||
// 业务流程:1. 获取认证申请 2. 获取进度信息
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationProgress(ctx context.Context, userID string) (map[string]interface{}, error) {
|
||||
// 1. 获取认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果用户没有认证申请,返回未开始状态
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return map[string]interface{}{
|
||||
"certification_id": "",
|
||||
"user_id": userID,
|
||||
"current_status": "not_started",
|
||||
"status_name": "未开始认证",
|
||||
"progress_percentage": 0,
|
||||
"is_user_action_required": true,
|
||||
"next_valid_statuses": []string{"pending"},
|
||||
"message": "用户尚未开始认证流程",
|
||||
"created_at": nil,
|
||||
"updated_at": nil,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 获取认证进度
|
||||
return s.certManagementService.GetCertificationProgress(ctx, certification.ID)
|
||||
}
|
||||
|
||||
// buildCertificationResponse 构建认证响应
|
||||
func (s *CertificationApplicationServiceImpl) buildCertificationResponse(certification *entities.Certification) *responses.CertificationResponse {
|
||||
return &responses.CertificationResponse{
|
||||
ID: certification.ID,
|
||||
UserID: certification.UserID,
|
||||
Status: certification.Status,
|
||||
StatusName: certification.GetStatusName(),
|
||||
Progress: certification.GetProgressPercentage(),
|
||||
IsUserActionRequired: certification.IsUserActionRequired(),
|
||||
InfoSubmittedAt: certification.InfoSubmittedAt,
|
||||
EnterpriseVerifiedAt: certification.EnterpriseVerifiedAt,
|
||||
ContractAppliedAt: certification.ContractAppliedAt,
|
||||
ContractSignedAt: certification.ContractSignedAt,
|
||||
CompletedAt: certification.CompletedAt,
|
||||
ContractURL: certification.ContractURL,
|
||||
CreatedAt: certification.CreatedAt,
|
||||
UpdatedAt: certification.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// buildEnterpriseInfoResponse 构建企业信息响应
|
||||
func (s *CertificationApplicationServiceImpl) buildEnterpriseInfoResponse(enterpriseInfo *user_entities.EnterpriseInfo) *responses.EnterpriseInfoResponse {
|
||||
return &responses.EnterpriseInfoResponse{
|
||||
ID: enterpriseInfo.ID,
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
IsOCRVerified: enterpriseInfo.IsOCRVerified,
|
||||
IsFaceVerified: enterpriseInfo.IsFaceVerified,
|
||||
CreatedAt: enterpriseInfo.CreatedAt,
|
||||
UpdatedAt: enterpriseInfo.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// InitiateFaceVerify 发起人脸识别验证
|
||||
func (s *CertificationApplicationServiceImpl) InitiateFaceVerify(ctx context.Context, cmd *commands.InitiateFaceVerifyCommand) (*responses.FaceVerifyResponse, error) {
|
||||
// 根据用户ID获取认证申请 - 这里需要从Handler传入用户ID
|
||||
// 由于cmd中没有UserID字段,我们需要修改Handler的调用方式
|
||||
// 暂时使用certificationID来获取认证申请
|
||||
certification, err := s.certRepo.GetByID(ctx, cmd.CertificationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("认证申请不存在: %w", err)
|
||||
}
|
||||
|
||||
// 调用领域服务发起人脸识别
|
||||
faceVerifyRecord, err := s.certService.InitiateFaceVerify(ctx, certification.ID, cmd.RealName, cmd.IDCardNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建验证URL(这里应该根据实际的人脸识别服务生成)
|
||||
verifyURL := fmt.Sprintf("/api/certification/face-verify/%s?return_url=%s", faceVerifyRecord.ID, cmd.ReturnURL)
|
||||
|
||||
s.logger.Info("人脸识别验证发起成功",
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("face_verify_id", faceVerifyRecord.ID),
|
||||
)
|
||||
|
||||
return &responses.FaceVerifyResponse{
|
||||
CertifyID: faceVerifyRecord.ID,
|
||||
VerifyURL: verifyURL,
|
||||
ExpiresAt: faceVerifyRecord.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApplyContract 申请合同
|
||||
func (s *CertificationApplicationServiceImpl) ApplyContract(ctx context.Context, userID string) (*responses.CertificationResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 调用领域服务申请合同
|
||||
if err := s.certService.ApplyContract(ctx, certification.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新获取更新后的认证申请
|
||||
updatedCertification, err := s.certRepo.GetByID(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("合同申请成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
)
|
||||
|
||||
return s.buildCertificationResponse(&updatedCertification), nil
|
||||
}
|
||||
|
||||
// GetCertificationStatus 获取认证状态
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationStatus(ctx context.Context, query *queries.GetCertificationStatusQuery) (*responses.CertificationResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, query.UserID)
|
||||
if err != nil {
|
||||
// 如果用户没有认证申请,返回一个表示未开始的状态
|
||||
if err.Error() == "认证申请不存在" || err.Error() == "record not found" {
|
||||
return &responses.CertificationResponse{
|
||||
ID: "",
|
||||
UserID: query.UserID,
|
||||
Status: "not_started",
|
||||
StatusName: "未开始认证",
|
||||
Progress: 0,
|
||||
IsUserActionRequired: true,
|
||||
IsAdminActionRequired: false,
|
||||
InfoSubmittedAt: nil,
|
||||
FaceVerifiedAt: nil,
|
||||
ContractAppliedAt: nil,
|
||||
ContractApprovedAt: nil,
|
||||
ContractSignedAt: nil,
|
||||
CompletedAt: nil,
|
||||
ContractURL: "",
|
||||
SigningURL: "",
|
||||
RejectReason: "",
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
}, nil
|
||||
// 企业认证成功后操作
|
||||
func (s *CertificationApplicationServiceImpl) completeEnterpriseAuth(ctx context.Context, certification *entities.Certification) error {
|
||||
err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 1. 获取企业信息提交记录
|
||||
enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(txCtx, certification.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取企业信息失败: %w", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := s.buildCertificationResponse(certification)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetCertificationDetails 获取认证详情
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationDetails(ctx context.Context, query *queries.GetCertificationDetailsQuery) (*responses.CertificationResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, query.UserID)
|
||||
if err != nil {
|
||||
// 如果用户没有认证申请,返回错误
|
||||
if err.Error() == "认证申请不存在" || err.Error() == "record not found" {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请")
|
||||
// 2. 转换状态
|
||||
if err := s.certWorkflowService.CompleteEnterpriseVerification(txCtx, certification.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取认证申请详细信息
|
||||
certificationWithDetails, err := s.certService.GetCertificationWithDetails(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := s.buildCertificationResponse(certificationWithDetails)
|
||||
|
||||
// 添加企业信息
|
||||
if certification.UserID != "" {
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, certification.UserID)
|
||||
if err == nil && enterpriseInfo != nil {
|
||||
response.Enterprise = &responses.EnterpriseInfoResponse{
|
||||
ID: enterpriseInfo.ID,
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
IsOCRVerified: enterpriseInfo.IsOCRVerified,
|
||||
IsFaceVerified: enterpriseInfo.IsFaceVerified,
|
||||
CreatedAt: enterpriseInfo.CreatedAt,
|
||||
UpdatedAt: enterpriseInfo.UpdatedAt,
|
||||
}
|
||||
// 3. 创建企业信息
|
||||
_, err = s.enterpriseService.CreateEnterpriseInfo(txCtx, certification.UserID, enterpriseRecord.CompanyName, enterpriseRecord.UnifiedSocialCode, enterpriseRecord.LegalPersonName, enterpriseRecord.LegalPersonID)
|
||||
if err != nil {
|
||||
s.logger.Warn("创建用户企业信息失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CompleteFaceVerify 完成人脸识别验证
|
||||
func (s *CertificationApplicationServiceImpl) CompleteFaceVerify(ctx context.Context, faceVerifyID string, isSuccess bool) error {
|
||||
return s.certService.CompleteFaceVerify(ctx, faceVerifyID, isSuccess)
|
||||
}
|
||||
|
||||
// ApproveContract 管理员审核合同
|
||||
func (s *CertificationApplicationServiceImpl) ApproveContract(ctx context.Context, certificationID, adminID, signingURL, approvalNotes string) error {
|
||||
return s.certService.ApproveContract(ctx, certificationID, adminID, signingURL, approvalNotes)
|
||||
}
|
||||
|
||||
// RejectContract 管理员拒绝合同
|
||||
func (s *CertificationApplicationServiceImpl) RejectContract(ctx context.Context, certificationID, adminID, rejectReason string) error {
|
||||
return s.certService.RejectContract(ctx, certificationID, adminID, rejectReason)
|
||||
}
|
||||
|
||||
// CompleteContractSign 完成合同签署
|
||||
func (s *CertificationApplicationServiceImpl) CompleteContractSign(ctx context.Context, certificationID, contractURL string) error {
|
||||
return s.certService.CompleteContractSign(ctx, certificationID, contractURL)
|
||||
}
|
||||
|
||||
// CompleteCertification 完成认证
|
||||
func (s *CertificationApplicationServiceImpl) CompleteCertification(ctx context.Context, certificationID string) error {
|
||||
return s.certService.CompleteCertification(ctx, certificationID)
|
||||
}
|
||||
|
||||
// RetryStep 重试认证步骤
|
||||
func (s *CertificationApplicationServiceImpl) RetryStep(ctx context.Context, cmd *commands.RetryStepCommand) error {
|
||||
switch cmd.Step {
|
||||
case "face_verify":
|
||||
return s.certService.RetryFaceVerify(ctx, cmd.CertificationID)
|
||||
case "restart":
|
||||
return s.certService.RestartCertification(ctx, cmd.CertificationID)
|
||||
default:
|
||||
return fmt.Errorf("不支持的重试步骤: %s", cmd.Step)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificationProgress 获取认证进度
|
||||
func (s *CertificationApplicationServiceImpl) GetCertificationProgress(ctx context.Context, userID string) (map[string]interface{}, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, userID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// 如果用户没有认证申请,返回未开始状态
|
||||
if err.Error() == "认证申请不存在" || err.Error() == "record not found" {
|
||||
return map[string]interface{}{
|
||||
"certification_id": "",
|
||||
"user_id": userID,
|
||||
"current_status": "not_started",
|
||||
"status_name": "未开始认证",
|
||||
"progress_percentage": 0,
|
||||
"is_user_action_required": true,
|
||||
"is_admin_action_required": false,
|
||||
"next_valid_statuses": []string{"pending"},
|
||||
"message": "用户尚未开始认证流程",
|
||||
"created_at": nil,
|
||||
"updated_at": nil,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
return fmt.Errorf("完成企业认证失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取认证进度
|
||||
return s.certService.GetCertificationProgress(ctx, certification.ID)
|
||||
}
|
||||
|
||||
// RetryFaceVerify 重试人脸识别
|
||||
func (s *CertificationApplicationServiceImpl) RetryFaceVerify(ctx context.Context, userID string) (*responses.FaceVerifyResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 调用领域服务重试人脸识别
|
||||
if err := s.certService.RetryFaceVerify(ctx, certification.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新发起人脸识别
|
||||
faceVerifyRecord, err := s.certService.InitiateFaceVerify(ctx, certification.ID, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建验证URL
|
||||
verifyURL := fmt.Sprintf("/api/certification/face-verify/%s", faceVerifyRecord.ID)
|
||||
|
||||
return &responses.FaceVerifyResponse{
|
||||
CertifyID: faceVerifyRecord.ID,
|
||||
VerifyURL: verifyURL,
|
||||
ExpiresAt: faceVerifyRecord.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RetryContractSign 重试合同签署
|
||||
func (s *CertificationApplicationServiceImpl) RetryContractSign(ctx context.Context, userID string) (*responses.CertificationResponse, error) {
|
||||
// 根据用户ID获取认证申请
|
||||
certification, err := s.certRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户尚未创建认证申请: %w", err)
|
||||
}
|
||||
|
||||
// 重新获取更新后的认证申请
|
||||
updatedCertification, err := s.certRepo.GetByID(ctx, certification.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("合同签署重试准备完成",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
)
|
||||
|
||||
return s.buildCertificationResponse(&updatedCertification), nil
|
||||
}
|
||||
|
||||
// RestartCertification 重新开始认证
|
||||
func (s *CertificationApplicationServiceImpl) RestartCertification(ctx context.Context, certificationID string) error {
|
||||
return s.certService.RestartCertification(ctx, certificationID)
|
||||
}
|
||||
|
||||
// buildCertificationResponse 构建认证响应
|
||||
func (s *CertificationApplicationServiceImpl) buildCertificationResponse(certification *entities.Certification) *responses.CertificationResponse {
|
||||
return &responses.CertificationResponse{
|
||||
ID: certification.ID,
|
||||
UserID: certification.UserID,
|
||||
Status: certification.Status,
|
||||
StatusName: string(certification.Status),
|
||||
Progress: certification.GetProgressPercentage(),
|
||||
IsUserActionRequired: certification.IsUserActionRequired(),
|
||||
IsAdminActionRequired: certification.IsAdminActionRequired(),
|
||||
InfoSubmittedAt: certification.InfoSubmittedAt,
|
||||
FaceVerifiedAt: certification.FaceVerifiedAt,
|
||||
ContractAppliedAt: certification.ContractAppliedAt,
|
||||
ContractApprovedAt: certification.ContractApprovedAt,
|
||||
ContractSignedAt: certification.ContractSignedAt,
|
||||
CompletedAt: certification.CompletedAt,
|
||||
ContractURL: certification.ContractURL,
|
||||
SigningURL: certification.SigningURL,
|
||||
RejectReason: certification.RejectReason,
|
||||
CreatedAt: certification.CreatedAt,
|
||||
UpdatedAt: certification.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// processOCRAsync 异步处理OCR识别
|
||||
func (s *CertificationApplicationServiceImpl) processOCRAsync(ctx context.Context, licenseID string, fileBytes []byte) {
|
||||
// 调用领域服务处理OCR识别
|
||||
if err := s.certService.ProcessOCRAsync(ctx, licenseID, fileBytes); err != nil {
|
||||
s.logger.Error("OCR处理失败",
|
||||
zap.String("license_id", licenseID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLicenseOCRResult 获取营业执照OCR识别结果
|
||||
func (s *CertificationApplicationServiceImpl) GetLicenseOCRResult(ctx context.Context, recordID string) (*responses.UploadLicenseResponse, error) {
|
||||
// 获取营业执照上传记录
|
||||
licenseRecord, err := s.licenseRepo.GetByID(ctx, recordID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取营业执照记录失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取营业执照记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := &responses.UploadLicenseResponse{
|
||||
UploadRecordID: licenseRecord.ID,
|
||||
FileURL: licenseRecord.FileURL,
|
||||
OCRProcessed: licenseRecord.OCRProcessed,
|
||||
OCRSuccess: licenseRecord.OCRSuccess,
|
||||
OCRConfidence: licenseRecord.OCRConfidence,
|
||||
OCRErrorMessage: licenseRecord.OCRErrorMessage,
|
||||
}
|
||||
|
||||
// 如果OCR成功,解析OCR结果
|
||||
if licenseRecord.OCRSuccess && licenseRecord.OCRRawData != "" {
|
||||
// 这里可以解析OCR原始数据,提取企业信息
|
||||
// 简化处理,直接返回原始数据中的关键信息
|
||||
// 实际项目中可以使用JSON解析
|
||||
response.EnterpriseName = "已识别" // 从OCR数据中提取
|
||||
response.CreditCode = "已识别" // 从OCR数据中提取
|
||||
response.LegalPerson = "已识别" // 从OCR数据中提取
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,62 +1,21 @@
|
||||
package commands
|
||||
|
||||
// CreateCertificationCommand 创建认证申请命令
|
||||
// 用于用户发起企业认证流程的初始请求
|
||||
type CreateCertificationCommand struct {
|
||||
UserID string `json:"user_id" binding:"required" comment:"用户唯一标识,从JWT token获取"`
|
||||
}
|
||||
|
||||
// UploadLicenseCommand 上传营业执照命令
|
||||
// 用于处理营业执照文件上传的业务逻辑
|
||||
type UploadLicenseCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
FileBytes []byte `json:"-" comment:"营业执照文件的二进制内容,从multipart/form-data获取"`
|
||||
FileName string `json:"-" comment:"营业执照文件的原始文件名,从multipart/form-data获取"`
|
||||
FileSize int64 `json:"-" comment:"营业执照文件的大小(字节),从multipart/form-data获取"`
|
||||
}
|
||||
|
||||
// SubmitEnterpriseInfoCommand 提交企业信息命令
|
||||
// 用于用户提交企业四要素信息,完成企业信息验证
|
||||
// 如果用户没有认证申请,系统会自动创建
|
||||
type SubmitEnterpriseInfoCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
CertificationID string `json:"-" comment:"认证申请唯一标识,从URL路径获取,不在JSON中暴露"`
|
||||
CompanyName string `json:"company_name" binding:"required" comment:"企业名称,如:北京科技有限公司"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" binding:"required" comment:"统一社会信用代码,18位企业唯一标识,如:91110000123456789X"`
|
||||
LegalPersonName string `json:"legal_person_name" binding:"required" comment:"法定代表人姓名,如:张三"`
|
||||
LegalPersonID string `json:"legal_person_id" binding:"required" comment:"法定代表人身份证号码,18位,如:110101199001011234"`
|
||||
LicenseUploadRecordID string `json:"license_upload_record_id" binding:"required" comment:"营业执照上传记录唯一标识,关联已上传的营业执照文件"`
|
||||
}
|
||||
|
||||
// InitiateFaceVerifyCommand 初始化人脸识别命令
|
||||
// 用于发起人脸识别验证流程,验证法定代表人身份
|
||||
type InitiateFaceVerifyCommand struct {
|
||||
CertificationID string `json:"-" comment:"认证申请唯一标识,从URL路径获取,不在JSON中暴露"`
|
||||
RealName string `json:"real_name" binding:"required" comment:"真实姓名,必须与营业执照上的法定代表人姓名一致"`
|
||||
IDCardNumber string `json:"id_card_number" binding:"required" comment:"身份证号码,18位,用于人脸识别身份验证"`
|
||||
ReturnURL string `json:"return_url" binding:"required" comment:"人脸识别完成后的回调地址,用于跳转回应用"`
|
||||
}
|
||||
|
||||
// ApplyContractCommand 申请合同命令
|
||||
// 用于用户申请电子合同,进入合同签署流程
|
||||
type ApplyContractCommand struct {
|
||||
CertificationID string `json:"-" comment:"认证申请唯一标识,从URL路径获取,不在JSON中暴露"`
|
||||
}
|
||||
|
||||
// RetryStepCommand 重试认证步骤命令
|
||||
// 用于用户重试失败的认证步骤,如人脸识别失败后的重试
|
||||
type RetryStepCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
CertificationID string `json:"-" comment:"认证申请唯一标识,从URL路径获取,不在JSON中暴露"`
|
||||
Step string `json:"step" binding:"required" comment:"重试的步骤名称,如:face_verify(人脸识别)、contract_sign(合同签署)"`
|
||||
}
|
||||
|
||||
// CreateEnterpriseInfoCommand 创建企业信息命令
|
||||
// 用于创建企业基本信息,通常在企业认证流程中使用
|
||||
// @Description 创建企业信息请求参数
|
||||
type CreateEnterpriseInfoCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
CompanyName string `json:"company_name" binding:"required" example:"示例企业有限公司" comment:"企业名称,如:示例企业有限公司"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" binding:"required" example:"91110000123456789X" comment:"统一社会信用代码,18位企业唯一标识,如:91110000123456789X"`
|
||||
LegalPersonName string `json:"legal_person_name" binding:"required" example:"张三" comment:"法定代表人姓名,如:张三"`
|
||||
LegalPersonID string `json:"legal_person_id" binding:"required" example:"110101199001011234" comment:"法定代表人身份证号码,18位,如:110101199001011234"`
|
||||
CompanyName string `json:"company_name" binding:"required,min=2,max=100" comment:"企业名称,如:北京科技有限公司"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" binding:"required,social_credit_code" comment:"统一社会信用代码,18位企业唯一标识,如:91110000123456789X"`
|
||||
LegalPersonName string `json:"legal_person_name" binding:"required,min=2,max=20" comment:"法定代表人姓名,如:张三"`
|
||||
LegalPersonID string `json:"legal_person_id" binding:"required,id_card" comment:"法定代表人身份证号码,18位,如:110101199001011234"`
|
||||
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号,11位,如:13800138000"`
|
||||
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
|
||||
}
|
||||
|
||||
// CompleteContractSignCommand 完成合同签署命令
|
||||
// 用于用户完成合同签署,提交合同URL
|
||||
type CompleteContractSignCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
ContractURL string `json:"contract_url" binding:"required,url,min=10,max=500" comment:"合同签署后的URL地址"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// GetContractSignURLCommand 获取合同签署链接命令
|
||||
type GetContractSignURLCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package queries
|
||||
// GetCertificationStatusQuery 获取认证状态查询
|
||||
// 用于查询用户当前认证申请的进度状态
|
||||
type GetCertificationStatusQuery struct {
|
||||
UserID string `json:"user_id" binding:"required" comment:"用户唯一标识,用于查询该用户的认证申请状态"`
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户唯一标识,用于查询该用户的认证申请状态"`
|
||||
}
|
||||
|
||||
// GetCertificationDetailsQuery 获取认证详情查询
|
||||
// 用于查询用户认证申请的详细信息,包括所有相关记录
|
||||
type GetCertificationDetailsQuery struct {
|
||||
UserID string `json:"user_id" binding:"required" comment:"用户唯一标识,用于查询该用户的认证申请详细信息"`
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户唯一标识,用于查询该用户的认证申请详细信息"`
|
||||
}
|
||||
|
||||
@@ -8,59 +8,38 @@ import (
|
||||
|
||||
// CertificationResponse 认证响应
|
||||
type CertificationResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
Progress int `json:"progress"`
|
||||
IsUserActionRequired bool `json:"is_user_action_required"`
|
||||
IsAdminActionRequired bool `json:"is_admin_action_required"`
|
||||
InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"`
|
||||
FaceVerifiedAt *time.Time `json:"face_verified_at,omitempty"`
|
||||
ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"`
|
||||
ContractApprovedAt *time.Time `json:"contract_approved_at,omitempty"`
|
||||
ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Enterprise *EnterpriseInfoResponse `json:"enterprise,omitempty"`
|
||||
ContractURL string `json:"contract_url,omitempty"`
|
||||
SigningURL string `json:"signing_url,omitempty"`
|
||||
RejectReason string `json:"reject_reason,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
Progress int `json:"progress"`
|
||||
IsUserActionRequired bool `json:"is_user_action_required"`
|
||||
InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"`
|
||||
EnterpriseVerifiedAt *time.Time `json:"enterprise_verified_at,omitempty"`
|
||||
ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"`
|
||||
ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Enterprise *EnterpriseInfoResponse `json:"enterprise,omitempty"`
|
||||
ContractURL string `json:"contract_url,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EnterpriseInfoResponse 企业信息响应
|
||||
type EnterpriseInfoResponse struct {
|
||||
ID string `json:"id"`
|
||||
CertificationID string `json:"certification_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
LegalPersonID string `json:"legal_person_id"`
|
||||
LicenseUploadRecordID string `json:"license_upload_record_id"`
|
||||
IsOCRVerified bool `json:"is_ocr_verified"`
|
||||
IsFaceVerified bool `json:"is_face_verified"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
LegalPersonID string `json:"legal_person_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UploadLicenseResponse 上传营业执照响应
|
||||
type UploadLicenseResponse struct {
|
||||
UploadRecordID string `json:"upload_record_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
OCRProcessed bool `json:"ocr_processed"`
|
||||
OCRSuccess bool `json:"ocr_success"`
|
||||
// OCR识别结果(如果成功)
|
||||
EnterpriseName string `json:"enterprise_name,omitempty"`
|
||||
CreditCode string `json:"credit_code,omitempty"`
|
||||
LegalPerson string `json:"legal_person,omitempty"`
|
||||
OCRConfidence float64 `json:"ocr_confidence,omitempty"`
|
||||
OCRErrorMessage string `json:"ocr_error_message,omitempty"`
|
||||
}
|
||||
|
||||
// FaceVerifyResponse 人脸识别响应
|
||||
type FaceVerifyResponse struct {
|
||||
CertifyID string `json:"certify_id"`
|
||||
VerifyURL string `json:"verify_url"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
// EnterpriseAuthURLResponse 企业认证链接响应
|
||||
type EnterpriseAuthURLResponse struct {
|
||||
EsignFlowID string `json:"esign_flow_id"` // e签宝认证流程ID
|
||||
AuthURL string `json:"auth_url"` // 认证链接
|
||||
ShortURL string `json:"short_url"` // 短链接
|
||||
ExpireAt string `json:"expire_at"` // 过期时间
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package responses
|
||||
|
||||
// ContractSignURLResponse 合同签署链接响应
|
||||
type ContractSignURLResponse struct {
|
||||
SignURL string `json:"sign_url"` // 签署链接
|
||||
ShortURL string `json:"short_url"` // 短链接
|
||||
SignFlowID string `json:"sign_flow_id"` // 签署流程ID
|
||||
ExpireAt string `json:"expire_at"` // 过期时间
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package certification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/certification/entities"
|
||||
"tyapi-server/internal/domains/certification/enums"
|
||||
"tyapi-server/internal/domains/certification/services"
|
||||
user_services "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/shared/database"
|
||||
esign_service "tyapi-server/internal/shared/esign"
|
||||
)
|
||||
|
||||
// EsignCallbackData e签宝回调数据结构
|
||||
type EsignCallbackData struct {
|
||||
Action string `json:"action"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
AuthFlowId string `json:"authFlowId,omitempty"`
|
||||
SignFlowId string `json:"signFlowId,omitempty"`
|
||||
CustomBizNum string `json:"customBizNum,omitempty"`
|
||||
SignOrder int `json:"signOrder,omitempty"`
|
||||
OperateTime int64 `json:"operateTime,omitempty"`
|
||||
SignResult int `json:"signResult,omitempty"`
|
||||
ResultDescription string `json:"resultDescription,omitempty"`
|
||||
AuthType string `json:"authType,omitempty"`
|
||||
SignFlowStatus string `json:"signFlowStatus,omitempty"`
|
||||
Operator *EsignOperator `json:"operator,omitempty"`
|
||||
PsnInfo *EsignPsnInfo `json:"psnInfo,omitempty"`
|
||||
Organization *EsignOrganization `json:"organization,omitempty"`
|
||||
}
|
||||
|
||||
// EsignOperator 签署人信息
|
||||
type EsignOperator struct {
|
||||
PsnId string `json:"psnId"`
|
||||
PsnAccount *EsignPsnAccount `json:"psnAccount"`
|
||||
}
|
||||
|
||||
// EsignPsnInfo 个人认证信息
|
||||
type EsignPsnInfo struct {
|
||||
PsnId string `json:"psnId"`
|
||||
PsnAccount *EsignPsnAccount `json:"psnAccount"`
|
||||
}
|
||||
|
||||
// EsignPsnAccount 个人账户信息
|
||||
type EsignPsnAccount struct {
|
||||
AccountMobile string `json:"accountMobile"`
|
||||
AccountEmail string `json:"accountEmail"`
|
||||
}
|
||||
|
||||
// EsignOrganization 企业信息
|
||||
type EsignOrganization struct {
|
||||
OrgName string `json:"orgName"`
|
||||
// 可以根据需要添加更多企业信息字段
|
||||
}
|
||||
|
||||
// EsignCallbackApplicationServiceImpl e签宝回调应用服务实现
|
||||
type EsignCallbackApplicationServiceImpl struct {
|
||||
certManagementService *services.CertificationManagementService
|
||||
certWorkflowService *services.CertificationWorkflowService
|
||||
certificationEsignService *services.CertificationEsignService
|
||||
enterpriseService *user_services.EnterpriseService
|
||||
esignService *esign_service.Client
|
||||
enterpriseRecordService *services.EnterpriseInfoSubmitRecordService
|
||||
txManager *database.TransactionManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewEsignCallbackApplicationService 创建e签宝回调应用服务
|
||||
func NewEsignCallbackApplicationService(
|
||||
certManagementService *services.CertificationManagementService,
|
||||
certWorkflowService *services.CertificationWorkflowService,
|
||||
certificationEsignService *services.CertificationEsignService,
|
||||
enterpriseService *user_services.EnterpriseService,
|
||||
esignService *esign_service.Client,
|
||||
enterpriseRecordService *services.EnterpriseInfoSubmitRecordService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
) EsignCallbackApplicationService {
|
||||
return &EsignCallbackApplicationServiceImpl{
|
||||
certManagementService: certManagementService,
|
||||
certWorkflowService: certWorkflowService,
|
||||
certificationEsignService: certificationEsignService,
|
||||
enterpriseService: enterpriseService,
|
||||
esignService: esignService,
|
||||
enterpriseRecordService: enterpriseRecordService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCallback 处理e签宝回调
|
||||
func (s *EsignCallbackApplicationServiceImpl) HandleCallback(ctx context.Context, callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error {
|
||||
s.logger.Info("开始处理e签宝回调", zap.Any("callback_data", callbackData))
|
||||
|
||||
// 1. 验签
|
||||
if err := s.verifySignature(callbackData, headers, queryParams); err != nil {
|
||||
s.logger.Error("e签宝回调验签失败", zap.Error(err))
|
||||
return fmt.Errorf("验签失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 解析回调数据为结构体
|
||||
var callback EsignCallbackData
|
||||
jsonBytes, err := json.Marshal(callbackData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, &callback); err != nil {
|
||||
return fmt.Errorf("解析回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 记录回调信息
|
||||
s.logger.Info("e签宝回调信息解析",
|
||||
zap.String("action", callback.Action),
|
||||
zap.String("auth_flow_id", callback.AuthFlowId),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
zap.String("auth_type", callback.AuthType),
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
zap.Int64("timestamp", callback.Timestamp),
|
||||
)
|
||||
|
||||
// 4. 根据回调类型处理业务逻辑
|
||||
switch callback.Action {
|
||||
case "AUTH_PASS":
|
||||
// 只处理企业认证通过
|
||||
if callback.AuthType == "ORG" {
|
||||
return s.handleEnterpriseAuthPass(ctx, &callback)
|
||||
}
|
||||
s.logger.Info("忽略非企业认证通过回调", zap.String("auth_type", callback.AuthType))
|
||||
return nil
|
||||
case "AUTH_FAIL":
|
||||
// 只处理企业认证失败
|
||||
if callback.AuthType == "ORG" {
|
||||
return s.handleEnterpriseAuthFail(ctx, &callback)
|
||||
}
|
||||
s.logger.Info("忽略非企业认证失败回调", zap.String("auth_type", callback.AuthType))
|
||||
return nil
|
||||
case "SIGN_FLOW_COMPLETE":
|
||||
// 合同签署流程完成
|
||||
return s.handleContractSignFlowComplete(ctx, &callback)
|
||||
default:
|
||||
s.logger.Info("忽略未知的回调动作", zap.String("action", callback.Action))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// verifySignature 验证e签宝回调签名
|
||||
func (s *EsignCallbackApplicationServiceImpl) verifySignature(callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error {
|
||||
// 1. 获取签名相关参数
|
||||
signature, ok := headers["X-Tsign-Open-Signature"]
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少签名头: X-Tsign-Open-Signature")
|
||||
}
|
||||
|
||||
timestamp, ok := headers["X-Tsign-Open-Timestamp"]
|
||||
if !ok {
|
||||
return fmt.Errorf("缺少时间戳头: X-Tsign-Open-Timestamp")
|
||||
}
|
||||
|
||||
// 2. 构建查询参数字符串
|
||||
var queryKeys []string
|
||||
for key := range queryParams {
|
||||
queryKeys = append(queryKeys, key)
|
||||
}
|
||||
sort.Strings(queryKeys) // 按ASCII码升序排序
|
||||
|
||||
var queryValues []string
|
||||
for _, key := range queryKeys {
|
||||
queryValues = append(queryValues, queryParams[key])
|
||||
}
|
||||
queryString := strings.Join(queryValues, "")
|
||||
|
||||
// 3. 获取请求体数据
|
||||
bodyData, err := s.getRequestBodyString(callbackData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取请求体数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 构建验签数据
|
||||
data := timestamp + queryString + bodyData
|
||||
|
||||
// 5. 计算签名
|
||||
expectedSignature := s.calculateSignature(data, s.esignService.GetConfig().AppSecret)
|
||||
|
||||
// 6. 比较签名
|
||||
if strings.ToLower(expectedSignature) != strings.ToLower(signature) {
|
||||
s.logger.Error("签名验证失败",
|
||||
zap.String("expected", strings.ToLower(expectedSignature)),
|
||||
zap.String("received", strings.ToLower(signature)),
|
||||
zap.String("data", data),
|
||||
)
|
||||
return fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
s.logger.Info("e签宝回调验签成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateSignature 计算HMAC-SHA256签名
|
||||
func (s *EsignCallbackApplicationServiceImpl) calculateSignature(data, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(data))
|
||||
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
}
|
||||
|
||||
// getRequestBodyString 获取请求体字符串
|
||||
func (s *EsignCallbackApplicationServiceImpl) getRequestBodyString(callbackData map[string]interface{}) (string, error) {
|
||||
// 将map转换为JSON字符串
|
||||
jsonBytes, err := json.Marshal(callbackData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("JSON序列化失败: %w", err)
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
// handleEnterpriseAuthPass 处理企业认证通过回调
|
||||
func (s *EsignCallbackApplicationServiceImpl) handleEnterpriseAuthPass(ctx context.Context, callback *EsignCallbackData) error {
|
||||
s.logger.Info("处理企业认证通过回调")
|
||||
|
||||
if callback.Organization == nil {
|
||||
return fmt.Errorf("回调数据中缺少organization字段")
|
||||
}
|
||||
|
||||
if callback.AuthFlowId == "" {
|
||||
return fmt.Errorf("回调数据中缺少authFlowId字段")
|
||||
}
|
||||
|
||||
// 查找对应的认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByAuthFlowID(ctx, callback.AuthFlowId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找认证申请失败: %w", err)
|
||||
}
|
||||
if certification.Status != enums.StatusInfoSubmitted {
|
||||
s.logger.Warn("当前状态不允许完成企业认证", zap.String("status", string(certification.Status)))
|
||||
return nil
|
||||
}
|
||||
if err := s.completeEnterpriseAuth(ctx, certification); err != nil {
|
||||
return fmt.Errorf("完成企业认证失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("企业认证通过处理完成",
|
||||
zap.String("user_id", certification.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("org_name", callback.Organization.OrgName),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleEnterpriseAuthFail 处理企业认证失败回调
|
||||
func (s *EsignCallbackApplicationServiceImpl) handleEnterpriseAuthFail(ctx context.Context, callback *EsignCallbackData) error {
|
||||
s.logger.Info("处理企业认证失败回调")
|
||||
|
||||
if callback.Organization == nil {
|
||||
return fmt.Errorf("回调数据中缺少organization字段")
|
||||
}
|
||||
|
||||
// 暂时忽略
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleContractSignFlowComplete 处理合同签署流程完成回调
|
||||
func (s *EsignCallbackApplicationServiceImpl) handleContractSignFlowComplete(ctx context.Context, callback *EsignCallbackData) error {
|
||||
s.logger.Info("处理合同签署流程完成回调")
|
||||
|
||||
if callback.SignFlowId == "" {
|
||||
return fmt.Errorf("回调数据中缺少signFlowId字段")
|
||||
}
|
||||
|
||||
if callback.SignFlowStatus == "" {
|
||||
return fmt.Errorf("回调数据中缺少signFlowStatus字段")
|
||||
}
|
||||
|
||||
// 查找对应的认证申请
|
||||
certification, err := s.certManagementService.GetCertificationByEsignFlowID(ctx, callback.SignFlowId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找认证申请失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据签署流程状态处理
|
||||
switch callback.SignFlowStatus {
|
||||
case "2": // 已完成(所有签署方完成签署)
|
||||
s.logger.Info("合同签署流程已完成,所有签署方完成签署")
|
||||
|
||||
// 完成合同签署
|
||||
if err := s.certWorkflowService.CompleteContractSign(ctx, certification.ID, "所有签署方完成签署"); err != nil {
|
||||
return fmt.Errorf("完成合同签署失败: %w", err)
|
||||
}
|
||||
|
||||
// 自动完成认证
|
||||
if err := s.certWorkflowService.CompleteCertification(ctx, certification.ID); err != nil {
|
||||
return fmt.Errorf("完成认证失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("合同签署流程完成处理成功",
|
||||
zap.String("user_id", certification.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
)
|
||||
|
||||
case "3": // 已撤销(发起方撤销签署任务)
|
||||
s.logger.Info("合同签署流程已撤销")
|
||||
|
||||
// 可以在这里添加撤销处理逻辑
|
||||
s.logger.Info("合同签署流程撤销处理完成",
|
||||
zap.String("user_id", certification.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
)
|
||||
|
||||
// 暂无撤销业务逻辑
|
||||
|
||||
case "5": // 已过期(签署截止日到期后触发)
|
||||
s.logger.Info("合同签署流程已过期")
|
||||
|
||||
// 可以在这里添加过期处理逻辑
|
||||
s.logger.Info("合同签署流程过期处理完成",
|
||||
zap.String("user_id", certification.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
)
|
||||
|
||||
// 暂无过期业务逻辑
|
||||
|
||||
case "7": // 已拒签(签署方拒绝签署)
|
||||
s.logger.Info("合同签署流程已拒签")
|
||||
|
||||
// 可以在这里添加拒签处理逻辑
|
||||
s.logger.Info("合同签署流程拒签处理完成",
|
||||
zap.String("user_id", certification.UserID),
|
||||
zap.String("certification_id", certification.ID),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
)
|
||||
|
||||
default:
|
||||
s.logger.Warn("未知的签署流程状态",
|
||||
zap.String("sign_flow_status", callback.SignFlowStatus),
|
||||
zap.String("sign_flow_id", callback.SignFlowId),
|
||||
)
|
||||
|
||||
// 暂无拒签业务逻辑
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 企业认证成功后操作
|
||||
func (s *EsignCallbackApplicationServiceImpl) completeEnterpriseAuth(ctx context.Context, certification *entities.Certification) error {
|
||||
err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 1. 获取企业信息提交记录
|
||||
enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(txCtx, certification.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取企业信息失败: %w", err)
|
||||
}
|
||||
// 2. 转换状态
|
||||
if err := s.certWorkflowService.CompleteEnterpriseVerification(txCtx, certification.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
// 3. 创建企业信息
|
||||
_, err = s.enterpriseService.CreateEnterpriseInfo(txCtx, certification.UserID, enterpriseRecord.CompanyName, enterpriseRecord.UnifiedSocialCode, enterpriseRecord.LegalPersonName, enterpriseRecord.LegalPersonID)
|
||||
if err != nil {
|
||||
s.logger.Warn("创建用户企业信息失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("完成企业认证失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -8,62 +8,62 @@ import (
|
||||
|
||||
// CreateWalletCommand 创建钱包命令
|
||||
type CreateWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
}
|
||||
|
||||
// UpdateWalletCommand 更新钱包命令
|
||||
type UpdateWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Balance decimal.Decimal `json:"balance" binding:"omitempty"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// RechargeWalletCommand 充值钱包命令
|
||||
type RechargeWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required,gt=0"`
|
||||
}
|
||||
|
||||
// RechargeCommand 充值命令
|
||||
type RechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required,gt=0"`
|
||||
}
|
||||
|
||||
// WithdrawWalletCommand 提现钱包命令
|
||||
type WithdrawWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required,gt=0"`
|
||||
}
|
||||
|
||||
// WithdrawCommand 提现命令
|
||||
type WithdrawCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required,gt=0"`
|
||||
}
|
||||
|
||||
// CreateUserSecretsCommand 创建用户密钥命令
|
||||
type CreateUserSecretsCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
ExpiresAt *time.Time `json:"expires_at" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// RegenerateAccessKeyCommand 重新生成访问密钥命令
|
||||
type RegenerateAccessKeyCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
ExpiresAt *time.Time `json:"expires_at" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// DeactivateUserSecretsCommand 停用用户密钥命令
|
||||
type DeactivateUserSecretsCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
}
|
||||
|
||||
// WalletTransactionCommand 钱包交易命令
|
||||
type WalletTransactionCommand struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
FromUserID string `json:"from_user_id" binding:"required"`
|
||||
ToUserID string `json:"to_user_id" binding:"required"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
FromUserID string `json:"from_user_id" binding:"required,uuid"`
|
||||
ToUserID string `json:"to_user_id" binding:"required,uuid"`
|
||||
Amount decimal.Decimal `json:"amount" binding:"required,gt=0"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=200"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -30,114 +30,92 @@ func NewCategoryApplicationService(
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func (s *CategoryApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查父分类是否存在
|
||||
if cmd.ParentID != nil && *cmd.ParentID != "" {
|
||||
_, err := s.categoryRepo.GetByID(ctx, *cmd.ParentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("父分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 验证分类编号唯一性
|
||||
if err := s.validateCategoryCode(cmd.Code, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// 3. 创建分类实体
|
||||
category := entities.ProductCategory{
|
||||
category := &entities.ProductCategory{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
ParentID: cmd.ParentID,
|
||||
Level: cmd.Level,
|
||||
Sort: cmd.Sort,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
}
|
||||
|
||||
|
||||
// 4. 保存到仓储
|
||||
_, err := s.categoryRepo.Create(ctx, category)
|
||||
createdCategory, err := s.categoryRepo.Create(ctx, *category)
|
||||
if err != nil {
|
||||
s.logger.Error("创建分类失败", zap.Error(err))
|
||||
s.logger.Error("创建分类失败", zap.Error(err), zap.String("code", cmd.Code))
|
||||
return fmt.Errorf("创建分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建分类成功", zap.String("name", cmd.Name))
|
||||
|
||||
s.logger.Info("创建分类成功", zap.String("id", createdCategory.ID), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *CategoryApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
|
||||
// 1. 获取现有分类
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
// 1. 参数验证
|
||||
if err := s.validateUpdateCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 获取现有分类
|
||||
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
if cmd.Name != "" {
|
||||
category.Name = cmd.Name
|
||||
|
||||
// 3. 验证分类编号唯一性(排除当前分类)
|
||||
if err := s.validateCategoryCode(cmd.Code, cmd.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Code != "" {
|
||||
category.Code = cmd.Code
|
||||
}
|
||||
if cmd.Description != "" {
|
||||
category.Description = cmd.Description
|
||||
}
|
||||
if cmd.ParentID != nil {
|
||||
category.ParentID = cmd.ParentID
|
||||
}
|
||||
if cmd.Level > 0 {
|
||||
category.Level = cmd.Level
|
||||
}
|
||||
if cmd.Sort > 0 {
|
||||
category.Sort = cmd.Sort
|
||||
}
|
||||
if cmd.IsEnabled != nil {
|
||||
category.IsEnabled = *cmd.IsEnabled
|
||||
}
|
||||
if cmd.IsVisible != nil {
|
||||
category.IsVisible = *cmd.IsVisible
|
||||
}
|
||||
|
||||
// 3. 保存到仓储
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("更新分类失败", zap.Error(err))
|
||||
|
||||
// 4. 更新分类信息
|
||||
existingCategory.Name = cmd.Name
|
||||
existingCategory.Code = cmd.Code
|
||||
existingCategory.Description = cmd.Description
|
||||
existingCategory.Sort = cmd.Sort
|
||||
existingCategory.IsEnabled = cmd.IsEnabled
|
||||
existingCategory.IsVisible = cmd.IsVisible
|
||||
|
||||
// 5. 保存到仓储
|
||||
if err := s.categoryRepo.Update(ctx, existingCategory); err != nil {
|
||||
s.logger.Error("更新分类失败", zap.Error(err), zap.String("id", cmd.ID))
|
||||
return fmt.Errorf("更新分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新分类成功", zap.String("id", cmd.ID))
|
||||
|
||||
s.logger.Info("更新分类成功", zap.String("id", cmd.ID), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *CategoryApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||
// 1. 检查分类是否存在
|
||||
_, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有子分类
|
||||
children, err := s.categoryRepo.FindByParentID(ctx, &cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("检查子分类失败", zap.Error(err))
|
||||
return fmt.Errorf("检查子分类失败: %w", err)
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return fmt.Errorf("分类下有子分类,无法删除")
|
||||
}
|
||||
|
||||
|
||||
// 2. 检查是否有产品(可选,根据业务需求决定)
|
||||
// 这里可以添加检查逻辑,如果有产品则不允许删除
|
||||
|
||||
// 3. 删除分类
|
||||
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除分类失败", zap.Error(err))
|
||||
s.logger.Error("删除分类失败", zap.Error(err), zap.String("id", cmd.ID))
|
||||
return fmt.Errorf("删除分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除分类成功", zap.String("id", cmd.ID))
|
||||
|
||||
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("code", existingCategory.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -158,16 +136,6 @@ func (s *CategoryApplicationServiceImpl) GetCategoryByID(ctx context.Context, qu
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToCategoryInfoResponse(&category)
|
||||
|
||||
// 加载父分类信息
|
||||
if category.ParentID != nil && *category.ParentID != "" {
|
||||
parent, err := s.categoryRepo.GetByID(ctx, *category.ParentID)
|
||||
if err == nil {
|
||||
parentResponse := s.convertToCategoryInfoResponse(&parent)
|
||||
response.Parent = parentResponse
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -177,8 +145,6 @@ func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, que
|
||||
repoQuery := &repoQueries.ListCategoriesQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
ParentID: query.ParentID,
|
||||
Level: query.Level,
|
||||
IsEnabled: query.IsEnabled,
|
||||
IsVisible: query.IsVisible,
|
||||
SortBy: query.SortBy,
|
||||
@@ -206,212 +172,13 @@ func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, que
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EnableCategory 启用分类
|
||||
func (s *CategoryApplicationServiceImpl) EnableCategory(ctx context.Context, cmd *commands.EnableCategoryCommand) error {
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
category.IsEnabled = true
|
||||
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("启用分类失败", zap.Error(err))
|
||||
return fmt.Errorf("启用分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("启用分类成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableCategory 禁用分类
|
||||
func (s *CategoryApplicationServiceImpl) DisableCategory(ctx context.Context, cmd *commands.DisableCategoryCommand) error {
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
category.IsEnabled = false
|
||||
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("禁用分类失败", zap.Error(err))
|
||||
return fmt.Errorf("禁用分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("禁用分类成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowCategory 显示分类
|
||||
func (s *CategoryApplicationServiceImpl) ShowCategory(ctx context.Context, cmd *commands.ShowCategoryCommand) error {
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
category.IsVisible = true
|
||||
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("显示分类失败", zap.Error(err))
|
||||
return fmt.Errorf("显示分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("显示分类成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HideCategory 隐藏分类
|
||||
func (s *CategoryApplicationServiceImpl) HideCategory(ctx context.Context, cmd *commands.HideCategoryCommand) error {
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
category.IsVisible = false
|
||||
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("隐藏分类失败", zap.Error(err))
|
||||
return fmt.Errorf("隐藏分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("隐藏分类成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCategory 移动分类
|
||||
func (s *CategoryApplicationServiceImpl) MoveCategory(ctx context.Context, cmd *commands.MoveCategoryCommand) error {
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 检查目标父分类是否存在
|
||||
if cmd.ParentID != nil && *cmd.ParentID != "" {
|
||||
_, err := s.categoryRepo.GetByID(ctx, *cmd.ParentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("目标父分类不存在: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
category.ParentID = cmd.ParentID
|
||||
if cmd.Sort > 0 {
|
||||
category.Sort = cmd.Sort
|
||||
}
|
||||
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("移动分类失败", zap.Error(err))
|
||||
return fmt.Errorf("移动分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("移动分类成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryTree 获取分类树
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoryTree(ctx context.Context, query *queries.GetCategoryTreeQuery) (*responses.CategoryTreeResponse, error) {
|
||||
categories, err := s.categoryRepo.GetCategoryTree(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类树失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类树失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
tree := s.buildCategoryTree(categories, query.IncludeDisabled, query.IncludeHidden)
|
||||
|
||||
return &responses.CategoryTreeResponse{
|
||||
Categories: tree,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCategoriesByLevel 根据层级获取分类
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoriesByLevel(ctx context.Context, query *queries.GetCategoriesByLevelQuery) ([]*responses.CategoryInfoResponse, error) {
|
||||
categories, err := s.categoryRepo.FindCategoriesByLevel(ctx, query.Level)
|
||||
if err != nil {
|
||||
s.logger.Error("根据层级获取分类失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("根据层级获取分类失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.CategoryInfoResponse, len(categories))
|
||||
for i, category := range categories {
|
||||
items[i] = s.convertToCategoryInfoResponse(category)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetCategoryPath 获取分类路径
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoryPath(ctx context.Context, query *queries.GetCategoryPathQuery) (*responses.CategoryPathResponse, error) {
|
||||
path, err := s.buildCategoryPath(ctx, query.CategoryID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类路径失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类路径失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为正确的类型
|
||||
pathItems := make([]responses.CategorySimpleResponse, len(path))
|
||||
for i, item := range path {
|
||||
pathItems[i] = *item
|
||||
}
|
||||
|
||||
return &responses.CategoryPathResponse{
|
||||
Path: pathItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCategoryStats 获取分类统计
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoryStats(ctx context.Context) (*responses.CategoryStatsResponse, error) {
|
||||
// 使用正确的CountOptions
|
||||
total, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类总数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类总数失败: %w", err)
|
||||
}
|
||||
|
||||
enabled, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{
|
||||
Filters: map[string]interface{}{"is_enabled": true},
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("获取启用分类数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取启用分类数失败: %w", err)
|
||||
}
|
||||
|
||||
visible, err := s.categoryRepo.Count(ctx, interfaces.CountOptions{
|
||||
Filters: map[string]interface{}{"is_visible": true},
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("获取可见分类数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取可见分类数失败: %w", err)
|
||||
}
|
||||
|
||||
return &responses.CategoryStatsResponse{
|
||||
TotalCategories: total,
|
||||
EnabledCategories: enabled,
|
||||
VisibleCategories: visible,
|
||||
RootCategories: total - enabled, // 简化计算,实际应该查询根分类数量
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return fmt.Errorf("分类名称不能为空")
|
||||
}
|
||||
if cmd.Level <= 0 {
|
||||
return fmt.Errorf("分类层级必须大于0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToCategoryInfoResponse 转换为分类信息响应
|
||||
func (s *CategoryApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
|
||||
return &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
Description: category.Description,
|
||||
ParentID: category.ParentID,
|
||||
Level: category.Level,
|
||||
Sort: category.Sort,
|
||||
IsEnabled: category.IsEnabled,
|
||||
IsVisible: category.IsVisible,
|
||||
@@ -420,76 +187,50 @@ func (s *CategoryApplicationServiceImpl) convertToCategoryInfoResponse(category
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *CategoryApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
ParentID: category.ParentID,
|
||||
Level: category.Level,
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CategoryApplicationServiceImpl) buildCategoryTree(categories []*entities.ProductCategory, includeDisabled, includeHidden bool) []responses.CategoryInfoResponse {
|
||||
// 构建ID到分类的映射
|
||||
categoryMap := make(map[string]*entities.ProductCategory)
|
||||
for _, category := range categories {
|
||||
// 根据过滤条件决定是否包含
|
||||
if !includeDisabled && !category.IsEnabled {
|
||||
continue
|
||||
}
|
||||
if !includeHidden && !category.IsVisible {
|
||||
continue
|
||||
}
|
||||
categoryMap[category.ID] = category
|
||||
// validateCreateCategory 验证创建分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
var roots []responses.CategoryInfoResponse
|
||||
for _, category := range categoryMap {
|
||||
if category.ParentID == nil || *category.ParentID == "" {
|
||||
// 根节点
|
||||
root := *s.convertToCategoryInfoResponse(category)
|
||||
root.Children = s.findChildren(category.ID, categoryMap)
|
||||
roots = append(roots, root)
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
|
||||
return roots
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CategoryApplicationServiceImpl) findChildren(parentID string, categoryMap map[string]*entities.ProductCategory) []responses.CategoryInfoResponse {
|
||||
var children []responses.CategoryInfoResponse
|
||||
for _, category := range categoryMap {
|
||||
if category.ParentID != nil && *category.ParentID == parentID {
|
||||
child := *s.convertToCategoryInfoResponse(category)
|
||||
child.Children = s.findChildren(category.ID, categoryMap)
|
||||
children = append(children, child)
|
||||
}
|
||||
// validateUpdateCategory 验证更新分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateUpdateCategory(cmd *commands.UpdateCategoryCommand) error {
|
||||
if cmd.ID == "" {
|
||||
return errors.New("分类ID不能为空")
|
||||
}
|
||||
return children
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CategoryApplicationServiceImpl) buildCategoryPath(ctx context.Context, categoryID string) ([]*responses.CategorySimpleResponse, error) {
|
||||
var path []*responses.CategorySimpleResponse
|
||||
|
||||
currentID := categoryID
|
||||
for currentID != "" {
|
||||
category, err := s.categoryRepo.GetByID(ctx, currentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分类失败: %w", err)
|
||||
}
|
||||
|
||||
path = append([]*responses.CategorySimpleResponse{
|
||||
s.convertToCategorySimpleResponse(&category),
|
||||
}, path...)
|
||||
|
||||
if category.ParentID != nil {
|
||||
currentID = *category.ParentID
|
||||
} else {
|
||||
currentID = ""
|
||||
}
|
||||
// validateCategoryCode 验证分类编号唯一性
|
||||
func (s *CategoryApplicationServiceImpl) validateCategoryCode(code, excludeID string) error {
|
||||
if code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
|
||||
return path, nil
|
||||
existingCategory, err := s.categoryRepo.FindByCode(context.Background(), code)
|
||||
if err == nil && existingCategory != nil && existingCategory.ID != excludeID {
|
||||
return errors.New("分类编号已存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,57 +2,26 @@ package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required" comment:"分类名称"`
|
||||
Code string `json:"code" binding:"required" comment:"分类编号"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
ParentID *string `json:"parent_id" comment:"父分类ID"`
|
||||
Level int `json:"level" binding:"min=1" comment:"分类层级"`
|
||||
Sort int `json:"sort" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
|
||||
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// UpdateCategoryCommand 更新分类命令
|
||||
type UpdateCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
ParentID *string `json:"parent_id" comment:"父分类ID"`
|
||||
Level int `json:"level" binding:"min=1" comment:"分类层级"`
|
||||
Sort int `json:"sort" comment:"排序"`
|
||||
IsEnabled *bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `json:"is_visible" comment:"是否展示"`
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
|
||||
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
|
||||
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// DeleteCategoryCommand 删除分类命令
|
||||
type DeleteCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// EnableCategoryCommand 启用分类命令
|
||||
type EnableCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// DisableCategoryCommand 禁用分类命令
|
||||
type DisableCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// ShowCategoryCommand 显示分类命令
|
||||
type ShowCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// HideCategoryCommand 隐藏分类命令
|
||||
type HideCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// MoveCategoryCommand 移动分类命令
|
||||
type MoveCategoryCommand struct {
|
||||
ID string `json:"-"`
|
||||
ParentID *string `json:"parent_id" comment:"新的父分类ID"`
|
||||
Sort int `json:"sort" comment:"新的排序"`
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
|
||||
}
|
||||
@@ -2,70 +2,42 @@ package commands
|
||||
|
||||
// CreateProductCommand 创建产品命令
|
||||
type CreateProductCommand struct {
|
||||
Name string `json:"name" binding:"required" comment:"产品名称"`
|
||||
Code string `json:"code" binding:"required" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" binding:"required" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" binding:"min=0" comment:"产品价格"`
|
||||
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
|
||||
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
|
||||
}
|
||||
|
||||
// UpdateProductCommand 更新产品命令
|
||||
type UpdateProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" binding:"min=0" comment:"产品价格"`
|
||||
IsEnabled *bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `json:"is_package" comment:"是否组合包"`
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
|
||||
Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
|
||||
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
|
||||
}
|
||||
|
||||
// DeleteProductCommand 删除产品命令
|
||||
type DeleteProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// EnableProductCommand 启用产品命令
|
||||
type EnableProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// DisableProductCommand 禁用产品命令
|
||||
type DisableProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// ShowProductCommand 显示产品命令
|
||||
type ShowProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// HideProductCommand 隐藏产品命令
|
||||
type HideProductCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// UpdateProductSEOCommand 更新产品SEO信息命令
|
||||
type UpdateProductSEOCommand struct {
|
||||
ID string `json:"-"`
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
@@ -2,56 +2,12 @@ package commands
|
||||
|
||||
// CreateSubscriptionCommand 创建订阅命令
|
||||
type CreateSubscriptionCommand struct {
|
||||
UserID string `json:"user_id" binding:"required" comment:"用户ID"`
|
||||
ProductID string `json:"product_id" binding:"required" comment:"产品ID"`
|
||||
Price float64 `json:"price" binding:"min=0" comment:"订阅价格"`
|
||||
APILimit int64 `json:"api_limit" comment:"API调用限制"`
|
||||
AutoRenew bool `json:"auto_renew" comment:"是否自动续费"`
|
||||
Duration string `json:"duration" comment:"订阅时长"`
|
||||
UserID string `json:"-" comment:"用户ID"`
|
||||
ProductID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// UpdateSubscriptionCommand 更新订阅命令
|
||||
type UpdateSubscriptionCommand struct {
|
||||
ID string `json:"-"`
|
||||
Price float64 `json:"price" binding:"min=0" comment:"订阅价格"`
|
||||
APILimit int64 `json:"api_limit" comment:"API调用限制"`
|
||||
AutoRenew *bool `json:"auto_renew" comment:"是否自动续费"`
|
||||
}
|
||||
|
||||
// CancelSubscriptionCommand 取消订阅命令
|
||||
type CancelSubscriptionCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// RenewSubscriptionCommand 续费订阅命令
|
||||
type RenewSubscriptionCommand struct {
|
||||
ID string `json:"-"`
|
||||
Duration string `json:"duration" binding:"required" comment:"续费时长"`
|
||||
}
|
||||
|
||||
// ActivateSubscriptionCommand 激活订阅命令
|
||||
type ActivateSubscriptionCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// DeactivateSubscriptionCommand 停用订阅命令
|
||||
type DeactivateSubscriptionCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// UpdateAPIUsageCommand 更新API使用量命令
|
||||
type UpdateAPIUsageCommand struct {
|
||||
ID string `json:"-"`
|
||||
APIUsed int64 `json:"api_used" binding:"min=0" comment:"API使用量"`
|
||||
}
|
||||
|
||||
// ResetAPIUsageCommand 重置API使用量命令
|
||||
type ResetAPIUsageCommand struct {
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
// SetAPILimitCommand 设置API限制命令
|
||||
type SetAPILimitCommand struct {
|
||||
ID string `json:"-"`
|
||||
APILimit int64 `json:"api_limit" binding:"min=0" comment:"API调用限制"`
|
||||
// UpdateSubscriptionPriceCommand 更新订阅价格命令
|
||||
type UpdateSubscriptionPriceCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||
}
|
||||
@@ -2,34 +2,16 @@ package queries
|
||||
|
||||
// ListCategoriesQuery 分类列表查询
|
||||
type ListCategoriesQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
ParentID *string `form:"parent_id" comment:"父分类ID"`
|
||||
Level *int `form:"level" comment:"分类层级"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
SortBy string `form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" comment:"排序方向"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code sort created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetCategoryQuery 获取分类详情查询
|
||||
type GetCategoryQuery struct {
|
||||
ID string `uri:"id" comment:"分类ID"`
|
||||
Code string `form:"code" comment:"分类编号"`
|
||||
}
|
||||
|
||||
// GetCategoryTreeQuery 获取分类树查询
|
||||
type GetCategoryTreeQuery struct {
|
||||
IncludeDisabled bool `form:"include_disabled" comment:"是否包含禁用分类"`
|
||||
IncludeHidden bool `form:"include_hidden" comment:"是否包含隐藏分类"`
|
||||
}
|
||||
|
||||
// GetCategoriesByLevelQuery 根据层级获取分类查询
|
||||
type GetCategoriesByLevelQuery struct {
|
||||
Level int `form:"level" binding:"min=1" comment:"分类层级"`
|
||||
}
|
||||
|
||||
// GetCategoryPathQuery 获取分类路径查询
|
||||
type GetCategoryPathQuery struct {
|
||||
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"分类编号"`
|
||||
}
|
||||
@@ -2,43 +2,43 @@ package queries
|
||||
|
||||
// ListProductsQuery 产品列表查询
|
||||
type ListProductsQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" comment:"排序方向"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// SearchProductsQuery 产品搜索查询
|
||||
type SearchProductsQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" comment:"排序方向"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
|
||||
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
|
||||
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `form:"is_visible" comment:"是否展示"`
|
||||
IsPackage *bool `form:"is_package" comment:"是否组合包"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetProductQuery 获取产品详情查询
|
||||
type GetProductQuery struct {
|
||||
ID string `uri:"id" comment:"产品ID"`
|
||||
Code string `form:"code" comment:"产品编号"`
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
}
|
||||
|
||||
// GetProductsByIDsQuery 根据ID列表获取产品查询
|
||||
type GetProductsByIDsQuery struct {
|
||||
IDs []string `form:"ids" binding:"required" comment:"产品ID列表"`
|
||||
IDs []string `form:"ids" binding:"required,dive,uuid" comment:"产品ID列表"`
|
||||
}
|
||||
|
||||
// GetSubscribableProductsQuery 获取可订阅产品查询
|
||||
|
||||
@@ -1,36 +1,31 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/product/entities"
|
||||
|
||||
// ListSubscriptionsQuery 订阅列表查询
|
||||
type ListSubscriptionsQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
UserID string `form:"user_id" comment:"用户ID"`
|
||||
ProductID string `form:"product_id" comment:"产品ID"`
|
||||
Status entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
|
||||
SortBy string `form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" comment:"排序方向"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
UserID string `form:"-" comment:"用户ID"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at updated_at price" comment:"排序字段"`
|
||||
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetSubscriptionQuery 获取订阅详情查询
|
||||
type GetSubscriptionQuery struct {
|
||||
ID string `uri:"id" binding:"required" comment:"订阅ID"`
|
||||
ID string `uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
}
|
||||
|
||||
// GetUserSubscriptionsQuery 获取用户订阅查询
|
||||
type GetUserSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
|
||||
Status *entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
|
||||
UserID string `form:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
|
||||
// GetProductSubscriptionsQuery 获取产品订阅查询
|
||||
type GetProductSubscriptionsQuery struct {
|
||||
ProductID string `form:"product_id" binding:"required" comment:"产品ID"`
|
||||
Status *entities.SubscriptionStatus `form:"status" comment:"订阅状态"`
|
||||
ProductID string `form:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// GetActiveSubscriptionsQuery 获取活跃订阅查询
|
||||
type GetActiveSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" comment:"用户ID"`
|
||||
UserID string `form:"user_id" binding:"omitempty,uuid" comment:"用户ID"`
|
||||
}
|
||||
|
||||
@@ -8,16 +8,10 @@ type CategoryInfoResponse struct {
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
ParentID *string `json:"parent_id" comment:"父分类ID"`
|
||||
Level int `json:"level" comment:"分类层级"`
|
||||
Sort int `json:"sort" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
|
||||
// 关联信息
|
||||
Parent *CategoryInfoResponse `json:"parent,omitempty" comment:"父分类"`
|
||||
Children []CategoryInfoResponse `json:"children,omitempty" comment:"子分类"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
@@ -30,29 +24,9 @@ type CategoryListResponse struct {
|
||||
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||
}
|
||||
|
||||
// CategoryTreeResponse 分类树响应
|
||||
type CategoryTreeResponse struct {
|
||||
Categories []CategoryInfoResponse `json:"categories" comment:"分类树"`
|
||||
}
|
||||
|
||||
// CategorySimpleResponse 分类简单信息响应
|
||||
type CategorySimpleResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
ParentID *string `json:"parent_id" comment:"父分类ID"`
|
||||
Level int `json:"level" comment:"分类层级"`
|
||||
}
|
||||
|
||||
// CategoryPathResponse 分类路径响应
|
||||
type CategoryPathResponse struct {
|
||||
Path []CategorySimpleResponse `json:"path" comment:"分类路径"`
|
||||
}
|
||||
|
||||
// CategoryStatsResponse 分类统计响应
|
||||
type CategoryStatsResponse struct {
|
||||
TotalCategories int64 `json:"total_categories" comment:"分类总数"`
|
||||
RootCategories int64 `json:"root_categories" comment:"根分类数"`
|
||||
EnabledCategories int64 `json:"enabled_categories" comment:"启用分类数"`
|
||||
VisibleCategories int64 `json:"visible_categories" comment:"可见分类数"`
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
}
|
||||
@@ -4,59 +4,60 @@ import "time"
|
||||
|
||||
// ProductInfoResponse 产品详情响应
|
||||
type ProductInfoResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// SEO信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
SEOKeywords string `json:"seo_keywords" comment:"SEO关键词"`
|
||||
|
||||
|
||||
// 关联信息
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ProductListResponse 产品列表响应
|
||||
type ProductListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
|
||||
// ProductSearchResponse 产品搜索响应
|
||||
type ProductSearchResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
|
||||
// ProductSimpleResponse 产品简单信息响应
|
||||
type ProductSimpleResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
}
|
||||
|
||||
// ProductStatsResponse 产品统计响应
|
||||
type ProductStatsResponse struct {
|
||||
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
||||
EnabledProducts int64 `json:"enabled_products" comment:"启用产品数"`
|
||||
VisibleProducts int64 `json:"visible_products" comment:"可见产品数"`
|
||||
PackageProducts int64 `json:"package_products" comment:"组合包产品数"`
|
||||
}
|
||||
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
||||
EnabledProducts int64 `json:"enabled_products" comment:"启用产品数"`
|
||||
VisibleProducts int64 `json:"visible_products" comment:"可见产品数"`
|
||||
PackageProducts int64 `json:"package_products" comment:"组合包产品数"`
|
||||
}
|
||||
|
||||
@@ -13,19 +13,11 @@ type ProductApplicationService interface {
|
||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error
|
||||
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
||||
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
||||
|
||||
GetProductByID(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error)
|
||||
ListProducts(ctx context.Context, query *queries.ListProductsQuery) (*responses.ProductListResponse, error)
|
||||
GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
|
||||
// 产品状态管理
|
||||
EnableProduct(ctx context.Context, cmd *commands.EnableProductCommand) error
|
||||
DisableProduct(ctx context.Context, cmd *commands.DisableProductCommand) error
|
||||
ShowProduct(ctx context.Context, cmd *commands.ShowProductCommand) error
|
||||
HideProduct(ctx context.Context, cmd *commands.HideProductCommand) error
|
||||
|
||||
// SEO管理
|
||||
UpdateProductSEO(ctx context.Context, cmd *commands.UpdateProductSEOCommand) error
|
||||
|
||||
|
||||
// 业务查询
|
||||
GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error)
|
||||
@@ -37,42 +29,26 @@ type CategoryApplicationService interface {
|
||||
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||
|
||||
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
|
||||
|
||||
// 分类状态管理
|
||||
EnableCategory(ctx context.Context, cmd *commands.EnableCategoryCommand) error
|
||||
DisableCategory(ctx context.Context, cmd *commands.DisableCategoryCommand) error
|
||||
ShowCategory(ctx context.Context, cmd *commands.ShowCategoryCommand) error
|
||||
HideCategory(ctx context.Context, cmd *commands.HideCategoryCommand) error
|
||||
|
||||
// 分类结构管理
|
||||
MoveCategory(ctx context.Context, cmd *commands.MoveCategoryCommand) error
|
||||
GetCategoryTree(ctx context.Context, query *queries.GetCategoryTreeQuery) (*responses.CategoryTreeResponse, error)
|
||||
GetCategoriesByLevel(ctx context.Context, query *queries.GetCategoriesByLevelQuery) ([]*responses.CategoryInfoResponse, error)
|
||||
GetCategoryPath(ctx context.Context, query *queries.GetCategoryPathQuery) (*responses.CategoryPathResponse, error)
|
||||
|
||||
// 统计
|
||||
GetCategoryStats(ctx context.Context) (*responses.CategoryStatsResponse, error)
|
||||
}
|
||||
|
||||
// SubscriptionApplicationService 订阅应用服务接口
|
||||
type SubscriptionApplicationService interface {
|
||||
// 订阅管理
|
||||
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
|
||||
|
||||
// 订阅管理
|
||||
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
|
||||
UpdateSubscription(ctx context.Context, cmd *commands.UpdateSubscriptionCommand) error
|
||||
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
|
||||
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
|
||||
// API使用管理
|
||||
UpdateAPIUsage(ctx context.Context, cmd *commands.UpdateAPIUsageCommand) error
|
||||
ResetAPIUsage(ctx context.Context, cmd *commands.ResetAPIUsageCommand) error
|
||||
|
||||
|
||||
// 业务查询
|
||||
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
|
||||
|
||||
|
||||
// 统计
|
||||
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,201 +2,114 @@ package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
|
||||
"go.uber.org/zap"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
)
|
||||
|
||||
// ProductApplicationServiceImpl 产品应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type ProductApplicationServiceImpl struct {
|
||||
productRepo repositories.ProductRepository
|
||||
categoryRepo repositories.ProductCategoryRepository
|
||||
subscriptionRepo repositories.SubscriptionRepository
|
||||
productService *services.ProductService
|
||||
logger *zap.Logger
|
||||
productManagementService *product_service.ProductManagementService
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProductApplicationService 创建产品应用服务
|
||||
func NewProductApplicationService(
|
||||
productRepo repositories.ProductRepository,
|
||||
categoryRepo repositories.ProductCategoryRepository,
|
||||
subscriptionRepo repositories.SubscriptionRepository,
|
||||
productService *services.ProductService,
|
||||
productManagementService *product_service.ProductManagementService,
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
logger *zap.Logger,
|
||||
) ProductApplicationService {
|
||||
return &ProductApplicationServiceImpl{
|
||||
productRepo: productRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
subscriptionRepo: subscriptionRepo,
|
||||
productService: productService,
|
||||
logger: logger,
|
||||
productManagementService: productManagementService,
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateProduct 创建产品
|
||||
// 业务流程:1. 构建产品实体 2. 创建产品
|
||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateProduct(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 验证产品编号唯一性
|
||||
if err := s.productService.ValidateProductCode(cmd.Code, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 创建产品实体
|
||||
// 1. 构建产品实体
|
||||
product := &entities.Product{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: cmd.Price,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: cmd.Price,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
SEOTitle: cmd.SEOTitle,
|
||||
SEODescription: cmd.SEODescription,
|
||||
SEOKeywords: cmd.SEOKeywords,
|
||||
}
|
||||
|
||||
// 4. 设置SEO信息
|
||||
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||||
|
||||
// 5. 调用领域服务验证
|
||||
if err := s.productService.ValidateProduct(product); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 6. 保存到仓储
|
||||
if _, err := s.productRepo.Create(ctx, *product); err != nil {
|
||||
s.logger.Error("创建产品失败", zap.Error(err))
|
||||
return fmt.Errorf("创建产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建产品成功", zap.String("id", product.ID), zap.String("name", product.Name))
|
||||
return nil
|
||||
// 2. 创建产品
|
||||
_, err := s.productManagementService.CreateProduct(ctx, product)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateProduct 更新产品
|
||||
// 业务流程:1. 获取现有产品 2. 更新产品信息 3. 保存产品
|
||||
func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error {
|
||||
// 1. 获取现有产品
|
||||
existingProduct, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
existingProduct, err := s.productManagementService.GetProductByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 参数验证
|
||||
if err := s.validateUpdateProduct(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 验证产品编号唯一性(如果修改了编号)
|
||||
if cmd.Code != "" && cmd.Code != existingProduct.Code {
|
||||
if err := s.productService.ValidateProductCode(cmd.Code, cmd.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
existingProduct.Code = cmd.Code
|
||||
}
|
||||
// 2. 更新产品信息
|
||||
existingProduct.Name = cmd.Name
|
||||
existingProduct.Code = cmd.Code
|
||||
existingProduct.Description = cmd.Description
|
||||
existingProduct.Content = cmd.Content
|
||||
existingProduct.CategoryID = cmd.CategoryID
|
||||
existingProduct.Price = cmd.Price
|
||||
existingProduct.IsEnabled = cmd.IsEnabled
|
||||
existingProduct.IsVisible = cmd.IsVisible
|
||||
existingProduct.IsPackage = cmd.IsPackage
|
||||
existingProduct.SEOTitle = cmd.SEOTitle
|
||||
existingProduct.SEODescription = cmd.SEODescription
|
||||
existingProduct.SEOKeywords = cmd.SEOKeywords
|
||||
|
||||
// 4. 更新字段
|
||||
if cmd.Name != "" {
|
||||
existingProduct.Name = cmd.Name
|
||||
}
|
||||
if cmd.Description != "" {
|
||||
existingProduct.Description = cmd.Description
|
||||
}
|
||||
if cmd.Content != "" {
|
||||
existingProduct.Content = cmd.Content
|
||||
}
|
||||
if cmd.CategoryID != "" {
|
||||
existingProduct.CategoryID = cmd.CategoryID
|
||||
}
|
||||
if cmd.Price >= 0 {
|
||||
existingProduct.Price = cmd.Price
|
||||
}
|
||||
if cmd.IsEnabled != nil {
|
||||
existingProduct.IsEnabled = *cmd.IsEnabled
|
||||
}
|
||||
if cmd.IsVisible != nil {
|
||||
existingProduct.IsVisible = *cmd.IsVisible
|
||||
}
|
||||
if cmd.IsPackage != nil {
|
||||
existingProduct.IsPackage = *cmd.IsPackage
|
||||
}
|
||||
|
||||
// 5. 更新SEO信息
|
||||
if cmd.SEOTitle != "" || cmd.SEODescription != "" || cmd.SEOKeywords != "" {
|
||||
existingProduct.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||||
}
|
||||
|
||||
// 6. 调用领域服务验证
|
||||
if err := s.productService.ValidateProduct(&existingProduct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. 保存到仓储
|
||||
if err := s.productRepo.Update(ctx, existingProduct); err != nil {
|
||||
s.logger.Error("更新产品失败", zap.Error(err))
|
||||
return fmt.Errorf("更新产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新产品成功", zap.String("id", existingProduct.ID))
|
||||
return nil
|
||||
// 3. 保存产品
|
||||
return s.productManagementService.UpdateProduct(ctx, existingProduct)
|
||||
}
|
||||
|
||||
// DeleteProduct 删除产品
|
||||
// 业务流程:1. 删除产品
|
||||
func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error {
|
||||
// 1. 检查产品是否存在
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有活跃订阅
|
||||
subscriptions, err := s.subscriptionRepo.FindByProductID(ctx, cmd.ID)
|
||||
if err == nil && len(subscriptions) > 0 {
|
||||
return errors.New("产品存在订阅,无法删除")
|
||||
}
|
||||
|
||||
// 3. 删除产品
|
||||
if err := s.productRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除产品失败", zap.Error(err))
|
||||
return fmt.Errorf("删除产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除产品成功", zap.String("id", cmd.ID), zap.String("name", product.Name))
|
||||
return nil
|
||||
return s.productManagementService.DeleteProduct(ctx, cmd.ID)
|
||||
}
|
||||
|
||||
// ListProducts 获取产品列表
|
||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, query *appQueries.ListProductsQuery) (*responses.ProductListResponse, error) {
|
||||
// 构建仓储查询
|
||||
repoQuery := &repoQueries.ListProductsQuery{
|
||||
Keyword: query.Keyword,
|
||||
CategoryID: query.CategoryID,
|
||||
MinPrice: query.MinPrice,
|
||||
MaxPrice: query.MaxPrice,
|
||||
IsEnabled: query.IsEnabled,
|
||||
IsVisible: query.IsVisible,
|
||||
IsPackage: query.IsPackage,
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
// 根据查询条件获取产品列表
|
||||
var products []*entities.Product
|
||||
var err error
|
||||
|
||||
if query.CategoryID != "" {
|
||||
products, err = s.productManagementService.GetProductsByCategory(ctx, query.CategoryID)
|
||||
} else if query.IsVisible != nil && *query.IsVisible {
|
||||
products, err = s.productManagementService.GetVisibleProducts(ctx)
|
||||
} else if query.IsEnabled != nil && *query.IsEnabled {
|
||||
products, err = s.productManagementService.GetEnabledProducts(ctx)
|
||||
} else {
|
||||
// 默认获取可见产品
|
||||
products, err = s.productManagementService.GetVisibleProducts(ctx)
|
||||
}
|
||||
|
||||
// 调用仓储
|
||||
products, total, err := s.productRepo.ListProducts(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取产品列表失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
@@ -206,7 +119,7 @@ func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, query
|
||||
}
|
||||
|
||||
return &responses.ProductListResponse{
|
||||
Total: total,
|
||||
Total: int64(len(items)),
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
@@ -214,169 +127,55 @@ func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, query
|
||||
}
|
||||
|
||||
// GetProductsByIDs 根据ID列表获取产品
|
||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
|
||||
products, err := s.productRepo.GetByIDs(ctx, query.IDs)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取产品列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.ProductInfoResponse, len(products))
|
||||
for i := range products {
|
||||
items[i] = s.convertToProductInfoResponse(&products[i])
|
||||
}
|
||||
|
||||
return items, nil
|
||||
// 这里需要扩展领域服务来支持批量获取
|
||||
// 暂时返回空列表
|
||||
return []*responses.ProductInfoResponse{}, nil
|
||||
}
|
||||
|
||||
// GetSubscribableProducts 获取可订阅的产品
|
||||
// GetSubscribableProducts 获取可订阅产品列表
|
||||
// 业务流程:1. 获取启用产品 2. 过滤可订阅产品 3. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetSubscribableProducts(ctx context.Context, query *appQueries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error) {
|
||||
products, err := s.productRepo.FindSubscribableProducts(ctx, query.UserID)
|
||||
products, err := s.productManagementService.GetEnabledProducts(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("获取可订阅产品失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取可订阅产品失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 过滤可订阅的产品
|
||||
var subscribableProducts []*entities.Product
|
||||
for _, product := range products {
|
||||
if product.CanBeSubscribed() {
|
||||
subscribableProducts = append(subscribableProducts, product)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.ProductInfoResponse, len(products))
|
||||
for i := range products {
|
||||
items[i] = s.convertToProductInfoResponse(products[i])
|
||||
items := make([]*responses.ProductInfoResponse, len(subscribableProducts))
|
||||
for i := range subscribableProducts {
|
||||
items[i] = s.convertToProductInfoResponse(subscribableProducts[i])
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetProductByID 根据ID获取产品
|
||||
// 业务流程:1. 获取产品信息 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
|
||||
var product *entities.Product
|
||||
var err error
|
||||
|
||||
if query.ID != "" {
|
||||
p, err := s.productRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
product = &p
|
||||
} else if query.Code != "" {
|
||||
product, err = s.productRepo.FindByCode(ctx, query.Code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("产品ID或编号不能为空")
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToProductInfoResponse(product)
|
||||
|
||||
// 加载分类信息
|
||||
if product.CategoryID != "" {
|
||||
category, err := s.categoryRepo.GetByID(ctx, product.CategoryID)
|
||||
if err == nil {
|
||||
response.Category = s.convertToCategoryInfoResponse(&category)
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// EnableProduct 启用产品
|
||||
func (s *ProductApplicationServiceImpl) EnableProduct(ctx context.Context, cmd *commands.EnableProductCommand) error {
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
product.Enable()
|
||||
|
||||
if err := s.productRepo.Update(ctx, product); err != nil {
|
||||
s.logger.Error("启用产品失败", zap.Error(err))
|
||||
return fmt.Errorf("启用产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("启用产品成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
return s.convertToProductInfoResponse(product), nil
|
||||
}
|
||||
|
||||
// DisableProduct 禁用产品
|
||||
func (s *ProductApplicationServiceImpl) DisableProduct(ctx context.Context, cmd *commands.DisableProductCommand) error {
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
product.Disable()
|
||||
|
||||
if err := s.productRepo.Update(ctx, product); err != nil {
|
||||
s.logger.Error("禁用产品失败", zap.Error(err))
|
||||
return fmt.Errorf("禁用产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("禁用产品成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowProduct 显示产品
|
||||
func (s *ProductApplicationServiceImpl) ShowProduct(ctx context.Context, cmd *commands.ShowProductCommand) error {
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
product.Show()
|
||||
|
||||
if err := s.productRepo.Update(ctx, product); err != nil {
|
||||
s.logger.Error("显示产品失败", zap.Error(err))
|
||||
return fmt.Errorf("显示产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("显示产品成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// HideProduct 隐藏产品
|
||||
func (s *ProductApplicationServiceImpl) HideProduct(ctx context.Context, cmd *commands.HideProductCommand) error {
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
product.Hide()
|
||||
|
||||
if err := s.productRepo.Update(ctx, product); err != nil {
|
||||
s.logger.Error("隐藏产品失败", zap.Error(err))
|
||||
return fmt.Errorf("隐藏产品失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("隐藏产品成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProductSEO 更新产品SEO信息
|
||||
func (s *ProductApplicationServiceImpl) UpdateProductSEO(ctx context.Context, cmd *commands.UpdateProductSEOCommand) error {
|
||||
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||||
|
||||
if err := s.productRepo.Update(ctx, product); err != nil {
|
||||
s.logger.Error("更新产品SEO失败", zap.Error(err))
|
||||
return fmt.Errorf("更新产品SEO失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新产品SEO成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProductStats 获取产品统计
|
||||
// GetProductStats 获取产品统计信息
|
||||
// 业务流程:1. 获取产品统计 2. 构建响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) {
|
||||
stats, err := s.productService.GetProductStats()
|
||||
stats, err := s.productSubscriptionService.GetProductStats(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品统计失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取产品统计失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.ProductStatsResponse{
|
||||
@@ -387,66 +186,42 @@ func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*r
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
func (s *ProductApplicationServiceImpl) validateCreateProduct(cmd *commands.CreateProductCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return errors.New("产品名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("产品编号不能为空")
|
||||
}
|
||||
if cmd.CategoryID == "" {
|
||||
return errors.New("产品分类不能为空")
|
||||
}
|
||||
if cmd.Price < 0 {
|
||||
return errors.New("产品价格不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ProductApplicationServiceImpl) validateUpdateProduct(cmd *commands.UpdateProductCommand) error {
|
||||
if cmd.ID == "" {
|
||||
return errors.New("产品ID不能为空")
|
||||
}
|
||||
if cmd.Price < 0 {
|
||||
return errors.New("产品价格不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToProductInfoResponse 转换为产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
|
||||
return &responses.ProductInfoResponse{
|
||||
ID: product.ID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
response := &responses.ProductInfoResponse{
|
||||
ID: product.ID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
if product.Category != nil {
|
||||
response.Category = s.convertToCategoryInfoResponse(product.Category)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// convertToCategoryInfoResponse 转换为分类信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
|
||||
return &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
Description: category.Description,
|
||||
ParentID: category.ParentID,
|
||||
Level: category.Level,
|
||||
Sort: category.Sort,
|
||||
IsEnabled: category.IsEnabled,
|
||||
IsVisible: category.IsVisible,
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,346 +2,149 @@ package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
|
||||
"go.uber.org/zap"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationServiceImpl 订阅应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type SubscriptionApplicationServiceImpl struct {
|
||||
subscriptionRepo repositories.SubscriptionRepository
|
||||
productRepo repositories.ProductRepository
|
||||
productService *services.ProductService
|
||||
logger *zap.Logger
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubscriptionApplicationService 创建订阅应用服务
|
||||
func NewSubscriptionApplicationService(
|
||||
subscriptionRepo repositories.SubscriptionRepository,
|
||||
productRepo repositories.ProductRepository,
|
||||
productService *services.ProductService,
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
logger *zap.Logger,
|
||||
) SubscriptionApplicationService {
|
||||
return &SubscriptionApplicationServiceImpl{
|
||||
subscriptionRepo: subscriptionRepo,
|
||||
productRepo: productRepo,
|
||||
productService: productService,
|
||||
logger: logger,
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSubscriptionPrice 更新订阅价格
|
||||
// 业务流程:1. 获取订阅 2. 更新价格 3. 保存订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
|
||||
// 1. 获取现有订阅
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 更新订阅价格
|
||||
subscription.Price = cmd.Price
|
||||
|
||||
// 3. 保存订阅
|
||||
// 这里需要扩展领域服务来支持更新操作
|
||||
// 暂时返回错误
|
||||
return fmt.Errorf("更新订阅价格功能暂未实现")
|
||||
}
|
||||
|
||||
// CreateSubscription 创建订阅
|
||||
// 业务流程:1. 创建订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateSubscription(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查产品是否存在且可订阅
|
||||
canSubscribe, err := s.productService.CanUserSubscribeProduct(cmd.UserID, cmd.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canSubscribe {
|
||||
return errors.New("产品不可订阅或用户已有活跃订阅")
|
||||
}
|
||||
|
||||
// 3. 检查产品是否存在
|
||||
_, err = s.productRepo.GetByID(ctx, cmd.ProductID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
|
||||
// 5. 创建订阅实体
|
||||
subscription := entities.Subscription{
|
||||
UserID: cmd.UserID,
|
||||
ProductID: cmd.ProductID,
|
||||
Status: entities.SubscriptionStatusActive,
|
||||
Price: cmd.Price,
|
||||
APIUsed: 0,
|
||||
}
|
||||
|
||||
// 6. 保存到仓储
|
||||
createdSubscription, err := s.subscriptionRepo.Create(ctx, subscription)
|
||||
if err != nil {
|
||||
s.logger.Error("创建订阅失败", zap.Error(err))
|
||||
return fmt.Errorf("创建订阅失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建订阅成功", zap.String("id", createdSubscription.ID), zap.String("user_id", cmd.UserID), zap.String("product_id", cmd.ProductID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSubscription 更新订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) UpdateSubscription(ctx context.Context, cmd *commands.UpdateSubscriptionCommand) error {
|
||||
// 1. 获取现有订阅
|
||||
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
if cmd.Price >= 0 {
|
||||
subscription.Price = cmd.Price
|
||||
}
|
||||
|
||||
// 3. 保存到仓储
|
||||
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
|
||||
s.logger.Error("更新订阅失败", zap.Error(err))
|
||||
return fmt.Errorf("更新订阅失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新订阅成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
_, err := s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSubscriptionByID 根据ID获取订阅
|
||||
// 业务流程:1. 获取订阅信息 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionByID(ctx context.Context, query *appQueries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error) {
|
||||
subscription, err := s.subscriptionRepo.GetByID(ctx, query.ID)
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("订阅不存在: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToSubscriptionInfoResponse(&subscription)
|
||||
|
||||
// 加载产品信息
|
||||
product, err := s.productRepo.GetByID(ctx, subscription.ProductID)
|
||||
if err == nil {
|
||||
response.Product = s.convertToProductSimpleResponse(&product)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return s.convertToSubscriptionInfoResponse(subscription), nil
|
||||
}
|
||||
|
||||
// ListSubscriptions 获取订阅列表
|
||||
// 业务流程:1. 获取订阅列表 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Context, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
|
||||
// 构建仓储查询
|
||||
repoQuery := &repoQueries.ListSubscriptionsQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
UserID: query.UserID,
|
||||
ProductID: query.ProductID,
|
||||
Status: query.Status,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
}
|
||||
|
||||
// 调用仓储
|
||||
subscriptions, total, err := s.subscriptionRepo.ListSubscriptions(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取订阅列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取订阅列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i, subscription := range subscriptions {
|
||||
items[i] = *s.convertToSubscriptionInfoResponse(subscription)
|
||||
}
|
||||
|
||||
// 这里需要扩展领域服务来支持列表查询
|
||||
// 暂时返回空列表
|
||||
return &responses.SubscriptionListResponse{
|
||||
Total: total,
|
||||
Total: 0,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
Items: []responses.SubscriptionInfoResponse{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
// UpdateAPIUsage 更新API使用量
|
||||
func (s *SubscriptionApplicationServiceImpl) UpdateAPIUsage(ctx context.Context, cmd *commands.UpdateAPIUsageCommand) error {
|
||||
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在: %w", err)
|
||||
}
|
||||
|
||||
subscription.IncrementAPIUsage(cmd.APIUsed)
|
||||
|
||||
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
|
||||
s.logger.Error("更新API使用量失败", zap.Error(err))
|
||||
return fmt.Errorf("更新API使用量失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新API使用量成功", zap.String("id", cmd.ID), zap.Int64("api_used", cmd.APIUsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetAPIUsage 重置API使用量
|
||||
func (s *SubscriptionApplicationServiceImpl) ResetAPIUsage(ctx context.Context, cmd *commands.ResetAPIUsageCommand) error {
|
||||
subscription, err := s.subscriptionRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在: %w", err)
|
||||
}
|
||||
|
||||
subscription.ResetAPIUsage()
|
||||
|
||||
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
|
||||
s.logger.Error("重置API使用量失败", zap.Error(err))
|
||||
return fmt.Errorf("重置API使用量失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("重置API使用量成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// GetUserSubscriptions 获取用户订阅
|
||||
// 业务流程:1. 获取用户订阅 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetUserSubscriptions(ctx context.Context, query *appQueries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
|
||||
subscriptions, err := s.subscriptionRepo.FindByUserID(ctx, query.UserID)
|
||||
subscriptions, err := s.productSubscriptionService.GetUserSubscriptions(ctx, query.UserID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户订阅失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取用户订阅失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤状态
|
||||
if query.Status != nil {
|
||||
filtered := make([]*entities.Subscription, 0)
|
||||
for _, sub := range subscriptions {
|
||||
if sub.Status == *query.Status {
|
||||
filtered = append(filtered, sub)
|
||||
}
|
||||
}
|
||||
subscriptions = filtered
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i, subscription := range subscriptions {
|
||||
items[i] = s.convertToSubscriptionInfoResponse(subscription)
|
||||
for i := range subscriptions {
|
||||
items[i] = s.convertToSubscriptionInfoResponse(subscriptions[i])
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetProductSubscriptions 获取产品订阅
|
||||
// 业务流程:1. 获取产品订阅 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context.Context, query *appQueries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
|
||||
subscriptions, err := s.subscriptionRepo.FindByProductID(ctx, query.ProductID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取产品订阅失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取产品订阅失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤状态
|
||||
if query.Status != nil {
|
||||
filtered := make([]*entities.Subscription, 0)
|
||||
for _, sub := range subscriptions {
|
||||
if sub.Status == *query.Status {
|
||||
filtered = append(filtered, sub)
|
||||
}
|
||||
}
|
||||
subscriptions = filtered
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i, subscription := range subscriptions {
|
||||
items[i] = s.convertToSubscriptionInfoResponse(subscription)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
// 这里需要扩展领域服务来支持按产品查询订阅
|
||||
// 暂时返回空列表
|
||||
return []*responses.SubscriptionInfoResponse{}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionUsage 获取订阅使用情况
|
||||
// 业务流程:1. 获取订阅使用情况 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("订阅不存在: %w", err)
|
||||
}
|
||||
|
||||
return &responses.SubscriptionUsageResponse{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
APIUsed: subscription.APIUsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionStats 获取订阅统计
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
|
||||
// 获取各种状态的订阅数量
|
||||
total, err := s.subscriptionRepo.CountActive(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("获取订阅统计失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取订阅统计失败: %w", err)
|
||||
}
|
||||
|
||||
// TODO: 计算总收入,需要从订单系统获取
|
||||
totalRevenue := 0.0
|
||||
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: total,
|
||||
TotalRevenue: totalRevenue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
func (s *SubscriptionApplicationServiceImpl) validateCreateSubscription(cmd *commands.CreateSubscriptionCommand) error {
|
||||
if cmd.UserID == "" {
|
||||
return errors.New("用户ID不能为空")
|
||||
}
|
||||
if cmd.ProductID == "" {
|
||||
return errors.New("产品ID不能为空")
|
||||
}
|
||||
if cmd.Price < 0 {
|
||||
return errors.New("订阅价格不能为负数")
|
||||
}
|
||||
if cmd.APILimit < 0 {
|
||||
return errors.New("API调用限制不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionApplicationServiceImpl) calculateEndDate(duration string) (*time.Time, error) {
|
||||
if duration == "" {
|
||||
return nil, nil // 永久订阅
|
||||
}
|
||||
|
||||
d, err := s.parseDuration(duration)
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endDate := time.Now().Add(d)
|
||||
return &endDate, nil
|
||||
return &responses.SubscriptionUsageResponse{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
APIUsed: subscription.APIUsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionApplicationServiceImpl) parseDuration(duration string) (time.Duration, error) {
|
||||
switch duration {
|
||||
case "7d":
|
||||
return 7 * 24 * time.Hour, nil
|
||||
case "30d":
|
||||
return 30 * 24 * time.Hour, nil
|
||||
case "90d":
|
||||
return 90 * 24 * time.Hour, nil
|
||||
case "365d":
|
||||
return 365 * 24 * time.Hour, nil
|
||||
default:
|
||||
return time.ParseDuration(duration)
|
||||
}
|
||||
// GetSubscriptionStats 获取订阅统计信息
|
||||
// 业务流程:1. 获取订阅统计 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
|
||||
// 这里需要扩展领域服务来支持统计功能
|
||||
// 暂时返回默认值
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: 0,
|
||||
TotalRevenue: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
return &responses.SubscriptionInfoResponse{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToProductSimpleResponse 转换为产品简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
|
||||
return &responses.ProductSimpleResponse{
|
||||
ID: product.ID,
|
||||
@@ -351,4 +154,12 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(prod
|
||||
Price: product.Price,
|
||||
IsPackage: product.IsPackage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,23 @@ package commands
|
||||
// RegisterUserCommand 用户注册命令
|
||||
// @Description 用户注册请求参数
|
||||
type RegisterUserCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password" example:"password123"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required,strong_password" example:"Password123"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password" example:"Password123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordCommand 密码登录命令
|
||||
// @Description 使用密码进行用户登录请求参数
|
||||
type LoginWithPasswordCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"Password123"`
|
||||
}
|
||||
|
||||
// LoginWithSMSCommand 短信验证码登录命令
|
||||
// @Description 使用短信验证码进行用户登录请求参数
|
||||
type LoginWithSMSCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
@@ -27,40 +27,41 @@ type LoginWithSMSCommand struct {
|
||||
// @Description 修改用户密码请求参数
|
||||
type ChangePasswordCommand struct {
|
||||
UserID string `json:"-"`
|
||||
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
|
||||
OldPassword string `json:"old_password" binding:"required,min=6,max=128" example:"OldPassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,strong_password" example:"NewPassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"NewPassword123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// ResetPasswordCommand 重置密码命令
|
||||
// @Description 重置用户密码请求参数(忘记密码时使用)
|
||||
type ResetPasswordCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
NewPassword string `json:"new_password" binding:"required,strong_password" example:"NewPassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"NewPassword123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// SendCodeCommand 发送验证码命令
|
||||
// @Description 发送短信验证码请求参数
|
||||
type SendCodeCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind certification" example:"register"`
|
||||
}
|
||||
|
||||
// UpdateProfileCommand 更新用户信息命令
|
||||
// @Description 更新用户基本信息请求参数
|
||||
type UpdateProfileCommand struct {
|
||||
UserID string `json:"-"`
|
||||
Phone string `json:"phone" binding:"omitempty,len=11" example:"13800138000"`
|
||||
// 可以在这里添加更多用户信息字段,如昵称、头像等
|
||||
UserID string `json:"-"`
|
||||
Phone string `json:"phone" binding:"omitempty,phone" example:"13800138000"`
|
||||
DisplayName string `json:"display_name" binding:"omitempty,min=2,max=50" example:"用户昵称"`
|
||||
Email string `json:"email" binding:"omitempty,email" example:"user@example.com"`
|
||||
}
|
||||
|
||||
// VerifyCodeCommand 验证验证码命令
|
||||
// @Description 验证短信验证码请求参数
|
||||
type VerifyCodeCommand struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind certification" example:"register"`
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ type LoginUserResponse struct {
|
||||
type UserProfileResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
Username string `json:"username,omitempty" example:"admin"`
|
||||
UserType string `json:"user_type" example:"user"`
|
||||
IsActive bool `json:"is_active" example:"true"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
|
||||
LoginCount int `json:"login_count" example:"10"`
|
||||
Permissions []string `json:"permissions,omitempty" example:"['user:read','user:write']"`
|
||||
EnterpriseInfo *EnterpriseInfoResponse `json:"enterprise_info,omitempty"`
|
||||
IsCertified bool `json:"is_certified" example:"false"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
|
||||
@@ -11,62 +11,59 @@ import (
|
||||
"tyapi-server/internal/application/user/dto/responses"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/events"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
)
|
||||
|
||||
// UserApplicationServiceImpl 用户应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type UserApplicationServiceImpl struct {
|
||||
userRepo repositories.UserRepository
|
||||
enterpriseInfoRepo repositories.EnterpriseInfoRepository
|
||||
smsCodeService *user_service.SMSCodeService
|
||||
eventBus interfaces.EventBus
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
userManagementService *user_service.UserManagementService
|
||||
userAuthService *user_service.UserAuthService
|
||||
smsCodeService *user_service.SMSCodeService
|
||||
enterpriseService *user_service.EnterpriseService
|
||||
eventBus interfaces.EventBus
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserApplicationService 创建用户应用服务
|
||||
func NewUserApplicationService(
|
||||
userRepo repositories.UserRepository,
|
||||
enterpriseInfoRepo repositories.EnterpriseInfoRepository,
|
||||
userManagementService *user_service.UserManagementService,
|
||||
userAuthService *user_service.UserAuthService,
|
||||
smsCodeService *user_service.SMSCodeService,
|
||||
enterpriseService *user_service.EnterpriseService,
|
||||
eventBus interfaces.EventBus,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) UserApplicationService {
|
||||
return &UserApplicationServiceImpl{
|
||||
userRepo: userRepo,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
smsCodeService: smsCodeService,
|
||||
eventBus: eventBus,
|
||||
jwtAuth: jwtAuth,
|
||||
logger: logger,
|
||||
userManagementService: userManagementService,
|
||||
userAuthService: userAuthService,
|
||||
smsCodeService: smsCodeService,
|
||||
enterpriseService: enterpriseService,
|
||||
eventBus: eventBus,
|
||||
jwtAuth: jwtAuth,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
// 业务流程:1. 验证短信验证码 2. 创建用户 3. 发布注册事件
|
||||
func (s *UserApplicationServiceImpl) Register(ctx context.Context, cmd *commands.RegisterUserCommand) (*responses.RegisterUserResponse, error) {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
if _, err := s.userRepo.GetByPhone(ctx, cmd.Phone); err == nil {
|
||||
return nil, fmt.Errorf("手机号已存在")
|
||||
}
|
||||
|
||||
user, err := entities.NewUser(cmd.Phone, cmd.Password)
|
||||
// 2. 创建用户
|
||||
user, err := s.userManagementService.CreateUser(ctx, cmd.Phone, cmd.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
createdUser, err := s.userRepo.Create(ctx, *user)
|
||||
if err != nil {
|
||||
s.logger.Error("创建用户失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 发布用户注册事件
|
||||
event := events.NewUserRegisteredEvent(user, "")
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
|
||||
@@ -75,35 +72,63 @@ func (s *UserApplicationServiceImpl) Register(ctx context.Context, cmd *commands
|
||||
s.logger.Info("用户注册成功", zap.String("user_id", user.ID), zap.String("phone", user.Phone))
|
||||
|
||||
return &responses.RegisterUserResponse{
|
||||
ID: createdUser.ID,
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoginWithPassword 密码登录
|
||||
// 业务流程:1. 验证用户密码 2. 生成访问令牌 3. 更新登录统计 4. 获取用户权限
|
||||
func (s *UserApplicationServiceImpl) LoginWithPassword(ctx context.Context, cmd *commands.LoginWithPasswordCommand) (*responses.LoginUserResponse, error) {
|
||||
user, err := s.userRepo.GetByPhone(ctx, cmd.Phone)
|
||||
// 1. 验证用户密码
|
||||
user, err := s.userAuthService.ValidatePassword(ctx, cmd.Phone, cmd.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.CanLogin() {
|
||||
return nil, fmt.Errorf("用户状态异常,无法登录")
|
||||
}
|
||||
|
||||
if !user.CheckPassword(cmd.Password) {
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
// 2. 生成包含用户类型的token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType)
|
||||
if err != nil {
|
||||
s.logger.Error("生成令牌失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("生成访问令牌失败")
|
||||
}
|
||||
|
||||
userProfile, err := s.GetUserProfile(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户信息失败: %w", err)
|
||||
// 3. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userManagementService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userManagementService.GetUserByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("重新获取用户信息失败", zap.Error(err))
|
||||
} else {
|
||||
user = updatedUser
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取用户权限(仅管理员)
|
||||
var permissions []string
|
||||
if user.IsAdmin() {
|
||||
permissions, err = s.userAuthService.GetUserPermissions(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户权限失败", zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
return &responses.LoginUserResponse{
|
||||
@@ -116,140 +141,163 @@ func (s *UserApplicationServiceImpl) LoginWithPassword(ctx context.Context, cmd
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
// 业务流程:1. 验证短信验证码 2. 验证用户登录状态 3. 生成访问令牌 4. 更新登录统计 5. 获取用户权限
|
||||
func (s *UserApplicationServiceImpl) LoginWithSMS(ctx context.Context, cmd *commands.LoginWithSMSCommand) (*responses.LoginUserResponse, error) {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByPhone(ctx, cmd.Phone)
|
||||
// 2. 验证用户登录状态
|
||||
user, err := s.userAuthService.ValidateUserLogin(ctx, cmd.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.CanLogin() {
|
||||
return nil, fmt.Errorf("用户状态异常,无法登录")
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
// 3. 生成包含用户类型的token
|
||||
accessToken, err := s.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone, user.UserType)
|
||||
if err != nil {
|
||||
s.logger.Error("生成令牌失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("生成访问令牌失败")
|
||||
}
|
||||
|
||||
userProfile, err := s.GetUserProfile(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户信息失败: %w", err)
|
||||
// 4. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userManagementService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userManagementService.GetUserByID(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("重新获取用户信息失败", zap.Error(err))
|
||||
} else {
|
||||
user = updatedUser
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 获取用户权限(仅管理员)
|
||||
var permissions []string
|
||||
if user.IsAdmin() {
|
||||
permissions, err = s.userAuthService.GetUserPermissions(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户权限失败", zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
return &responses.LoginUserResponse{
|
||||
User: userProfile,
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24h
|
||||
ExpiresIn: int64(s.jwtAuth.GetExpiresIn().Seconds()), // 168h
|
||||
LoginMethod: "sms",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendSMS 发送短信验证码
|
||||
// 业务流程:1. 发送短信验证码
|
||||
func (s *UserApplicationServiceImpl) SendSMS(ctx context.Context, cmd *commands.SendCodeCommand) error {
|
||||
return s.smsCodeService.SendCode(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), "", "")
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// 业务流程:1. 修改用户密码
|
||||
func (s *UserApplicationServiceImpl) ChangePassword(ctx context.Context, cmd *commands.ChangePasswordCommand) error {
|
||||
user, err := s.userRepo.GetByID(ctx, cmd.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
if err := s.smsCodeService.VerifyCode(ctx, user.Phone, cmd.Code, entities.SMSSceneChangePassword); err != nil {
|
||||
return fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
if err := user.ChangePassword(cmd.OldPassword, cmd.NewPassword, cmd.ConfirmNewPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("密码更新失败: %w", err)
|
||||
}
|
||||
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, "")
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("发布密码修改事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("密码修改成功", zap.String("user_id", cmd.UserID))
|
||||
return nil
|
||||
return s.userAuthService.ChangePassword(ctx, cmd.UserID, cmd.OldPassword, cmd.NewPassword)
|
||||
}
|
||||
|
||||
// ResetPassword 重置密码
|
||||
// 业务流程:1. 验证短信验证码 2. 重置用户密码
|
||||
func (s *UserApplicationServiceImpl) ResetPassword(ctx context.Context, cmd *commands.ResetPasswordCommand) error {
|
||||
user, err := s.userRepo.GetByPhone(ctx, cmd.Phone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneResetPassword); err != nil {
|
||||
return fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
if err := user.ResetPassword(cmd.NewPassword, cmd.ConfirmNewPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(ctx, *user); err != nil {
|
||||
return fmt.Errorf("密码更新失败: %w", err)
|
||||
}
|
||||
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, "")
|
||||
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 nil
|
||||
// 2. 重置用户密码
|
||||
return s.userAuthService.ResetPassword(ctx, cmd.Phone, cmd.NewPassword)
|
||||
}
|
||||
|
||||
// GetUserProfile 获取用户信息
|
||||
// GetUserProfile 获取用户资料
|
||||
// 业务流程:1. 获取用户信息 2. 获取企业信息 3. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID string) (*responses.UserProfileResponse, error) {
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
// 1. 获取用户信息(包含企业信息)
|
||||
user, err := s.enterpriseService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
// 获取企业信息(如果存在)
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Debug("用户暂无企业信息", zap.String("user_id", userID))
|
||||
response.IsCertified = false
|
||||
} else {
|
||||
response.EnterpriseInfo = &responses.EnterpriseInfoResponse{
|
||||
ID: enterpriseInfo.ID,
|
||||
CompanyName: enterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: enterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: enterpriseInfo.LegalPersonID,
|
||||
IsOCRVerified: enterpriseInfo.IsOCRVerified,
|
||||
IsFaceVerified: enterpriseInfo.IsFaceVerified,
|
||||
IsCertified: enterpriseInfo.IsCertified,
|
||||
CertifiedAt: enterpriseInfo.CertifiedAt,
|
||||
CreatedAt: enterpriseInfo.CreatedAt,
|
||||
UpdatedAt: enterpriseInfo.UpdatedAt,
|
||||
// 2. 获取用户权限(仅管理员)
|
||||
var permissions []string
|
||||
if user.IsAdmin() {
|
||||
permissions, err = s.userAuthService.GetUserPermissions(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户权限失败", zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
response.IsCertified = enterpriseInfo.IsCertified
|
||||
}
|
||||
|
||||
return response, nil
|
||||
// 3. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
Permissions: permissions,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
// 4. 添加企业信息
|
||||
if user.EnterpriseInfo != nil {
|
||||
userProfile.EnterpriseInfo = &responses.EnterpriseInfoResponse{
|
||||
ID: user.EnterpriseInfo.ID,
|
||||
CompanyName: user.EnterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: user.EnterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: user.EnterpriseInfo.LegalPersonName,
|
||||
LegalPersonID: user.EnterpriseInfo.LegalPersonID,
|
||||
CreatedAt: user.EnterpriseInfo.CreatedAt,
|
||||
UpdatedAt: user.EnterpriseInfo.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return userProfile, nil
|
||||
}
|
||||
|
||||
// GetUser 获取用户信息
|
||||
// 业务流程:1. 获取用户信息 2. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) GetUser(ctx context.Context, query *queries.GetUserQuery) (*responses.UserProfileResponse, error) {
|
||||
// ... implementation
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
user, err := s.userManagementService.GetUserByID(ctx, query.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
LoginCount: user.LoginCount,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user