f
This commit is contained in:
1417
internal/application/api/api_application_service.go
Normal file
1417
internal/application/api/api_application_service.go
Normal file
File diff suppressed because it is too large
Load Diff
71
internal/application/api/commands/api_call_commands.go
Normal file
71
internal/application/api/commands/api_call_commands.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package commands
|
||||
|
||||
type ApiCallCommand struct {
|
||||
ClientIP string `json:"-"`
|
||||
AccessId string `json:"-"`
|
||||
ApiName string `json:"-"`
|
||||
Data string `json:"data" binding:"required"`
|
||||
Options ApiCallOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type ApiCallOptions struct {
|
||||
Json bool `json:"json,omitempty"` // 是否返回JSON格式
|
||||
IsDebug bool `json:"is_debug,omitempty"` // 是否为调试调用
|
||||
}
|
||||
|
||||
// EncryptCommand 加密命令
|
||||
type EncryptCommand struct {
|
||||
Data map[string]interface{} `json:"data" binding:"required"`
|
||||
SecretKey string `json:"secret_key" binding:"required"`
|
||||
}
|
||||
|
||||
// DecryptCommand 解密命令
|
||||
type DecryptCommand struct {
|
||||
EncryptedData string `json:"encrypted_data" binding:"required"`
|
||||
SecretKey string `json:"secret_key" binding:"required"`
|
||||
}
|
||||
|
||||
// SaveApiCallCommand 保存API调用命令
|
||||
type SaveApiCallCommand struct {
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Status string `json:"status"`
|
||||
Cost float64 `json:"cost"`
|
||||
ErrorType string `json:"error_type"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
}
|
||||
|
||||
// ProcessDeductionCommand 处理扣款命令
|
||||
type ProcessDeductionCommand struct {
|
||||
UserID string `json:"user_id"`
|
||||
Amount string `json:"amount"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
}
|
||||
|
||||
// UpdateUsageStatsCommand 更新使用统计命令
|
||||
type UpdateUsageStatsCommand struct {
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Increment int `json:"increment"`
|
||||
}
|
||||
|
||||
// RecordApiLogCommand 记录API日志命令
|
||||
type RecordApiLogCommand struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiName string `json:"api_name"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ResponseSize int64 `json:"response_size"`
|
||||
}
|
||||
|
||||
// ProcessCompensationCommand 处理补偿命令
|
||||
type ProcessCompensationCommand struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
104
internal/application/api/dto/api_call_validation.go
Normal file
104
internal/application/api/dto/api_call_validation.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
api_entities "hyapi-server/internal/domains/api/entities"
|
||||
product_entities "hyapi-server/internal/domains/product/entities"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ApiCallValidationResult API调用验证结果
|
||||
type ApiCallValidationResult struct {
|
||||
UserID string `json:"user_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
SubscriptionID string `json:"subscription_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
|
||||
// 新增字段
|
||||
ContractCode string `json:"contract_code"`
|
||||
ApiCall *api_entities.ApiCall `json:"api_call"`
|
||||
RequestParams map[string]interface{} `json:"request_params"`
|
||||
Product *product_entities.Product `json:"product"`
|
||||
Subscription *product_entities.Subscription `json:"subscription"`
|
||||
}
|
||||
|
||||
// GetUserID 获取用户ID
|
||||
func (r *ApiCallValidationResult) GetUserID() string {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// GetProductID 获取产品ID
|
||||
func (r *ApiCallValidationResult) GetProductID() string {
|
||||
return r.ProductID
|
||||
}
|
||||
|
||||
// GetSubscriptionID 获取订阅ID
|
||||
func (r *ApiCallValidationResult) GetSubscriptionID() string {
|
||||
return r.SubscriptionID
|
||||
}
|
||||
|
||||
// GetAmount 获取金额
|
||||
func (r *ApiCallValidationResult) GetAmount() decimal.Decimal {
|
||||
return r.Amount
|
||||
}
|
||||
|
||||
// GetSecretKey 获取密钥
|
||||
func (r *ApiCallValidationResult) GetSecretKey() string {
|
||||
return r.SecretKey
|
||||
}
|
||||
|
||||
// IsValidResult 检查是否有效
|
||||
func (r *ApiCallValidationResult) IsValidResult() bool {
|
||||
return r.IsValid
|
||||
}
|
||||
|
||||
// GetErrorMessage 获取错误消息
|
||||
func (r *ApiCallValidationResult) GetErrorMessage() string {
|
||||
return r.ErrorMessage
|
||||
}
|
||||
|
||||
// NewApiCallValidationResult 创建新的API调用验证结果
|
||||
func NewApiCallValidationResult() *ApiCallValidationResult {
|
||||
return &ApiCallValidationResult{
|
||||
IsValid: true,
|
||||
RequestParams: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetApiUser 设置API用户
|
||||
func (r *ApiCallValidationResult) SetApiUser(apiUser *api_entities.ApiUser) {
|
||||
r.UserID = apiUser.UserId
|
||||
r.SecretKey = apiUser.SecretKey
|
||||
}
|
||||
|
||||
// SetProduct 设置产品
|
||||
func (r *ApiCallValidationResult) SetProduct(product *product_entities.Product) {
|
||||
r.ProductID = product.ID
|
||||
r.Product = product
|
||||
// 注意:这里不设置Amount,应该通过SetSubscription来设置实际的扣费金额
|
||||
}
|
||||
|
||||
// SetApiCall 设置API调用
|
||||
func (r *ApiCallValidationResult) SetApiCall(apiCall *api_entities.ApiCall) {
|
||||
r.ApiCall = apiCall
|
||||
}
|
||||
|
||||
// SetRequestParams 设置请求参数
|
||||
func (r *ApiCallValidationResult) SetRequestParams(params map[string]interface{}) {
|
||||
r.RequestParams = params
|
||||
}
|
||||
|
||||
// SetContractCode 设置合同代码
|
||||
func (r *ApiCallValidationResult) SetContractCode(code string) {
|
||||
r.ContractCode = code
|
||||
}
|
||||
|
||||
// SetSubscription 设置订阅信息(包含实际扣费金额)
|
||||
func (r *ApiCallValidationResult) SetSubscription(subscription *product_entities.Subscription) {
|
||||
r.SubscriptionID = subscription.ID
|
||||
r.Amount = subscription.Price // 使用订阅价格作为扣费金额
|
||||
r.Subscription = subscription
|
||||
}
|
||||
103
internal/application/api/dto/api_response.go
Normal file
103
internal/application/api/dto/api_response.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// ApiCallResponse API调用响应结构
|
||||
type ApiCallResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
TransactionId string `json:"transaction_id"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ApiKeysResponse API密钥响应结构
|
||||
type ApiKeysResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
AccessID string `json:"access_id"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// 白名单相关DTO
|
||||
type WhiteListResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Remark string `json:"remark"` // 备注
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type WhiteListRequest struct {
|
||||
IPAddress string `json:"ip_address" binding:"required,ip"`
|
||||
Remark string `json:"remark"` // 备注(可选)
|
||||
}
|
||||
|
||||
type WhiteListListResponse struct {
|
||||
Items []WhiteListResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// API调用记录相关DTO
|
||||
type ApiCallRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
AccessId string `json:"access_id"`
|
||||
UserId string `json:"user_id"`
|
||||
ProductId *string `json:"product_id,omitempty"`
|
||||
ProductName *string `json:"product_name,omitempty"`
|
||||
TransactionId string `json:"transaction_id"`
|
||||
ClientIp string `json:"client_ip"`
|
||||
RequestParams string `json:"request_params"`
|
||||
Status string `json:"status"`
|
||||
StartAt string `json:"start_at"`
|
||||
EndAt *string `json:"end_at,omitempty"`
|
||||
Cost *string `json:"cost,omitempty"`
|
||||
ErrorType *string `json:"error_type,omitempty"`
|
||||
ErrorMsg *string `json:"error_msg,omitempty"`
|
||||
TranslatedErrorMsg *string `json:"translated_error_msg,omitempty"`
|
||||
CompanyName *string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UserSimpleResponse 用户简单信息响应
|
||||
type UserSimpleResponse struct {
|
||||
ID string `json:"id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
type ApiCallListResponse struct {
|
||||
Items []ApiCallRecordResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// EncryptResponse 加密响应
|
||||
type EncryptResponse struct {
|
||||
EncryptedData string `json:"encrypted_data"`
|
||||
}
|
||||
|
||||
// NewSuccessResponse 创建成功响应
|
||||
func NewSuccessResponse(transactionId, data string) *ApiCallResponse {
|
||||
return &ApiCallResponse{
|
||||
Code: 0,
|
||||
Message: "业务成功",
|
||||
TransactionId: transactionId,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorResponse 创建错误响应
|
||||
func NewErrorResponse(code int, message, transactionId string) *ApiCallResponse {
|
||||
return &ApiCallResponse{
|
||||
Code: code,
|
||||
Message: message,
|
||||
TransactionId: transactionId,
|
||||
Data: "",
|
||||
}
|
||||
}
|
||||
19
internal/application/api/dto/form_config_dto.go
Normal file
19
internal/application/api/dto/form_config_dto.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
// FormField 表单字段配置
|
||||
type FormField struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Validation string `json:"validation"`
|
||||
Description string `json:"description"`
|
||||
Example string `json:"example"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
}
|
||||
|
||||
// FormConfigResponse 表单配置响应
|
||||
type FormConfigResponse struct {
|
||||
ApiCode string `json:"api_code"`
|
||||
Fields []FormField `json:"fields"`
|
||||
}
|
||||
61
internal/application/api/errors.go
Normal file
61
internal/application/api/errors.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import "errors"
|
||||
|
||||
// API调用相关错误类型
|
||||
var (
|
||||
ErrQueryEmpty = errors.New("查询为空")
|
||||
ErrSystem = errors.New("接口异常")
|
||||
ErrDecryptFail = errors.New("解密失败")
|
||||
ErrRequestParam = errors.New("请求参数结构不正确")
|
||||
ErrInvalidParam = errors.New("参数校验不正确")
|
||||
ErrInvalidIP = errors.New("未经授权的IP")
|
||||
ErrMissingAccessId = errors.New("缺少Access-Id")
|
||||
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
||||
ErrFrozenAccount = errors.New("账户已冻结")
|
||||
ErrArrears = errors.New("账户余额不足,无法请求")
|
||||
ErrInsufficientBalance = errors.New("钱包余额不足")
|
||||
ErrProductNotFound = errors.New("产品不存在")
|
||||
ErrProductDisabled = errors.New("产品已停用")
|
||||
ErrNotSubscribed = errors.New("未订阅此产品")
|
||||
ErrProductNotSubscribed = errors.New("未订阅此产品")
|
||||
ErrSubscriptionExpired = errors.New("订阅已过期")
|
||||
ErrSubscriptionSuspended = errors.New("订阅已暂停")
|
||||
ErrBusiness = errors.New("业务失败")
|
||||
)
|
||||
|
||||
// 错误码映射 - 严格按照用户要求
|
||||
var ErrorCodeMap = map[error]int{
|
||||
ErrQueryEmpty: 1000,
|
||||
ErrSystem: 1001,
|
||||
ErrDecryptFail: 1002,
|
||||
ErrRequestParam: 1003,
|
||||
ErrInvalidParam: 1003,
|
||||
ErrInvalidIP: 1004,
|
||||
ErrMissingAccessId: 1005,
|
||||
ErrInvalidAccessId: 1006,
|
||||
ErrFrozenAccount: 1007,
|
||||
ErrArrears: 1007,
|
||||
ErrInsufficientBalance: 1007,
|
||||
ErrProductNotFound: 1008,
|
||||
ErrProductDisabled: 1008,
|
||||
ErrNotSubscribed: 1008,
|
||||
ErrProductNotSubscribed: 1008,
|
||||
ErrSubscriptionExpired: 1008,
|
||||
ErrSubscriptionSuspended: 1008,
|
||||
ErrBusiness: 2001,
|
||||
}
|
||||
|
||||
// GetErrorCode 获取错误对应的错误码
|
||||
func GetErrorCode(err error) int {
|
||||
if code, exists := ErrorCodeMap[err]; exists {
|
||||
return code
|
||||
}
|
||||
return 1001 // 默认返回接口异常
|
||||
}
|
||||
|
||||
// GetErrorMessage 获取错误对应的错误消息
|
||||
func GetErrorMessage(err error) string {
|
||||
// 直接返回预定义的错误消息
|
||||
return err.Error()
|
||||
}
|
||||
40
internal/application/api/utils/error_translator.go
Normal file
40
internal/application/api/utils/error_translator.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package utils
|
||||
|
||||
// TranslateErrorMsg 翻译错误信息
|
||||
func TranslateErrorMsg(errorType, errorMsg *string) *string {
|
||||
if errorType == nil || errorMsg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 错误类型到中文描述的映射
|
||||
errorTypeTranslations := map[string]string{
|
||||
"invalid_access": "无效的访问凭证",
|
||||
"frozen_account": "账户已被冻结",
|
||||
"invalid_ip": "IP地址未授权",
|
||||
"arrears": "账户余额不足",
|
||||
"not_subscribed": "未订阅该产品",
|
||||
"product_not_found": "产品不存在",
|
||||
"product_disabled": "产品已停用",
|
||||
"system_error": "接口异常",
|
||||
"datasource_error": "数据源异常",
|
||||
"invalid_param": "参数校验失败",
|
||||
"decrypt_fail": "参数解密失败",
|
||||
"query_empty": "查询结果为空",
|
||||
}
|
||||
|
||||
// 获取错误类型的中文描述
|
||||
translatedType, exists := errorTypeTranslations[*errorType]
|
||||
if !exists {
|
||||
// 如果没有找到对应的翻译,返回原始错误信息
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
// 构建翻译后的错误信息
|
||||
translatedMsg := translatedType
|
||||
if *errorMsg != "" && *errorMsg != *errorType {
|
||||
// 如果原始错误信息不是错误类型本身,则组合显示
|
||||
translatedMsg = translatedType + ":" + *errorMsg
|
||||
}
|
||||
|
||||
return &translatedMsg
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/article/dto/queries"
|
||||
"hyapi-server/internal/application/article/dto/responses"
|
||||
)
|
||||
|
||||
// AnnouncementApplicationService 公告应用服务接口
|
||||
type AnnouncementApplicationService interface {
|
||||
// 公告管理
|
||||
CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error
|
||||
UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error
|
||||
DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error
|
||||
GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error)
|
||||
ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error)
|
||||
|
||||
// 公告状态管理
|
||||
PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error
|
||||
PublishAnnouncementByID(ctx context.Context, announcementID string) error // 通过ID发布公告 (用于定时任务)
|
||||
WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error
|
||||
ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error
|
||||
SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error
|
||||
UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error
|
||||
CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error
|
||||
|
||||
// 统计信息
|
||||
GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/article/dto/queries"
|
||||
"hyapi-server/internal/application/article/dto/responses"
|
||||
"hyapi-server/internal/domains/article/entities"
|
||||
"hyapi-server/internal/domains/article/repositories"
|
||||
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
|
||||
"hyapi-server/internal/domains/article/services"
|
||||
task_entities "hyapi-server/internal/infrastructure/task/entities"
|
||||
task_interfaces "hyapi-server/internal/infrastructure/task/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AnnouncementApplicationServiceImpl 公告应用服务实现
|
||||
type AnnouncementApplicationServiceImpl struct {
|
||||
announcementRepo repositories.AnnouncementRepository
|
||||
announcementService *services.AnnouncementService
|
||||
taskManager task_interfaces.TaskManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAnnouncementApplicationService 创建公告应用服务
|
||||
func NewAnnouncementApplicationService(
|
||||
announcementRepo repositories.AnnouncementRepository,
|
||||
announcementService *services.AnnouncementService,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) AnnouncementApplicationService {
|
||||
return &AnnouncementApplicationServiceImpl{
|
||||
announcementRepo: announcementRepo,
|
||||
announcementService: announcementService,
|
||||
taskManager: taskManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAnnouncement 创建公告
|
||||
func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error {
|
||||
// 1. 创建公告实体
|
||||
announcement := &entities.Announcement{
|
||||
Title: cmd.Title,
|
||||
Content: cmd.Content,
|
||||
Status: entities.AnnouncementStatusDraft,
|
||||
}
|
||||
|
||||
// 2. 调用领域服务验证
|
||||
if err := s.announcementService.ValidateAnnouncement(announcement); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存公告
|
||||
_, err := s.announcementRepo.Create(ctx, *announcement)
|
||||
if err != nil {
|
||||
s.logger.Error("创建公告失败", zap.Error(err))
|
||||
return fmt.Errorf("创建公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAnnouncement 更新公告
|
||||
func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error {
|
||||
// 1. 获取原公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以编辑
|
||||
if err := s.announcementService.CanEdit(&announcement); err != nil {
|
||||
return fmt.Errorf("公告状态不允许编辑: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
if cmd.Title != "" {
|
||||
announcement.Title = cmd.Title
|
||||
}
|
||||
if cmd.Content != "" {
|
||||
announcement.Content = cmd.Content
|
||||
}
|
||||
|
||||
// 4. 验证更新后的公告
|
||||
if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAnnouncement 删除公告
|
||||
func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error {
|
||||
// 1. 检查公告是否存在
|
||||
_, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除公告
|
||||
if err := s.announcementRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除公告成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnnouncementByID 获取公告详情
|
||||
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := responses.FromAnnouncementEntity(&announcement)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListAnnouncements 获取公告列表
|
||||
func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) {
|
||||
// 1. 构建仓储查询
|
||||
repoQuery := &repoQueries.ListAnnouncementQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
Title: query.Title,
|
||||
OrderBy: query.OrderBy,
|
||||
OrderDir: query.OrderDir,
|
||||
}
|
||||
|
||||
// 2. 调用仓储
|
||||
announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取公告列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
items := responses.FromAnnouncementEntityList(announcements)
|
||||
|
||||
response := &responses.AnnouncementListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
s.logger.Info("获取公告列表成功", zap.Int64("total", total))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// PublishAnnouncement 发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以发布
|
||||
if err := s.announcementService.CanPublish(&announcement); err != nil {
|
||||
return fmt.Errorf("无法发布公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 发布公告
|
||||
if err := announcement.Publish(); err != nil {
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("发布公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishAnnouncementByID 通过ID发布公告 (用于定时任务)
|
||||
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, announcementID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已取消定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
s.logger.Info("公告定时发布已取消,跳过执行",
|
||||
zap.String("id", announcementID),
|
||||
zap.String("status", string(announcement.Status)))
|
||||
return nil // 静默返回,不报错
|
||||
}
|
||||
|
||||
// 3. 检查定时发布时间是否匹配
|
||||
if announcement.ScheduledAt == nil {
|
||||
s.logger.Info("公告没有定时发布时间,跳过执行",
|
||||
zap.String("id", announcementID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 发布公告
|
||||
if err := announcement.Publish(); err != nil {
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawAnnouncement 撤回公告
|
||||
func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以撤回
|
||||
if err := s.announcementService.CanWithdraw(&announcement); err != nil {
|
||||
return fmt.Errorf("无法撤回公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 撤回公告
|
||||
if err := announcement.Withdraw(); err != nil {
|
||||
return fmt.Errorf("撤回公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("撤回公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("撤回公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveAnnouncement 归档公告
|
||||
func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以归档
|
||||
if err := s.announcementService.CanArchive(&announcement); err != nil {
|
||||
return fmt.Errorf("无法归档公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 归档公告
|
||||
announcement.Status = entities.AnnouncementStatusArchived
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("归档公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("归档公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublishAnnouncement 定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 检查是否可以定时发布
|
||||
if err := s.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil {
|
||||
return fmt.Errorf("无法设置定时发布: %w", err)
|
||||
}
|
||||
|
||||
// 4. 取消旧任务(如果存在)
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 6. 创建并异步入队公告发布任务
|
||||
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 设置定时发布
|
||||
if err := announcement.SchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishAnnouncement 更新定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 检查是否已设置定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
return fmt.Errorf("公告未设置定时发布,无法修改时间")
|
||||
}
|
||||
|
||||
// 4. 取消旧任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 6. 创建并异步入队新的公告发布任务
|
||||
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 更新定时发布时间
|
||||
if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("更新定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelSchedulePublishAnnouncement 取消定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已设置定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
return fmt.Errorf("公告未设置定时发布,无需取消")
|
||||
}
|
||||
|
||||
// 3. 取消任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
// 继续执行,即使取消任务失败也尝试取消定时发布状态
|
||||
}
|
||||
|
||||
// 4. 取消定时发布
|
||||
if err := announcement.CancelSchedulePublish(); err != nil {
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnnouncementStats 获取公告统计信息
|
||||
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) {
|
||||
// 1. 统计总数
|
||||
total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("统计公告总数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 统计各状态数量
|
||||
published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished)
|
||||
if err != nil {
|
||||
s.logger.Error("统计已发布公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("统计草稿公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived)
|
||||
if err != nil {
|
||||
s.logger.Error("统计归档公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 统计定时发布数量(需要查询有scheduled_at的草稿)
|
||||
scheduled, err := s.announcementRepo.FindScheduled(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("统计定时发布公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
response := &responses.AnnouncementStatsResponse{
|
||||
TotalAnnouncements: total + published + archived,
|
||||
PublishedAnnouncements: published,
|
||||
DraftAnnouncements: draft,
|
||||
ArchivedAnnouncements: archived,
|
||||
ScheduledAnnouncements: int64(len(scheduled)),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
48
internal/application/article/article_application_service.go
Normal file
48
internal/application/article/article_application_service.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/article/dto/queries"
|
||||
"hyapi-server/internal/application/article/dto/responses"
|
||||
)
|
||||
|
||||
// ArticleApplicationService 文章应用服务接口
|
||||
type ArticleApplicationService interface {
|
||||
// 文章管理
|
||||
CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error
|
||||
UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error
|
||||
DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error
|
||||
GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error)
|
||||
ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
|
||||
ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
|
||||
|
||||
// 文章状态管理
|
||||
PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error
|
||||
PublishArticleByID(ctx context.Context, articleID string) error
|
||||
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||
UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||
CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error
|
||||
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
|
||||
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
|
||||
|
||||
// 文章交互
|
||||
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
|
||||
|
||||
// 统计信息
|
||||
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
|
||||
|
||||
// 分类管理
|
||||
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 *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
|
||||
|
||||
// 标签管理
|
||||
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
|
||||
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
|
||||
DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error
|
||||
GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error)
|
||||
ListTags(ctx context.Context) (*responses.TagListResponse, error)
|
||||
}
|
||||
836
internal/application/article/article_application_service_impl.go
Normal file
836
internal/application/article/article_application_service_impl.go
Normal file
@@ -0,0 +1,836 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/article/dto/queries"
|
||||
"hyapi-server/internal/application/article/dto/responses"
|
||||
"hyapi-server/internal/domains/article/entities"
|
||||
"hyapi-server/internal/domains/article/repositories"
|
||||
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
|
||||
"hyapi-server/internal/domains/article/services"
|
||||
task_entities "hyapi-server/internal/infrastructure/task/entities"
|
||||
task_interfaces "hyapi-server/internal/infrastructure/task/interfaces"
|
||||
shared_interfaces "hyapi-server/internal/shared/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ArticleApplicationServiceImpl 文章应用服务实现
|
||||
type ArticleApplicationServiceImpl struct {
|
||||
articleRepo repositories.ArticleRepository
|
||||
categoryRepo repositories.CategoryRepository
|
||||
tagRepo repositories.TagRepository
|
||||
articleService *services.ArticleService
|
||||
taskManager task_interfaces.TaskManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewArticleApplicationService 创建文章应用服务
|
||||
func NewArticleApplicationService(
|
||||
articleRepo repositories.ArticleRepository,
|
||||
categoryRepo repositories.CategoryRepository,
|
||||
tagRepo repositories.TagRepository,
|
||||
articleService *services.ArticleService,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) ArticleApplicationService {
|
||||
return &ArticleApplicationServiceImpl{
|
||||
articleRepo: articleRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
tagRepo: tagRepo,
|
||||
articleService: articleService,
|
||||
taskManager: taskManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateArticle 创建文章
|
||||
func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateArticle(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建文章实体
|
||||
article := &entities.Article{
|
||||
Title: cmd.Title,
|
||||
Content: cmd.Content,
|
||||
Summary: cmd.Summary,
|
||||
CoverImage: cmd.CoverImage,
|
||||
CategoryID: cmd.CategoryID,
|
||||
IsFeatured: cmd.IsFeatured,
|
||||
Status: entities.ArticleStatusDraft,
|
||||
}
|
||||
|
||||
// 3. 调用领域服务验证
|
||||
if err := s.articleService.ValidateArticle(article); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存文章
|
||||
_, err := s.articleRepo.Create(ctx, *article)
|
||||
if err != nil {
|
||||
s.logger.Error("创建文章失败", zap.Error(err))
|
||||
return fmt.Errorf("创建文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 处理标签关联
|
||||
if len(cmd.TagIDs) > 0 {
|
||||
for _, tagID := range cmd.TagIDs {
|
||||
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateArticle 更新文章
|
||||
func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error {
|
||||
// 1. 获取原文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以编辑
|
||||
if !article.CanEdit() {
|
||||
return fmt.Errorf("文章状态不允许编辑")
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
if cmd.Title != "" {
|
||||
article.Title = cmd.Title
|
||||
}
|
||||
if cmd.Content != "" {
|
||||
article.Content = cmd.Content
|
||||
}
|
||||
if cmd.Summary != "" {
|
||||
article.Summary = cmd.Summary
|
||||
}
|
||||
if cmd.CoverImage != "" {
|
||||
article.CoverImage = cmd.CoverImage
|
||||
}
|
||||
if cmd.CategoryID != "" {
|
||||
article.CategoryID = cmd.CategoryID
|
||||
}
|
||||
article.IsFeatured = cmd.IsFeatured
|
||||
|
||||
// 4. 验证更新后的文章
|
||||
if err := s.articleService.ValidateArticle(&article); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 处理标签关联
|
||||
// 先清除现有标签
|
||||
existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID)
|
||||
for _, tag := range existingTags {
|
||||
s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID)
|
||||
}
|
||||
|
||||
// 添加新标签
|
||||
for _, tagID := range cmd.TagIDs {
|
||||
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("更新文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteArticle 删除文章
|
||||
func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error {
|
||||
// 1. 检查文章是否存在
|
||||
_, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除文章
|
||||
if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除文章成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArticleByID 根据ID获取文章
|
||||
func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := responses.FromArticleEntity(&article)
|
||||
|
||||
s.logger.Info("获取文章成功", zap.String("id", article.ID))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListArticles 获取文章列表
|
||||
func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
|
||||
// 1. 构建仓储查询
|
||||
repoQuery := &repoQueries.ListArticleQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
CategoryID: query.CategoryID,
|
||||
TagID: query.TagID,
|
||||
Title: query.Title,
|
||||
Summary: query.Summary,
|
||||
IsFeatured: query.IsFeatured,
|
||||
OrderBy: query.OrderBy,
|
||||
OrderDir: query.OrderDir,
|
||||
}
|
||||
|
||||
// 2. 调用仓储
|
||||
articles, total, err := s.articleRepo.ListArticles(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取文章列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
items := responses.FromArticleEntitiesToListItemList(articles)
|
||||
|
||||
response := &responses.ArticleListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListArticlesForAdmin 获取文章列表(管理员端)
|
||||
func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
|
||||
// 1. 构建仓储查询
|
||||
repoQuery := &repoQueries.ListArticleQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
CategoryID: query.CategoryID,
|
||||
TagID: query.TagID,
|
||||
Title: query.Title,
|
||||
Summary: query.Summary,
|
||||
IsFeatured: query.IsFeatured,
|
||||
OrderBy: query.OrderBy,
|
||||
OrderDir: query.OrderDir,
|
||||
}
|
||||
|
||||
// 2. 调用仓储
|
||||
articles, total, err := s.articleRepo.ListArticlesForAdmin(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取文章列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
items := responses.FromArticleEntitiesToListItemList(articles)
|
||||
|
||||
response := &responses.ArticleListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// PublishArticle 发布文章
|
||||
func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 发布文章
|
||||
if err := article.Publish(); err != nil {
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("发布文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishArticleByID 通过ID发布文章 (用于定时任务)
|
||||
func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, articleID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已取消定时发布
|
||||
if !article.IsScheduled() {
|
||||
s.logger.Info("文章定时发布已取消,跳过执行",
|
||||
zap.String("id", articleID),
|
||||
zap.String("status", string(article.Status)))
|
||||
return nil // 静默返回,不报错
|
||||
}
|
||||
|
||||
// 3. 检查定时发布时间是否匹配
|
||||
if article.ScheduledAt == nil {
|
||||
s.logger.Info("文章没有定时发布时间,跳过执行",
|
||||
zap.String("id", articleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 发布文章
|
||||
if err := article.Publish(); err != nil {
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublishArticle 定时发布文章
|
||||
func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 取消旧任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 4. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 5. 创建并异步入队文章发布任务
|
||||
if err := taskFactory.CreateAndEnqueueArticlePublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 6. 设置定时发布
|
||||
if err := article.SchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelSchedulePublishArticle 取消定时发布文章
|
||||
func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已设置定时发布
|
||||
if !article.IsScheduled() {
|
||||
return fmt.Errorf("文章未设置定时发布")
|
||||
}
|
||||
|
||||
// 3. 取消定时任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||
// 不返回错误,继续执行取消定时发布
|
||||
}
|
||||
|
||||
// 4. 取消定时发布
|
||||
if err := article.CancelSchedulePublish(); err != nil {
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveArticle 归档文章
|
||||
func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 归档文章
|
||||
if err := article.Archive(); err != nil {
|
||||
return fmt.Errorf("归档文章失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("归档文章失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("归档文章成功", zap.String("id", article.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error {
|
||||
// 1. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 设置推荐状态
|
||||
article.SetFeatured(cmd.IsFeatured)
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置推荐状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordView 记录阅读
|
||||
func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error {
|
||||
// 1. 增加阅读量
|
||||
if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil {
|
||||
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
|
||||
return fmt.Errorf("记录阅读失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("记录阅读成功", zap.String("id", articleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetArticleStats 获取文章统计
|
||||
func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) {
|
||||
// 1. 获取各种统计
|
||||
totalArticles, err := s.articleRepo.CountByStatus(ctx, "")
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章总数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished)
|
||||
if err != nil {
|
||||
s.logger.Error("获取已发布文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("获取草稿文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived)
|
||||
if err != nil {
|
||||
s.logger.Error("获取归档文章数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
response := &responses.ArticleStatsResponse{
|
||||
TotalArticles: totalArticles,
|
||||
PublishedArticles: publishedArticles,
|
||||
DraftArticles: draftArticles,
|
||||
ArchivedArticles: archivedArticles,
|
||||
TotalViews: 0, // TODO: 实现总阅读量统计
|
||||
}
|
||||
|
||||
s.logger.Info("获取文章统计成功")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// validateCreateArticle 验证创建文章参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
|
||||
if cmd.Title == "" {
|
||||
return fmt.Errorf("文章标题不能为空")
|
||||
}
|
||||
if cmd.Content == "" {
|
||||
return fmt.Errorf("文章内容不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 分类相关方法 ====================
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateCategory(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建分类实体
|
||||
category := &entities.Category{
|
||||
Name: cmd.Name,
|
||||
Description: cmd.Description,
|
||||
}
|
||||
|
||||
// 3. 保存分类
|
||||
_, err := s.categoryRepo.Create(ctx, *category)
|
||||
if err != nil {
|
||||
s.logger.Error("创建分类失败", zap.Error(err))
|
||||
return fmt.Errorf("创建分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
|
||||
// 1. 获取原分类
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
category.Name = cmd.Name
|
||||
category.Description = cmd.Description
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新分类成功", zap.String("id", category.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||
// 1. 检查分类是否存在
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有文章使用此分类
|
||||
count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除分类失败: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count)
|
||||
}
|
||||
|
||||
// 3. 删除分类
|
||||
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryByID 获取分类详情
|
||||
func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
|
||||
// 1. 获取分类
|
||||
category, err := s.categoryRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
SortOrder: category.SortOrder,
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
|
||||
// 1. 获取分类列表
|
||||
categories, err := s.categoryRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.CategoryInfoResponse, len(categories))
|
||||
for i, category := range categories {
|
||||
items[i] = responses.CategoryInfoResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Description: category.Description,
|
||||
SortOrder: category.SortOrder,
|
||||
CreatedAt: category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := &responses.CategoryListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ==================== 标签相关方法 ====================
|
||||
|
||||
// CreateTag 创建标签
|
||||
func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateTag(cmd); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建标签实体
|
||||
tag := &entities.Tag{
|
||||
Name: cmd.Name,
|
||||
Color: cmd.Color,
|
||||
}
|
||||
|
||||
// 3. 保存标签
|
||||
_, err := s.tagRepo.Create(ctx, *tag)
|
||||
if err != nil {
|
||||
s.logger.Error("创建标签失败", zap.Error(err))
|
||||
return fmt.Errorf("创建标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error {
|
||||
// 1. 获取原标签
|
||||
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
tag.Name = cmd.Name
|
||||
tag.Color = cmd.Color
|
||||
|
||||
// 3. 保存更新
|
||||
if err := s.tagRepo.Update(ctx, tag); err != nil {
|
||||
s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新标签成功", zap.String("id", tag.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签
|
||||
func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error {
|
||||
// 1. 检查标签是否存在
|
||||
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除标签
|
||||
if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除标签失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTagByID 获取标签详情
|
||||
func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) {
|
||||
// 1. 获取标签
|
||||
tag, err := s.tagRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("标签不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := &responses.TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListTags 获取标签列表
|
||||
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
|
||||
// 1. 获取标签列表
|
||||
tags, err := s.tagRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||
if err != nil {
|
||||
s.logger.Error("获取标签列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
items := make([]responses.TagInfoResponse, len(tags))
|
||||
for i, tag := range tags {
|
||||
items[i] = responses.TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := &responses.TagListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishArticle 修改定时发布时间
|
||||
func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取文章
|
||||
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("文章不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 检查是否已设置定时发布
|
||||
if !article.IsScheduled() {
|
||||
return fmt.Errorf("文章未设置定时发布,无法修改时间")
|
||||
}
|
||||
|
||||
// 4. 更新数据库中的任务调度时间
|
||||
if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
|
||||
s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 更新定时发布
|
||||
if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("更新定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 保存更新
|
||||
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("修改定时发布时间成功",
|
||||
zap.String("id", article.ID),
|
||||
zap.Time("new_scheduled_time", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 验证方法 ====================
|
||||
|
||||
// validateCreateCategory 验证创建分类参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return fmt.Errorf("分类名称不能为空")
|
||||
}
|
||||
if len(cmd.Name) > 50 {
|
||||
return fmt.Errorf("分类名称长度不能超过50个字符")
|
||||
}
|
||||
if len(cmd.Description) > 200 {
|
||||
return fmt.Errorf("分类描述长度不能超过200个字符")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCreateTag 验证创建标签参数
|
||||
func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return fmt.Errorf("标签名称不能为空")
|
||||
}
|
||||
if len(cmd.Name) > 30 {
|
||||
return fmt.Errorf("标签名称长度不能超过30个字符")
|
||||
}
|
||||
if cmd.Color == "" {
|
||||
return fmt.Errorf("标签颜色不能为空")
|
||||
}
|
||||
// TODO: 添加十六进制颜色格式验证
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateAnnouncementCommand 创建公告命令
|
||||
type CreateAnnouncementCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"公告标题"`
|
||||
Content string `json:"content" binding:"required" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// UpdateAnnouncementCommand 更新公告命令
|
||||
type UpdateAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// DeleteAnnouncementCommand 删除公告命令
|
||||
type DeleteAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// PublishAnnouncementCommand 发布公告命令
|
||||
type PublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// WithdrawAnnouncementCommand 撤回公告命令
|
||||
type WithdrawAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// ArchiveAnnouncementCommand 归档公告命令
|
||||
type ArchiveAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// SchedulePublishAnnouncementCommand 定时发布公告命令
|
||||
type SchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
|
||||
type UpdateSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
|
||||
type CancelSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package commands
|
||||
|
||||
// CreateArticleCommand 创建文章命令
|
||||
type CreateArticleCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"文章标题"`
|
||||
Content string `json:"content" binding:"required" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// UpdateArticleCommand 更新文章命令
|
||||
type UpdateArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Content string `json:"content" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// DeleteArticleCommand 删除文章命令
|
||||
type DeleteArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// PublishArticleCommand 发布文章命令
|
||||
type PublishArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// ArchiveArticleCommand 归档文章命令
|
||||
type ArchiveArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// SetFeaturedCommand 设置推荐状态命令
|
||||
type SetFeaturedCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// CancelScheduleCommand 取消定时发布命令
|
||||
type CancelScheduleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// UpdateCategoryCommand 更新分类命令
|
||||
type UpdateCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// DeleteCategoryCommand 删除分类命令
|
||||
type DeleteCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SchedulePublishCommand 定时发布文章命令
|
||||
type SchedulePublishCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
19
internal/application/article/dto/commands/tag_commands.go
Normal file
19
internal/application/article/dto/commands/tag_commands.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateTagCommand 创建标签命令
|
||||
type CreateTagCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// UpdateTagCommand 更新标签命令
|
||||
type UpdateTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// DeleteTagCommand 删除标签命令
|
||||
type DeleteTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package queries
|
||||
|
||||
import "hyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListAnnouncementQuery 公告列表查询
|
||||
type ListAnnouncementQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Status entities.AnnouncementStatus `form:"status" comment:"公告状态"`
|
||||
Title string `form:"title" comment:"标题关键词"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetAnnouncementQuery 获取公告详情查询
|
||||
type GetAnnouncementQuery struct {
|
||||
ID string `uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
54
internal/application/article/dto/queries/article_queries.go
Normal file
54
internal/application/article/dto/queries/article_queries.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package queries
|
||||
|
||||
import "hyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListArticleQuery 文章列表查询
|
||||
type ListArticleQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||
TagID string `form:"tag_id" comment:"标签ID"`
|
||||
Title string `form:"title" comment:"标题关键词"`
|
||||
Summary string `form:"summary" comment:"摘要关键词"`
|
||||
IsFeatured *bool `form:"is_featured" comment:"是否推荐"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// SearchArticleQuery 文章搜索查询
|
||||
type SearchArticleQuery 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"`
|
||||
AuthorID string `form:"author_id" comment:"作者ID"`
|
||||
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetArticleQuery 获取文章详情查询
|
||||
type GetArticleQuery struct {
|
||||
ID string `uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// GetArticlesByAuthorQuery 获取作者文章查询
|
||||
type GetArticlesByAuthorQuery struct {
|
||||
AuthorID string `uri:"author_id" binding:"required" comment:"作者ID"`
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
|
||||
// GetArticlesByCategoryQuery 获取分类文章查询
|
||||
type GetArticlesByCategoryQuery struct {
|
||||
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
|
||||
// GetFeaturedArticlesQuery 获取推荐文章查询
|
||||
type GetFeaturedArticlesQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetCategoryQuery 获取分类详情查询
|
||||
type GetCategoryQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
6
internal/application/article/dto/queries/tag_queries.go
Normal file
6
internal/application/article/dto/queries/tag_queries.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetTagQuery 获取标签详情查询
|
||||
type GetTagQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"hyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// AnnouncementInfoResponse 公告详情响应
|
||||
type AnnouncementInfoResponse struct {
|
||||
ID string `json:"id" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
Status string `json:"status" comment:"公告状态"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// AnnouncementListItemResponse 公告列表项响应
|
||||
type AnnouncementListItemResponse struct {
|
||||
ID string `json:"id" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
Status string `json:"status" comment:"公告状态"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// AnnouncementListResponse 公告列表响应
|
||||
type AnnouncementListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []AnnouncementListItemResponse `json:"items" comment:"公告列表"`
|
||||
}
|
||||
|
||||
// AnnouncementStatsResponse 公告统计响应
|
||||
type AnnouncementStatsResponse struct {
|
||||
TotalAnnouncements int64 `json:"total_announcements" comment:"公告总数"`
|
||||
PublishedAnnouncements int64 `json:"published_announcements" comment:"已发布公告数"`
|
||||
DraftAnnouncements int64 `json:"draft_announcements" comment:"草稿公告数"`
|
||||
ArchivedAnnouncements int64 `json:"archived_announcements" comment:"归档公告数"`
|
||||
ScheduledAnnouncements int64 `json:"scheduled_announcements" comment:"定时发布公告数"`
|
||||
}
|
||||
|
||||
// FromAnnouncementEntity 从公告实体转换为响应对象
|
||||
func FromAnnouncementEntity(announcement *entities.Announcement) *AnnouncementInfoResponse {
|
||||
if announcement == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &AnnouncementInfoResponse{
|
||||
ID: announcement.ID,
|
||||
Title: announcement.Title,
|
||||
Content: announcement.Content,
|
||||
Status: string(announcement.Status),
|
||||
ScheduledAt: announcement.ScheduledAt,
|
||||
CreatedAt: announcement.CreatedAt,
|
||||
UpdatedAt: announcement.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// FromAnnouncementEntityList 从公告实体列表转换为列表项响应
|
||||
func FromAnnouncementEntityList(announcements []*entities.Announcement) []AnnouncementListItemResponse {
|
||||
items := make([]AnnouncementListItemResponse, 0, len(announcements))
|
||||
for _, announcement := range announcements {
|
||||
items = append(items, AnnouncementListItemResponse{
|
||||
ID: announcement.ID,
|
||||
Title: announcement.Title,
|
||||
Content: announcement.Content,
|
||||
Status: string(announcement.Status),
|
||||
ScheduledAt: announcement.ScheduledAt,
|
||||
CreatedAt: announcement.CreatedAt,
|
||||
UpdatedAt: announcement.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
219
internal/application/article/dto/responses/article_responses.go
Normal file
219
internal/application/article/dto/responses/article_responses.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"hyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// ArticleInfoResponse 文章详情响应
|
||||
type ArticleInfoResponse struct {
|
||||
ID string `json:"id" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Content string `json:"content" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Status string `json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ArticleListItemResponse 文章列表项响应(不包含content)
|
||||
type ArticleListItemResponse struct {
|
||||
ID string `json:"id" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Status string `json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ArticleListResponse 文章列表响应
|
||||
type ArticleListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ArticleListItemResponse `json:"items" comment:"文章列表"`
|
||||
}
|
||||
|
||||
// CategoryInfoResponse 分类信息响应
|
||||
type CategoryInfoResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
SortOrder int `json:"sort_order" comment:"排序"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// TagInfoResponse 标签信息响应
|
||||
type TagInfoResponse struct {
|
||||
ID string `json:"id" comment:"标签ID"`
|
||||
Name string `json:"name" comment:"标签名称"`
|
||||
Color string `json:"color" comment:"标签颜色"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// CategoryListResponse 分类列表响应
|
||||
type CategoryListResponse struct {
|
||||
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||
Total int `json:"total" comment:"总数"`
|
||||
}
|
||||
|
||||
// TagListResponse 标签列表响应
|
||||
type TagListResponse struct {
|
||||
Items []TagInfoResponse `json:"items" comment:"标签列表"`
|
||||
Total int `json:"total" comment:"总数"`
|
||||
}
|
||||
|
||||
|
||||
// ArticleStatsResponse 文章统计响应
|
||||
type ArticleStatsResponse struct {
|
||||
TotalArticles int64 `json:"total_articles" comment:"文章总数"`
|
||||
PublishedArticles int64 `json:"published_articles" comment:"已发布文章数"`
|
||||
DraftArticles int64 `json:"draft_articles" comment:"草稿文章数"`
|
||||
ArchivedArticles int64 `json:"archived_articles" comment:"归档文章数"`
|
||||
TotalViews int64 `json:"total_views" comment:"总阅读量"`
|
||||
}
|
||||
|
||||
// FromArticleEntity 从文章实体转换为响应对象
|
||||
func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
|
||||
if article == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &ArticleInfoResponse{
|
||||
ID: article.ID,
|
||||
Title: article.Title,
|
||||
Content: article.Content,
|
||||
Summary: article.Summary,
|
||||
CoverImage: article.CoverImage,
|
||||
CategoryID: article.CategoryID,
|
||||
Status: string(article.Status),
|
||||
IsFeatured: article.IsFeatured,
|
||||
PublishedAt: article.PublishedAt,
|
||||
ScheduledAt: article.ScheduledAt,
|
||||
ViewCount: article.ViewCount,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换分类信息
|
||||
if article.Category != nil {
|
||||
response.Category = &CategoryInfoResponse{
|
||||
ID: article.Category.ID,
|
||||
Name: article.Category.Name,
|
||||
Description: article.Category.Description,
|
||||
SortOrder: article.Category.SortOrder,
|
||||
CreatedAt: article.Category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 转换标签信息
|
||||
if len(article.Tags) > 0 {
|
||||
response.Tags = make([]TagInfoResponse, len(article.Tags))
|
||||
for i, tag := range article.Tags {
|
||||
response.Tags[i] = TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// FromArticleEntityToListItem 从文章实体转换为列表项响应对象(不包含content)
|
||||
func FromArticleEntityToListItem(article *entities.Article) *ArticleListItemResponse {
|
||||
if article == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &ArticleListItemResponse{
|
||||
ID: article.ID,
|
||||
Title: article.Title,
|
||||
Summary: article.Summary,
|
||||
CoverImage: article.CoverImage,
|
||||
CategoryID: article.CategoryID,
|
||||
Status: string(article.Status),
|
||||
IsFeatured: article.IsFeatured,
|
||||
PublishedAt: article.PublishedAt,
|
||||
ScheduledAt: article.ScheduledAt,
|
||||
ViewCount: article.ViewCount,
|
||||
CreatedAt: article.CreatedAt,
|
||||
UpdatedAt: article.UpdatedAt,
|
||||
}
|
||||
|
||||
// 转换分类信息
|
||||
if article.Category != nil {
|
||||
response.Category = &CategoryInfoResponse{
|
||||
ID: article.Category.ID,
|
||||
Name: article.Category.Name,
|
||||
Description: article.Category.Description,
|
||||
SortOrder: article.Category.SortOrder,
|
||||
CreatedAt: article.Category.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 转换标签信息
|
||||
if len(article.Tags) > 0 {
|
||||
response.Tags = make([]TagInfoResponse, len(article.Tags))
|
||||
for i, tag := range article.Tags {
|
||||
response.Tags[i] = TagInfoResponse{
|
||||
ID: tag.ID,
|
||||
Name: tag.Name,
|
||||
Color: tag.Color,
|
||||
CreatedAt: tag.CreatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// FromArticleEntities 从文章实体列表转换为响应对象列表
|
||||
func FromArticleEntities(articles []*entities.Article) []ArticleInfoResponse {
|
||||
if len(articles) == 0 {
|
||||
return []ArticleInfoResponse{}
|
||||
}
|
||||
|
||||
responses := make([]ArticleInfoResponse, len(articles))
|
||||
for i, article := range articles {
|
||||
if response := FromArticleEntity(article); response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
|
||||
// FromArticleEntitiesToListItemList 从文章实体列表转换为列表项响应对象列表(不包含content)
|
||||
func FromArticleEntitiesToListItemList(articles []*entities.Article) []ArticleListItemResponse {
|
||||
if len(articles) == 0 {
|
||||
return []ArticleListItemResponse{}
|
||||
}
|
||||
|
||||
responses := make([]ArticleListItemResponse, len(articles))
|
||||
for i, article := range articles {
|
||||
if response := FromArticleEntityToListItem(article); response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
126
internal/application/article/task_management_service.go
Normal file
126
internal/application/article/task_management_service.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"hyapi-server/internal/domains/article/entities"
|
||||
"hyapi-server/internal/domains/article/repositories"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TaskManagementService 任务管理服务
|
||||
type TaskManagementService struct {
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTaskManagementService 创建任务管理服务
|
||||
func NewTaskManagementService(
|
||||
scheduledTaskRepo repositories.ScheduledTaskRepository,
|
||||
logger *zap.Logger,
|
||||
) *TaskManagementService {
|
||||
return &TaskManagementService{
|
||||
scheduledTaskRepo: scheduledTaskRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (s *TaskManagementService) GetTaskStatus(ctx context.Context, taskID string) (*entities.ScheduledTask, error) {
|
||||
task, err := s.scheduledTaskRepo.GetByTaskID(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取任务状态失败: %w", err)
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// GetArticleTaskStatus 获取文章的定时任务状态
|
||||
func (s *TaskManagementService) GetArticleTaskStatus(ctx context.Context, articleID string) (*entities.ScheduledTask, error) {
|
||||
task, err := s.scheduledTaskRepo.GetByArticleID(ctx, articleID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文章定时任务状态失败: %w", err)
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// CancelTask 取消任务
|
||||
func (s *TaskManagementService) CancelTask(ctx context.Context, taskID string) error {
|
||||
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, taskID); err != nil {
|
||||
return fmt.Errorf("取消任务失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("任务已取消", zap.String("task_id", taskID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTasks 获取活动任务列表
|
||||
func (s *TaskManagementService) GetActiveTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
|
||||
tasks, err := s.scheduledTaskRepo.GetActiveTasks(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取活动任务列表失败: %w", err)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetExpiredTasks 获取过期任务列表
|
||||
func (s *TaskManagementService) GetExpiredTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
|
||||
tasks, err := s.scheduledTaskRepo.GetExpiredTasks(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取过期任务列表失败: %w", err)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// CleanupExpiredTasks 清理过期任务
|
||||
func (s *TaskManagementService) CleanupExpiredTasks(ctx context.Context) error {
|
||||
expiredTasks, err := s.GetExpiredTasks(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range expiredTasks {
|
||||
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, task.TaskID); err != nil {
|
||||
s.logger.Warn("清理过期任务失败", zap.String("task_id", task.TaskID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
s.logger.Info("已清理过期任务", zap.String("task_id", task.TaskID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskStats 获取任务统计信息
|
||||
func (s *TaskManagementService) GetTaskStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
activeTasks, err := s.GetActiveTasks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expiredTasks, err := s.GetExpiredTasks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"active_tasks_count": len(activeTasks),
|
||||
"expired_tasks_count": len(expiredTasks),
|
||||
"total_tasks_count": len(activeTasks) + len(expiredTasks),
|
||||
"next_task_time": nil,
|
||||
"last_cleanup_time": time.Now(),
|
||||
}
|
||||
|
||||
// 计算下一个任务时间
|
||||
if len(activeTasks) > 0 {
|
||||
nextTask := activeTasks[0]
|
||||
for _, task := range activeTasks {
|
||||
if task.ScheduledAt.Before(nextTask.ScheduledAt) {
|
||||
nextTask = task
|
||||
}
|
||||
}
|
||||
stats["next_task_time"] = nextTask.ScheduledAt
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package certification
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapi-server/internal/application/certification/dto/commands"
|
||||
"hyapi-server/internal/application/certification/dto/queries"
|
||||
"hyapi-server/internal/application/certification/dto/responses"
|
||||
)
|
||||
|
||||
// CertificationApplicationService 认证应用服务接口
|
||||
// 负责用例协调,提供精简的应用层接口
|
||||
type CertificationApplicationService interface {
|
||||
// ================ 用户操作用例 ================
|
||||
// 提交企业信息
|
||||
SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error)
|
||||
// 确认状态
|
||||
ConfirmAuth(ctx context.Context, cmd *queries.ConfirmAuthCommand) (*responses.ConfirmAuthResponse, error)
|
||||
// 确认签署
|
||||
ConfirmSign(ctx context.Context, cmd *queries.ConfirmSignCommand) (*responses.ConfirmSignResponse, error)
|
||||
// 申请合同签署
|
||||
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
|
||||
|
||||
// OCR营业执照识别
|
||||
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
|
||||
|
||||
// ================ 查询用例 ================
|
||||
|
||||
// 获取认证详情
|
||||
GetCertification(ctx context.Context, query *queries.GetCertificationQuery) (*responses.CertificationResponse, error)
|
||||
|
||||
// 获取认证列表(管理员)
|
||||
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error)
|
||||
|
||||
// ================ 管理员后台操作用例 ================
|
||||
|
||||
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
|
||||
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
|
||||
|
||||
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
|
||||
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
|
||||
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
|
||||
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
|
||||
// AdminApproveSubmitRecord 管理端审核通过(按提交记录 ID)
|
||||
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
|
||||
// AdminRejectSubmitRecord 管理端审核拒绝(按提交记录 ID)
|
||||
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
|
||||
// AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准:info_submitted=通过 / info_rejected=拒绝)
|
||||
AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error
|
||||
|
||||
// ================ e签宝回调处理 ================
|
||||
|
||||
// 处理e签宝回调
|
||||
HandleEsignCallback(ctx context.Context, cmd *commands.EsignCallbackCommand) error
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"hyapi-server/internal/domains/certification/enums"
|
||||
)
|
||||
|
||||
// CreateCertificationCommand 创建认证申请命令
|
||||
type CreateCertificationCommand struct {
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// ApplyContractCommand 申请合同命令
|
||||
type ApplyContractCommand struct {
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
}
|
||||
|
||||
// RetryOperationCommand 重试操作命令
|
||||
type RetryOperationCommand struct {
|
||||
CertificationID string `json:"certification_id" validate:"required"`
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=enterprise_verification contract_application"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// EsignCallbackCommand e签宝回调命令
|
||||
type EsignCallbackCommand struct {
|
||||
Data *EsignCallbackData `json:"data"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
QueryParams map[string]string `json:"query_params"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// 可以根据需要添加更多企业信息字段
|
||||
}
|
||||
|
||||
// AdminCompleteCertificationCommand 管理员代用户完成认证命令(可不关联合同)
|
||||
type AdminCompleteCertificationCommand struct {
|
||||
// AdminID 从JWT中获取,不从请求体传递,因此不做必填校验
|
||||
AdminID string `json:"-"`
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
CompanyName string `json:"company_name" validate:"required,min=2,max=100"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" validate:"required"`
|
||||
LegalPersonName string `json:"legal_person_name" validate:"required,min=2,max=20"`
|
||||
LegalPersonID string `json:"legal_person_id" validate:"required"`
|
||||
LegalPersonPhone string `json:"legal_person_phone" validate:"required"`
|
||||
EnterpriseAddress string `json:"enterprise_address" validate:"required"`
|
||||
// 备注信息,用于记录后台操作原因
|
||||
Reason string `json:"reason" validate:"required"`
|
||||
}
|
||||
// ForceTransitionStatusCommand 强制状态转换命令(管理员)
|
||||
type ForceTransitionStatusCommand struct {
|
||||
CertificationID string `json:"certification_id" validate:"required"`
|
||||
AdminID string `json:"admin_id" validate:"required"`
|
||||
TargetStatus enums.CertificationStatus `json:"target_status" validate:"required"`
|
||||
Reason string `json:"reason" validate:"required"`
|
||||
Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证
|
||||
}
|
||||
|
||||
// AdminTransitionCertificationStatusCommand 管理端变更认证状态(以状态机为准,用于审核通过/拒绝等)
|
||||
type AdminTransitionCertificationStatusCommand struct {
|
||||
AdminID string `json:"-"`
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
TargetStatus string `json:"target_status" validate:"required,oneof=info_submitted info_rejected"` // 审核通过 -> info_submitted;审核拒绝 -> info_rejected
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// SubmitEnterpriseInfoCommand 提交企业信息命令
|
||||
type SubmitEnterpriseInfoCommand struct {
|
||||
UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"`
|
||||
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"`
|
||||
EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"`
|
||||
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
|
||||
|
||||
// 营业执照图片 URL(单张)
|
||||
BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"`
|
||||
// 办公场地图片 URL 列表(前端传 string 数组)
|
||||
OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"`
|
||||
|
||||
// 授权代表信息(与前端 authorized_rep_* 及表字段一致)
|
||||
AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"`
|
||||
AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"`
|
||||
AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"`
|
||||
AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"`
|
||||
|
||||
// 应用场景
|
||||
APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"`
|
||||
ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// GetContractSignURLCommand 获取合同签署链接命令
|
||||
type GetContractSignURLCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/certification/enums"
|
||||
domainQueries "hyapi-server/internal/domains/certification/repositories/queries"
|
||||
)
|
||||
|
||||
// GetCertificationQuery 获取认证详情查询
|
||||
type GetCertificationQuery struct {
|
||||
UserID string `json:"user_id,omitempty"` // 用于权限验证
|
||||
}
|
||||
|
||||
// ConfirmAuthCommand 确认认证状态命令
|
||||
type ConfirmAuthCommand struct {
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// ConfirmSignCommand 确认签署状态命令
|
||||
type ConfirmSignCommand struct {
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetUserCertificationsQuery 获取用户认证列表查询
|
||||
type GetUserCertificationsQuery struct {
|
||||
UserID string `json:"user_id" validate:"required"`
|
||||
Status enums.CertificationStatus `json:"status,omitempty"`
|
||||
IncludeCompleted bool `json:"include_completed,omitempty"`
|
||||
IncludeFailed bool `json:"include_failed,omitempty"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// ToDomainQuery 转换为领域查询对象
|
||||
func (q *GetUserCertificationsQuery) ToDomainQuery() *domainQueries.UserCertificationsQuery {
|
||||
domainQuery := &domainQueries.UserCertificationsQuery{
|
||||
UserID: q.UserID,
|
||||
Status: q.Status,
|
||||
IncludeCompleted: q.IncludeCompleted,
|
||||
IncludeFailed: q.IncludeFailed,
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
}
|
||||
domainQuery.DefaultValues()
|
||||
return domainQuery
|
||||
}
|
||||
|
||||
// ListCertificationsQuery 认证列表查询(管理员)
|
||||
type ListCertificationsQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
SortBy string `json:"sort_by"`
|
||||
SortOrder string `json:"sort_order"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Status enums.CertificationStatus `json:"status,omitempty"`
|
||||
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
|
||||
FailureReason enums.FailureReason `json:"failure_reason,omitempty"`
|
||||
CreatedAfter *time.Time `json:"created_after,omitempty"`
|
||||
CreatedBefore *time.Time `json:"created_before,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
LegalPersonName string `json:"legal_person_name,omitempty"`
|
||||
SearchKeyword string `json:"search_keyword,omitempty"`
|
||||
}
|
||||
|
||||
// ToDomainQuery 转换为领域查询对象
|
||||
func (q *ListCertificationsQuery) ToDomainQuery() *domainQueries.ListCertificationsQuery {
|
||||
domainQuery := &domainQueries.ListCertificationsQuery{
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
SortBy: q.SortBy,
|
||||
SortOrder: q.SortOrder,
|
||||
UserID: q.UserID,
|
||||
Status: q.Status,
|
||||
Statuses: q.Statuses,
|
||||
FailureReason: q.FailureReason,
|
||||
CreatedAfter: q.CreatedAfter,
|
||||
CreatedBefore: q.CreatedBefore,
|
||||
CompanyName: q.CompanyName,
|
||||
LegalPersonName: q.LegalPersonName,
|
||||
SearchKeyword: q.SearchKeyword,
|
||||
}
|
||||
domainQuery.DefaultValues()
|
||||
return domainQuery
|
||||
}
|
||||
|
||||
// SearchCertificationsQuery 搜索认证查询
|
||||
type SearchCertificationsQuery struct {
|
||||
Keyword string `json:"keyword" validate:"required,min=2"`
|
||||
SearchFields []string `json:"search_fields,omitempty"`
|
||||
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
SortBy string `json:"sort_by"`
|
||||
SortOrder string `json:"sort_order"`
|
||||
ExactMatch bool `json:"exact_match,omitempty"`
|
||||
}
|
||||
|
||||
// ToDomainQuery 转换为领域查询对象
|
||||
func (q *SearchCertificationsQuery) ToDomainQuery() *domainQueries.SearchCertificationsQuery {
|
||||
domainQuery := &domainQueries.SearchCertificationsQuery{
|
||||
Keyword: q.Keyword,
|
||||
SearchFields: q.SearchFields,
|
||||
Statuses: q.Statuses,
|
||||
UserID: q.UserID,
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
SortBy: q.SortBy,
|
||||
SortOrder: q.SortOrder,
|
||||
ExactMatch: q.ExactMatch,
|
||||
}
|
||||
domainQuery.DefaultValues()
|
||||
return domainQuery
|
||||
}
|
||||
|
||||
// GetCertificationStatisticsQuery 认证统计查询
|
||||
type GetCertificationStatisticsQuery struct {
|
||||
StartDate time.Time `json:"start_date" validate:"required"`
|
||||
EndDate time.Time `json:"end_date" validate:"required"`
|
||||
Period string `json:"period" validate:"oneof=daily weekly monthly yearly"`
|
||||
GroupBy []string `json:"group_by,omitempty"`
|
||||
UserIDs []string `json:"user_ids,omitempty"`
|
||||
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
|
||||
IncludeProgressStats bool `json:"include_progress_stats,omitempty"`
|
||||
IncludeRetryStats bool `json:"include_retry_stats,omitempty"`
|
||||
IncludeTimeStats bool `json:"include_time_stats,omitempty"`
|
||||
}
|
||||
|
||||
// ToDomainQuery 转换为领域查询对象
|
||||
func (q *GetCertificationStatisticsQuery) ToDomainQuery() *domainQueries.CertificationStatisticsQuery {
|
||||
return &domainQueries.CertificationStatisticsQuery{
|
||||
StartDate: q.StartDate,
|
||||
EndDate: q.EndDate,
|
||||
Period: q.Period,
|
||||
GroupBy: q.GroupBy,
|
||||
UserIDs: q.UserIDs,
|
||||
Statuses: q.Statuses,
|
||||
IncludeProgressStats: q.IncludeProgressStats,
|
||||
IncludeRetryStats: q.IncludeRetryStats,
|
||||
IncludeTimeStats: q.IncludeTimeStats,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystemMonitoringQuery 系统监控查询
|
||||
type GetSystemMonitoringQuery struct {
|
||||
TimeRange string `json:"time_range" validate:"oneof=1h 6h 24h 7d 30d"`
|
||||
Metrics []string `json:"metrics,omitempty"` // 指定要获取的指标类型
|
||||
}
|
||||
|
||||
// GetAvailableMetrics 获取可用的监控指标
|
||||
func (q *GetSystemMonitoringQuery) GetAvailableMetrics() []string {
|
||||
return []string{
|
||||
"certification_count",
|
||||
"success_rate",
|
||||
"failure_rate",
|
||||
"avg_processing_time",
|
||||
"status_distribution",
|
||||
"retry_count",
|
||||
"esign_callback_success_rate",
|
||||
}
|
||||
}
|
||||
|
||||
// GetTimeRangeDuration 获取时间范围对应的持续时间
|
||||
func (q *GetSystemMonitoringQuery) GetTimeRangeDuration() time.Duration {
|
||||
switch q.TimeRange {
|
||||
case "1h":
|
||||
return time.Hour
|
||||
case "6h":
|
||||
return 6 * time.Hour
|
||||
case "24h":
|
||||
return 24 * time.Hour
|
||||
case "7d":
|
||||
return 7 * 24 * time.Hour
|
||||
case "30d":
|
||||
return 30 * 24 * time.Hour
|
||||
default:
|
||||
return 24 * time.Hour // 默认24小时
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldIncludeMetric 检查是否应该包含指定指标
|
||||
func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
|
||||
if len(q.Metrics) == 0 {
|
||||
return true // 如果没有指定,包含所有指标
|
||||
}
|
||||
|
||||
for _, m := range q.Metrics {
|
||||
if m == metric {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询(以状态机 certification_status 为准,不做审核状态筛选)
|
||||
type AdminListSubmitRecordsQuery struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
CertificationStatus string `json:"certification_status" form:"certification_status"` // 按认证状态筛选,如 info_pending_review / info_submitted / info_rejected,空为全部
|
||||
CompanyName string `json:"company_name" form:"company_name"` // 企业名称(模糊搜索)
|
||||
LegalPersonPhone string `json:"legal_person_phone" form:"legal_person_phone"` // 法人手机号
|
||||
LegalPersonName string `json:"legal_person_name" form:"legal_person_name"` // 法人姓名(模糊搜索)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/certification/entities/value_objects"
|
||||
"hyapi-server/internal/domains/certification/enums"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
|
||||
// 企业信息
|
||||
EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info,omitempty"`
|
||||
|
||||
// 合同信息
|
||||
ContractInfo *value_objects.ContractInfo `json:"contract_info,omitempty"`
|
||||
|
||||
// 时间戳
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
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"`
|
||||
|
||||
// 业务状态
|
||||
IsCompleted bool `json:"is_completed"`
|
||||
IsFailed bool `json:"is_failed"`
|
||||
IsUserActionRequired bool `json:"is_user_action_required"`
|
||||
|
||||
// 失败信息
|
||||
FailureReason enums.FailureReason `json:"failure_reason,omitempty"`
|
||||
FailureReasonName string `json:"failure_reason_name,omitempty"`
|
||||
FailureMessage string `json:"failure_message,omitempty"`
|
||||
CanRetry bool `json:"can_retry,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
|
||||
// 用户操作提示
|
||||
NextAction string `json:"next_action,omitempty"`
|
||||
AvailableActions []string `json:"available_actions,omitempty"`
|
||||
|
||||
// 元数据
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ConfirmAuthResponse 确认认证状态响应
|
||||
type ConfirmAuthResponse struct {
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// ConfirmSignResponse 确认签署状态响应
|
||||
type ConfirmSignResponse struct {
|
||||
Status enums.CertificationStatus `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// CertificationListResponse 认证列表响应
|
||||
type CertificationListResponse struct {
|
||||
Items []*CertificationResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// ContractSignUrlResponse 合同签署URL响应
|
||||
type ContractSignUrlResponse struct {
|
||||
CertificationID string `json:"certification_id"`
|
||||
ContractSignURL string `json:"contract_sign_url"`
|
||||
ContractURL string `json:"contract_url,omitempty"`
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty"`
|
||||
NextAction string `json:"next_action"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SystemMonitoringResponse 系统监控响应
|
||||
type SystemMonitoringResponse struct {
|
||||
TimeRange string `json:"time_range"`
|
||||
Metrics map[string]interface{} `json:"metrics"`
|
||||
Alerts []SystemAlert `json:"alerts,omitempty"`
|
||||
SystemHealth SystemHealthStatus `json:"system_health"`
|
||||
LastUpdatedAt time.Time `json:"last_updated_at"`
|
||||
}
|
||||
|
||||
// SystemAlert 系统警告
|
||||
type SystemAlert struct {
|
||||
Level string `json:"level"` // info, warning, error, critical
|
||||
Type string `json:"type"` // 警告类型
|
||||
Message string `json:"message"` // 警告消息
|
||||
Metric string `json:"metric"` // 相关指标
|
||||
Value interface{} `json:"value"` // 当前值
|
||||
Threshold interface{} `json:"threshold"` // 阈值
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// SystemHealthStatus 系统健康状态
|
||||
type SystemHealthStatus struct {
|
||||
Overall string `json:"overall"` // healthy, warning, critical
|
||||
Components map[string]string `json:"components"` // 各组件状态
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// AdminSubmitRecordItem 管理端提交记录列表项
|
||||
type AdminSubmitRecordItem struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
SubmitAt time.Time `json:"submit_at"`
|
||||
Status string `json:"status"`
|
||||
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准:info_pending_review/info_submitted/info_rejected 等
|
||||
}
|
||||
|
||||
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL)
|
||||
type AdminSubmitRecordDetail struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
LegalPersonID string `json:"legal_person_id"`
|
||||
LegalPersonPhone string `json:"legal_person_phone"`
|
||||
EnterpriseAddress string `json:"enterprise_address"`
|
||||
AuthorizedRepName string `json:"authorized_rep_name"`
|
||||
AuthorizedRepID string `json:"authorized_rep_id"`
|
||||
AuthorizedRepPhone string `json:"authorized_rep_phone"`
|
||||
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组
|
||||
BusinessLicenseImageURL string `json:"business_license_image_url"`
|
||||
OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串
|
||||
APIUsage string `json:"api_usage"`
|
||||
ScenarioAttachmentURLs string `json:"scenario_attachment_urls"`
|
||||
Status string `json:"status"`
|
||||
SubmitAt time.Time `json:"submit_at"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
FailedAt *time.Time `json:"failed_at,omitempty"`
|
||||
FailureReason string `json:"failure_reason,omitempty"`
|
||||
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AdminSubmitRecordsListResponse 管理端提交记录列表响应
|
||||
type AdminSubmitRecordsListResponse struct {
|
||||
Items []*AdminSubmitRecordItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// ================ 响应构建辅助方法 ================
|
||||
|
||||
// NewCertificationListResponse 创建认证列表响应
|
||||
func NewCertificationListResponse(items []*CertificationResponse, total int64, page, pageSize int) *CertificationListResponse {
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
return &CertificationListResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
// NewContractSignUrlResponse 创建合同签署URL响应
|
||||
func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextAction, message string) *ContractSignUrlResponse {
|
||||
response := &ContractSignUrlResponse{
|
||||
CertificationID: certificationID,
|
||||
ContractSignURL: signURL,
|
||||
ContractURL: contractURL,
|
||||
NextAction: nextAction,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
// 设置过期时间(默认24小时)
|
||||
expireAt := time.Now().Add(24 * time.Hour)
|
||||
response.ExpireAt = &expireAt
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// NewSystemAlert 创建系统警告
|
||||
func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert {
|
||||
return &SystemAlert{
|
||||
Level: level,
|
||||
Type: alertType,
|
||||
Message: message,
|
||||
Metric: metric,
|
||||
Value: value,
|
||||
Threshold: threshold,
|
||||
CreatedAt: time.Now(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// IsHealthy 检查系统是否健康
|
||||
func (r *SystemMonitoringResponse) IsHealthy() bool {
|
||||
return r.SystemHealth.Overall == "healthy"
|
||||
}
|
||||
|
||||
// GetCriticalAlerts 获取严重警告
|
||||
func (r *SystemMonitoringResponse) GetCriticalAlerts() []*SystemAlert {
|
||||
var criticalAlerts []*SystemAlert
|
||||
for i := range r.Alerts {
|
||||
if r.Alerts[i].Level == "critical" {
|
||||
criticalAlerts = append(criticalAlerts, &r.Alerts[i])
|
||||
}
|
||||
}
|
||||
return criticalAlerts
|
||||
}
|
||||
|
||||
// HasAlerts 检查是否有警告
|
||||
func (r *SystemMonitoringResponse) HasAlerts() bool {
|
||||
return len(r.Alerts) > 0
|
||||
}
|
||||
|
||||
// GetMetricValue 获取指标值
|
||||
func (r *SystemMonitoringResponse) GetMetricValue(metric string) (interface{}, bool) {
|
||||
value, exists := r.Metrics[metric]
|
||||
return value, exists
|
||||
}
|
||||
@@ -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,55 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// BusinessLicenseResult 营业执照识别结果
|
||||
type BusinessLicenseResult struct {
|
||||
CompanyName string `json:"company_name"` // 企业名称
|
||||
UnifiedSocialCode string `json:"unified_social_code"` // 统一社会信用代码
|
||||
LegalPersonName string `json:"legal_person_name"` // 法定代表人姓名
|
||||
LegalPersonID string `json:"legal_person_id"` // 法定代表人身份证号
|
||||
RegisteredCapital string `json:"registered_capital"` // 注册资本
|
||||
BusinessScope string `json:"business_scope"` // 经营范围
|
||||
Address string `json:"address"` // 企业地址
|
||||
IssueDate string `json:"issue_date"` // 发证日期
|
||||
ValidPeriod string `json:"valid_period"` // 有效期
|
||||
Confidence float64 `json:"confidence"` // 识别置信度
|
||||
ProcessedAt time.Time `json:"processed_at"` // 处理时间
|
||||
}
|
||||
|
||||
// IDCardResult 身份证识别结果
|
||||
type IDCardResult struct {
|
||||
Name string `json:"name"` // 姓名
|
||||
IDCardNumber string `json:"id_card_number"` // 身份证号
|
||||
Gender string `json:"gender"` // 性别
|
||||
Nation string `json:"nation"` // 民族
|
||||
Birthday string `json:"birthday"` // 出生日期
|
||||
Address string `json:"address"` // 住址
|
||||
IssuingAgency string `json:"issuing_agency"` // 签发机关
|
||||
ValidPeriod string `json:"valid_period"` // 有效期限
|
||||
Side string `json:"side"` // 身份证面(front/back)
|
||||
Confidence float64 `json:"confidence"` // 识别置信度
|
||||
ProcessedAt time.Time `json:"processed_at"` // 处理时间
|
||||
}
|
||||
|
||||
// GeneralTextResult 通用文字识别结果
|
||||
type GeneralTextResult struct {
|
||||
Words []TextLine `json:"words"` // 识别的文字行
|
||||
Confidence float64 `json:"confidence"` // 整体置信度
|
||||
ProcessedAt time.Time `json:"processed_at"` // 处理时间
|
||||
}
|
||||
|
||||
// TextLine 文字行
|
||||
type TextLine struct {
|
||||
Text string `json:"text"` // 文字内容
|
||||
Confidence float64 `json:"confidence"` // 置信度
|
||||
Position Position `json:"position"` // 位置信息
|
||||
}
|
||||
|
||||
// Position 位置信息
|
||||
type Position struct {
|
||||
X int `json:"x"` // X坐标
|
||||
Y int `json:"y"` // Y坐标
|
||||
Width int `json:"width"` // 宽度
|
||||
Height int `json:"height"` // 高度
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package commands
|
||||
|
||||
// CreateWalletCommand 创建钱包命令
|
||||
type CreateWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
}
|
||||
|
||||
// TransferRechargeCommand 对公转账充值命令
|
||||
type TransferRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
TransferOrderID string `json:"transfer_order_id" binding:"required" comment:"转账订单号"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
}
|
||||
|
||||
// GiftRechargeCommand 赠送充值命令
|
||||
type GiftRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
}
|
||||
|
||||
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
||||
type CreateAlipayRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
||||
}
|
||||
|
||||
// CreateWechatRechargeCommand 创建微信充值订单命令
|
||||
type CreateWechatRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码,兼容传入native/wx_h5/h5
|
||||
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid(用于小程序/H5)
|
||||
}
|
||||
121
internal/application/finance/dto/invoice_responses.go
Normal file
121
internal/application/finance/dto/invoice_responses.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// InvoiceApplicationResponse 发票申请响应
|
||||
type InvoiceApplicationResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Status entities.ApplicationStatus `json:"status"`
|
||||
InvoiceInfo *value_objects.InvoiceInfo `json:"invoice_info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// InvoiceInfoResponse 发票信息响应
|
||||
type InvoiceInfoResponse struct {
|
||||
CompanyName string `json:"company_name"` // 从企业认证信息获取,只读
|
||||
TaxpayerID string `json:"taxpayer_id"` // 从企业认证信息获取,只读
|
||||
BankName string `json:"bank_name"` // 用户可编辑
|
||||
BankAccount string `json:"bank_account"` // 用户可编辑
|
||||
CompanyAddress string `json:"company_address"` // 用户可编辑
|
||||
CompanyPhone string `json:"company_phone"` // 用户可编辑
|
||||
ReceivingEmail string `json:"receiving_email"` // 用户可编辑
|
||||
IsComplete bool `json:"is_complete"`
|
||||
MissingFields []string `json:"missing_fields,omitempty"`
|
||||
// 字段权限标识
|
||||
CompanyNameReadOnly bool `json:"company_name_read_only"` // 公司名称是否只读
|
||||
TaxpayerIDReadOnly bool `json:"taxpayer_id_read_only"` // 纳税人识别号是否只读
|
||||
}
|
||||
|
||||
// InvoiceRecordResponse 发票记录响应
|
||||
type InvoiceRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Status entities.ApplicationStatus `json:"status"`
|
||||
// 开票信息(快照数据)
|
||||
CompanyName string `json:"company_name"` // 公司名称
|
||||
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号
|
||||
BankName string `json:"bank_name"` // 开户银行
|
||||
BankAccount string `json:"bank_account"` // 银行账号
|
||||
CompanyAddress string `json:"company_address"` // 企业地址
|
||||
CompanyPhone string `json:"company_phone"` // 企业电话
|
||||
ReceivingEmail string `json:"receiving_email"` // 接收邮箱
|
||||
// 文件信息
|
||||
FileName *string `json:"file_name,omitempty"`
|
||||
FileSize *int64 `json:"file_size,omitempty"`
|
||||
FileURL *string `json:"file_url,omitempty"`
|
||||
// 时间信息
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// 拒绝原因
|
||||
RejectReason *string `json:"reject_reason,omitempty"`
|
||||
}
|
||||
|
||||
// InvoiceRecordsResponse 发票记录列表响应
|
||||
type InvoiceRecordsResponse struct {
|
||||
Records []*InvoiceRecordResponse `json:"records"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// FileDownloadResponse 文件下载响应
|
||||
type FileDownloadResponse struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileContent []byte `json:"file_content"`
|
||||
}
|
||||
|
||||
// AvailableAmountResponse 可开票金额响应
|
||||
type AvailableAmountResponse struct {
|
||||
AvailableAmount decimal.Decimal `json:"available_amount"` // 可开票金额
|
||||
TotalRecharged decimal.Decimal `json:"total_recharged"` // 总充值金额
|
||||
TotalGifted decimal.Decimal `json:"total_gifted"` // 总赠送金额
|
||||
TotalInvoiced decimal.Decimal `json:"total_invoiced"` // 已开票金额
|
||||
PendingApplications decimal.Decimal `json:"pending_applications"` // 待处理申请金额
|
||||
}
|
||||
|
||||
// PendingApplicationResponse 待处理申请响应
|
||||
type PendingApplicationResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Status entities.ApplicationStatus `json:"status"`
|
||||
CompanyName string `json:"company_name"`
|
||||
TaxpayerID string `json:"taxpayer_id"`
|
||||
BankName string `json:"bank_name"`
|
||||
BankAccount string `json:"bank_account"`
|
||||
CompanyAddress string `json:"company_address"`
|
||||
CompanyPhone string `json:"company_phone"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
FileName *string `json:"file_name,omitempty"`
|
||||
FileSize *int64 `json:"file_size,omitempty"`
|
||||
FileURL *string `json:"file_url,omitempty"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
RejectReason *string `json:"reject_reason,omitempty"`
|
||||
}
|
||||
|
||||
// PendingApplicationsResponse 待处理申请列表响应
|
||||
type PendingApplicationsResponse struct {
|
||||
Applications []*PendingApplicationResponse `json:"applications"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
21
internal/application/finance/dto/queries/finance_queries.go
Normal file
21
internal/application/finance/dto/queries/finance_queries.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package queries
|
||||
|
||||
// GetWalletInfoQuery 获取钱包信息查询
|
||||
type GetWalletInfoQuery struct {
|
||||
UserID string `form:"user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// GetWalletQuery 获取钱包查询
|
||||
type GetWalletQuery struct {
|
||||
UserID string `form:"user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// GetWalletStatsQuery 获取钱包统计查询
|
||||
type GetWalletStatsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required"`
|
||||
}
|
||||
|
||||
// GetUserSecretsQuery 获取用户密钥查询
|
||||
type GetUserSecretsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// AlipayOrderStatusResponse 支付宝订单状态响应
|
||||
type AlipayOrderStatusResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
TradeNo *string `json:"trade_no"` // 支付宝交易号
|
||||
Status string `json:"status"` // 订单状态
|
||||
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||
ErrorCode *string `json:"error_code"` // 错误码
|
||||
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||
}
|
||||
171
internal/application/finance/dto/responses/finance_responses.go
Normal file
171
internal/application/finance/dto/responses/finance_responses.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// WalletResponse 钱包响应
|
||||
type WalletResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TransactionResponse 交易响应
|
||||
type TransactionResponse struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
}
|
||||
|
||||
// UserSecretsResponse 用户密钥响应
|
||||
type UserSecretsResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
AccessID string `json:"access_id"`
|
||||
AccessKey string `json:"access_key"`
|
||||
IsActive bool `json:"is_active"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletStatsResponse 钱包统计响应
|
||||
type WalletStatsResponse struct {
|
||||
TotalWallets int64 `json:"total_wallets"`
|
||||
ActiveWallets int64 `json:"active_wallets"`
|
||||
TotalBalance decimal.Decimal `json:"total_balance"`
|
||||
TodayTransactions int64 `json:"today_transactions"`
|
||||
TodayVolume decimal.Decimal `json:"today_volume"`
|
||||
}
|
||||
|
||||
// RechargeRecordResponse 充值记录响应
|
||||
type RechargeRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||
WechatOrderID string `json:"wechat_order_id,omitempty"`
|
||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||
Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等
|
||||
Notes string `json:"notes,omitempty"`
|
||||
OperatorID string `json:"operator_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionResponse 钱包交易记录响应
|
||||
type WalletTransactionResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionListResponse 钱包交易记录列表响应
|
||||
type WalletTransactionListResponse struct {
|
||||
Items []WalletTransactionResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// RechargeRecordListResponse 充值记录列表响应
|
||||
type RechargeRecordListResponse struct {
|
||||
Items []RechargeRecordResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
||||
type AlipayRechargeOrderResponse struct {
|
||||
PayURL string `json:"pay_url"` // 支付链接
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
}
|
||||
|
||||
// RechargeConfigResponse 充值配置响应
|
||||
type RechargeConfigResponse struct {
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
RechargeBonusEnabled bool `json:"recharge_bonus_enabled"` // 是否启用充值赠送
|
||||
ApiStoreRechargeTip string `json:"api_store_recharge_tip"` // API 商店充值提示(大额/批量联系商务)
|
||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
// AlipayRechargeBonusRuleResponse 支付宝充值赠送规则响应
|
||||
type AlipayRechargeBonusRuleResponse struct {
|
||||
RechargeAmount float64 `json:"recharge_amount"`
|
||||
BonusAmount float64 `json:"bonus_amount"`
|
||||
}
|
||||
|
||||
// UserSimpleResponse 用户简单信息响应
|
||||
type UserSimpleResponse struct {
|
||||
ID string `json:"id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// PurchaseRecordResponse 购买记录响应
|
||||
type PurchaseRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
TradeNo *string `json:"trade_no,omitempty"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
PayAmount *decimal.Decimal `json:"pay_amount,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Platform string `json:"platform"`
|
||||
PayChannel string `json:"pay_channel"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
BuyerID string `json:"buyer_id,omitempty"`
|
||||
SellerID string `json:"seller_id,omitempty"`
|
||||
ReceiptAmount decimal.Decimal `json:"receipt_amount,omitempty"`
|
||||
NotifyTime *time.Time `json:"notify_time,omitempty"`
|
||||
ReturnTime *time.Time `json:"return_time,omitempty"`
|
||||
PayTime *time.Time `json:"pay_time,omitempty"`
|
||||
FilePath *string `json:"file_path,omitempty"`
|
||||
FileSize *int64 `json:"file_size,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PurchaseRecordListResponse 购买记录列表响应
|
||||
type PurchaseRecordListResponse struct {
|
||||
Items []PurchaseRecordResponse `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// WechatOrderStatusResponse 微信订单状态响应
|
||||
type WechatOrderStatusResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
TransactionID *string `json:"transaction_id"` // 微信支付交易号
|
||||
Status string `json:"status"` // 订单状态
|
||||
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||
ErrorCode *string `json:"error_code"` // 错误码
|
||||
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package responses
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// WechatRechargeOrderResponse 微信充值下单响应
|
||||
type WechatRechargeOrderResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数)
|
||||
}
|
||||
52
internal/application/finance/finance_application_service.go
Normal file
52
internal/application/finance/finance_application_service.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package finance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"hyapi-server/internal/application/finance/dto/commands"
|
||||
"hyapi-server/internal/application/finance/dto/queries"
|
||||
"hyapi-server/internal/application/finance/dto/responses"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// FinanceApplicationService 财务应用服务接口
|
||||
type FinanceApplicationService interface {
|
||||
// 钱包管理
|
||||
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
|
||||
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
|
||||
|
||||
// 充值管理
|
||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
||||
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
|
||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
|
||||
// 交易记录
|
||||
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
// 导出功能
|
||||
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
|
||||
// 支付宝回调处理
|
||||
HandleAlipayCallback(ctx context.Context, r *http.Request) error
|
||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||
|
||||
// 微信支付回调处理
|
||||
HandleWechatPayCallback(ctx context.Context, r *http.Request) error
|
||||
HandleWechatRefundCallback(ctx context.Context, r *http.Request) error
|
||||
GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error)
|
||||
|
||||
// 充值记录
|
||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 购买记录
|
||||
GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
|
||||
GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
|
||||
|
||||
// 获取充值配置
|
||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||
}
|
||||
2119
internal/application/finance/finance_application_service_impl.go
Normal file
2119
internal/application/finance/finance_application_service_impl.go
Normal file
File diff suppressed because it is too large
Load Diff
786
internal/application/finance/invoice_application_service.go
Normal file
786
internal/application/finance/invoice_application_service.go
Normal file
@@ -0,0 +1,786 @@
|
||||
package finance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/application/finance/dto"
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
finance_repo "hyapi-server/internal/domains/finance/repositories"
|
||||
"hyapi-server/internal/domains/finance/services"
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
user_repo "hyapi-server/internal/domains/user/repositories"
|
||||
user_service "hyapi-server/internal/domains/user/services"
|
||||
"hyapi-server/internal/infrastructure/external/notification"
|
||||
"hyapi-server/internal/infrastructure/external/storage"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ==================== 用户端发票应用服务 ====================
|
||||
|
||||
// InvoiceApplicationService 发票应用服务接口
|
||||
// 职责:跨域协调、数据聚合、事务管理、外部服务调用
|
||||
type InvoiceApplicationService interface {
|
||||
// ApplyInvoice 申请开票
|
||||
ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error)
|
||||
|
||||
// GetUserInvoiceInfo 获取用户发票信息
|
||||
GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error)
|
||||
|
||||
// UpdateUserInvoiceInfo 更新用户发票信息
|
||||
UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error
|
||||
|
||||
// GetUserInvoiceRecords 获取用户开票记录
|
||||
GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error)
|
||||
|
||||
// DownloadInvoiceFile 下载发票文件
|
||||
DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error)
|
||||
|
||||
// GetAvailableAmount 获取可开票金额
|
||||
GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error)
|
||||
}
|
||||
|
||||
// InvoiceApplicationServiceImpl 发票应用服务实现
|
||||
type InvoiceApplicationServiceImpl struct {
|
||||
// 仓储层依赖
|
||||
invoiceRepo finance_repo.InvoiceApplicationRepository
|
||||
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
|
||||
userRepo user_repo.UserRepository
|
||||
rechargeRecordRepo finance_repo.RechargeRecordRepository
|
||||
walletRepo finance_repo.WalletRepository
|
||||
|
||||
// 领域服务依赖
|
||||
invoiceDomainService services.InvoiceDomainService
|
||||
invoiceAggregateService services.InvoiceAggregateService
|
||||
userInvoiceInfoService services.UserInvoiceInfoService
|
||||
userAggregateService user_service.UserAggregateService
|
||||
|
||||
// 外部服务依赖
|
||||
storageService *storage.QiNiuStorageService
|
||||
logger *zap.Logger
|
||||
wechatWorkServer *notification.WeChatWorkService
|
||||
}
|
||||
|
||||
// NewInvoiceApplicationService 创建发票应用服务
|
||||
func NewInvoiceApplicationService(
|
||||
invoiceRepo finance_repo.InvoiceApplicationRepository,
|
||||
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
|
||||
userRepo user_repo.UserRepository,
|
||||
userAggregateService user_service.UserAggregateService,
|
||||
rechargeRecordRepo finance_repo.RechargeRecordRepository,
|
||||
walletRepo finance_repo.WalletRepository,
|
||||
invoiceDomainService services.InvoiceDomainService,
|
||||
invoiceAggregateService services.InvoiceAggregateService,
|
||||
userInvoiceInfoService services.UserInvoiceInfoService,
|
||||
storageService *storage.QiNiuStorageService,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) InvoiceApplicationService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if cfg != nil && cfg.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger)
|
||||
}
|
||||
|
||||
return &InvoiceApplicationServiceImpl{
|
||||
invoiceRepo: invoiceRepo,
|
||||
userInvoiceInfoRepo: userInvoiceInfoRepo,
|
||||
userRepo: userRepo,
|
||||
userAggregateService: userAggregateService,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
walletRepo: walletRepo,
|
||||
invoiceDomainService: invoiceDomainService,
|
||||
invoiceAggregateService: invoiceAggregateService,
|
||||
userInvoiceInfoService: userInvoiceInfoService,
|
||||
storageService: storageService,
|
||||
logger: logger,
|
||||
wechatWorkServer: wechatSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyInvoice 申请开票
|
||||
func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error) {
|
||||
// 1. 验证用户是否存在
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.ID == "" {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 2. 验证发票类型
|
||||
invoiceType := value_objects.InvoiceType(req.InvoiceType)
|
||||
if !invoiceType.IsValid() {
|
||||
return nil, fmt.Errorf("无效的发票类型")
|
||||
}
|
||||
|
||||
// 3. 获取用户企业认证信息
|
||||
userWithEnterprise, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 检查用户是否有企业认证信息
|
||||
if userWithEnterprise.EnterpriseInfo == nil {
|
||||
return nil, fmt.Errorf("用户未完成企业认证,无法申请开票")
|
||||
}
|
||||
|
||||
// 5. 获取用户开票信息
|
||||
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(
|
||||
ctx,
|
||||
userID,
|
||||
userWithEnterprise.EnterpriseInfo.CompanyName,
|
||||
userWithEnterprise.EnterpriseInfo.UnifiedSocialCode,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 验证开票信息完整性
|
||||
invoiceInfo := value_objects.NewInvoiceInfo(
|
||||
userInvoiceInfo.CompanyName,
|
||||
userInvoiceInfo.TaxpayerID,
|
||||
userInvoiceInfo.BankName,
|
||||
userInvoiceInfo.BankAccount,
|
||||
userInvoiceInfo.CompanyAddress,
|
||||
userInvoiceInfo.CompanyPhone,
|
||||
userInvoiceInfo.ReceivingEmail,
|
||||
)
|
||||
|
||||
if err := s.userInvoiceInfoService.ValidateInvoiceInfo(ctx, invoiceInfo, invoiceType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. 计算可开票金额
|
||||
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("计算可开票金额失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 验证开票金额
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的金额格式: %w", err)
|
||||
}
|
||||
|
||||
if err := s.invoiceDomainService.ValidateInvoiceAmount(ctx, amount, availableAmount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 9. 调用聚合服务申请开票
|
||||
aggregateReq := services.ApplyInvoiceRequest{
|
||||
InvoiceType: invoiceType,
|
||||
Amount: req.Amount,
|
||||
InvoiceInfo: invoiceInfo,
|
||||
}
|
||||
|
||||
application, err := s.invoiceAggregateService.ApplyInvoice(ctx, userID, aggregateReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 10. 构建响应DTO
|
||||
resp := &dto.InvoiceApplicationResponse{
|
||||
ID: application.ID,
|
||||
UserID: application.UserID,
|
||||
InvoiceType: application.InvoiceType,
|
||||
Amount: application.Amount,
|
||||
Status: application.Status,
|
||||
InvoiceInfo: invoiceInfo,
|
||||
CreatedAt: application.CreatedAt,
|
||||
}
|
||||
|
||||
// 11. 企业微信通知(忽略发送错误),只使用企业名称和联系电话
|
||||
if s.wechatWorkServer != nil {
|
||||
companyName := userWithEnterprise.EnterpriseInfo.CompanyName
|
||||
phone := user.Phone
|
||||
if userWithEnterprise.EnterpriseInfo.LegalPersonPhone != "" {
|
||||
phone = userWithEnterprise.EnterpriseInfo.LegalPersonPhone
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(
|
||||
"### 【海宇数据】用户申请开发票\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 申请开票金额:%s 元\n"+
|
||||
"> 发票类型:%s\n"+
|
||||
"> 申请时间:%s\n",
|
||||
companyName,
|
||||
phone,
|
||||
application.Amount.String(),
|
||||
string(application.InvoiceType),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkServer.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfo 获取用户发票信息
|
||||
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
|
||||
// 1. 获取用户企业认证信息
|
||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取企业认证信息
|
||||
var companyName, taxpayerID string
|
||||
var companyNameReadOnly, taxpayerIDReadOnly bool
|
||||
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
|
||||
companyNameReadOnly = true
|
||||
taxpayerIDReadOnly = true
|
||||
}
|
||||
|
||||
// 3. 获取用户开票信息(包含企业认证信息)
|
||||
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 构建响应DTO
|
||||
return &dto.InvoiceInfoResponse{
|
||||
CompanyName: userInvoiceInfo.CompanyName,
|
||||
TaxpayerID: userInvoiceInfo.TaxpayerID,
|
||||
BankName: userInvoiceInfo.BankName,
|
||||
BankAccount: userInvoiceInfo.BankAccount,
|
||||
CompanyAddress: userInvoiceInfo.CompanyAddress,
|
||||
CompanyPhone: userInvoiceInfo.CompanyPhone,
|
||||
ReceivingEmail: userInvoiceInfo.ReceivingEmail,
|
||||
IsComplete: userInvoiceInfo.IsComplete(),
|
||||
MissingFields: userInvoiceInfo.GetMissingFields(),
|
||||
// 字段权限标识
|
||||
CompanyNameReadOnly: companyNameReadOnly, // 公司名称只读(从企业认证信息获取)
|
||||
TaxpayerIDReadOnly: taxpayerIDReadOnly, // 纳税人识别号只读(从企业认证信息获取)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateUserInvoiceInfo 更新用户发票信息
|
||||
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
|
||||
// 1. 获取用户企业认证信息
|
||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查用户是否有企业认证信息
|
||||
if user.EnterpriseInfo == nil {
|
||||
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
|
||||
}
|
||||
|
||||
// 3. 创建开票信息对象,公司名称和纳税人识别号从企业认证信息中获取
|
||||
invoiceInfo := value_objects.NewInvoiceInfo(
|
||||
"", // 公司名称将由服务层从企业认证信息中获取
|
||||
"", // 纳税人识别号将由服务层从企业认证信息中获取
|
||||
req.BankName,
|
||||
req.BankAccount,
|
||||
req.CompanyAddress,
|
||||
req.CompanyPhone,
|
||||
req.ReceivingEmail,
|
||||
)
|
||||
|
||||
// 4. 使用包含企业认证信息的方法
|
||||
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
|
||||
ctx,
|
||||
userID,
|
||||
invoiceInfo,
|
||||
user.EnterpriseInfo.CompanyName,
|
||||
user.EnterpriseInfo.UnifiedSocialCode,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserInvoiceRecords 获取用户开票记录
|
||||
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error) {
|
||||
// 1. 验证用户是否存在
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.ID == "" {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 2. 获取发票申请记录
|
||||
var status entities.ApplicationStatus
|
||||
if req.Status != "" {
|
||||
status = entities.ApplicationStatus(req.Status)
|
||||
}
|
||||
|
||||
// 3. 解析时间范围
|
||||
var startTime, endTime *time.Time
|
||||
if req.StartTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
|
||||
endTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取发票申请记录(需要更新仓储层方法以支持时间筛选)
|
||||
applications, total, err := s.invoiceRepo.FindByUserIDAndStatusWithTimeRange(ctx, userID, status, startTime, endTime, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 构建响应DTO
|
||||
records := make([]*dto.InvoiceRecordResponse, len(applications))
|
||||
for i, app := range applications {
|
||||
// 使用快照信息(申请时的开票信息)
|
||||
records[i] = &dto.InvoiceRecordResponse{
|
||||
ID: app.ID,
|
||||
UserID: app.UserID,
|
||||
InvoiceType: app.InvoiceType,
|
||||
Amount: app.Amount,
|
||||
Status: app.Status,
|
||||
CompanyName: app.CompanyName, // 使用快照的公司名称
|
||||
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
|
||||
BankName: app.BankName, // 使用快照的银行名称
|
||||
BankAccount: app.BankAccount, // 使用快照的银行账号
|
||||
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
|
||||
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
|
||||
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
|
||||
FileName: app.FileName,
|
||||
FileSize: app.FileSize,
|
||||
FileURL: app.FileURL,
|
||||
ProcessedAt: app.ProcessedAt,
|
||||
CreatedAt: app.CreatedAt,
|
||||
RejectReason: app.RejectReason,
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.InvoiceRecordsResponse{
|
||||
Records: records,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadInvoiceFile 下载发票文件
|
||||
func (s *InvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error) {
|
||||
// 1. 查找申请记录
|
||||
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if application == nil {
|
||||
return nil, fmt.Errorf("申请记录不存在")
|
||||
}
|
||||
|
||||
// 2. 验证权限(只能下载自己的发票)
|
||||
if application.UserID != userID {
|
||||
return nil, fmt.Errorf("无权访问此发票")
|
||||
}
|
||||
|
||||
// 3. 验证状态(只能下载已完成的发票)
|
||||
if application.Status != entities.ApplicationStatusCompleted {
|
||||
return nil, fmt.Errorf("发票尚未通过审核")
|
||||
}
|
||||
|
||||
// 4. 验证文件信息
|
||||
if application.FileURL == nil || *application.FileURL == "" {
|
||||
return nil, fmt.Errorf("发票文件不存在")
|
||||
}
|
||||
|
||||
// 5. 从七牛云下载文件内容
|
||||
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 构建响应DTO
|
||||
return &dto.FileDownloadResponse{
|
||||
FileID: *application.FileID,
|
||||
FileName: *application.FileName,
|
||||
FileSize: *application.FileSize,
|
||||
FileURL: *application.FileURL,
|
||||
FileContent: fileContent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAvailableAmount 获取可开票金额
|
||||
func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) {
|
||||
// 1. 验证用户是否存在
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.ID == "" {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 2. 计算可开票金额
|
||||
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 获取待处理申请金额
|
||||
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 构建响应DTO
|
||||
return &dto.AvailableAmountResponse{
|
||||
AvailableAmount: availableAmount,
|
||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
|
||||
TotalGifted: totalGifted,
|
||||
TotalInvoiced: totalInvoiced,
|
||||
PendingApplications: pendingAmount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateAvailableAmount 计算可开票金额(私有方法)
|
||||
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, err
|
||||
}
|
||||
|
||||
// 2. 获取待处理中的申请金额
|
||||
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, err
|
||||
}
|
||||
fmt.Println("realRecharged", realRecharged)
|
||||
fmt.Println("totalGifted", totalGifted)
|
||||
fmt.Println("totalInvoiced", totalInvoiced)
|
||||
fmt.Println("pendingAmount", pendingAmount)
|
||||
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
||||
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
||||
fmt.Println("availableAmount", availableAmount)
|
||||
// 确保可开票金额不为负数
|
||||
if availableAmount.LessThan(decimal.Zero) {
|
||||
availableAmount = decimal.Zero
|
||||
}
|
||||
|
||||
return availableAmount, nil
|
||||
}
|
||||
|
||||
// getAmountSummary 获取金额汇总(私有方法)
|
||||
func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, userID string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
|
||||
// 1. 获取用户所有成功的充值记录
|
||||
rechargeRecords, err := s.rechargeRecordRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
|
||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
|
||||
var totalGifted decimal.Decimal // 总赠送金额
|
||||
for _, record := range rechargeRecords {
|
||||
if record.IsSuccess() {
|
||||
if record.RechargeType == entities.RechargeTypeGift {
|
||||
// 赠送金额不计入可开票金额
|
||||
totalGifted = totalGifted.Add(record.Amount)
|
||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
|
||||
// 支付宝充值、微信充值和对公转账计入可开票金额
|
||||
realRecharged = realRecharged.Add(record.Amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 获取用户所有发票申请记录(包括待处理、已完成、已拒绝)
|
||||
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000) // 获取所有记录
|
||||
if err != nil {
|
||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
|
||||
}
|
||||
|
||||
var totalInvoiced decimal.Decimal
|
||||
for _, application := range applications {
|
||||
// 计算已完成的发票申请金额
|
||||
if application.IsCompleted() {
|
||||
totalInvoiced = totalInvoiced.Add(application.Amount)
|
||||
}
|
||||
// 注意:待处理中的申请金额不计算在已开票金额中,但会在可开票金额计算时被扣除
|
||||
}
|
||||
|
||||
return realRecharged, totalGifted, totalInvoiced, nil
|
||||
}
|
||||
|
||||
// getPendingApplicationsAmount 获取待处理申请的总金额(私有方法)
|
||||
func (s *InvoiceApplicationServiceImpl) getPendingApplicationsAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
// 获取用户所有发票申请记录
|
||||
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000)
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
|
||||
}
|
||||
|
||||
var pendingAmount decimal.Decimal
|
||||
for _, application := range applications {
|
||||
// 只计算待处理状态的申请金额
|
||||
if application.Status == entities.ApplicationStatusPending {
|
||||
pendingAmount = pendingAmount.Add(application.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
return pendingAmount, nil
|
||||
}
|
||||
|
||||
// ==================== 管理员端发票应用服务 ====================
|
||||
|
||||
// AdminInvoiceApplicationService 管理员发票应用服务接口
|
||||
type AdminInvoiceApplicationService interface {
|
||||
// GetPendingApplications 获取发票申请列表(支持筛选)
|
||||
GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error)
|
||||
|
||||
// ApproveInvoiceApplication 通过发票申请
|
||||
ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
|
||||
|
||||
// RejectInvoiceApplication 拒绝发票申请
|
||||
RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
|
||||
|
||||
// DownloadInvoiceFile 下载发票文件(管理员)
|
||||
DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error)
|
||||
}
|
||||
|
||||
// AdminInvoiceApplicationServiceImpl 管理员发票应用服务实现
|
||||
type AdminInvoiceApplicationServiceImpl struct {
|
||||
invoiceRepo finance_repo.InvoiceApplicationRepository
|
||||
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
|
||||
userRepo user_repo.UserRepository
|
||||
invoiceAggregateService services.InvoiceAggregateService
|
||||
storageService *storage.QiNiuStorageService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAdminInvoiceApplicationService 创建管理员发票应用服务
|
||||
func NewAdminInvoiceApplicationService(
|
||||
invoiceRepo finance_repo.InvoiceApplicationRepository,
|
||||
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
|
||||
userRepo user_repo.UserRepository,
|
||||
invoiceAggregateService services.InvoiceAggregateService,
|
||||
storageService *storage.QiNiuStorageService,
|
||||
logger *zap.Logger,
|
||||
) AdminInvoiceApplicationService {
|
||||
return &AdminInvoiceApplicationServiceImpl{
|
||||
invoiceRepo: invoiceRepo,
|
||||
userInvoiceInfoRepo: userInvoiceInfoRepo,
|
||||
userRepo: userRepo,
|
||||
invoiceAggregateService: invoiceAggregateService,
|
||||
storageService: storageService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPendingApplications 获取发票申请列表(支持筛选)
|
||||
func (s *AdminInvoiceApplicationServiceImpl) GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error) {
|
||||
// 1. 解析状态筛选
|
||||
var status entities.ApplicationStatus
|
||||
if req.Status != "" {
|
||||
status = entities.ApplicationStatus(req.Status)
|
||||
}
|
||||
|
||||
// 2. 解析时间范围
|
||||
var startTime, endTime *time.Time
|
||||
if req.StartTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
|
||||
startTime = &t
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
|
||||
endTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 获取发票申请记录(支持筛选)
|
||||
var applications []*entities.InvoiceApplication
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if status != "" {
|
||||
// 按状态筛选
|
||||
applications, total, err = s.invoiceRepo.FindByStatusWithTimeRange(ctx, status, startTime, endTime, req.Page, req.PageSize)
|
||||
} else {
|
||||
// 获取所有记录(按时间筛选)
|
||||
applications, total, err = s.invoiceRepo.FindAllWithTimeRange(ctx, startTime, endTime, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 构建响应DTO
|
||||
pendingApplications := make([]*dto.PendingApplicationResponse, len(applications))
|
||||
for i, app := range applications {
|
||||
// 使用快照信息
|
||||
pendingApplications[i] = &dto.PendingApplicationResponse{
|
||||
ID: app.ID,
|
||||
UserID: app.UserID,
|
||||
InvoiceType: app.InvoiceType,
|
||||
Amount: app.Amount,
|
||||
Status: app.Status,
|
||||
CompanyName: app.CompanyName, // 使用快照的公司名称
|
||||
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
|
||||
BankName: app.BankName, // 使用快照的银行名称
|
||||
BankAccount: app.BankAccount, // 使用快照的银行账号
|
||||
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
|
||||
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
|
||||
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
|
||||
FileName: app.FileName,
|
||||
FileSize: app.FileSize,
|
||||
FileURL: app.FileURL,
|
||||
ProcessedAt: app.ProcessedAt,
|
||||
CreatedAt: app.CreatedAt,
|
||||
RejectReason: app.RejectReason,
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.PendingApplicationsResponse{
|
||||
Applications: pendingApplications,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApproveInvoiceApplication 通过发票申请
|
||||
func (s *AdminInvoiceApplicationServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error {
|
||||
// 1. 验证申请是否存在
|
||||
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if application == nil {
|
||||
return fmt.Errorf("发票申请不存在")
|
||||
}
|
||||
|
||||
// 2. 验证申请状态
|
||||
if application.Status != entities.ApplicationStatusPending {
|
||||
return fmt.Errorf("发票申请状态不允许处理")
|
||||
}
|
||||
|
||||
// 3. 调用聚合服务处理申请
|
||||
aggregateReq := services.ApproveInvoiceRequest{
|
||||
AdminNotes: req.AdminNotes,
|
||||
}
|
||||
|
||||
return s.invoiceAggregateService.ApproveInvoiceApplication(ctx, applicationID, file, aggregateReq)
|
||||
}
|
||||
|
||||
// RejectInvoiceApplication 拒绝发票申请
|
||||
func (s *AdminInvoiceApplicationServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error {
|
||||
// 1. 验证申请是否存在
|
||||
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if application == nil {
|
||||
return fmt.Errorf("发票申请不存在")
|
||||
}
|
||||
|
||||
// 2. 验证申请状态
|
||||
if application.Status != entities.ApplicationStatusPending {
|
||||
return fmt.Errorf("发票申请状态不允许处理")
|
||||
}
|
||||
|
||||
// 3. 调用聚合服务处理申请
|
||||
aggregateReq := services.RejectInvoiceRequest{
|
||||
Reason: req.Reason,
|
||||
}
|
||||
|
||||
return s.invoiceAggregateService.RejectInvoiceApplication(ctx, applicationID, aggregateReq)
|
||||
}
|
||||
|
||||
// ==================== 请求和响应DTO ====================
|
||||
|
||||
type ApplyInvoiceRequest struct {
|
||||
InvoiceType string `json:"invoice_type" binding:"required"` // 发票类型:general/special
|
||||
Amount string `json:"amount" binding:"required"` // 开票金额
|
||||
}
|
||||
|
||||
type UpdateInvoiceInfoRequest struct {
|
||||
CompanyName string `json:"company_name"` // 公司名称(从企业认证信息获取,用户不可修改)
|
||||
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号(从企业认证信息获取,用户不可修改)
|
||||
BankName string `json:"bank_name"` // 银行名称
|
||||
CompanyAddress string `json:"company_address"` // 公司地址
|
||||
BankAccount string `json:"bank_account"` // 银行账户
|
||||
CompanyPhone string `json:"company_phone"` // 企业注册电话
|
||||
ReceivingEmail string `json:"receiving_email" binding:"required,email"` // 发票接收邮箱
|
||||
}
|
||||
|
||||
type GetInvoiceRecordsRequest struct {
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
Status string `json:"status"` // 状态筛选
|
||||
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
|
||||
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
|
||||
}
|
||||
|
||||
type GetPendingApplicationsRequest struct {
|
||||
Page int `json:"page"` // 页码
|
||||
PageSize int `json:"page_size"` // 每页数量
|
||||
Status string `json:"status"` // 状态筛选:pending/completed/rejected
|
||||
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
|
||||
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
|
||||
}
|
||||
|
||||
type ApproveInvoiceRequest struct {
|
||||
AdminNotes string `json:"admin_notes"` // 管理员备注
|
||||
}
|
||||
|
||||
type RejectInvoiceRequest struct {
|
||||
Reason string `json:"reason" binding:"required"` // 拒绝原因
|
||||
}
|
||||
|
||||
// DownloadInvoiceFile 下载发票文件(管理员)
|
||||
func (s *AdminInvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error) {
|
||||
// 1. 查找申请记录
|
||||
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if application == nil {
|
||||
return nil, fmt.Errorf("申请记录不存在")
|
||||
}
|
||||
|
||||
// 2. 验证状态(只能下载已完成的发票)
|
||||
if application.Status != entities.ApplicationStatusCompleted {
|
||||
return nil, fmt.Errorf("发票尚未通过审核")
|
||||
}
|
||||
|
||||
// 3. 验证文件信息
|
||||
if application.FileURL == nil || *application.FileURL == "" {
|
||||
return nil, fmt.Errorf("发票文件不存在")
|
||||
}
|
||||
|
||||
// 4. 从七牛云下载文件内容
|
||||
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 构建响应DTO
|
||||
return &dto.FileDownloadResponse{
|
||||
FileID: *application.FileID,
|
||||
FileName: *application.FileName,
|
||||
FileSize: *application.FileSize,
|
||||
FileURL: *application.FileURL,
|
||||
FileContent: fileContent,
|
||||
}, nil
|
||||
}
|
||||
19
internal/application/product/category_application_service.go
Normal file
19
internal/application/product/category_application_service.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// CategoryApplicationService 分类应用服务接口
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
repoQueries "hyapi-server/internal/domains/product/repositories/queries"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CategoryApplicationServiceImpl 分类应用服务实现
|
||||
type CategoryApplicationServiceImpl struct {
|
||||
categoryRepo repositories.ProductCategoryRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCategoryApplicationService 创建分类应用服务
|
||||
func NewCategoryApplicationService(
|
||||
categoryRepo repositories.ProductCategoryRepository,
|
||||
logger *zap.Logger,
|
||||
) CategoryApplicationService {
|
||||
return &CategoryApplicationServiceImpl{
|
||||
categoryRepo: categoryRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
func (s *CategoryApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 验证分类编号唯一性
|
||||
if err := s.validateCategoryCode(cmd.Code, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 创建分类实体
|
||||
category := &entities.ProductCategory{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Sort: cmd.Sort,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
}
|
||||
|
||||
// 4. 保存到仓储
|
||||
createdCategory, err := s.categoryRepo.Create(ctx, *category)
|
||||
if err != nil {
|
||||
s.logger.Error("创建分类失败", zap.Error(err), zap.String("code", cmd.Code))
|
||||
return fmt.Errorf("创建分类失败: %w", err)
|
||||
}
|
||||
|
||||
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. 参数验证
|
||||
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)
|
||||
}
|
||||
|
||||
// 3. 验证分类编号唯一性(排除当前分类)
|
||||
if err := s.validateCategoryCode(cmd.Code, cmd.ID); err != nil {
|
||||
return 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), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *CategoryApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||
// 1. 检查分类是否存在
|
||||
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否有产品(可选,根据业务需求决定)
|
||||
// 这里可以添加检查逻辑,如果有产品则不允许删除
|
||||
|
||||
// 3. 删除分类
|
||||
if err := s.categoryRepo.Delete(ctx, cmd.ID); 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), zap.String("code", existingCategory.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryByID 根据ID获取分类
|
||||
func (s *CategoryApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
|
||||
var category entities.ProductCategory
|
||||
var err error
|
||||
|
||||
if query.ID != "" {
|
||||
category, err = s.categoryRepo.GetByID(ctx, query.ID)
|
||||
} else {
|
||||
return nil, fmt.Errorf("分类ID不能为空")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToCategoryInfoResponse(&category)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListCategories 获取分类列表
|
||||
func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error) {
|
||||
// 构建仓储查询
|
||||
repoQuery := &repoQueries.ListCategoriesQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
IsEnabled: query.IsEnabled,
|
||||
IsVisible: query.IsVisible,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
}
|
||||
|
||||
// 调用仓储
|
||||
categories, total, err := s.categoryRepo.ListCategories(ctx, repoQuery)
|
||||
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 &responses.CategoryListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, 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,
|
||||
Sort: category.Sort,
|
||||
IsEnabled: category.IsEnabled,
|
||||
IsVisible: category.IsVisible,
|
||||
CreatedAt: category.CreatedAt,
|
||||
UpdatedAt: category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *CategoryApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Code: category.Code,
|
||||
}
|
||||
}
|
||||
|
||||
// validateCreateCategory 验证创建分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUpdateCategory 验证更新分类参数
|
||||
func (s *CategoryApplicationServiceImpl) validateUpdateCategory(cmd *commands.UpdateCategoryCommand) error {
|
||||
if cmd.ID == "" {
|
||||
return errors.New("分类ID不能为空")
|
||||
}
|
||||
if cmd.Name == "" {
|
||||
return errors.New("分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCategoryCode 验证分类编号唯一性
|
||||
func (s *CategoryApplicationServiceImpl) validateCategoryCode(code, excludeID string) error {
|
||||
if code == "" {
|
||||
return errors.New("分类编号不能为空")
|
||||
}
|
||||
|
||||
existingCategory, err := s.categoryRepo.FindByCode(context.Background(), code)
|
||||
if err == nil && existingCategory != nil && existingCategory.ID != excludeID {
|
||||
return errors.New("分类编号已存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1305
internal/application/product/component_report_order_service.go
Normal file
1305
internal/application/product/component_report_order_service.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/services"
|
||||
)
|
||||
|
||||
// DocumentationApplicationServiceInterface 文档应用服务接口
|
||||
type DocumentationApplicationServiceInterface interface {
|
||||
// CreateDocumentation 创建文档
|
||||
CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
DeleteDocumentation(ctx context.Context, id string) error
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
|
||||
}
|
||||
|
||||
// DocumentationApplicationService 文档应用服务
|
||||
type DocumentationApplicationService struct {
|
||||
docService *services.ProductDocumentationService
|
||||
}
|
||||
|
||||
// NewDocumentationApplicationService 创建文档应用服务实例
|
||||
func NewDocumentationApplicationService(docService *services.ProductDocumentationService) *DocumentationApplicationService {
|
||||
return &DocumentationApplicationService{
|
||||
docService: docService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDocumentation 创建文档
|
||||
func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 创建文档实体
|
||||
doc := &entities.ProductDocumentation{
|
||||
RequestURL: cmd.RequestURL,
|
||||
RequestMethod: cmd.RequestMethod,
|
||||
BasicInfo: cmd.BasicInfo,
|
||||
RequestParams: cmd.RequestParams,
|
||||
ResponseFields: cmd.ResponseFields,
|
||||
ResponseExample: cmd.ResponseExample,
|
||||
ErrorCodes: cmd.ErrorCodes,
|
||||
PDFFilePath: cmd.PDFFilePath,
|
||||
}
|
||||
|
||||
// 调用领域服务创建文档
|
||||
err := s.docService.CreateDocumentation(ctx, cmd.ProductID, doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 调用领域服务更新文档
|
||||
err := s.docService.UpdateDocumentation(ctx, id,
|
||||
cmd.RequestURL,
|
||||
cmd.RequestMethod,
|
||||
cmd.BasicInfo,
|
||||
cmd.RequestParams,
|
||||
cmd.ResponseFields,
|
||||
cmd.ResponseExample,
|
||||
cmd.ErrorCodes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取更新后的文档
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新PDF文件路径(如果提供)
|
||||
if cmd.PDFFilePath != "" {
|
||||
doc.PDFFilePath = cmd.PDFFilePath
|
||||
err = s.docService.UpdateDocumentationEntity(ctx, doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
|
||||
}
|
||||
// 重新获取更新后的文档以确保获取最新数据
|
||||
doc, err = s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
func (s *DocumentationApplicationService) DeleteDocumentation(ctx context.Context, id string) error {
|
||||
return s.docService.DeleteDocumentation(ctx, id)
|
||||
}
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error) {
|
||||
docs, err := s.docService.GetDocumentationsByProductIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var docResponses []responses.DocumentationResponse
|
||||
for _, doc := range docs {
|
||||
docResponses = append(docResponses, responses.NewDocumentationResponse(doc))
|
||||
}
|
||||
|
||||
return docResponses, nil
|
||||
}
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
|
||||
// 通过产品ID获取文档
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取文档失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取文档时已经包含了产品信息(通过GetDocumentationWithProduct)
|
||||
// 如果没有产品信息,通过文档ID获取
|
||||
if doc.Product == nil && doc.ID != "" {
|
||||
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
|
||||
if err == nil && docWithProduct != nil {
|
||||
doc = docWithProduct
|
||||
}
|
||||
}
|
||||
|
||||
var markdown strings.Builder
|
||||
|
||||
// 添加文档标题
|
||||
productName := "产品"
|
||||
if doc.Product != nil {
|
||||
productName = doc.Product.Name
|
||||
}
|
||||
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
|
||||
|
||||
// 添加产品基本信息
|
||||
if doc.Product != nil {
|
||||
markdown.WriteString("## 产品信息\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
|
||||
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
|
||||
if doc.Product.Description != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
|
||||
}
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式
|
||||
markdown.WriteString("## 请求方式\n\n")
|
||||
if doc.RequestURL != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
|
||||
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式详细说明
|
||||
if doc.BasicInfo != "" {
|
||||
markdown.WriteString("### 请求方式说明\n\n")
|
||||
markdown.WriteString(doc.BasicInfo)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加请求参数
|
||||
if doc.RequestParams != "" {
|
||||
markdown.WriteString("## 请求参数\n\n")
|
||||
markdown.WriteString(doc.RequestParams)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加返回字段说明
|
||||
if doc.ResponseFields != "" {
|
||||
markdown.WriteString("## 返回字段说明\n\n")
|
||||
markdown.WriteString(doc.ResponseFields)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加响应示例
|
||||
if doc.ResponseExample != "" {
|
||||
markdown.WriteString("## 响应示例\n\n")
|
||||
markdown.WriteString(doc.ResponseExample)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加错误代码
|
||||
if doc.ErrorCodes != "" {
|
||||
markdown.WriteString("## 错误代码\n\n")
|
||||
markdown.WriteString(doc.ErrorCodes)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加文档版本信息
|
||||
markdown.WriteString("---\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
|
||||
if doc.UpdatedAt.Year() > 1900 {
|
||||
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
|
||||
return markdown.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
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:"-" 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:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
// CreateDocumentationCommand 创建文档命令
|
||||
type CreateDocumentationCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required" validate:"required"`
|
||||
RequestURL string `json:"request_url" binding:"required" validate:"required"`
|
||||
RequestMethod string `json:"request_method" binding:"required" validate:"required"`
|
||||
BasicInfo string `json:"basic_info" validate:"required"`
|
||||
RequestParams string `json:"request_params" validate:"required"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateDocumentationCommand 更新文档命令
|
||||
type UpdateDocumentationCommand struct {
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package commands
|
||||
|
||||
// AddPackageItemCommand 添加组合包子产品命令
|
||||
type AddPackageItemCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
|
||||
}
|
||||
|
||||
// UpdatePackageItemCommand 更新组合包子产品命令
|
||||
type UpdatePackageItemCommand struct {
|
||||
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
|
||||
}
|
||||
|
||||
// ReorderPackageItemsCommand 重新排序组合包子产品命令
|
||||
type ReorderPackageItemsCommand struct {
|
||||
ItemIDs []string `json:"item_ids" binding:"required,dive,uuid" comment:"子产品ID列表"`
|
||||
}
|
||||
|
||||
// UpdatePackageItemsCommand 批量更新组合包子产品命令
|
||||
type UpdatePackageItemsCommand struct {
|
||||
Items []PackageItemData `json:"items" binding:"required,dive" comment:"子产品列表"`
|
||||
}
|
||||
|
||||
// PackageItemData 组合包子产品数据
|
||||
type PackageItemData struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
|
||||
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package commands
|
||||
|
||||
// CreateProductCommand 创建产品命令
|
||||
type CreateProductCommand struct {
|
||||
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"`
|
||||
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
||||
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
|
||||
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// 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:"-" 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"`
|
||||
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
||||
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
|
||||
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// 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:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// CreateProductApiConfigCommand 创建产品API配置命令
|
||||
type CreateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
|
||||
// UpdateProductApiConfigCommand 更新产品API配置命令
|
||||
type UpdateProductApiConfigCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
|
||||
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
|
||||
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
|
||||
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package commands
|
||||
|
||||
// CreateSubCategoryCommand 创建二级分类命令
|
||||
type CreateSubCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
|
||||
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
|
||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
|
||||
Sort int `json:"sort" binding:"min=0" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// UpdateSubCategoryCommand 更新二级分类命令
|
||||
type UpdateSubCategoryCommand struct {
|
||||
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,min=2,max=50" comment:"二级分类编号"`
|
||||
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
|
||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
|
||||
Sort int `json:"sort" binding:"min=0" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
}
|
||||
|
||||
// DeleteSubCategoryCommand 删除二级分类命令
|
||||
type DeleteSubCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
// CreateSubscriptionCommand 创建订阅命令
|
||||
type CreateSubscriptionCommand struct {
|
||||
UserID string `json:"-" comment:"用户ID"`
|
||||
ProductID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// UpdateSubscriptionPriceCommand 更新订阅价格命令
|
||||
type UpdateSubscriptionPriceCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
|
||||
type BatchUpdateSubscriptionPricesCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
|
||||
Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
|
||||
CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"`
|
||||
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
|
||||
}
|
||||
17
internal/application/product/dto/queries/category_queries.go
Normal file
17
internal/application/product/dto/queries/category_queries.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package queries
|
||||
|
||||
// ListCategoriesQuery 分类列表查询
|
||||
type ListCategoriesQuery struct {
|
||||
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" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"分类编号"`
|
||||
}
|
||||
10
internal/application/product/dto/queries/package_queries.go
Normal file
10
internal/application/product/dto/queries/package_queries.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package queries
|
||||
|
||||
// GetAvailableProductsQuery 获取可选子产品查询
|
||||
type GetAvailableProductsQuery struct {
|
||||
ExcludePackageID string `form:"exclude_package_id" binding:"omitempty,uuid" comment:"排除的组合包ID"`
|
||||
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
|
||||
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
}
|
||||
54
internal/application/product/dto/queries/product_queries.go
Normal file
54
internal/application/product/dto/queries/product_queries.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package queries
|
||||
|
||||
// ListProductsQuery 产品列表查询
|
||||
type ListProductsQuery struct {
|
||||
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:"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" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
}
|
||||
|
||||
// GetProductDetailQuery 获取产品详情查询(支持可选文档)
|
||||
type GetProductDetailQuery struct {
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
WithDocument *bool `form:"with_document" comment:"是否包含文档信息"`
|
||||
}
|
||||
|
||||
// GetProductsByIDsQuery 根据ID列表获取产品查询
|
||||
type GetProductsByIDsQuery struct {
|
||||
IDs []string `form:"ids" binding:"required,dive,uuid" comment:"产品ID列表"`
|
||||
}
|
||||
|
||||
// GetSubscribableProductsQuery 获取可订阅产品查询
|
||||
type GetSubscribableProductsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package queries
|
||||
|
||||
// GetSubCategoryQuery 获取二级分类查询
|
||||
type GetSubCategoryQuery struct {
|
||||
ID string `json:"id" form:"id" binding:"omitempty,uuid" comment:"二级分类ID"`
|
||||
}
|
||||
|
||||
// ListSubCategoriesQuery 获取二级分类列表查询
|
||||
type ListSubCategoriesQuery struct {
|
||||
Page int `json:"page" form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
CategoryID string `json:"category_id" form:"category_id" binding:"omitempty,uuid" comment:"一级分类ID"`
|
||||
IsEnabled *bool `json:"is_enabled" form:"is_enabled" comment:"是否启用"`
|
||||
IsVisible *bool `json:"is_visible" form:"is_visible" comment:"是否展示"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序方向"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package queries
|
||||
|
||||
// ListSubscriptionsQuery 订阅列表查询
|
||||
type ListSubscriptionsQuery struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
|
||||
UserID string `form:"user_id" binding:"omitempty" 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:"排序方向"`
|
||||
|
||||
// 新增筛选字段
|
||||
CompanyName string `form:"company_name" binding:"omitempty,max=100" comment:"企业名称"`
|
||||
ProductName string `form:"product_name" binding:"omitempty,max=100" comment:"产品名称"`
|
||||
StartTime string `form:"start_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅开始时间"`
|
||||
EndTime string `form:"end_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅结束时间"`
|
||||
}
|
||||
|
||||
// GetSubscriptionQuery 获取订阅详情查询
|
||||
type GetSubscriptionQuery struct {
|
||||
ID string `uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
}
|
||||
|
||||
// GetUserSubscriptionsQuery 获取用户订阅查询
|
||||
type GetUserSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
|
||||
// GetProductSubscriptionsQuery 获取产品订阅查询
|
||||
type GetProductSubscriptionsQuery struct {
|
||||
ProductID string `form:"product_id" binding:"required,uuid" comment:"产品ID"`
|
||||
}
|
||||
|
||||
// GetActiveSubscriptionsQuery 获取活跃订阅查询
|
||||
type GetActiveSubscriptionsQuery struct {
|
||||
UserID string `form:"user_id" binding:"omitempty,uuid" comment:"用户ID"`
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// CategoryInfoResponse 分类详情响应
|
||||
type CategoryInfoResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
Description string `json:"description" comment:"分类描述"`
|
||||
Sort int `json:"sort" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// CategoryListResponse 分类列表响应
|
||||
type CategoryListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||
}
|
||||
|
||||
// CategorySimpleResponse 分类简单信息响应
|
||||
type CategorySimpleResponse struct {
|
||||
ID string `json:"id" comment:"分类ID"`
|
||||
Name string `json:"name" comment:"分类名称"`
|
||||
Code string `json:"code" comment:"分类编号"`
|
||||
}
|
||||
|
||||
// SubCategoryInfoResponse 二级分类详情响应
|
||||
type SubCategoryInfoResponse struct {
|
||||
ID string `json:"id" comment:"二级分类ID"`
|
||||
Name string `json:"name" comment:"二级分类名称"`
|
||||
Code string `json:"code" comment:"二级分类编号"`
|
||||
Description string `json:"description" comment:"二级分类描述"`
|
||||
CategoryID string `json:"category_id" comment:"一级分类ID"`
|
||||
Sort int `json:"sort" comment:"排序"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||
|
||||
// 关联信息
|
||||
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// SubCategoryListResponse 二级分类列表响应
|
||||
type SubCategoryListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []SubCategoryInfoResponse `json:"items" comment:"二级分类列表"`
|
||||
}
|
||||
|
||||
// SubCategorySimpleResponse 二级分类简单信息响应
|
||||
type SubCategorySimpleResponse struct {
|
||||
ID string `json:"id" comment:"二级分类ID"`
|
||||
Name string `json:"name" comment:"二级分类名称"`
|
||||
Code string `json:"code" comment:"二级分类编号"`
|
||||
CategoryID string `json:"category_id" comment:"一级分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// DocumentationResponse 文档响应
|
||||
type DocumentationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
Version string `json:"version"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// NewDocumentationResponse 从实体创建响应
|
||||
func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationResponse {
|
||||
return DocumentationResponse{
|
||||
ID: doc.ID,
|
||||
ProductID: doc.ProductID,
|
||||
RequestURL: doc.RequestURL,
|
||||
RequestMethod: doc.RequestMethod,
|
||||
BasicInfo: doc.BasicInfo,
|
||||
RequestParams: doc.RequestParams,
|
||||
ResponseFields: doc.ResponseFields,
|
||||
ResponseExample: doc.ResponseExample,
|
||||
ErrorCodes: doc.ErrorCodes,
|
||||
Version: doc.Version,
|
||||
PDFFilePath: doc.PDFFilePath,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// ProductApiConfigResponse 产品API配置响应
|
||||
type ProductApiConfigResponse struct {
|
||||
ID string `json:"id" comment:"配置ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
RequestParams []RequestParamResponse `json:"request_params" comment:"请求参数配置"`
|
||||
ResponseFields []ResponseFieldResponse `json:"response_fields" comment:"响应字段配置"`
|
||||
ResponseExample map[string]interface{} `json:"response_example" comment:"响应示例"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// RequestParamResponse 请求参数响应
|
||||
type RequestParamResponse struct {
|
||||
Name string `json:"name" comment:"参数名称"`
|
||||
Field string `json:"field" comment:"参数字段名"`
|
||||
Type string `json:"type" comment:"参数类型"`
|
||||
Required bool `json:"required" comment:"是否必填"`
|
||||
Description string `json:"description" comment:"参数描述"`
|
||||
Example string `json:"example" comment:"参数示例"`
|
||||
Validation string `json:"validation" comment:"验证规则"`
|
||||
}
|
||||
|
||||
// ResponseFieldResponse 响应字段响应
|
||||
type ResponseFieldResponse struct {
|
||||
Name string `json:"name" comment:"字段名称"`
|
||||
Path string `json:"path" comment:"字段路径"`
|
||||
Type string `json:"type" comment:"字段类型"`
|
||||
Description string `json:"description" comment:"字段描述"`
|
||||
Required bool `json:"required" comment:"是否必填"`
|
||||
Example string `json:"example" comment:"字段示例"`
|
||||
}
|
||||
|
||||
// ProductApiConfigListResponse 产品API配置列表响应
|
||||
type ProductApiConfigListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductApiConfigResponse `json:"items" comment:"配置列表"`
|
||||
}
|
||||
146
internal/application/product/dto/responses/product_responses.go
Normal file
146
internal/application/product/dto/responses/product_responses.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// PackageItemResponse 组合包项目响应
|
||||
type PackageItemResponse struct {
|
||||
ID string `json:"id" comment:"项目ID"`
|
||||
ProductID string `json:"product_id" comment:"子产品ID"`
|
||||
ProductCode string `json:"product_code" comment:"子产品编号"`
|
||||
ProductName string `json:"product_name" comment:"子产品名称"`
|
||||
SortOrder int `json:"sort_order" comment:"排序"`
|
||||
Price float64 `json:"price" comment:"子产品价格"`
|
||||
CostPrice float64 `json:"cost_price" comment:"子产品成本价"`
|
||||
}
|
||||
|
||||
// ProductInfoResponse 产品详情响应
|
||||
type ProductInfoResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" 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"`
|
||||
SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// 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:"一级分类信息"`
|
||||
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
|
||||
|
||||
// 组合包信息
|
||||
PackageItems []*PackageItemResponse `json:"package_items,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:"每页数量"`
|
||||
Items []ProductInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
|
||||
// ProductSearchResponse 产品搜索响应
|
||||
type ProductSearchResponse struct {
|
||||
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"`
|
||||
OldID *string `json:"old_id,omitempty" 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:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
}
|
||||
|
||||
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||
type ProductSimpleAdminResponse struct {
|
||||
ProductSimpleResponse
|
||||
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// 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:"组合包产品数"`
|
||||
}
|
||||
|
||||
// ProductAdminInfoResponse 管理员产品详情响应
|
||||
type ProductAdminInfoResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" 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"`
|
||||
SubCategoryID *string `json:"sub_category_id,omitempty" comment:"二级分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||
Remark string `json:"remark" comment:"备注"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
|
||||
// UI组件相关字段
|
||||
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格(组合包使用)"`
|
||||
|
||||
// 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:"一级分类信息"`
|
||||
SubCategory *SubCategoryInfoResponse `json:"sub_category,omitempty" comment:"二级分类信息"`
|
||||
|
||||
// 组合包信息
|
||||
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
|
||||
|
||||
// 文档信息
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ProductInfoWithDocumentResponse 包含文档的产品详情响应
|
||||
type ProductInfoWithDocumentResponse struct {
|
||||
ProductInfoResponse
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
}
|
||||
|
||||
// ProductAdminListResponse 管理员产品列表响应
|
||||
type ProductAdminListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []ProductAdminInfoResponse `json:"items" comment:"产品列表"`
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserSimpleResponse 用户简单信息响应
|
||||
type UserSimpleResponse struct {
|
||||
ID string `json:"id" comment:"用户ID"`
|
||||
CompanyName string `json:"company_name" comment:"公司名称"`
|
||||
Phone string `json:"phone" comment:"手机号"`
|
||||
}
|
||||
|
||||
// SubscriptionInfoResponse 订阅详情响应
|
||||
type SubscriptionInfoResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
UserID string `json:"user_id" comment:"用户ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
|
||||
// 关联信息
|
||||
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
||||
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
||||
// 管理员端使用,包含成本价的产品信息
|
||||
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// SubscriptionListResponse 订阅列表响应
|
||||
type SubscriptionListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"`
|
||||
}
|
||||
|
||||
// SubscriptionSimpleResponse 订阅简单信息响应
|
||||
type SubscriptionSimpleResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionUsageResponse 订阅使用情况响应
|
||||
type SubscriptionUsageResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionStatsResponse 订阅统计响应
|
||||
type SubscriptionStatsResponse struct {
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
|
||||
TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/services"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProductApiConfigApplicationService 产品API配置应用服务接口
|
||||
type ProductApiConfigApplicationService interface {
|
||||
// 获取产品API配置
|
||||
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 根据产品代码获取API配置
|
||||
GetProductApiConfigByCode(ctx context.Context, productCode string) (*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 批量获取产品API配置
|
||||
GetProductApiConfigsByProductIDs(ctx context.Context, productIDs []string) ([]*responses.ProductApiConfigResponse, error)
|
||||
|
||||
// 创建产品API配置
|
||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||
|
||||
// 更新产品API配置
|
||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||
|
||||
// 删除产品API配置
|
||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||
}
|
||||
|
||||
// ProductApiConfigApplicationServiceImpl 产品API配置应用服务实现
|
||||
type ProductApiConfigApplicationServiceImpl struct {
|
||||
apiConfigService services.ProductApiConfigService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProductApiConfigApplicationService 创建产品API配置应用服务
|
||||
func NewProductApiConfigApplicationService(
|
||||
apiConfigService services.ProductApiConfigService,
|
||||
logger *zap.Logger,
|
||||
) ProductApiConfigApplicationService {
|
||||
return &ProductApiConfigApplicationServiceImpl{
|
||||
apiConfigService: apiConfigService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProductApiConfig 获取产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error) {
|
||||
config, err := s.apiConfigService.GetApiConfigByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToResponse(config), nil
|
||||
}
|
||||
|
||||
// GetProductApiConfigByCode 根据产品代码获取API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfigByCode(ctx context.Context, productCode string) (*responses.ProductApiConfigResponse, error) {
|
||||
config, err := s.apiConfigService.GetApiConfigByProductCode(ctx, productCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToResponse(config), nil
|
||||
}
|
||||
|
||||
// GetProductApiConfigsByProductIDs 批量获取产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) GetProductApiConfigsByProductIDs(ctx context.Context, productIDs []string) ([]*responses.ProductApiConfigResponse, error) {
|
||||
configs, err := s.apiConfigService.GetApiConfigsByProductIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var responses []*responses.ProductApiConfigResponse
|
||||
for _, config := range configs {
|
||||
responses = append(responses, s.convertToResponse(config))
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// CreateProductApiConfig 创建产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) CreateProductApiConfig(ctx context.Context, productID string, configResponse *responses.ProductApiConfigResponse) error {
|
||||
// 检查是否已存在配置
|
||||
exists, err := s.apiConfigService.ExistsByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return errors.New("产品API配置已存在")
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := s.convertToEntity(configResponse)
|
||||
config.ProductID = productID
|
||||
|
||||
return s.apiConfigService.CreateApiConfig(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateProductApiConfig 更新产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) UpdateProductApiConfig(ctx context.Context, configID string, configResponse *responses.ProductApiConfigResponse) error {
|
||||
// 获取现有配置
|
||||
existingConfig, err := s.apiConfigService.GetApiConfigByProductID(ctx, configResponse.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
config := s.convertToEntity(configResponse)
|
||||
config.ID = configID
|
||||
config.ProductID = existingConfig.ProductID
|
||||
|
||||
return s.apiConfigService.UpdateApiConfig(ctx, config)
|
||||
}
|
||||
|
||||
// DeleteProductApiConfig 删除产品API配置
|
||||
func (s *ProductApiConfigApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
||||
return s.apiConfigService.DeleteApiConfig(ctx, configID)
|
||||
}
|
||||
|
||||
// convertToResponse 转换为响应DTO
|
||||
func (s *ProductApiConfigApplicationServiceImpl) convertToResponse(config *entities.ProductApiConfig) *responses.ProductApiConfigResponse {
|
||||
requestParams, _ := config.GetRequestParams()
|
||||
responseFields, _ := config.GetResponseFields()
|
||||
responseExample, _ := config.GetResponseExample()
|
||||
|
||||
// 转换请求参数
|
||||
var requestParamResponses []responses.RequestParamResponse
|
||||
for _, param := range requestParams {
|
||||
requestParamResponses = append(requestParamResponses, responses.RequestParamResponse{
|
||||
Name: param.Name,
|
||||
Field: param.Field,
|
||||
Type: param.Type,
|
||||
Required: param.Required,
|
||||
Description: param.Description,
|
||||
Example: param.Example,
|
||||
Validation: param.Validation,
|
||||
})
|
||||
}
|
||||
|
||||
// 转换响应字段
|
||||
var responseFieldResponses []responses.ResponseFieldResponse
|
||||
for _, field := range responseFields {
|
||||
responseFieldResponses = append(responseFieldResponses, responses.ResponseFieldResponse{
|
||||
Name: field.Name,
|
||||
Path: field.Path,
|
||||
Type: field.Type,
|
||||
Description: field.Description,
|
||||
Required: field.Required,
|
||||
Example: field.Example,
|
||||
})
|
||||
}
|
||||
|
||||
return &responses.ProductApiConfigResponse{
|
||||
ID: config.ID,
|
||||
ProductID: config.ProductID,
|
||||
RequestParams: requestParamResponses,
|
||||
ResponseFields: responseFieldResponses,
|
||||
ResponseExample: responseExample,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToEntity 转换为实体
|
||||
func (s *ProductApiConfigApplicationServiceImpl) convertToEntity(configResponse *responses.ProductApiConfigResponse) *entities.ProductApiConfig {
|
||||
// 转换请求参数
|
||||
var requestParams []entities.RequestParam
|
||||
for _, param := range configResponse.RequestParams {
|
||||
requestParams = append(requestParams, entities.RequestParam{
|
||||
Name: param.Name,
|
||||
Field: param.Field,
|
||||
Type: param.Type,
|
||||
Required: param.Required,
|
||||
Description: param.Description,
|
||||
Example: param.Example,
|
||||
Validation: param.Validation,
|
||||
})
|
||||
}
|
||||
|
||||
// 转换响应字段
|
||||
var responseFields []entities.ResponseField
|
||||
for _, field := range configResponse.ResponseFields {
|
||||
responseFields = append(responseFields, entities.ResponseField{
|
||||
Name: field.Name,
|
||||
Path: field.Path,
|
||||
Type: field.Type,
|
||||
Description: field.Description,
|
||||
Required: field.Required,
|
||||
Example: field.Example,
|
||||
})
|
||||
}
|
||||
|
||||
config := &entities.ProductApiConfig{}
|
||||
|
||||
// 设置JSON字段
|
||||
config.SetRequestParams(requestParams)
|
||||
config.SetResponseFields(responseFields)
|
||||
config.SetResponseExample(configResponse.ResponseExample)
|
||||
|
||||
return config
|
||||
}
|
||||
50
internal/application/product/product_application_service.go
Normal file
50
internal/application/product/product_application_service.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// ProductApplicationService 产品应用服务接口
|
||||
type ProductApplicationService interface {
|
||||
// 产品管理
|
||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, 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, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error)
|
||||
GetProductsByIDs(ctx context.Context, query *queries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
|
||||
// 管理员专用方法
|
||||
ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error)
|
||||
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error)
|
||||
|
||||
// 用户端专用方法
|
||||
GetProductByIDForUser(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error)
|
||||
|
||||
// 业务查询
|
||||
GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error)
|
||||
|
||||
// 组合包管理
|
||||
AddPackageItem(ctx context.Context, packageID string, cmd *commands.AddPackageItemCommand) error
|
||||
UpdatePackageItem(ctx context.Context, packageID, itemID string, cmd *commands.UpdatePackageItemCommand) error
|
||||
RemovePackageItem(ctx context.Context, packageID, itemID string) error
|
||||
ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error
|
||||
UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error
|
||||
|
||||
// 可选子产品查询(管理员端,返回包含成本价的数据)
|
||||
GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductAdminListResponse, error)
|
||||
|
||||
// API配置管理
|
||||
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||
}
|
||||
1242
internal/application/product/product_application_service_impl.go
Normal file
1242
internal/application/product/product_application_service_impl.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// SubCategoryApplicationService 二级分类应用服务接口
|
||||
type SubCategoryApplicationService interface {
|
||||
// 二级分类管理
|
||||
CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error
|
||||
UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error
|
||||
DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error
|
||||
|
||||
GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error)
|
||||
ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error)
|
||||
ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SubCategoryApplicationServiceImpl 二级分类应用服务实现
|
||||
type SubCategoryApplicationServiceImpl struct {
|
||||
categoryRepo repositories.ProductCategoryRepository
|
||||
subCategoryRepo repositories.ProductSubCategoryRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubCategoryApplicationService 创建二级分类应用服务
|
||||
func NewSubCategoryApplicationService(
|
||||
categoryRepo repositories.ProductCategoryRepository,
|
||||
subCategoryRepo repositories.ProductSubCategoryRepository,
|
||||
logger *zap.Logger,
|
||||
) SubCategoryApplicationService {
|
||||
return &SubCategoryApplicationServiceImpl{
|
||||
categoryRepo: categoryRepo,
|
||||
subCategoryRepo: subCategoryRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSubCategory 创建二级分类
|
||||
func (s *SubCategoryApplicationServiceImpl) CreateSubCategory(ctx context.Context, cmd *commands.CreateSubCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateCreateSubCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 验证一级分类是否存在
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("一级分类不存在: %w", err)
|
||||
}
|
||||
if !category.IsValid() {
|
||||
return errors.New("一级分类已禁用或删除")
|
||||
}
|
||||
|
||||
// 3. 验证二级分类编号唯一性
|
||||
if err := s.validateSubCategoryCode(cmd.Code, "", cmd.CategoryID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 创建二级分类实体
|
||||
subCategory := &entities.ProductSubCategory{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Sort: cmd.Sort,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
}
|
||||
|
||||
// 5. 保存到仓储
|
||||
createdSubCategory, err := s.subCategoryRepo.Create(ctx, *subCategory)
|
||||
if err != nil {
|
||||
s.logger.Error("创建二级分类失败", zap.Error(err), zap.String("code", cmd.Code))
|
||||
return fmt.Errorf("创建二级分类失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建二级分类成功", zap.String("id", createdSubCategory.ID), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSubCategory 更新二级分类
|
||||
func (s *SubCategoryApplicationServiceImpl) UpdateSubCategory(ctx context.Context, cmd *commands.UpdateSubCategoryCommand) error {
|
||||
// 1. 参数验证
|
||||
if err := s.validateUpdateSubCategory(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 获取现有二级分类
|
||||
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("二级分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 验证一级分类是否存在
|
||||
category, err := s.categoryRepo.GetByID(ctx, cmd.CategoryID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("一级分类不存在: %w", err)
|
||||
}
|
||||
if !category.IsValid() {
|
||||
return errors.New("一级分类已禁用或删除")
|
||||
}
|
||||
|
||||
// 4. 验证二级分类编号唯一性(排除当前分类)
|
||||
if err := s.validateSubCategoryCode(cmd.Code, cmd.ID, cmd.CategoryID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 更新二级分类信息
|
||||
existingSubCategory.Name = cmd.Name
|
||||
existingSubCategory.Code = cmd.Code
|
||||
existingSubCategory.Description = cmd.Description
|
||||
existingSubCategory.CategoryID = cmd.CategoryID
|
||||
existingSubCategory.Sort = cmd.Sort
|
||||
existingSubCategory.IsEnabled = cmd.IsEnabled
|
||||
existingSubCategory.IsVisible = cmd.IsVisible
|
||||
|
||||
// 6. 保存到仓储
|
||||
if err := s.subCategoryRepo.Update(ctx, *existingSubCategory); 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), zap.String("code", cmd.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSubCategory 删除二级分类
|
||||
func (s *SubCategoryApplicationServiceImpl) DeleteSubCategory(ctx context.Context, cmd *commands.DeleteSubCategoryCommand) error {
|
||||
// 1. 检查二级分类是否存在
|
||||
existingSubCategory, err := s.subCategoryRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("二级分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除二级分类
|
||||
if err := s.subCategoryRepo.Delete(ctx, cmd.ID); 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), zap.String("code", existingSubCategory.Code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSubCategoryByID 根据ID获取二级分类
|
||||
func (s *SubCategoryApplicationServiceImpl) GetSubCategoryByID(ctx context.Context, query *queries.GetSubCategoryQuery) (*responses.SubCategoryInfoResponse, error) {
|
||||
subCategory, err := s.subCategoryRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("二级分类不存在: %w", err)
|
||||
}
|
||||
|
||||
// 加载一级分类信息
|
||||
if subCategory.CategoryID != "" {
|
||||
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
|
||||
if err == nil {
|
||||
subCategory.Category = &category
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
response := s.convertToSubCategoryInfoResponse(subCategory)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListSubCategories 获取二级分类列表
|
||||
func (s *SubCategoryApplicationServiceImpl) ListSubCategories(ctx context.Context, query *queries.ListSubCategoriesQuery) (*responses.SubCategoryListResponse, error) {
|
||||
// 构建查询条件
|
||||
categoryID := query.CategoryID
|
||||
isEnabled := query.IsEnabled
|
||||
isVisible := query.IsVisible
|
||||
|
||||
var subCategories []*entities.ProductSubCategory
|
||||
var err error
|
||||
|
||||
// 根据条件查询
|
||||
if categoryID != "" {
|
||||
// 按一级分类查询
|
||||
subCategories, err = s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
|
||||
} else {
|
||||
// 查询所有二级分类
|
||||
subCategories, err = s.subCategoryRepo.List(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("获取二级分类列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤状态
|
||||
filteredSubCategories := make([]*entities.ProductSubCategory, 0)
|
||||
for _, subCategory := range subCategories {
|
||||
if isEnabled != nil && *isEnabled != subCategory.IsEnabled {
|
||||
continue
|
||||
}
|
||||
if isVisible != nil && *isVisible != subCategory.IsVisible {
|
||||
continue
|
||||
}
|
||||
filteredSubCategories = append(filteredSubCategories, subCategory)
|
||||
}
|
||||
|
||||
// 加载一级分类信息
|
||||
for _, subCategory := range filteredSubCategories {
|
||||
if subCategory.CategoryID != "" {
|
||||
category, err := s.categoryRepo.GetByID(ctx, subCategory.CategoryID)
|
||||
if err == nil {
|
||||
subCategory.Category = &category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]responses.SubCategoryInfoResponse, len(filteredSubCategories))
|
||||
for i, subCategory := range filteredSubCategories {
|
||||
items[i] = *s.convertToSubCategoryInfoResponse(subCategory)
|
||||
}
|
||||
|
||||
return &responses.SubCategoryListResponse{
|
||||
Total: int64(len(items)),
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListSubCategoriesByCategoryID 根据一级分类ID获取二级分类列表
|
||||
func (s *SubCategoryApplicationServiceImpl) ListSubCategoriesByCategoryID(ctx context.Context, categoryID string) ([]*responses.SubCategorySimpleResponse, error) {
|
||||
subCategories, err := s.subCategoryRepo.FindByCategoryID(ctx, categoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取二级分类列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.SubCategorySimpleResponse, len(subCategories))
|
||||
for i, subCategory := range subCategories {
|
||||
items[i] = &responses.SubCategorySimpleResponse{
|
||||
ID: subCategory.ID,
|
||||
Name: subCategory.Name,
|
||||
Code: subCategory.Code,
|
||||
CategoryID: subCategory.CategoryID,
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// convertToSubCategoryInfoResponse 转换为二级分类信息响应
|
||||
func (s *SubCategoryApplicationServiceImpl) convertToSubCategoryInfoResponse(subCategory *entities.ProductSubCategory) *responses.SubCategoryInfoResponse {
|
||||
response := &responses.SubCategoryInfoResponse{
|
||||
ID: subCategory.ID,
|
||||
Name: subCategory.Name,
|
||||
Code: subCategory.Code,
|
||||
Description: subCategory.Description,
|
||||
CategoryID: subCategory.CategoryID,
|
||||
Sort: subCategory.Sort,
|
||||
IsEnabled: subCategory.IsEnabled,
|
||||
IsVisible: subCategory.IsVisible,
|
||||
CreatedAt: subCategory.CreatedAt,
|
||||
UpdatedAt: subCategory.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加一级分类信息
|
||||
if subCategory.Category != nil {
|
||||
response.Category = &responses.CategoryInfoResponse{
|
||||
ID: subCategory.Category.ID,
|
||||
Name: subCategory.Category.Name,
|
||||
Description: subCategory.Category.Description,
|
||||
Sort: subCategory.Category.Sort,
|
||||
IsEnabled: subCategory.Category.IsEnabled,
|
||||
IsVisible: subCategory.Category.IsVisible,
|
||||
CreatedAt: subCategory.Category.CreatedAt,
|
||||
UpdatedAt: subCategory.Category.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// validateCreateSubCategory 验证创建二级分类参数
|
||||
func (s *SubCategoryApplicationServiceImpl) validateCreateSubCategory(cmd *commands.CreateSubCategoryCommand) error {
|
||||
if cmd.Name == "" {
|
||||
return errors.New("二级分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("二级分类编号不能为空")
|
||||
}
|
||||
if cmd.CategoryID == "" {
|
||||
return errors.New("一级分类ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUpdateSubCategory 验证更新二级分类参数
|
||||
func (s *SubCategoryApplicationServiceImpl) validateUpdateSubCategory(cmd *commands.UpdateSubCategoryCommand) error {
|
||||
if cmd.ID == "" {
|
||||
return errors.New("二级分类ID不能为空")
|
||||
}
|
||||
if cmd.Name == "" {
|
||||
return errors.New("二级分类名称不能为空")
|
||||
}
|
||||
if cmd.Code == "" {
|
||||
return errors.New("二级分类编号不能为空")
|
||||
}
|
||||
if cmd.CategoryID == "" {
|
||||
return errors.New("一级分类ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSubCategoryCode 验证二级分类编号唯一性
|
||||
func (s *SubCategoryApplicationServiceImpl) validateSubCategoryCode(code, excludeID, categoryID string) error {
|
||||
if code == "" {
|
||||
return errors.New("二级分类编号不能为空")
|
||||
}
|
||||
|
||||
existingSubCategory, err := s.subCategoryRepo.FindByCode(context.Background(), code)
|
||||
if err == nil && existingSubCategory != nil && existingSubCategory.ID != excludeID {
|
||||
// 如果指定了分类ID,检查是否在同一分类下
|
||||
if categoryID == "" || existingSubCategory.CategoryID == categoryID {
|
||||
return errors.New("二级分类编号已存在")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
"hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationService 订阅应用服务接口
|
||||
type SubscriptionApplicationService interface {
|
||||
// 订阅管理
|
||||
UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error
|
||||
|
||||
// 订阅管理
|
||||
CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error
|
||||
GetSubscriptionByID(ctx context.Context, query *queries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error)
|
||||
ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
|
||||
// 我的订阅(用户专用)
|
||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
||||
CancelMySubscription(ctx context.Context, userID string, subscriptionID string) 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)
|
||||
|
||||
// 一键改价
|
||||
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/application/product/dto/commands"
|
||||
appQueries "hyapi-server/internal/application/product/dto/queries"
|
||||
"hyapi-server/internal/application/product/dto/responses"
|
||||
domain_api_repo "hyapi-server/internal/domains/api/repositories"
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
repoQueries "hyapi-server/internal/domains/product/repositories/queries"
|
||||
product_service "hyapi-server/internal/domains/product/services"
|
||||
user_repositories "hyapi-server/internal/domains/user/repositories"
|
||||
)
|
||||
|
||||
// SubscriptionApplicationServiceImpl 订阅应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type SubscriptionApplicationServiceImpl struct {
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
userRepo user_repositories.UserRepository
|
||||
apiCallRepository domain_api_repo.ApiCallRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSubscriptionApplicationService 创建订阅应用服务
|
||||
func NewSubscriptionApplicationService(
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
userRepo user_repositories.UserRepository,
|
||||
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||
logger *zap.Logger,
|
||||
) SubscriptionApplicationService {
|
||||
return &SubscriptionApplicationServiceImpl{
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
userRepo: userRepo,
|
||||
apiCallRepository: apiCallRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSubscriptionPrice 更新订阅价格
|
||||
// 业务流程:1. 获取订阅 2. 更新价格 3. 保存订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
|
||||
return s.productSubscriptionService.UpdateSubscriptionPriceWithUIComponent(ctx, cmd.ID, cmd.Price, cmd.UIComponentPrice)
|
||||
}
|
||||
|
||||
// BatchUpdateSubscriptionPrices 一键改价
|
||||
// 业务流程:1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
|
||||
func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error {
|
||||
// 记录请求参数
|
||||
s.logger.Info("开始批量更新订阅价格",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("adjustment_type", cmd.AdjustmentType),
|
||||
zap.Float64("discount", cmd.Discount),
|
||||
zap.Float64("cost_multiple", cmd.CostMultiple),
|
||||
zap.String("scope", cmd.Scope))
|
||||
|
||||
// 验证调整方式对应的参数
|
||||
if cmd.AdjustmentType == "discount" && cmd.Discount <= 0 {
|
||||
return fmt.Errorf("按售价折扣调整时,折扣比例必须大于0")
|
||||
}
|
||||
if cmd.AdjustmentType == "cost_multiple" && cmd.CostMultiple <= 0 {
|
||||
return fmt.Errorf("按成本价倍数调整时,倍数必须大于0")
|
||||
}
|
||||
|
||||
subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{
|
||||
UserID: cmd.UserID,
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("获取到订阅列表",
|
||||
zap.Int("total_subscriptions", len(subscriptions)))
|
||||
|
||||
// 根据范围筛选订阅
|
||||
var targetSubscriptions []*entities.Subscription
|
||||
for _, sub := range subscriptions {
|
||||
if cmd.Scope == "all" {
|
||||
// 所有订阅都修改
|
||||
targetSubscriptions = append(targetSubscriptions, sub)
|
||||
} else if cmd.Scope == "undiscounted" {
|
||||
// 只修改未打折的订阅(价格等于产品原价)
|
||||
if sub.Product != nil && sub.Price.Equal(sub.Product.Price) {
|
||||
targetSubscriptions = append(targetSubscriptions, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新价格
|
||||
updatedCount := 0
|
||||
skippedCount := 0
|
||||
for _, sub := range targetSubscriptions {
|
||||
if sub.Product == nil {
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
var newPrice decimal.Decimal
|
||||
|
||||
if cmd.AdjustmentType == "discount" {
|
||||
// 按售价折扣调整
|
||||
discountRatio := cmd.Discount / 10
|
||||
newPrice = sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio))
|
||||
} else if cmd.AdjustmentType == "cost_multiple" {
|
||||
// 按成本价倍数调整
|
||||
// 检查成本价是否有效(必须大于0)
|
||||
// 使用严格检查:成本价必须大于0
|
||||
if !sub.Product.CostPrice.GreaterThan(decimal.Zero) {
|
||||
// 跳过没有成本价或成本价为0的产品
|
||||
skippedCount++
|
||||
s.logger.Info("跳过未设置成本价或成本价为0的订阅",
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.String("product_id", sub.ProductID),
|
||||
zap.String("product_name", sub.Product.Name),
|
||||
zap.String("cost_price", sub.Product.CostPrice.String()))
|
||||
continue
|
||||
}
|
||||
// 计算成本价倍数后的价格
|
||||
newPrice = sub.Product.CostPrice.Mul(decimal.NewFromFloat(cmd.CostMultiple))
|
||||
} else {
|
||||
s.logger.Warn("未知的调整方式",
|
||||
zap.String("adjustment_type", cmd.AdjustmentType),
|
||||
zap.String("subscription_id", sub.ID))
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 四舍五入到2位小数
|
||||
newPrice = newPrice.Round(2)
|
||||
|
||||
err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64())
|
||||
if err != nil {
|
||||
s.logger.Error("批量更新订阅价格失败",
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.Error(err))
|
||||
skippedCount++
|
||||
// 继续处理其他订阅,不中断整个流程
|
||||
} else {
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("批量更新订阅价格完成",
|
||||
zap.Int("total", len(targetSubscriptions)),
|
||||
zap.Int("updated", updatedCount),
|
||||
zap.Int("skipped", skippedCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSubscription 创建订阅
|
||||
// 业务流程:1. 创建订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
||||
_, 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.productSubscriptionService.GetSubscriptionByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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, // 管理员可以按用户筛选
|
||||
Keyword: query.Keyword,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
CompanyName: query.CompanyName,
|
||||
ProductName: query.ProductName,
|
||||
StartTime: query.StartTime,
|
||||
EndTime: query.EndTime,
|
||||
}
|
||||
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
|
||||
if resp != nil {
|
||||
items[i] = *resp // 解引用指针
|
||||
}
|
||||
}
|
||||
return &responses.SubscriptionListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListMySubscriptions 获取我的订阅列表(用户用)
|
||||
// 业务流程:1. 获取用户订阅列表 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) ListMySubscriptions(ctx context.Context, userID string, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
|
||||
repoQuery := &repoQueries.ListSubscriptionsQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
UserID: userID, // 强制设置为当前用户ID
|
||||
Keyword: query.Keyword,
|
||||
SortBy: query.SortBy,
|
||||
SortOrder: query.SortOrder,
|
||||
CompanyName: query.CompanyName,
|
||||
ProductName: query.ProductName,
|
||||
StartTime: query.StartTime,
|
||||
EndTime: query.EndTime,
|
||||
}
|
||||
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
resp := s.convertToSubscriptionInfoResponse(subscriptions[i])
|
||||
if resp != nil {
|
||||
items[i] = *resp // 解引用指针
|
||||
}
|
||||
}
|
||||
return &responses.SubscriptionListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserSubscriptions 获取用户订阅
|
||||
// 业务流程:1. 获取用户订阅 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetUserSubscriptions(ctx context.Context, query *appQueries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
|
||||
subscriptions, err := s.productSubscriptionService.GetUserSubscriptions(ctx, query.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应对象
|
||||
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
|
||||
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) {
|
||||
// 这里需要扩展领域服务来支持按产品查询订阅
|
||||
// 暂时返回空列表
|
||||
return []*responses.SubscriptionInfoResponse{}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionUsage 获取订阅使用情况
|
||||
// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||
// 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 根据用户ID和产品ID统计API调用次数
|
||||
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
|
||||
if err != nil {
|
||||
s.logger.Warn("统计API调用次数失败,使用订阅记录中的值",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("user_id", subscription.UserID),
|
||||
zap.String("product_id", subscription.ProductID),
|
||||
zap.Error(err))
|
||||
// 如果统计失败,使用订阅实体中的APIUsed字段作为备选
|
||||
apiCallCount = subscription.APIUsed
|
||||
}
|
||||
|
||||
return &responses.SubscriptionUsageResponse{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
APIUsed: apiCallCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSubscriptionStats 获取订阅统计信息
|
||||
// 业务流程:1. 获取订阅统计 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
|
||||
stats, err := s.productSubscriptionService.GetSubscriptionStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: stats["total_subscriptions"].(int64),
|
||||
TotalRevenue: stats["total_revenue"].(float64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMySubscriptionStats 获取我的订阅统计信息
|
||||
// 业务流程:1. 获取用户订阅统计 2. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error) {
|
||||
stats, err := s.productSubscriptionService.GetUserSubscriptionStats(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responses.SubscriptionStatsResponse{
|
||||
TotalSubscriptions: stats["total_subscriptions"].(int64),
|
||||
TotalRevenue: stats["total_revenue"].(float64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelMySubscription 取消我的订阅
|
||||
// 业务流程:1. 验证订阅是否属于当前用户 2. 取消订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
|
||||
// 1. 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("订阅不存在")
|
||||
}
|
||||
|
||||
// 2. 验证订阅是否属于当前用户
|
||||
if subscription.UserID != userID {
|
||||
s.logger.Warn("用户尝试取消不属于自己的订阅",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("subscription_user_id", subscription.UserID))
|
||||
return fmt.Errorf("无权取消此订阅")
|
||||
}
|
||||
|
||||
// 3. 取消订阅(软删除)
|
||||
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
|
||||
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("取消订阅失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("用户取消订阅成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
// 查询用户信息
|
||||
var userInfo *responses.UserSimpleResponse
|
||||
if subscription.UserID != "" {
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知公司"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
userInfo = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var productResponse *responses.ProductSimpleResponse
|
||||
if subscription.Product != nil {
|
||||
productResponse = s.convertToProductSimpleResponse(subscription.Product)
|
||||
}
|
||||
|
||||
// 获取UI组件价格,如果订阅中没有设置,则从产品中获取
|
||||
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
|
||||
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
|
||||
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
|
||||
}
|
||||
|
||||
return &responses.SubscriptionInfoResponse{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
Product: productResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToProductSimpleResponse 转换为产品简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
|
||||
var categoryResponse *responses.CategorySimpleResponse
|
||||
if product.Category != nil {
|
||||
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
|
||||
}
|
||||
|
||||
return &responses.ProductSimpleResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
Category: categoryResponse,
|
||||
IsPackage: product.IsPackage,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponseForAdmin 转换为订阅信息响应(管理员端,包含成本价)
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseForAdmin(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
// 查询用户信息
|
||||
var userInfo *responses.UserSimpleResponse
|
||||
if subscription.UserID != "" {
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知公司"
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
userInfo = &responses.UserSimpleResponse{
|
||||
ID: user.ID,
|
||||
CompanyName: companyName,
|
||||
Phone: user.Phone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var productAdminResponse *responses.ProductSimpleAdminResponse
|
||||
if subscription.Product != nil {
|
||||
productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product)
|
||||
}
|
||||
|
||||
// 获取UI组件价格,如果订阅中没有设置,则从产品中获取
|
||||
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
|
||||
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
|
||||
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
|
||||
}
|
||||
|
||||
return &responses.SubscriptionInfoResponse{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
ProductAdmin: productAdminResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToProductSimpleAdminResponse 转换为管理员产品简单信息响应(包含成本价)
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse(product *entities.Product) *responses.ProductSimpleAdminResponse {
|
||||
var categoryResponse *responses.CategorySimpleResponse
|
||||
if product.Category != nil {
|
||||
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
|
||||
}
|
||||
|
||||
return &responses.ProductSimpleAdminResponse{
|
||||
ProductSimpleResponse: responses.ProductSimpleResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
Category: categoryResponse,
|
||||
IsPackage: product.IsPackage,
|
||||
},
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
}
|
||||
}
|
||||
|
||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||
if category == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &responses.CategorySimpleResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
}
|
||||
}
|
||||
743
internal/application/product/ui_component_application_service.go
Normal file
743
internal/application/product/ui_component_application_service.go
Normal file
@@ -0,0 +1,743 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UIComponentApplicationService UI组件应用服务接口
|
||||
type UIComponentApplicationService interface {
|
||||
// 基本CRUD操作
|
||||
CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error)
|
||||
CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error)
|
||||
GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error)
|
||||
GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error)
|
||||
UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error
|
||||
DeleteUIComponent(ctx context.Context, id string) error
|
||||
ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error)
|
||||
|
||||
// 文件操作
|
||||
UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error)
|
||||
UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error
|
||||
DownloadUIComponentFile(ctx context.Context, id string) (string, error)
|
||||
GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error)
|
||||
DeleteUIComponentFolder(ctx context.Context, id string) error
|
||||
|
||||
// 产品关联操作
|
||||
AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error
|
||||
GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error)
|
||||
RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error
|
||||
}
|
||||
|
||||
// CreateUIComponentRequest 创建UI组件请求
|
||||
type CreateUIComponentRequest struct {
|
||||
ComponentCode string `json:"component_code" binding:"required"`
|
||||
ComponentName string `json:"component_name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsActive bool `json:"is_active"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// UpdateUIComponentRequest 更新UI组件请求
|
||||
type UpdateUIComponentRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
ComponentCode string `json:"component_code"`
|
||||
ComponentName string `json:"component_name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// ListUIComponentsRequest 获取UI组件列表请求
|
||||
type ListUIComponentsRequest struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=10"`
|
||||
Keyword string `form:"keyword"`
|
||||
IsActive *bool `form:"is_active"`
|
||||
SortBy string `form:"sort_by,default=sort_order"`
|
||||
SortOrder string `form:"sort_order,default=asc"`
|
||||
}
|
||||
|
||||
// ListUIComponentsResponse 获取UI组件列表响应
|
||||
type ListUIComponentsResponse struct {
|
||||
Components []entities.UIComponent `json:"components"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// AssociateUIComponentRequest 关联UI组件到产品请求
|
||||
type AssociateUIComponentRequest struct {
|
||||
ProductID string `json:"product_id" binding:"required"`
|
||||
UIComponentID string `json:"ui_component_id" binding:"required"`
|
||||
Price float64 `json:"price" binding:"required,min=0"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
// UIComponentApplicationServiceImpl UI组件应用服务实现
|
||||
type UIComponentApplicationServiceImpl struct {
|
||||
uiComponentRepo repositories.UIComponentRepository
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository
|
||||
fileStorageService FileStorageService
|
||||
fileService UIComponentFileService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// FileStorageService 文件存储服务接口
|
||||
type FileStorageService interface {
|
||||
StoreFile(ctx context.Context, file io.Reader, filename string) (string, error)
|
||||
GetFileURL(ctx context.Context, filePath string) (string, error)
|
||||
DeleteFile(ctx context.Context, filePath string) error
|
||||
}
|
||||
|
||||
// NewUIComponentApplicationService 创建UI组件应用服务
|
||||
func NewUIComponentApplicationService(
|
||||
uiComponentRepo repositories.UIComponentRepository,
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository,
|
||||
fileStorageService FileStorageService,
|
||||
fileService UIComponentFileService,
|
||||
logger *zap.Logger,
|
||||
) UIComponentApplicationService {
|
||||
return &UIComponentApplicationServiceImpl{
|
||||
uiComponentRepo: uiComponentRepo,
|
||||
productUIComponentRepo: productUIComponentRepo,
|
||||
fileStorageService: fileStorageService,
|
||||
fileService: fileService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUIComponent 创建UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponent(ctx context.Context, req CreateUIComponentRequest) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Create(ctx, component)
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFile 创建UI组件并上传文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx context.Context, req CreateUIComponentRequest, file *multipart.FileHeader) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if file != nil {
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, file.Filename); err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
createdComponent.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFiles 创建UI组件并上传多个文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFiles(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if len(files) > 0 {
|
||||
// 处理每个文件
|
||||
var extractedFiles []string
|
||||
for _, fileHeader := range files {
|
||||
// 打开上传的文件
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, createdComponent.ID, createdComponent.ComponentCode, src, fileHeader.Filename); err != nil {
|
||||
src.Close()
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
src.Close()
|
||||
|
||||
// 记录已处理的文件,用于日志
|
||||
extractedFiles = append(extractedFiles, fileHeader.Filename)
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
for _, fileHeader := range files {
|
||||
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
|
||||
hasZipFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有ZIP文件,则标记为已解压
|
||||
if hasZipFile {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// CreateUIComponentWithFilesAndPaths 创建UI组件并上传带路径的文件
|
||||
func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFilesAndPaths(ctx context.Context, req CreateUIComponentRequest, files []*multipart.FileHeader, paths []string) (entities.UIComponent, error) {
|
||||
// 检查编码是否已存在
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil {
|
||||
return entities.UIComponent{}, ErrComponentCodeAlreadyExists
|
||||
}
|
||||
|
||||
// 创建组件
|
||||
component := entities.UIComponent{
|
||||
ComponentCode: req.ComponentCode,
|
||||
ComponentName: req.ComponentName,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
IsActive: req.IsActive,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
|
||||
createdComponent, err := s.uiComponentRepo.Create(ctx, component)
|
||||
if err != nil {
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 如果有文件,则上传并处理文件
|
||||
if len(files) > 0 {
|
||||
// 打开所有文件
|
||||
var readers []io.Reader
|
||||
var filenames []string
|
||||
var filePaths []string
|
||||
|
||||
for i, fileHeader := range files {
|
||||
// 打开上传的文件
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
// 关闭已打开的文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
readers = append(readers, src)
|
||||
filenames = append(filenames, fileHeader.Filename)
|
||||
|
||||
// 确定文件路径
|
||||
var path string
|
||||
if i < len(paths) && paths[i] != "" {
|
||||
path = paths[i]
|
||||
} else {
|
||||
path = fileHeader.Filename
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
}
|
||||
|
||||
// 使用新的批量上传方法
|
||||
if err := s.fileService.UploadMultipleFiles(ctx, createdComponent.ID, createdComponent.ComponentCode, readers, filenames, filePaths); err != nil {
|
||||
// 关闭已打开的文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
// 删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
|
||||
// 关闭所有文件
|
||||
for _, r := range readers {
|
||||
if closer, ok := r.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
for _, fileHeader := range files {
|
||||
if strings.HasSuffix(strings.ToLower(fileHeader.Filename), ".zip") {
|
||||
hasZipFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有ZIP文件,则标记为已解压
|
||||
if hasZipFile {
|
||||
createdComponent.IsExtracted = true
|
||||
}
|
||||
|
||||
// 更新组件信息
|
||||
err = s.uiComponentRepo.Update(ctx, createdComponent)
|
||||
if err != nil {
|
||||
// 尝试删除已创建的组件记录
|
||||
_ = s.uiComponentRepo.Delete(ctx, createdComponent.ID)
|
||||
return entities.UIComponent{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return createdComponent, nil
|
||||
}
|
||||
|
||||
// GetUIComponentByID 根据ID获取UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentByID(ctx context.Context, id string) (*entities.UIComponent, error) {
|
||||
return s.uiComponentRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetUIComponentByCode 根据编码获取UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentByCode(ctx context.Context, code string) (*entities.UIComponent, error) {
|
||||
return s.uiComponentRepo.GetByCode(ctx, code)
|
||||
}
|
||||
|
||||
// UpdateUIComponent 更新UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) UpdateUIComponent(ctx context.Context, req UpdateUIComponentRequest) error {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 如果更新编码,检查是否与其他组件冲突
|
||||
if req.ComponentCode != "" && req.ComponentCode != component.ComponentCode {
|
||||
existing, _ := s.uiComponentRepo.GetByCode(ctx, req.ComponentCode)
|
||||
if existing != nil && existing.ID != req.ID {
|
||||
return ErrComponentCodeAlreadyExists
|
||||
}
|
||||
component.ComponentCode = req.ComponentCode
|
||||
}
|
||||
|
||||
if req.ComponentName != "" {
|
||||
component.ComponentName = req.ComponentName
|
||||
}
|
||||
if req.Description != "" {
|
||||
component.Description = req.Description
|
||||
}
|
||||
if req.Version != "" {
|
||||
component.Version = req.Version
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
component.IsActive = *req.IsActive
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
component.SortOrder = *req.SortOrder
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
|
||||
// DeleteUIComponent 删除UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
|
||||
return fmt.Errorf("获取UI组件失败: %w", err)
|
||||
}
|
||||
if component == nil {
|
||||
s.logger.Warn("UI组件不存在", zap.String("id", id))
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 记录组件信息
|
||||
s.logger.Info("开始删除UI组件",
|
||||
zap.String("id", id),
|
||||
zap.String("componentCode", component.ComponentCode),
|
||||
zap.String("componentName", component.ComponentName),
|
||||
zap.Bool("isExtracted", component.IsExtracted),
|
||||
zap.Any("filePath", component.FilePath),
|
||||
zap.Any("folderPath", component.FolderPath))
|
||||
|
||||
// 使用智能删除方法,根据组件编码和上传时间删除相关文件
|
||||
if err := s.fileService.DeleteFilesByComponentCode(component.ComponentCode, component.FileUploadTime); err != nil {
|
||||
// 记录错误但不阻止删除数据库记录
|
||||
s.logger.Error("删除组件文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("componentCode", component.ComponentCode),
|
||||
zap.Any("fileUploadTime", component.FileUploadTime))
|
||||
}
|
||||
|
||||
// 删除关联的文件(FilePath指向的文件)
|
||||
if component.FilePath != nil {
|
||||
if err := s.fileStorageService.DeleteFile(ctx, *component.FilePath); err != nil {
|
||||
s.logger.Error("删除文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("filePath", *component.FilePath))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
if err := s.uiComponentRepo.Delete(ctx, id); err != nil {
|
||||
s.logger.Error("删除UI组件数据库记录失败",
|
||||
zap.Error(err),
|
||||
zap.String("id", id))
|
||||
return fmt.Errorf("删除UI组件数据库记录失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("UI组件删除成功",
|
||||
zap.String("id", id),
|
||||
zap.String("componentCode", component.ComponentCode))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUIComponents 获取UI组件列表
|
||||
func (s *UIComponentApplicationServiceImpl) ListUIComponents(ctx context.Context, req ListUIComponentsRequest) (ListUIComponentsResponse, error) {
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
if req.Keyword != "" {
|
||||
filters["keyword"] = req.Keyword
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
filters["is_active"] = *req.IsActive
|
||||
}
|
||||
filters["page"] = req.Page
|
||||
filters["page_size"] = req.PageSize
|
||||
filters["sort_by"] = req.SortBy
|
||||
filters["sort_order"] = req.SortOrder
|
||||
|
||||
components, total, err := s.uiComponentRepo.List(ctx, filters)
|
||||
if err != nil {
|
||||
return ListUIComponentsResponse{}, err
|
||||
}
|
||||
|
||||
return ListUIComponentsResponse{
|
||||
Components: components,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadUIComponentFile 上传UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) UploadUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) (string, error) {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if component == nil {
|
||||
return "", ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 检查文件大小(100MB)
|
||||
if file.Size > 100*1024*1024 {
|
||||
return "", ErrInvalidFileType // 复用此错误表示文件太大
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 生成文件路径
|
||||
filePath := filepath.Join("ui-components", id+"_"+file.Filename)
|
||||
|
||||
// 存储文件
|
||||
storedPath, err := s.fileStorageService.StoreFile(ctx, src, filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 删除旧文件
|
||||
if component.FilePath != nil {
|
||||
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath)
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
component.FilePath = &storedPath
|
||||
component.FileSize = &file.Size
|
||||
component.FileType = &fileType
|
||||
if err := s.uiComponentRepo.Update(ctx, *component); err != nil {
|
||||
// 如果更新失败,尝试删除已上传的文件
|
||||
_ = s.fileStorageService.DeleteFile(ctx, storedPath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return storedPath, nil
|
||||
}
|
||||
|
||||
// DownloadUIComponentFile 下载UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) DownloadUIComponentFile(ctx context.Context, id string) (string, error) {
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if component == nil {
|
||||
return "", ErrComponentNotFound
|
||||
}
|
||||
|
||||
if component.FilePath == nil {
|
||||
return "", ErrComponentFileNotFound
|
||||
}
|
||||
|
||||
return s.fileStorageService.GetFileURL(ctx, *component.FilePath)
|
||||
}
|
||||
|
||||
// AssociateUIComponentToProduct 关联UI组件到产品
|
||||
func (s *UIComponentApplicationServiceImpl) AssociateUIComponentToProduct(ctx context.Context, req AssociateUIComponentRequest) error {
|
||||
// 检查组件是否存在
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, req.UIComponentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
relation := entities.ProductUIComponent{
|
||||
ProductID: req.ProductID,
|
||||
UIComponentID: req.UIComponentID,
|
||||
Price: decimal.NewFromFloat(req.Price),
|
||||
IsEnabled: req.IsEnabled,
|
||||
}
|
||||
|
||||
_, err = s.productUIComponentRepo.Create(ctx, relation)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProductUIComponents 获取产品的UI组件列表
|
||||
func (s *UIComponentApplicationServiceImpl) GetProductUIComponents(ctx context.Context, productID string) ([]entities.ProductUIComponent, error) {
|
||||
return s.productUIComponentRepo.GetByProductID(ctx, productID)
|
||||
}
|
||||
|
||||
// RemoveUIComponentFromProduct 从产品中移除UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) RemoveUIComponentFromProduct(ctx context.Context, productID, componentID string) error {
|
||||
// 查找关联记录
|
||||
relations, err := s.productUIComponentRepo.GetByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 找到要删除的关联记录
|
||||
var relationID string
|
||||
for _, relation := range relations {
|
||||
if relation.UIComponentID == componentID {
|
||||
relationID = relation.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if relationID == "" {
|
||||
return ErrProductComponentRelationNotFound
|
||||
}
|
||||
|
||||
return s.productUIComponentRepo.Delete(ctx, relationID)
|
||||
}
|
||||
|
||||
// UploadAndExtractUIComponentFile 上传并解压UI组件文件
|
||||
func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx context.Context, id string, file *multipart.FileHeader) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 打开上传的文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开上传文件失败: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传并解压文件
|
||||
if err := s.fileService.UploadAndExtract(ctx, id, component.ComponentCode, src, file.Filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
component.FolderPath = &folderPath
|
||||
component.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
component.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
component.IsExtracted = true
|
||||
}
|
||||
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
|
||||
// GetUIComponentFolderContent 获取UI组件文件夹内容
|
||||
func (s *UIComponentApplicationServiceImpl) GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error) {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if component == nil {
|
||||
return nil, ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 如果没有文件夹路径,返回空
|
||||
if component.FolderPath == nil {
|
||||
return []FileInfo{}, nil
|
||||
}
|
||||
|
||||
// 获取文件夹内容
|
||||
return s.fileService.GetFolderContent(*component.FolderPath)
|
||||
}
|
||||
|
||||
// DeleteUIComponentFolder 删除UI组件文件夹
|
||||
func (s *UIComponentApplicationServiceImpl) DeleteUIComponentFolder(ctx context.Context, id string) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if component == nil {
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 注意:我们不再删除整个UI目录,因为所有组件共享同一个目录
|
||||
// 这里只更新组件信息,标记为未上传状态
|
||||
// 更新组件信息
|
||||
component.FolderPath = nil
|
||||
component.IsExtracted = false
|
||||
return s.uiComponentRepo.Update(ctx, *component)
|
||||
}
|
||||
21
internal/application/product/ui_component_errors.go
Normal file
21
internal/application/product/ui_component_errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package product
|
||||
|
||||
import "errors"
|
||||
|
||||
// UI组件相关错误定义
|
||||
var (
|
||||
// ErrComponentNotFound UI组件不存在
|
||||
ErrComponentNotFound = errors.New("UI组件不存在")
|
||||
|
||||
// ErrComponentCodeAlreadyExists UI组件编码已存在
|
||||
ErrComponentCodeAlreadyExists = errors.New("UI组件编码已存在")
|
||||
|
||||
// ErrComponentFileNotFound UI组件文件不存在
|
||||
ErrComponentFileNotFound = errors.New("UI组件文件不存在")
|
||||
|
||||
// ErrInvalidFileType 无效的文件类型
|
||||
ErrInvalidFileType = errors.New("无效的文件类型,仅支持ZIP文件")
|
||||
|
||||
// ErrProductComponentRelationNotFound 产品UI组件关联不存在
|
||||
ErrProductComponentRelationNotFound = errors.New("产品UI组件关联不存在")
|
||||
)
|
||||
459
internal/application/product/ui_component_file_service.go
Normal file
459
internal/application/product/ui_component_file_service.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UIComponentFileService UI组件文件服务接口
|
||||
type UIComponentFileService interface {
|
||||
// 上传并解压UI组件文件
|
||||
UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error
|
||||
|
||||
// 批量上传UI组件文件(支持文件夹结构)
|
||||
UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error
|
||||
|
||||
// 根据组件编码创建文件夹
|
||||
CreateFolderByCode(componentCode string) (string, error)
|
||||
|
||||
// 删除组件文件夹
|
||||
DeleteFolder(folderPath string) error
|
||||
|
||||
// 检查文件夹是否存在
|
||||
FolderExists(folderPath string) bool
|
||||
|
||||
// 获取文件夹内容
|
||||
GetFolderContent(folderPath string) ([]FileInfo, error)
|
||||
|
||||
// 根据组件编码和上传时间智能删除组件相关文件
|
||||
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type"` // "file" or "folder"
|
||||
Modified time.Time `json:"modified"`
|
||||
}
|
||||
|
||||
// UIComponentFileServiceImpl UI组件文件服务实现
|
||||
type UIComponentFileServiceImpl struct {
|
||||
basePath string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUIComponentFileService 创建UI组件文件服务
|
||||
func NewUIComponentFileService(basePath string, logger *zap.Logger) UIComponentFileService {
|
||||
// 确保基础路径存在
|
||||
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||
logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath))
|
||||
}
|
||||
|
||||
return &UIComponentFileServiceImpl{
|
||||
basePath: basePath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAndExtract 上传并解压UI组件文件
|
||||
func (s *UIComponentFileServiceImpl) UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error {
|
||||
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
|
||||
folderPath := s.basePath
|
||||
|
||||
// 确保基础目录存在
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建基础目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存上传的文件
|
||||
filePath := filepath.Join(folderPath, filename)
|
||||
savedFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer savedFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(savedFile, file); err != nil {
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 仅对ZIP文件执行解压逻辑
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
|
||||
// 解压文件到基础目录
|
||||
if err := s.extractZipFile(filePath, folderPath); err != nil {
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("解压文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
|
||||
s.logger.Info("UI组件文件上传并解压成功",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("folderPath", folderPath))
|
||||
} else {
|
||||
s.logger.Info("UI组件文件上传成功(未解压)",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadMultipleFiles 批量上传UI组件文件(支持文件夹结构)
|
||||
func (s *UIComponentFileServiceImpl) UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error {
|
||||
// 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹
|
||||
folderPath := s.basePath
|
||||
|
||||
// 确保基础目录存在
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建基础目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理每个文件
|
||||
for i, file := range files {
|
||||
filename := filenames[i]
|
||||
path := paths[i]
|
||||
|
||||
// 如果有路径信息,创建对应的子文件夹
|
||||
if path != "" && path != filename {
|
||||
// 获取文件所在目录
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "." {
|
||||
// 创建子文件夹
|
||||
subDirPath := filepath.Join(folderPath, dir)
|
||||
if err := os.MkdirAll(subDirPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建子文件夹失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确定文件保存路径
|
||||
var filePath string
|
||||
if path != "" && path != filename {
|
||||
// 有路径信息,使用完整路径
|
||||
filePath = filepath.Join(folderPath, path)
|
||||
} else {
|
||||
// 没有路径信息,直接保存在根目录
|
||||
filePath = filepath.Join(folderPath, filename)
|
||||
}
|
||||
|
||||
// 保存上传的文件
|
||||
savedFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer savedFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
if _, err := io.Copy(savedFile, file); err != nil {
|
||||
// 删除部分写入的文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 对ZIP文件执行解压逻辑
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".zip") {
|
||||
// 确定解压目录
|
||||
var extractDir string
|
||||
if path != "" && path != filename {
|
||||
// 有路径信息,解压到对应目录
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "." {
|
||||
extractDir = filepath.Join(folderPath, dir)
|
||||
} else {
|
||||
extractDir = folderPath
|
||||
}
|
||||
} else {
|
||||
// 没有路径信息,解压到根目录
|
||||
extractDir = folderPath
|
||||
}
|
||||
|
||||
// 解压文件
|
||||
if err := s.extractZipFile(filePath, extractDir); err != nil {
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
return fmt.Errorf("解压文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除ZIP文件
|
||||
_ = os.Remove(filePath)
|
||||
|
||||
s.logger.Info("UI组件文件上传并解压成功",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath),
|
||||
zap.String("extractDir", extractDir))
|
||||
} else {
|
||||
s.logger.Info("UI组件文件上传成功(未解压)",
|
||||
zap.String("componentID", componentID),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("filePath", filePath))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateFolderByCode 根据组件编码创建文件夹
|
||||
func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (string, error) {
|
||||
folderPath := filepath.Join(s.basePath, componentCode)
|
||||
|
||||
// 创建文件夹(如果不存在)
|
||||
if err := os.MkdirAll(folderPath, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
return folderPath, nil
|
||||
}
|
||||
|
||||
// DeleteFolder 删除组件文件夹
|
||||
func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error {
|
||||
// 记录尝试删除的文件夹路径
|
||||
s.logger.Info("尝试删除文件夹", zap.String("folderPath", folderPath))
|
||||
|
||||
// 获取文件夹信息,用于调试
|
||||
if info, err := os.Stat(folderPath); err == nil {
|
||||
s.logger.Info("文件夹信息",
|
||||
zap.String("folderPath", folderPath),
|
||||
zap.Bool("isDir", info.IsDir()),
|
||||
zap.Int64("size", info.Size()),
|
||||
zap.Time("modTime", info.ModTime()))
|
||||
} else {
|
||||
s.logger.Error("获取文件夹信息失败",
|
||||
zap.Error(err),
|
||||
zap.String("folderPath", folderPath))
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if !s.FolderExists(folderPath) {
|
||||
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
|
||||
return nil // 文件夹不存在,不视为错误
|
||||
}
|
||||
|
||||
// 尝试删除文件夹
|
||||
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
|
||||
if err := os.RemoveAll(folderPath); err != nil {
|
||||
s.logger.Error("删除文件夹失败",
|
||||
zap.Error(err),
|
||||
zap.String("folderPath", folderPath))
|
||||
return fmt.Errorf("删除文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除组件文件夹成功", zap.String("folderPath", folderPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FolderExists 检查文件夹是否存在
|
||||
func (s *UIComponentFileServiceImpl) FolderExists(folderPath string) bool {
|
||||
info, err := os.Stat(folderPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// GetFolderContent 获取文件夹内容
|
||||
func (s *UIComponentFileServiceImpl) GetFolderContent(folderPath string) ([]FileInfo, error) {
|
||||
var files []FileInfo
|
||||
|
||||
err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 跳过根目录
|
||||
if path == folderPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取相对路径
|
||||
relPath, err := filepath.Rel(folderPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileType := "file"
|
||||
if info.IsDir() {
|
||||
fileType = "folder"
|
||||
}
|
||||
|
||||
files = append(files, FileInfo{
|
||||
Name: info.Name(),
|
||||
Path: relPath,
|
||||
Size: info.Size(),
|
||||
Type: fileType,
|
||||
Modified: info.ModTime(),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// extractZipFile 解压ZIP文件
|
||||
func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) error {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开ZIP文件失败: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, file := range reader.File {
|
||||
path := filepath.Join(destPath, file.Name)
|
||||
|
||||
// 防止路径遍历攻击
|
||||
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(destPath)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("无效的文件路径: %s", file.Name)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
// 创建目录
|
||||
if err := os.MkdirAll(path, file.Mode()); err != nil {
|
||||
return fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开ZIP内文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 确保父目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
fileReader.Close()
|
||||
return fmt.Errorf("创建父目录失败: %w", err)
|
||||
}
|
||||
|
||||
destFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
fileReader.Close()
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(destFile, fileReader)
|
||||
fileReader.Close()
|
||||
destFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFilesByComponentCode 根据组件编码和上传时间智能删除组件相关文件
|
||||
func (s *UIComponentFileServiceImpl) DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error {
|
||||
// 记录基础路径和组件编码
|
||||
s.logger.Info("开始删除组件文件",
|
||||
zap.String("basePath", s.basePath),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.Any("uploadTime", uploadTime))
|
||||
|
||||
// 1. 查找名为组件编码的文件夹
|
||||
componentDir := filepath.Join(s.basePath, componentCode)
|
||||
s.logger.Info("检查组件文件夹", zap.String("componentDir", componentDir))
|
||||
|
||||
if s.FolderExists(componentDir) {
|
||||
s.logger.Info("找到组件文件夹,开始删除", zap.String("componentDir", componentDir))
|
||||
if err := s.DeleteFolder(componentDir); err != nil {
|
||||
s.logger.Error("删除组件文件夹失败",
|
||||
zap.Error(err),
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.String("componentDir", componentDir))
|
||||
return fmt.Errorf("删除组件文件夹失败: %w", err)
|
||||
}
|
||||
s.logger.Info("成功删除组件文件夹", zap.String("componentCode", componentCode))
|
||||
return nil
|
||||
} else {
|
||||
s.logger.Info("组件文件夹不存在", zap.String("componentDir", componentDir))
|
||||
}
|
||||
|
||||
// 2. 查找文件名包含组件编码的文件
|
||||
pattern := filepath.Join(s.basePath, "*"+componentCode+"*")
|
||||
s.logger.Info("查找匹配文件", zap.String("pattern", pattern))
|
||||
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
s.logger.Error("查找组件文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("pattern", pattern))
|
||||
return fmt.Errorf("查找组件文件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("找到匹配文件",
|
||||
zap.Strings("files", files),
|
||||
zap.Int("count", len(files)))
|
||||
|
||||
// 3. 如果没有上传时间,删除所有匹配的文件
|
||||
if uploadTime == nil {
|
||||
for _, file := range files {
|
||||
if err := os.Remove(file); err != nil {
|
||||
s.logger.Error("删除文件失败", zap.String("file", file), zap.Error(err))
|
||||
} else {
|
||||
s.logger.Info("成功删除文件", zap.String("file", file))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 如果有上传时间,根据文件修改时间和上传时间的匹配度来删除文件
|
||||
var deletedFiles []string
|
||||
for _, file := range files {
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取文件信息失败", zap.String("file", file), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算文件修改时间与上传时间的差异(以秒为单位)
|
||||
timeDiff := fileInfo.ModTime().Sub(*uploadTime).Seconds()
|
||||
|
||||
// 如果时间差在60秒内,认为是最匹配的文件
|
||||
if timeDiff < 60 && timeDiff > -60 {
|
||||
if err := os.Remove(file); err != nil {
|
||||
s.logger.Warn("删除文件失败", zap.String("file", file), zap.Error(err))
|
||||
} else {
|
||||
deletedFiles = append(deletedFiles, file)
|
||||
s.logger.Info("成功删除文件", zap.String("file", file),
|
||||
zap.Time("uploadTime", *uploadTime),
|
||||
zap.Time("fileModTime", fileInfo.ModTime()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到匹配的文件,记录警告但返回成功
|
||||
if len(deletedFiles) == 0 && len(files) > 0 {
|
||||
s.logger.Warn("没有找到匹配时间戳的文件",
|
||||
zap.String("componentCode", componentCode),
|
||||
zap.Time("uploadTime", *uploadTime),
|
||||
zap.Int("foundFiles", len(files)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
412
internal/application/statistics/commands_queries.go
Normal file
412
internal/application/statistics/commands_queries.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ================ 命令对象 ================
|
||||
|
||||
// CreateMetricCommand 创建指标命令
|
||||
type CreateMetricCommand struct {
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" validate:"required" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
Value float64 `json:"value" validate:"min=0" comment:"指标值"`
|
||||
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `json:"date" validate:"required" comment:"统计日期"`
|
||||
}
|
||||
|
||||
// UpdateMetricCommand 更新指标命令
|
||||
type UpdateMetricCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||
Value float64 `json:"value" validate:"min=0" comment:"新指标值"`
|
||||
}
|
||||
|
||||
// DeleteMetricCommand 删除指标命令
|
||||
type DeleteMetricCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||
}
|
||||
|
||||
// GenerateReportCommand 生成报告命令
|
||||
type GenerateReportCommand struct {
|
||||
ReportType string `json:"report_type" validate:"required" comment:"报告类型"`
|
||||
Title string `json:"title" validate:"required" comment:"报告标题"`
|
||||
Period string `json:"period" validate:"required" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
GeneratedBy string `json:"generated_by" validate:"required" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// CreateDashboardCommand 创建仪表板命令
|
||||
type CreateDashboardCommand struct {
|
||||
Name string `json:"name" validate:"required" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" validate:"required" comment:"创建者ID"`
|
||||
}
|
||||
|
||||
// UpdateDashboardCommand 更新仪表板命令
|
||||
type UpdateDashboardCommand struct {
|
||||
ID string `json:"id" validate:"required" comment:"仪表板ID"`
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||
}
|
||||
|
||||
// SetDefaultDashboardCommand 设置默认仪表板命令
|
||||
type SetDefaultDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||
}
|
||||
|
||||
// ActivateDashboardCommand 激活仪表板命令
|
||||
type ActivateDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
ActivatedBy string `json:"activated_by" validate:"required" comment:"激活者ID"`
|
||||
}
|
||||
|
||||
// DeactivateDashboardCommand 停用仪表板命令
|
||||
type DeactivateDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
DeactivatedBy string `json:"deactivated_by" validate:"required" comment:"停用者ID"`
|
||||
}
|
||||
|
||||
// DeleteDashboardCommand 删除仪表板命令
|
||||
type DeleteDashboardCommand struct {
|
||||
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
DeletedBy string `json:"deleted_by" validate:"required" comment:"删除者ID"`
|
||||
}
|
||||
|
||||
// ExportDataCommand 导出数据命令
|
||||
type ExportDataCommand struct {
|
||||
Format string `json:"format" validate:"required" comment:"导出格式"`
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
StartDate time.Time `json:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" validate:"required" comment:"结束日期"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
GroupBy string `json:"group_by" comment:"分组维度"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
Columns []string `json:"columns" comment:"导出列"`
|
||||
IncludeCharts bool `json:"include_charts" comment:"是否包含图表"`
|
||||
ExportedBy string `json:"exported_by" validate:"required" comment:"导出者ID"`
|
||||
}
|
||||
|
||||
// TriggerAggregationCommand 触发数据聚合命令
|
||||
type TriggerAggregationCommand struct {
|
||||
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||
Period string `json:"period" validate:"required" comment:"聚合周期"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Force bool `json:"force" comment:"是否强制重新聚合"`
|
||||
TriggeredBy string `json:"triggered_by" validate:"required" comment:"触发者ID"`
|
||||
}
|
||||
|
||||
// Validate 验证触发聚合命令
|
||||
func (c *TriggerAggregationCommand) Validate() error {
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.Period == "" {
|
||||
return fmt.Errorf("聚合周期不能为空")
|
||||
}
|
||||
if c.TriggeredBy == "" {
|
||||
return fmt.Errorf("触发者ID不能为空")
|
||||
}
|
||||
// 验证周期类型
|
||||
validPeriods := []string{"hourly", "daily", "weekly", "monthly"}
|
||||
isValidPeriod := false
|
||||
for _, period := range validPeriods {
|
||||
if c.Period == period {
|
||||
isValidPeriod = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValidPeriod {
|
||||
return fmt.Errorf("不支持的聚合周期: %s", c.Period)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ================ 查询对象 ================
|
||||
|
||||
// GetMetricsQuery 获取指标查询
|
||||
type GetMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// GetRealtimeMetricsQuery 获取实时指标查询
|
||||
type GetRealtimeMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
TimeRange string `json:"time_range" form:"time_range" comment:"时间范围"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
}
|
||||
|
||||
// GetHistoricalMetricsQuery 获取历史指标查询
|
||||
type GetHistoricalMetricsQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
AggregateBy string `json:"aggregate_by" form:"aggregate_by" comment:"聚合维度"`
|
||||
GroupBy string `json:"group_by" form:"group_by" comment:"分组维度"`
|
||||
}
|
||||
|
||||
// GetDashboardDataQuery 获取仪表板数据查询
|
||||
type GetDashboardDataQuery struct {
|
||||
UserRole string `json:"user_role" form:"user_role" validate:"required" comment:"用户角色"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
MetricTypes []string `json:"metric_types" form:"metric_types" comment:"指标类型列表"`
|
||||
Dimensions []string `json:"dimensions" form:"dimensions" comment:"统计维度列表"`
|
||||
}
|
||||
|
||||
// GetReportsQuery 获取报告查询
|
||||
type GetReportsQuery struct {
|
||||
ReportType string `json:"report_type" form:"report_type" comment:"报告类型"`
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
Status string `json:"status" form:"status" comment:"报告状态"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
GeneratedBy string `json:"generated_by" form:"generated_by" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// GetDashboardsQuery 获取仪表板查询
|
||||
type GetDashboardsQuery struct {
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
IsDefault *bool `json:"is_default" form:"is_default" comment:"是否默认"`
|
||||
IsActive *bool `json:"is_active" form:"is_active" comment:"是否激活"`
|
||||
AccessLevel string `json:"access_level" form:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" form:"created_by" comment:"创建者ID"`
|
||||
Name string `json:"name" form:"name" comment:"仪表板名称"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// GetReportQuery 获取单个报告查询
|
||||
type GetReportQuery struct {
|
||||
ReportID string `json:"report_id" form:"report_id" validate:"required" comment:"报告ID"`
|
||||
}
|
||||
|
||||
// GetDashboardQuery 获取单个仪表板查询
|
||||
type GetDashboardQuery struct {
|
||||
DashboardID string `json:"dashboard_id" form:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||
}
|
||||
|
||||
// GetMetricQuery 获取单个指标查询
|
||||
type GetMetricQuery struct {
|
||||
MetricID string `json:"metric_id" form:"metric_id" validate:"required" comment:"指标ID"`
|
||||
}
|
||||
|
||||
// CalculateGrowthRateQuery 计算增长率查询
|
||||
type CalculateGrowthRateQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
CurrentPeriod time.Time `json:"current_period" form:"current_period" validate:"required" comment:"当前周期"`
|
||||
PreviousPeriod time.Time `json:"previous_period" form:"previous_period" validate:"required" comment:"上一周期"`
|
||||
}
|
||||
|
||||
// CalculateTrendQuery 计算趋势查询
|
||||
type CalculateTrendQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// CalculateCorrelationQuery 计算相关性查询
|
||||
type CalculateCorrelationQuery struct {
|
||||
MetricType1 string `json:"metric_type1" form:"metric_type1" validate:"required" comment:"指标类型1"`
|
||||
MetricName1 string `json:"metric_name1" form:"metric_name1" validate:"required" comment:"指标名称1"`
|
||||
MetricType2 string `json:"metric_type2" form:"metric_type2" validate:"required" comment:"指标类型2"`
|
||||
MetricName2 string `json:"metric_name2" form:"metric_name2" validate:"required" comment:"指标名称2"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// CalculateMovingAverageQuery 计算移动平均查询
|
||||
type CalculateMovingAverageQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
WindowSize int `json:"window_size" form:"window_size" validate:"min=1" comment:"窗口大小"`
|
||||
}
|
||||
|
||||
// CalculateSeasonalityQuery 计算季节性查询
|
||||
type CalculateSeasonalityQuery struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||
}
|
||||
|
||||
// ================ 响应对象 ================
|
||||
|
||||
// CommandResponse 命令响应
|
||||
type CommandResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// QueryResponse 查询响应
|
||||
type QueryResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// ListResponse 列表响应
|
||||
type ListResponse struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data ListDataDTO `json:"data" comment:"数据列表"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// ListDataDTO 列表数据DTO
|
||||
type ListDataDTO struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []interface{} `json:"items" comment:"数据列表"`
|
||||
}
|
||||
|
||||
// ================ 验证方法 ================
|
||||
|
||||
// Validate 验证创建指标命令
|
||||
func (c *CreateMetricCommand) Validate() error {
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.MetricName == "" {
|
||||
return fmt.Errorf("指标名称不能为空")
|
||||
}
|
||||
if c.Value < 0 {
|
||||
return fmt.Errorf("指标值不能为负数")
|
||||
}
|
||||
if c.Date.IsZero() {
|
||||
return fmt.Errorf("统计日期不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证更新指标命令
|
||||
func (c *UpdateMetricCommand) Validate() error {
|
||||
if c.ID == "" {
|
||||
return fmt.Errorf("指标ID不能为空")
|
||||
}
|
||||
if c.Value < 0 {
|
||||
return fmt.Errorf("指标值不能为负数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证生成报告命令
|
||||
func (c *GenerateReportCommand) Validate() error {
|
||||
if c.ReportType == "" {
|
||||
return fmt.Errorf("报告类型不能为空")
|
||||
}
|
||||
if c.Title == "" {
|
||||
return fmt.Errorf("报告标题不能为空")
|
||||
}
|
||||
if c.Period == "" {
|
||||
return fmt.Errorf("统计周期不能为空")
|
||||
}
|
||||
if c.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if c.GeneratedBy == "" {
|
||||
return fmt.Errorf("生成者ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证创建仪表板命令
|
||||
func (c *CreateDashboardCommand) Validate() error {
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("仪表板名称不能为空")
|
||||
}
|
||||
if c.UserRole == "" {
|
||||
return fmt.Errorf("用户角色不能为空")
|
||||
}
|
||||
if c.CreatedBy == "" {
|
||||
return fmt.Errorf("创建者ID不能为空")
|
||||
}
|
||||
if c.RefreshInterval < 30 {
|
||||
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证更新仪表板命令
|
||||
func (c *UpdateDashboardCommand) Validate() error {
|
||||
if c.ID == "" {
|
||||
return fmt.Errorf("仪表板ID不能为空")
|
||||
}
|
||||
if c.UpdatedBy == "" {
|
||||
return fmt.Errorf("更新者ID不能为空")
|
||||
}
|
||||
if c.RefreshInterval < 30 {
|
||||
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证导出数据命令
|
||||
func (c *ExportDataCommand) Validate() error {
|
||||
if c.Format == "" {
|
||||
return fmt.Errorf("导出格式不能为空")
|
||||
}
|
||||
if c.MetricType == "" {
|
||||
return fmt.Errorf("指标类型不能为空")
|
||||
}
|
||||
if c.StartDate.IsZero() || c.EndDate.IsZero() {
|
||||
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||
}
|
||||
if c.StartDate.After(c.EndDate) {
|
||||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if c.ExportedBy == "" {
|
||||
return fmt.Errorf("导出者ID不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
258
internal/application/statistics/dtos.go
Normal file
258
internal/application/statistics/dtos.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsMetricDTO 统计指标DTO
|
||||
type StatisticsMetricDTO struct {
|
||||
ID string `json:"id" comment:"统计指标唯一标识"`
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
Value float64 `json:"value" comment:"指标值"`
|
||||
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||
Date time.Time `json:"date" comment:"统计日期"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// StatisticsReportDTO 统计报告DTO
|
||||
type StatisticsReportDTO struct {
|
||||
ID string `json:"id" comment:"报告唯一标识"`
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Content string `json:"content" comment:"报告内容"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
Status string `json:"status" comment:"报告状态"`
|
||||
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
|
||||
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// StatisticsDashboardDTO 统计仪表板DTO
|
||||
type StatisticsDashboardDTO struct {
|
||||
ID string `json:"id" comment:"仪表板唯一标识"`
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
IsDefault bool `json:"is_default" comment:"是否为默认仪表板"`
|
||||
IsActive bool `json:"is_active" comment:"是否激活"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// DashboardDataDTO 仪表板数据DTO
|
||||
type DashboardDataDTO struct {
|
||||
// API调用统计
|
||||
APICalls struct {
|
||||
TotalCount int64 `json:"total_count" comment:"总调用次数"`
|
||||
SuccessCount int64 `json:"success_count" comment:"成功调用次数"`
|
||||
FailedCount int64 `json:"failed_count" comment:"失败调用次数"`
|
||||
SuccessRate float64 `json:"success_rate" comment:"成功率"`
|
||||
AvgResponseTime float64 `json:"avg_response_time" comment:"平均响应时间"`
|
||||
} `json:"api_calls"`
|
||||
|
||||
// 用户统计
|
||||
Users struct {
|
||||
TotalCount int64 `json:"total_count" comment:"总用户数"`
|
||||
CertifiedCount int64 `json:"certified_count" comment:"认证用户数"`
|
||||
ActiveCount int64 `json:"active_count" comment:"活跃用户数"`
|
||||
CertificationRate float64 `json:"certification_rate" comment:"认证完成率"`
|
||||
RetentionRate float64 `json:"retention_rate" comment:"留存率"`
|
||||
} `json:"users"`
|
||||
|
||||
// 财务统计
|
||||
Finance struct {
|
||||
TotalAmount float64 `json:"total_amount" comment:"总金额"`
|
||||
RechargeAmount float64 `json:"recharge_amount" comment:"充值金额"`
|
||||
DeductAmount float64 `json:"deduct_amount" comment:"扣款金额"`
|
||||
NetAmount float64 `json:"net_amount" comment:"净金额"`
|
||||
} `json:"finance"`
|
||||
|
||||
// 产品统计
|
||||
Products struct {
|
||||
TotalProducts int64 `json:"total_products" comment:"总产品数"`
|
||||
ActiveProducts int64 `json:"active_products" comment:"活跃产品数"`
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"总订阅数"`
|
||||
ActiveSubscriptions int64 `json:"active_subscriptions" comment:"活跃订阅数"`
|
||||
} `json:"products"`
|
||||
|
||||
// 认证统计
|
||||
Certification struct {
|
||||
TotalCertifications int64 `json:"total_certifications" comment:"总认证数"`
|
||||
CompletedCertifications int64 `json:"completed_certifications" comment:"完成认证数"`
|
||||
PendingCertifications int64 `json:"pending_certifications" comment:"待处理认证数"`
|
||||
FailedCertifications int64 `json:"failed_certifications" comment:"失败认证数"`
|
||||
CompletionRate float64 `json:"completion_rate" comment:"完成率"`
|
||||
} `json:"certification"`
|
||||
|
||||
// 时间信息
|
||||
Period struct {
|
||||
StartDate string `json:"start_date" comment:"开始日期"`
|
||||
EndDate string `json:"end_date" comment:"结束日期"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
} `json:"period"`
|
||||
|
||||
// 元数据
|
||||
Metadata struct {
|
||||
GeneratedAt string `json:"generated_at" comment:"生成时间"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
DataVersion string `json:"data_version" comment:"数据版本"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
// RealtimeMetricsDTO 实时指标DTO
|
||||
type RealtimeMetricsDTO struct {
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
Metrics map[string]float64 `json:"metrics" comment:"指标数据"`
|
||||
Timestamp time.Time `json:"timestamp" comment:"时间戳"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// HistoricalMetricsDTO 历史指标DTO
|
||||
type HistoricalMetricsDTO struct {
|
||||
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" comment:"统计维度"`
|
||||
DataPoints []DataPointDTO `json:"data_points" comment:"数据点"`
|
||||
Summary MetricsSummaryDTO `json:"summary" comment:"汇总信息"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// DataPointDTO 数据点DTO
|
||||
type DataPointDTO struct {
|
||||
Date time.Time `json:"date" comment:"日期"`
|
||||
Value float64 `json:"value" comment:"值"`
|
||||
Label string `json:"label" comment:"标签"`
|
||||
}
|
||||
|
||||
// MetricsSummaryDTO 指标汇总DTO
|
||||
type MetricsSummaryDTO struct {
|
||||
Total float64 `json:"total" comment:"总值"`
|
||||
Average float64 `json:"average" comment:"平均值"`
|
||||
Max float64 `json:"max" comment:"最大值"`
|
||||
Min float64 `json:"min" comment:"最小值"`
|
||||
Count int64 `json:"count" comment:"数据点数量"`
|
||||
GrowthRate float64 `json:"growth_rate" comment:"增长率"`
|
||||
Trend string `json:"trend" comment:"趋势"`
|
||||
}
|
||||
|
||||
// ReportContentDTO 报告内容DTO
|
||||
type ReportContentDTO struct {
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||
Details map[string]interface{} `json:"details" comment:"详细信息"`
|
||||
Charts []ChartDTO `json:"charts" comment:"图表数据"`
|
||||
Tables []TableDTO `json:"tables" comment:"表格数据"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
}
|
||||
|
||||
// ChartDTO 图表DTO
|
||||
type ChartDTO struct {
|
||||
Type string `json:"type" comment:"图表类型"`
|
||||
Title string `json:"title" comment:"图表标题"`
|
||||
Data map[string]interface{} `json:"data" comment:"图表数据"`
|
||||
Options map[string]interface{} `json:"options" comment:"图表选项"`
|
||||
Description string `json:"description" comment:"图表描述"`
|
||||
}
|
||||
|
||||
// TableDTO 表格DTO
|
||||
type TableDTO struct {
|
||||
Title string `json:"title" comment:"表格标题"`
|
||||
Headers []string `json:"headers" comment:"表头"`
|
||||
Rows [][]interface{} `json:"rows" comment:"表格行数据"`
|
||||
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||
Description string `json:"description" comment:"表格描述"`
|
||||
}
|
||||
|
||||
// ExportDataDTO 导出数据DTO
|
||||
type ExportDataDTO struct {
|
||||
Format string `json:"format" comment:"导出格式"`
|
||||
FileName string `json:"file_name" comment:"文件名"`
|
||||
Data []map[string]interface{} `json:"data" comment:"导出数据"`
|
||||
Headers []string `json:"headers" comment:"表头"`
|
||||
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||
DownloadURL string `json:"download_url" comment:"下载链接"`
|
||||
}
|
||||
|
||||
// StatisticsQueryDTO 统计查询DTO
|
||||
type StatisticsQueryDTO struct {
|
||||
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||
}
|
||||
|
||||
// ReportGenerationDTO 报告生成DTO
|
||||
type ReportGenerationDTO struct {
|
||||
ReportType string `json:"report_type" comment:"报告类型"`
|
||||
Title string `json:"title" comment:"报告标题"`
|
||||
Period string `json:"period" comment:"统计周期"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||
Format string `json:"format" comment:"输出格式"`
|
||||
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||
}
|
||||
|
||||
// DashboardConfigDTO 仪表板配置DTO
|
||||
type DashboardConfigDTO struct {
|
||||
Name string `json:"name" comment:"仪表板名称"`
|
||||
Description string `json:"description" comment:"仪表板描述"`
|
||||
UserRole string `json:"user_role" comment:"用户角色"`
|
||||
Layout string `json:"layout" comment:"布局配置"`
|
||||
Widgets string `json:"widgets" comment:"组件配置"`
|
||||
Settings string `json:"settings" comment:"设置配置"`
|
||||
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||
}
|
||||
|
||||
// StatisticsResponseDTO 统计响应DTO
|
||||
type StatisticsResponseDTO struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data interface{} `json:"data" comment:"响应数据"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
// PaginationDTO 分页DTO
|
||||
type PaginationDTO struct {
|
||||
Page int `json:"page" comment:"当前页"`
|
||||
PageSize int `json:"page_size" comment:"每页大小"`
|
||||
Total int64 `json:"total" comment:"总数量"`
|
||||
Pages int `json:"pages" comment:"总页数"`
|
||||
HasNext bool `json:"has_next" comment:"是否有下一页"`
|
||||
HasPrev bool `json:"has_prev" comment:"是否有上一页"`
|
||||
}
|
||||
|
||||
// StatisticsListResponseDTO 统计列表响应DTO
|
||||
type StatisticsListResponseDTO struct {
|
||||
Success bool `json:"success" comment:"是否成功"`
|
||||
Message string `json:"message" comment:"响应消息"`
|
||||
Data []interface{} `json:"data" comment:"数据列表"`
|
||||
Pagination PaginationDTO `json:"pagination" comment:"分页信息"`
|
||||
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsApplicationService 统计应用服务接口
|
||||
// 负责统计功能的业务逻辑编排和协调
|
||||
type StatisticsApplicationService interface {
|
||||
// ================ 指标管理 ================
|
||||
|
||||
// CreateMetric 创建统计指标
|
||||
CreateMetric(ctx context.Context, cmd *CreateMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// UpdateMetric 更新统计指标
|
||||
UpdateMetric(ctx context.Context, cmd *UpdateMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// DeleteMetric 删除统计指标
|
||||
DeleteMetric(ctx context.Context, cmd *DeleteMetricCommand) (*CommandResponse, error)
|
||||
|
||||
// GetMetric 获取单个指标
|
||||
GetMetric(ctx context.Context, query *GetMetricQuery) (*QueryResponse, error)
|
||||
|
||||
// GetMetrics 获取指标列表
|
||||
GetMetrics(ctx context.Context, query *GetMetricsQuery) (*ListResponse, error)
|
||||
|
||||
// ================ 实时统计 ================
|
||||
|
||||
// GetRealtimeMetrics 获取实时指标
|
||||
GetRealtimeMetrics(ctx context.Context, query *GetRealtimeMetricsQuery) (*QueryResponse, error)
|
||||
|
||||
// UpdateRealtimeMetric 更新实时指标
|
||||
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
|
||||
|
||||
// ================ 历史统计 ================
|
||||
|
||||
// GetHistoricalMetrics 获取历史指标
|
||||
GetHistoricalMetrics(ctx context.Context, query *GetHistoricalMetricsQuery) (*QueryResponse, error)
|
||||
|
||||
// AggregateMetrics 聚合指标
|
||||
AggregateMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) error
|
||||
|
||||
// ================ 仪表板管理 ================
|
||||
|
||||
// CreateDashboard 创建仪表板
|
||||
CreateDashboard(ctx context.Context, cmd *CreateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// UpdateDashboard 更新仪表板
|
||||
UpdateDashboard(ctx context.Context, cmd *UpdateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// DeleteDashboard 删除仪表板
|
||||
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// GetDashboard 获取单个仪表板
|
||||
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*QueryResponse, error)
|
||||
|
||||
// GetDashboards 获取仪表板列表
|
||||
GetDashboards(ctx context.Context, query *GetDashboardsQuery) (*ListResponse, error)
|
||||
|
||||
// SetDefaultDashboard 设置默认仪表板
|
||||
SetDefaultDashboard(ctx context.Context, cmd *SetDefaultDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// ActivateDashboard 激活仪表板
|
||||
ActivateDashboard(ctx context.Context, cmd *ActivateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// DeactivateDashboard 停用仪表板
|
||||
DeactivateDashboard(ctx context.Context, cmd *DeactivateDashboardCommand) (*CommandResponse, error)
|
||||
|
||||
// GetDashboardData 获取仪表板数据
|
||||
GetDashboardData(ctx context.Context, query *GetDashboardDataQuery) (*QueryResponse, error)
|
||||
|
||||
// ================ 报告管理 ================
|
||||
|
||||
// GenerateReport 生成报告
|
||||
GenerateReport(ctx context.Context, cmd *GenerateReportCommand) (*CommandResponse, error)
|
||||
|
||||
// GetReport 获取单个报告
|
||||
GetReport(ctx context.Context, query *GetReportQuery) (*QueryResponse, error)
|
||||
|
||||
// GetReports 获取报告列表
|
||||
GetReports(ctx context.Context, query *GetReportsQuery) (*ListResponse, error)
|
||||
|
||||
// DeleteReport 删除报告
|
||||
DeleteReport(ctx context.Context, reportID string) (*CommandResponse, error)
|
||||
|
||||
// ================ 统计分析 ================
|
||||
|
||||
// CalculateGrowthRate 计算增长率
|
||||
CalculateGrowthRate(ctx context.Context, query *CalculateGrowthRateQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateTrend 计算趋势
|
||||
CalculateTrend(ctx context.Context, query *CalculateTrendQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateCorrelation 计算相关性
|
||||
CalculateCorrelation(ctx context.Context, query *CalculateCorrelationQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateMovingAverage 计算移动平均
|
||||
CalculateMovingAverage(ctx context.Context, query *CalculateMovingAverageQuery) (*QueryResponse, error)
|
||||
|
||||
// CalculateSeasonality 计算季节性
|
||||
CalculateSeasonality(ctx context.Context, query *CalculateSeasonalityQuery) (*QueryResponse, error)
|
||||
|
||||
// ================ 数据导出 ================
|
||||
|
||||
// ExportData 导出数据
|
||||
ExportData(ctx context.Context, cmd *ExportDataCommand) (*CommandResponse, error)
|
||||
|
||||
// ================ 定时任务 ================
|
||||
|
||||
// ProcessHourlyAggregation 处理小时级聚合
|
||||
ProcessHourlyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessDailyAggregation 处理日级聚合
|
||||
ProcessDailyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessWeeklyAggregation 处理周级聚合
|
||||
ProcessWeeklyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// ProcessMonthlyAggregation 处理月级聚合
|
||||
ProcessMonthlyAggregation(ctx context.Context, date time.Time) error
|
||||
|
||||
// CleanupExpiredData 清理过期数据
|
||||
CleanupExpiredData(ctx context.Context) error
|
||||
|
||||
// ================ 管理员专用方法 ================
|
||||
|
||||
// AdminGetSystemStatistics 管理员获取系统统计
|
||||
AdminGetSystemStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminTriggerAggregation 管理员触发数据聚合
|
||||
AdminTriggerAggregation(ctx context.Context, cmd *TriggerAggregationCommand) (*CommandResponse, error)
|
||||
|
||||
// AdminGetUserStatistics 管理员获取单个用户统计
|
||||
AdminGetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||
|
||||
// ================ 管理员独立域统计接口 ================
|
||||
|
||||
// AdminGetUserDomainStatistics 管理员获取用户域统计
|
||||
AdminGetUserDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetApiDomainStatistics 管理员获取API域统计
|
||||
AdminGetApiDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetConsumptionDomainStatistics 管理员获取消费域统计
|
||||
AdminGetConsumptionDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// AdminGetRechargeDomainStatistics 管理员获取充值域统计
|
||||
AdminGetRechargeDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||
|
||||
// ================ 公开和用户统计方法 ================
|
||||
|
||||
// GetPublicStatistics 获取公开统计信息
|
||||
GetPublicStatistics(ctx context.Context) (*QueryResponse, error)
|
||||
|
||||
// GetUserStatistics 获取用户统计信息
|
||||
GetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||
|
||||
// ================ 独立统计接口 ================
|
||||
|
||||
// GetApiCallsStatistics 获取API调用统计
|
||||
GetApiCallsStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetConsumptionStatistics 获取消费统计
|
||||
GetConsumptionStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetRechargeStatistics 获取充值统计
|
||||
GetRechargeStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||
|
||||
// GetLatestProducts 获取最新产品推荐
|
||||
GetLatestProducts(ctx context.Context, limit int) (*QueryResponse, error)
|
||||
|
||||
// ================ 管理员排行榜接口 ================
|
||||
|
||||
// AdminGetUserCallRanking 获取用户调用排行榜
|
||||
AdminGetUserCallRanking(ctx context.Context, rankingType, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetRechargeRanking 获取充值排行榜
|
||||
AdminGetRechargeRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetApiPopularityRanking 获取API受欢迎程度排行榜
|
||||
AdminGetApiPopularityRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||
|
||||
// AdminGetTodayCertifiedEnterprises 获取今日认证企业列表
|
||||
AdminGetTodayCertifiedEnterprises(ctx context.Context, limit int) (*QueryResponse, error)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
77
internal/application/user/dto/commands/user_commands.go
Normal file
77
internal/application/user/dto/commands/user_commands.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package commands
|
||||
|
||||
// RegisterUserCommand 用户注册命令
|
||||
// @Description 用户注册请求参数
|
||||
type RegisterUserCommand struct {
|
||||
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,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,phone" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// ChangePasswordCommand 修改密码命令
|
||||
// @Description 修改用户密码请求参数
|
||||
type ChangePasswordCommand struct {
|
||||
UserID string `json:"-"`
|
||||
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,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 发送短信验证码请求参数。只接收编码后的data字段(使用自定义编码方案,非Base64)
|
||||
type SendCodeCommand struct {
|
||||
// 编码后的数据(使用自定义编码方案的JSON字符串,包含所有参数:phone, scene, timestamp, nonce, signature)
|
||||
Data string `json:"data" binding:"required" example:"K8mN9vP2sL7kH3oB6yC1zA5uF0qE9tW..."` // 自定义编码后的数据
|
||||
|
||||
// 阿里云滑块验证码参数(直接接收,不参与编码)
|
||||
CaptchaVerifyParam string `json:"captchaVerifyParam,omitempty" example:"..."` // 滑块验证码验证参数
|
||||
|
||||
// 以下字段从data解码后填充,不直接接收
|
||||
Phone string `json:"-"` // 从data解码后获取
|
||||
Scene string `json:"-"` // 从data解码后获取
|
||||
Timestamp int64 `json:"-"` // 从data解码后获取
|
||||
Nonce string `json:"-"` // 从data解码后获取
|
||||
Signature string `json:"-"` // 从data解码后获取
|
||||
}
|
||||
|
||||
// UpdateProfileCommand 更新用户信息命令
|
||||
// @Description 更新用户基本信息请求参数
|
||||
type UpdateProfileCommand struct {
|
||||
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,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 certification" example:"register"`
|
||||
}
|
||||
6
internal/application/user/dto/queries/get_user_query.go
Normal file
6
internal/application/user/dto/queries/get_user_query.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetUserQuery 获取用户信息查询
|
||||
type GetUserQuery struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
31
internal/application/user/dto/queries/list_users_query.go
Normal file
31
internal/application/user/dto/queries/list_users_query.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package queries
|
||||
|
||||
import "hyapi-server/internal/domains/user/repositories/queries"
|
||||
|
||||
// ListUsersQuery 用户列表查询DTO
|
||||
type ListUsersQuery struct {
|
||||
Page int `json:"page" validate:"min=1"`
|
||||
PageSize int `json:"page_size" validate:"min=1,max=100"`
|
||||
Phone string `json:"phone"`
|
||||
UserType string `json:"user_type"` // 用户类型: user/admin
|
||||
IsActive *bool `json:"is_active"` // 是否激活
|
||||
IsCertified *bool `json:"is_certified"` // 是否已认证
|
||||
CompanyName string `json:"company_name"` // 企业名称
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
|
||||
// ToDomainQuery 转换为领域查询对象
|
||||
func (q *ListUsersQuery) ToDomainQuery() *queries.ListUsersQuery {
|
||||
return &queries.ListUsersQuery{
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
Phone: q.Phone,
|
||||
UserType: q.UserType,
|
||||
IsActive: q.IsActive,
|
||||
IsCertified: q.IsCertified,
|
||||
CompanyName: q.CompanyName,
|
||||
StartDate: q.StartDate,
|
||||
EndDate: q.EndDate,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package responses
|
||||
|
||||
import "time"
|
||||
|
||||
// UserListItem 用户列表项
|
||||
type UserListItem struct {
|
||||
ID string `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
UserType string `json:"user_type"`
|
||||
Username string `json:"username"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsCertified bool `json:"is_certified"`
|
||||
LoginCount int `json:"login_count"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// 企业信息
|
||||
EnterpriseInfo *EnterpriseInfoItem `json:"enterprise_info,omitempty"`
|
||||
|
||||
// 钱包信息
|
||||
WalletBalance string `json:"wallet_balance,omitempty"`
|
||||
}
|
||||
|
||||
// EnterpriseInfoItem 企业信息项
|
||||
type EnterpriseInfoItem struct {
|
||||
ID string `json:"id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
UnifiedSocialCode string `json:"unified_social_code"`
|
||||
LegalPersonName string `json:"legal_person_name"`
|
||||
LegalPersonPhone string `json:"legal_person_phone"`
|
||||
EnterpriseAddress string `json:"enterprise_address"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// 合同信息
|
||||
Contracts []*ContractInfoItem `json:"contracts,omitempty"`
|
||||
}
|
||||
|
||||
// ContractInfoItem 合同信息项
|
||||
type ContractInfoItem struct {
|
||||
ID string `json:"id"`
|
||||
ContractName string `json:"contract_name"`
|
||||
ContractType string `json:"contract_type"` // 合同类型代码
|
||||
ContractTypeName string `json:"contract_type_name"` // 合同类型中文名称
|
||||
ContractFileURL string `json:"contract_file_url"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Items []*UserListItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// UserStatsResponse 用户统计响应
|
||||
type UserStatsResponse struct {
|
||||
TotalUsers int64 `json:"total_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
CertifiedUsers int64 `json:"certified_users"`
|
||||
}
|
||||
|
||||
// UserDetailResponse 用户详情响应
|
||||
type UserDetailResponse struct {
|
||||
*UserListItem
|
||||
}
|
||||
69
internal/application/user/dto/responses/user_responses.go
Normal file
69
internal/application/user/dto/responses/user_responses.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RegisterUserResponse 用户注册响应
|
||||
// @Description 用户注册成功响应
|
||||
type RegisterUserResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
}
|
||||
|
||||
// EnterpriseInfoResponse 企业信息响应
|
||||
// @Description 企业信息响应
|
||||
type EnterpriseInfoResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
CompanyName string `json:"company_name" example:"示例企业有限公司"`
|
||||
UnifiedSocialCode string `json:"unified_social_code" example:"91110000123456789X"`
|
||||
LegalPersonName string `json:"legal_person_name" example:"张三"`
|
||||
LegalPersonID string `json:"legal_person_id" example:"110101199001011234"`
|
||||
LegalPersonPhone string `json:"legal_person_phone" example:"13800138000"`
|
||||
EnterpriseAddress string `json:"enterprise_address" example:"北京市朝阳区xxx街道xxx号"`
|
||||
CertifiedAt *time.Time `json:"certified_at,omitempty" example:"2024-01-01T00:00:00Z"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// LoginUserResponse 用户登录响应
|
||||
// @Description 用户登录成功响应
|
||||
type LoginUserResponse struct {
|
||||
User *UserProfileResponse `json:"user"`
|
||||
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
||||
TokenType string `json:"token_type" example:"Bearer"`
|
||||
ExpiresIn int64 `json:"expires_in" example:"86400"`
|
||||
LoginMethod string `json:"login_method" example:"password"`
|
||||
}
|
||||
|
||||
// UserProfileResponse 用户信息响应
|
||||
// @Description 用户基本信息
|
||||
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"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// SendCodeResponse 发送验证码响应
|
||||
// @Description 发送短信验证码成功响应
|
||||
type SendCodeResponse struct {
|
||||
Message string `json:"message" example:"验证码发送成功"`
|
||||
ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"`
|
||||
}
|
||||
|
||||
// UpdateProfileResponse 更新用户信息响应
|
||||
// @Description 更新用户信息成功响应
|
||||
type UpdateProfileResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapi-server/internal/application/user/dto/commands"
|
||||
"hyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
func (s *UserApplicationServiceImpl) SendCode(ctx context.Context, cmd *commands.SendCodeCommand, clientIP, userAgent string) error {
|
||||
// 1. 检查频率限制
|
||||
if err := s.smsCodeService.CheckRateLimit(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), clientIP, userAgent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.smsCodeService.SendCode(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), clientIP, userAgent, cmd.CaptchaVerifyParam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
internal/application/user/user_application_service.go
Normal file
25
internal/application/user/user_application_service.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapi-server/internal/application/user/dto/commands"
|
||||
"hyapi-server/internal/application/user/dto/queries"
|
||||
"hyapi-server/internal/application/user/dto/responses"
|
||||
)
|
||||
|
||||
// UserApplicationService 用户应用服务接口
|
||||
type UserApplicationService interface {
|
||||
Register(ctx context.Context, cmd *commands.RegisterUserCommand) (*responses.RegisterUserResponse, error)
|
||||
LoginWithPassword(ctx context.Context, cmd *commands.LoginWithPasswordCommand) (*responses.LoginUserResponse, error)
|
||||
LoginWithSMS(ctx context.Context, cmd *commands.LoginWithSMSCommand) (*responses.LoginUserResponse, error)
|
||||
ChangePassword(ctx context.Context, cmd *commands.ChangePasswordCommand) error
|
||||
ResetPassword(ctx context.Context, cmd *commands.ResetPasswordCommand) error
|
||||
GetUserProfile(ctx context.Context, userID string) (*responses.UserProfileResponse, error)
|
||||
SendCode(ctx context.Context, cmd *commands.SendCodeCommand, clientIP, userAgent string) error
|
||||
|
||||
// 管理员功能
|
||||
ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error)
|
||||
GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error)
|
||||
GetUserStats(ctx context.Context) (*responses.UserStatsResponse, error)
|
||||
}
|
||||
461
internal/application/user/user_application_service_impl.go
Normal file
461
internal/application/user/user_application_service_impl.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/application/user/dto/commands"
|
||||
"hyapi-server/internal/application/user/dto/queries"
|
||||
"hyapi-server/internal/application/user/dto/responses"
|
||||
finance_service "hyapi-server/internal/domains/finance/services"
|
||||
"hyapi-server/internal/domains/user/entities"
|
||||
"hyapi-server/internal/domains/user/events"
|
||||
user_service "hyapi-server/internal/domains/user/services"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
"hyapi-server/internal/shared/middleware"
|
||||
)
|
||||
|
||||
// UserApplicationServiceImpl 用户应用服务实现
|
||||
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
|
||||
type UserApplicationServiceImpl struct {
|
||||
userAggregateService user_service.UserAggregateService
|
||||
userAuthService *user_service.UserAuthService
|
||||
smsCodeService *user_service.SMSCodeService
|
||||
walletService finance_service.WalletAggregateService
|
||||
contractService user_service.ContractAggregateService
|
||||
eventBus interfaces.EventBus
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserApplicationService 创建用户应用服务
|
||||
func NewUserApplicationService(
|
||||
userAggregateService user_service.UserAggregateService,
|
||||
userAuthService *user_service.UserAuthService,
|
||||
smsCodeService *user_service.SMSCodeService,
|
||||
walletService finance_service.WalletAggregateService,
|
||||
contractService user_service.ContractAggregateService,
|
||||
eventBus interfaces.EventBus,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
logger *zap.Logger,
|
||||
) UserApplicationService {
|
||||
return &UserApplicationServiceImpl{
|
||||
userAggregateService: userAggregateService,
|
||||
userAuthService: userAuthService,
|
||||
smsCodeService: smsCodeService,
|
||||
walletService: walletService,
|
||||
contractService: contractService,
|
||||
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("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 2. 创建用户
|
||||
user, err := s.userAggregateService.CreateUser(ctx, cmd.Phone, cmd.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 发布用户注册事件
|
||||
event := events.NewUserRegisteredEvent(user, "")
|
||||
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 &responses.RegisterUserResponse{
|
||||
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) {
|
||||
// 1. 验证用户密码
|
||||
user, err := s.userAuthService.ValidatePassword(ctx, cmd.Phone, cmd.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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("生成访问令牌失败")
|
||||
}
|
||||
|
||||
// 3. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userAggregateService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userAggregateService.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{
|
||||
User: userProfile,
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24h
|
||||
LoginMethod: "password",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 2. 验证用户登录状态
|
||||
user, err := s.userAuthService.ValidateUserLogin(ctx, cmd.Phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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("生成访问令牌失败")
|
||||
}
|
||||
|
||||
// 4. 如果是管理员,更新登录统计
|
||||
if user.IsAdmin() {
|
||||
if err := s.userAggregateService.UpdateLoginStats(ctx, user.ID); err != nil {
|
||||
s.logger.Error("更新登录统计失败", zap.Error(err))
|
||||
}
|
||||
// 重新获取用户信息以获取最新的登录统计
|
||||
updatedUser, err := s.userAggregateService.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: int64(s.jwtAuth.GetExpiresIn().Seconds()), // 168h
|
||||
LoginMethod: "sms",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// 业务流程:1. 修改用户密码
|
||||
func (s *UserApplicationServiceImpl) ChangePassword(ctx context.Context, cmd *commands.ChangePasswordCommand) error {
|
||||
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 {
|
||||
// 1. 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.Phone, cmd.Code, entities.SMSSceneResetPassword); err != nil {
|
||||
return fmt.Errorf("验证码错误或已过期")
|
||||
}
|
||||
|
||||
// 2. 重置用户密码
|
||||
return s.userAuthService.ResetPassword(ctx, cmd.Phone, cmd.NewPassword)
|
||||
}
|
||||
|
||||
// GetUserProfile 获取用户资料
|
||||
// 业务流程:1. 获取用户信息 2. 获取企业信息 3. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) GetUserProfile(ctx context.Context, userID string) (*responses.UserProfileResponse, error) {
|
||||
// 1. 获取用户信息(包含企业信息)
|
||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建用户信息
|
||||
userProfile := &responses.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
Username: user.Username,
|
||||
UserType: user.UserType,
|
||||
IsActive: user.Active,
|
||||
IsCertified: user.IsCertified,
|
||||
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,
|
||||
LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone,
|
||||
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
|
||||
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) {
|
||||
user, err := s.userAggregateService.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
|
||||
}
|
||||
|
||||
// ListUsers 获取用户列表(管理员功能)
|
||||
// 业务流程:1. 查询用户列表 2. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error) {
|
||||
// 1. 查询用户列表
|
||||
users, total, err := s.userAggregateService.ListUsers(ctx, query.ToDomainQuery())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 构建响应数据
|
||||
items := make([]*responses.UserListItem, 0, len(users))
|
||||
for _, user := range users {
|
||||
item := &responses.UserListItem{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
UserType: user.UserType,
|
||||
Username: user.Username,
|
||||
IsActive: user.Active,
|
||||
IsCertified: user.IsCertified,
|
||||
LoginCount: user.LoginCount,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加企业信息
|
||||
if user.EnterpriseInfo != nil {
|
||||
item.EnterpriseInfo = &responses.EnterpriseInfoItem{
|
||||
ID: user.EnterpriseInfo.ID,
|
||||
CompanyName: user.EnterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: user.EnterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: user.EnterpriseInfo.LegalPersonName,
|
||||
LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone,
|
||||
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
|
||||
CreatedAt: user.EnterpriseInfo.CreatedAt,
|
||||
}
|
||||
|
||||
// 获取企业合同信息
|
||||
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
|
||||
if err == nil && len(contracts) > 0 {
|
||||
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
|
||||
for _, contract := range contracts {
|
||||
contractItems = append(contractItems, &responses.ContractInfoItem{
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ContractTypeName: contract.GetContractTypeName(),
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
})
|
||||
}
|
||||
item.EnterpriseInfo.Contracts = contractItems
|
||||
}
|
||||
}
|
||||
|
||||
// 添加钱包余额信息
|
||||
wallet, err := s.walletService.LoadWalletByUserId(ctx, user.ID)
|
||||
if err == nil && wallet != nil {
|
||||
item.WalletBalance = wallet.Balance.String()
|
||||
} else {
|
||||
item.WalletBalance = "0"
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return &responses.UserListResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserDetail 获取用户详情(管理员功能)
|
||||
// 业务流程:1. 查询用户详情 2. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error) {
|
||||
// 1. 查询用户详情(包含企业信息)
|
||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 构建响应数据
|
||||
item := &responses.UserListItem{
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
UserType: user.UserType,
|
||||
Username: user.Username,
|
||||
IsActive: user.Active,
|
||||
IsCertified: user.IsCertified,
|
||||
LoginCount: user.LoginCount,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加企业信息
|
||||
if user.EnterpriseInfo != nil {
|
||||
item.EnterpriseInfo = &responses.EnterpriseInfoItem{
|
||||
ID: user.EnterpriseInfo.ID,
|
||||
CompanyName: user.EnterpriseInfo.CompanyName,
|
||||
UnifiedSocialCode: user.EnterpriseInfo.UnifiedSocialCode,
|
||||
LegalPersonName: user.EnterpriseInfo.LegalPersonName,
|
||||
LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone,
|
||||
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
|
||||
CreatedAt: user.EnterpriseInfo.CreatedAt,
|
||||
}
|
||||
|
||||
// 获取企业合同信息
|
||||
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
|
||||
if err == nil && len(contracts) > 0 {
|
||||
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
|
||||
for _, contract := range contracts {
|
||||
contractItems = append(contractItems, &responses.ContractInfoItem{
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ContractTypeName: contract.GetContractTypeName(),
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
})
|
||||
}
|
||||
item.EnterpriseInfo.Contracts = contractItems
|
||||
}
|
||||
}
|
||||
|
||||
// 添加钱包余额信息
|
||||
wallet, err := s.walletService.LoadWalletByUserId(ctx, user.ID)
|
||||
if err == nil && wallet != nil {
|
||||
item.WalletBalance = wallet.Balance.String()
|
||||
} else {
|
||||
item.WalletBalance = "0"
|
||||
}
|
||||
|
||||
return &responses.UserDetailResponse{
|
||||
UserListItem: item,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserStats 获取用户统计信息(管理员功能)
|
||||
// 业务流程:1. 查询用户统计信息 2. 构建响应数据
|
||||
func (s *UserApplicationServiceImpl) GetUserStats(ctx context.Context) (*responses.UserStatsResponse, error) {
|
||||
// 1. 查询用户统计信息
|
||||
stats, err := s.userAggregateService.GetUserStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 构建响应数据
|
||||
return &responses.UserStatsResponse{
|
||||
TotalUsers: stats.TotalUsers,
|
||||
ActiveUsers: stats.ActiveUsers,
|
||||
CertifiedUsers: stats.CertifiedUsers,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user