This commit is contained in:
2026-04-21 22:36:48 +08:00
commit 488c695fdf
748 changed files with 266838 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View 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"`
}

View 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
}

View 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: "",
}
}

View 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"`
}

View 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()
}

View 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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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)
}

View 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
}

View File

@@ -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"`
}

View File

@@ -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:"是否推荐"`
}

View File

@@ -0,0 +1,6 @@
package commands
// CancelScheduleCommand 取消定时发布命令
type CancelScheduleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}

View File

@@ -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"`
}

View File

@@ -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)
}

View 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"`
}

View File

@@ -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"`
}

View 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:"每页数量"`
}

View File

@@ -0,0 +1,6 @@
package queries
// GetCategoryQuery 获取分类详情查询
type GetCategoryQuery struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
}

View File

@@ -0,0 +1,6 @@
package queries
// GetTagQuery 获取标签详情查询
type GetTagQuery struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
}

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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

View File

@@ -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列表"`
}

View File

@@ -0,0 +1,6 @@
package commands
// GetContractSignURLCommand 获取合同签署链接命令
type GetContractSignURLCommand struct {
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
}

View File

@@ -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"` // 法人姓名(模糊搜索)
}

View File

@@ -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
}

View File

@@ -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"` // 过期时间
}

View File

@@ -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"` // 高度
}

View File

@@ -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
}

View 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"`
}

View 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"`
}

View File

@@ -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"` // 是否可以重试
}

View 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"`
}

View File

@@ -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"` // 是否可以重试
}

View File

@@ -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参数
}

View 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)
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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)
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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:"排序"`
}

View File

@@ -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:"配置信息"`
}

View File

@@ -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"`
}

View File

@@ -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:所有)"`
}

View 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:"分类编号"`
}

View 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:"每页数量"`
}

View 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"`
}

View File

@@ -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:"排序方向"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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,
}
}

View File

@@ -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:"配置列表"`
}

View 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:"产品列表"`
}

View File

@@ -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:"总收入"`
}

View File

@@ -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
}

View 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

View 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)
}

View 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组件关联不存在")
)

View 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
}

View 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
}

View 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:"错误信息"`
}

View File

@@ -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

View 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"`
}

View File

@@ -0,0 +1,6 @@
package queries
// GetUserQuery 获取用户信息查询
type GetUserQuery struct {
UserID string `json:"user_id"`
}

View 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,
}
}

View File

@@ -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
}

View 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"`
}

View File

@@ -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
}

View 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)
}

View 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
}