Compare commits
135 Commits
59b3d76f57
...
report-rou
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c46937ea | |||
| cc3472ff40 | |||
| 451d869361 | |||
| 08ea153cac | |||
| 6147878dfe | |||
| be47a0f045 | |||
| 810696e0f0 | |||
| 17dbaf1ccb | |||
| 18f3d10518 | |||
| 0d4953c6d3 | |||
| 3f64600f02 | |||
| 2c89b8cb26 | |||
| 09d7a4f076 | |||
| 83d0fd6587 | |||
| 0fd28054f1 | |||
| ce858983ee | |||
| 9b2bffae15 | |||
| c68ece5bea | |||
| 398d2cee74 | |||
| b6c8d93af5 | |||
| b423aa6be8 | |||
| a47c306c87 | |||
| af88bcc8eb | |||
| 89367fb2ee | |||
| 05b6623e75 | |||
| bfedec249f | |||
| 9f669a9c94 | |||
| 0f5c4f4303 | |||
| d9c2d9f103 | |||
| 7e0d58b295 | |||
| a17ff2140e | |||
| 6a2241bc66 | |||
| e57bef6609 | |||
| 81639a81e6 | |||
| aaf17321ff | |||
| a8a4ff2d37 | |||
| 619deeb456 | |||
| f12c3fb8ad | |||
| 4ce8fe4023 | |||
| 7b45b43a0e | |||
| 752b90b048 | |||
| 68def7e08b | |||
| b0e8974d6c | |||
| b41d41ddf3 | |||
| b08a63fc99 | |||
| 1f06f21faf | |||
| 3f5a126bfa | |||
| 17ff48a642 | |||
| af629e96c2 | |||
| 63252fa30f | |||
| 1cf64e831c | |||
| 577c2bc581 | |||
| 6d73dad88e | |||
| 937c812ea5 | |||
| 63e2fba464 | |||
| 9c776b8bf3 | |||
| 500264e9e5 | |||
| b90935a7c3 | |||
| c404e797f3 | |||
| ce9052f85b | |||
| 11fe48809e | |||
| 785818f73d | |||
| c10fb27b93 | |||
| 4b0ab842f4 | |||
| 4c16e7a333 | |||
| 8d0f1e6aa3 | |||
| 7fc072e608 | |||
| a53727757c | |||
| 90d0324a1a | |||
| 15d0759cfb | |||
| 604174cce7 | |||
| 1bfeac0504 | |||
| 00a3f0f1e9 | |||
| f00cee7410 | |||
| 3745a3768f | |||
| a3d0b341a9 | |||
| b74c02b9f0 | |||
| c740ae5639 | |||
| 5233f0f0f0 | |||
| b4134d7942 | |||
| 7e2af0e4f5 | |||
| 2773c1a60b | |||
| bb88c78c82 | |||
| 90bb1d017e | |||
| da37b4d7bc | |||
| 657d51ad57 | |||
| 368dc2669c | |||
| 79cd87bd18 | |||
| 99d9405db0 | |||
| 3d3ca98eb7 | |||
| 96f22b4249 | |||
| e5a5e85e5d | |||
| adc9db7f78 | |||
| b92dfd0d58 | |||
| b05d694755 | |||
| 5a6e95906c | |||
| a49d58365e | |||
| 309a9a4c96 | |||
| 532b92713b | |||
| 1b931cb816 | |||
| 7b1b75e7a9 | |||
| 2685df85c3 | |||
| 353c57c98b | |||
| cfad2bce09 | |||
| 6874f67c45 | |||
| a0fc9dc246 | |||
| c46c1e23a1 | |||
| e05ad9e223 | |||
| c563b2266b | |||
| c1f127e9b1 | |||
| c579e53ad1 | |||
| d73659fed3 | |||
| c7c4ab7a19 | |||
| 2f3817c8f0 | |||
| 16a8cd5506 | |||
| ebacec8e16 | |||
| 5c5c2abfcd | |||
| 5d5372e359 | |||
| 34ff6ce916 | |||
| a2008e66e6 | |||
| ecc7495954 | |||
| f324f15397 | |||
| 50a4fa86ce | |||
| 5051aea55c | |||
| 4031277a91 | |||
| 958f23487c | |||
| b1049cd984 | |||
| a91bde0c67 | |||
| 2a93d120f1 | |||
| 267ff92998 | |||
| 365a2a8886 | |||
| 5dad8a3ccb | |||
| 4c9bb7cbf7 | |||
| 9f8630784d | |||
| ecfe7a6fd6 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -26,6 +26,7 @@ Thumbs.db
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
console
|
console
|
||||||
|
worker
|
||||||
|
|
||||||
# 依赖目录
|
# 依赖目录
|
||||||
vendor/
|
vendor/
|
||||||
@@ -34,6 +35,11 @@ vendor/
|
|||||||
coverage.out
|
coverage.out
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
|
# 字体文件(大文件,不进行版本控制)
|
||||||
|
internal/shared/pdf/fonts/*.ttf
|
||||||
|
internal/shared/pdf/fonts/*.ttc
|
||||||
|
internal/shared/pdf/fonts/*.otf
|
||||||
|
|
||||||
# 其他
|
# 其他
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.dll
|
||||||
|
|||||||
@@ -1,288 +0,0 @@
|
|||||||
# DDD规范企业认证信息自动填充实现总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
根据DDD(领域驱动设计)架构规范,重新设计了企业认证信息自动填充功能。在DDD中,跨域操作应该通过应用服务层来协调,而不是在领域服务层直接操作其他领域的仓储。
|
|
||||||
|
|
||||||
## DDD架构规范
|
|
||||||
|
|
||||||
### 1. 领域边界原则
|
|
||||||
- **领域服务层**:只能操作本领域的仓储和实体
|
|
||||||
- **应用服务层**:负责跨域协调,调用不同领域的服务
|
|
||||||
- **聚合根**:每个领域有自己的聚合根,不能直接访问其他领域的聚合根
|
|
||||||
|
|
||||||
### 2. 依赖方向
|
|
||||||
```
|
|
||||||
应用服务层 → 领域服务层 → 仓储层
|
|
||||||
↓
|
|
||||||
跨域协调
|
|
||||||
```
|
|
||||||
|
|
||||||
## 重新设计架构
|
|
||||||
|
|
||||||
### 1. 领域服务层(Finance Domain)
|
|
||||||
|
|
||||||
#### `UserInvoiceInfoService`接口更新
|
|
||||||
```go
|
|
||||||
type UserInvoiceInfoService interface {
|
|
||||||
// 基础方法
|
|
||||||
GetUserInvoiceInfo(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error)
|
|
||||||
CreateOrUpdateUserInvoiceInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo) (*entities.UserInvoiceInfo, error)
|
|
||||||
|
|
||||||
// 新增:包含企业认证信息的方法
|
|
||||||
GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
|
|
||||||
CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
|
|
||||||
|
|
||||||
ValidateInvoiceInfo(ctx context.Context, invoiceInfo *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
|
|
||||||
DeleteUserInvoiceInfo(ctx context.Context, userID string) error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 实现特点
|
|
||||||
- **移除跨域依赖**:不再直接依赖`user_repo.UserRepository`
|
|
||||||
- **参数化设计**:通过方法参数接收企业认证信息
|
|
||||||
- **保持纯净性**:领域服务只处理本领域的业务逻辑
|
|
||||||
|
|
||||||
### 2. 应用服务层(Application Layer)
|
|
||||||
|
|
||||||
#### `InvoiceApplicationService`更新
|
|
||||||
```go
|
|
||||||
type InvoiceApplicationServiceImpl struct {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 跨域协调逻辑
|
|
||||||
```go
|
|
||||||
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,
|
|
||||||
// ... 其他字段
|
|
||||||
CompanyNameReadOnly: companyNameReadOnly,
|
|
||||||
TaxpayerIDReadOnly: taxpayerIDReadOnly,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 依赖注入更新
|
|
||||||
|
|
||||||
#### 容器配置
|
|
||||||
```go
|
|
||||||
// 用户聚合服务
|
|
||||||
fx.Annotate(
|
|
||||||
user_service.NewUserAggregateService,
|
|
||||||
fx.ResultTags(`name:"userAggregateService"`),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 用户开票信息服务(移除userRepo依赖)
|
|
||||||
fx.Annotate(
|
|
||||||
finance_service.NewUserInvoiceInfoService,
|
|
||||||
fx.ParamTags(
|
|
||||||
`name:"userInvoiceInfoRepo"`,
|
|
||||||
),
|
|
||||||
fx.ResultTags(`name:"userInvoiceInfoService"`),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 发票应用服务(添加userAggregateService依赖)
|
|
||||||
fx.Annotate(
|
|
||||||
finance.NewInvoiceApplicationService,
|
|
||||||
fx.As(new(finance.InvoiceApplicationService)),
|
|
||||||
fx.ParamTags(
|
|
||||||
`name:"invoiceRepo"`,
|
|
||||||
`name:"userInvoiceInfoRepo"`,
|
|
||||||
`name:"userRepo"`,
|
|
||||||
`name:"userAggregateService"`, // 新增
|
|
||||||
`name:"rechargeRecordRepo"`,
|
|
||||||
`name:"walletRepo"`,
|
|
||||||
`name:"domainService"`,
|
|
||||||
`name:"aggregateService"`,
|
|
||||||
`name:"userInvoiceInfoService"`,
|
|
||||||
`name:"storageService"`,
|
|
||||||
`name:"logger"`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
## 架构优势
|
|
||||||
|
|
||||||
### 1. 符合DDD规范
|
|
||||||
- **领域边界清晰**:每个领域只处理自己的业务逻辑
|
|
||||||
- **依赖方向正确**:应用服务层负责跨域协调
|
|
||||||
- **聚合根隔离**:不同领域的聚合根不直接交互
|
|
||||||
|
|
||||||
### 2. 可维护性
|
|
||||||
- **职责分离**:领域服务专注于本领域逻辑
|
|
||||||
- **易于测试**:可以独立测试每个领域服务
|
|
||||||
- **扩展性好**:新增跨域功能时只需修改应用服务层
|
|
||||||
|
|
||||||
### 3. 业务逻辑清晰
|
|
||||||
- **数据流向明确**:企业认证信息 → 应用服务 → 开票信息
|
|
||||||
- **错误处理统一**:在应用服务层统一处理跨域错误
|
|
||||||
- **权限控制集中**:在应用服务层统一控制访问权限
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 1. 获取开票信息流程
|
|
||||||
```
|
|
||||||
用户请求 → 应用服务层
|
|
||||||
↓
|
|
||||||
调用UserAggregateService.GetUserWithEnterpriseInfo()
|
|
||||||
↓
|
|
||||||
获取企业认证信息
|
|
||||||
↓
|
|
||||||
调用UserInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo()
|
|
||||||
↓
|
|
||||||
返回开票信息(包含企业认证信息)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 更新开票信息流程
|
|
||||||
```
|
|
||||||
用户请求 → 应用服务层
|
|
||||||
↓
|
|
||||||
调用UserAggregateService.GetUserWithEnterpriseInfo()
|
|
||||||
↓
|
|
||||||
验证企业认证状态
|
|
||||||
↓
|
|
||||||
调用UserInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo()
|
|
||||||
↓
|
|
||||||
保存开票信息(企业认证信息自动填充)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 技术实现要点
|
|
||||||
|
|
||||||
### 1. 接口设计
|
|
||||||
- **向后兼容**:保留原有的基础方法
|
|
||||||
- **功能扩展**:新增包含企业认证信息的方法
|
|
||||||
- **参数传递**:通过方法参数传递跨域数据
|
|
||||||
|
|
||||||
### 2. 错误处理
|
|
||||||
- **分层处理**:在应用服务层处理跨域错误
|
|
||||||
- **错误传播**:领域服务层错误向上传播
|
|
||||||
- **用户友好**:提供清晰的错误信息
|
|
||||||
|
|
||||||
### 3. 性能优化
|
|
||||||
- **减少查询**:应用服务层缓存企业认证信息
|
|
||||||
- **批量操作**:支持批量获取和更新
|
|
||||||
- **异步处理**:非关键路径支持异步处理
|
|
||||||
|
|
||||||
## 代码示例
|
|
||||||
|
|
||||||
### 1. 领域服务实现
|
|
||||||
```go
|
|
||||||
// GetUserInvoiceInfoWithEnterpriseInfo 获取用户开票信息(包含企业认证信息)
|
|
||||||
func (s *UserInvoiceInfoServiceImpl) GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error) {
|
|
||||||
info, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到开票信息记录,创建新的实体
|
|
||||||
if info == nil {
|
|
||||||
info = &entities.UserInvoiceInfo{
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
UserID: userID,
|
|
||||||
CompanyName: "",
|
|
||||||
TaxpayerID: "",
|
|
||||||
BankName: "",
|
|
||||||
BankAccount: "",
|
|
||||||
CompanyAddress: "",
|
|
||||||
CompanyPhone: "",
|
|
||||||
ReceivingEmail: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用传入的企业认证信息填充公司名称和纳税人识别号
|
|
||||||
if companyName != "" {
|
|
||||||
info.CompanyName = companyName
|
|
||||||
}
|
|
||||||
if taxpayerID != "" {
|
|
||||||
info.TaxpayerID = taxpayerID
|
|
||||||
}
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 应用服务实现
|
|
||||||
```go
|
|
||||||
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
|
|
||||||
// 获取用户企业认证信息
|
|
||||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否有企业认证信息
|
|
||||||
if user.EnterpriseInfo == nil {
|
|
||||||
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建开票信息对象
|
|
||||||
invoiceInfo := value_objects.NewInvoiceInfo(
|
|
||||||
"", // 公司名称将由服务层从企业认证信息中获取
|
|
||||||
"", // 纳税人识别号将由服务层从企业认证信息中获取
|
|
||||||
req.BankName,
|
|
||||||
req.BankAccount,
|
|
||||||
req.CompanyAddress,
|
|
||||||
req.CompanyPhone,
|
|
||||||
req.ReceivingEmail,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 使用包含企业认证信息的方法
|
|
||||||
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
|
|
||||||
ctx,
|
|
||||||
userID,
|
|
||||||
invoiceInfo,
|
|
||||||
user.EnterpriseInfo.CompanyName,
|
|
||||||
user.EnterpriseInfo.UnifiedSocialCode,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
通过按照DDD规范重新设计,我们实现了:
|
|
||||||
|
|
||||||
1. ✅ **架构规范**:严格遵循DDD的领域边界和依赖方向
|
|
||||||
2. ✅ **职责分离**:领域服务专注于本领域逻辑,应用服务负责跨域协调
|
|
||||||
3. ✅ **可维护性**:代码结构清晰,易于理解和维护
|
|
||||||
4. ✅ **可扩展性**:新增跨域功能时只需修改应用服务层
|
|
||||||
5. ✅ **业务逻辑**:企业认证信息自动填充功能完整实现
|
|
||||||
|
|
||||||
这种设计既满足了业务需求,又符合DDD架构规范,是一个优秀的架构实现。
|
|
||||||
@@ -50,9 +50,11 @@ WORKDIR /app
|
|||||||
COPY --from=builder /app/tyapi-server .
|
COPY --from=builder /app/tyapi-server .
|
||||||
|
|
||||||
# 复制配置文件
|
# 复制配置文件
|
||||||
COPY --chown=tyapi:tyapi config.yaml .
|
COPY config.yaml .
|
||||||
COPY --chown=tyapi:tyapi configs/ ./configs/
|
COPY configs/ ./configs/
|
||||||
|
|
||||||
|
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||||
|
COPY resources ./resources
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
64
Dockerfile.worker
Normal file
64
Dockerfile.worker
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 第一阶段:构建阶段
|
||||||
|
FROM golang:1.23.4-alpine AS builder
|
||||||
|
|
||||||
|
# 设置Go代理和Alpine镜像源
|
||||||
|
ENV GOPROXY https://goproxy.cn,direct
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装必要的包
|
||||||
|
RUN apk add --no-cache git tzdata
|
||||||
|
|
||||||
|
# 复制模块文件
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# 下载依赖
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建应用程序
|
||||||
|
ARG VERSION=1.0.0
|
||||||
|
ARG COMMIT=dev
|
||||||
|
ARG BUILD_TIME
|
||||||
|
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
|
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
|
||||||
|
-a -installsuffix cgo \
|
||||||
|
-o worker \
|
||||||
|
cmd/worker/main.go
|
||||||
|
|
||||||
|
# 第二阶段:运行阶段
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
# 设置Alpine镜像源
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||||
|
|
||||||
|
# 安装必要的包
|
||||||
|
RUN apk --no-cache add tzdata curl
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 从构建阶段复制二进制文件
|
||||||
|
COPY --from=builder /app/worker .
|
||||||
|
|
||||||
|
# 复制配置文件
|
||||||
|
COPY --chown=tyapi:tyapi config.yaml .
|
||||||
|
COPY --chown=tyapi:tyapi configs/ ./configs/
|
||||||
|
|
||||||
|
# 暴露端口(如果需要)
|
||||||
|
# EXPOSE 8080
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["./worker", "-env=production"]
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
# Handler请求绑定方式更新总结
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
根据用户要求,将handler中的请求体参数绑定方式从`ShouldBindJSON`统一更新为使用`h.validator.BindAndValidate`,以保持代码风格的一致性和更好的验证处理。
|
|
||||||
|
|
||||||
## 主要变更
|
|
||||||
|
|
||||||
### 1. 更新的文件
|
|
||||||
|
|
||||||
#### `internal/infrastructure/http/handlers/finance_handler.go`
|
|
||||||
|
|
||||||
**更新的方法:**
|
|
||||||
|
|
||||||
1. **ApplyInvoice** - 申请开票
|
|
||||||
```go
|
|
||||||
// 更新前
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新后
|
|
||||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **UpdateUserInvoiceInfo** - 更新用户发票信息
|
|
||||||
```go
|
|
||||||
// 更新前
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新后
|
|
||||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **RejectInvoiceApplication** - 拒绝发票申请
|
|
||||||
```go
|
|
||||||
// 更新前
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新后
|
|
||||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `internal/infrastructure/http/handlers/api_handler.go`
|
|
||||||
|
|
||||||
**更新的方法:**
|
|
||||||
|
|
||||||
1. **AddWhiteListIP** - 添加白名单IP
|
|
||||||
```go
|
|
||||||
// 更新前
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新后
|
|
||||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
|
||||||
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 保持不变的文件
|
|
||||||
|
|
||||||
#### `internal/shared/validator/validator.go`
|
|
||||||
- `BindAndValidate`方法内部仍然使用`c.ShouldBindJSON(dto)`
|
|
||||||
- 这是正确的,因为validator需要先绑定JSON数据,然后再进行验证
|
|
||||||
- 这是validator的实现细节,不需要修改
|
|
||||||
|
|
||||||
## 技术优势
|
|
||||||
|
|
||||||
### 1. 统一的验证处理
|
|
||||||
- 所有handler都使用相同的验证方式
|
|
||||||
- 统一的错误处理和响应格式
|
|
||||||
- 更好的代码一致性
|
|
||||||
|
|
||||||
### 2. 更好的错误信息
|
|
||||||
- `BindAndValidate`提供了更详细的验证错误信息
|
|
||||||
- 支持中文字段名显示
|
|
||||||
- 更友好的用户错误提示
|
|
||||||
|
|
||||||
### 3. 验证规则支持
|
|
||||||
- 支持自定义验证规则
|
|
||||||
- 支持字段翻译
|
|
||||||
- 支持复杂的业务验证逻辑
|
|
||||||
|
|
||||||
### 4. 代码维护性
|
|
||||||
- 统一的验证逻辑
|
|
||||||
- 便于后续验证规则的修改
|
|
||||||
- 减少重复代码
|
|
||||||
|
|
||||||
## 验证流程
|
|
||||||
|
|
||||||
### 1. 使用`h.validator.BindAndValidate`的流程
|
|
||||||
```
|
|
||||||
请求到达 → BindAndValidate → JSON绑定 → 结构体验证 → 返回结果
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 错误处理流程
|
|
||||||
```
|
|
||||||
验证失败 → 格式化错误信息 → 返回BadRequest响应 → 前端显示错误
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证器功能
|
|
||||||
|
|
||||||
### 1. 支持的验证标签
|
|
||||||
- `required` - 必填字段
|
|
||||||
- `email` - 邮箱格式
|
|
||||||
- `min/max` - 长度限制
|
|
||||||
- `phone` - 手机号格式
|
|
||||||
- `strong_password` - 强密码
|
|
||||||
- `social_credit_code` - 统一社会信用代码
|
|
||||||
- `id_card` - 身份证号
|
|
||||||
- `price` - 价格格式
|
|
||||||
- `uuid` - UUID格式
|
|
||||||
- `url` - URL格式
|
|
||||||
- 等等...
|
|
||||||
|
|
||||||
### 2. 错误信息本地化
|
|
||||||
- 支持中文字段名
|
|
||||||
- 支持中文错误消息
|
|
||||||
- 支持自定义错误消息
|
|
||||||
|
|
||||||
## 兼容性
|
|
||||||
|
|
||||||
### 1. API接口保持不变
|
|
||||||
- 请求和响应格式完全一致
|
|
||||||
- 前端调用方式无需修改
|
|
||||||
- 向后兼容
|
|
||||||
|
|
||||||
### 2. 错误响应格式
|
|
||||||
- 保持原有的错误响应结构
|
|
||||||
- 错误信息更加详细和友好
|
|
||||||
- 支持字段级别的错误信息
|
|
||||||
|
|
||||||
## 后续建议
|
|
||||||
|
|
||||||
### 1. 代码审查
|
|
||||||
- 检查其他handler文件是否还有使用`ShouldBindJSON`的地方
|
|
||||||
- 确保所有新的handler都使用`BindAndValidate`
|
|
||||||
|
|
||||||
### 2. 测试验证
|
|
||||||
- 验证所有API接口的请求绑定是否正常工作
|
|
||||||
- 测试各种验证错误场景
|
|
||||||
- 确保错误信息显示正确
|
|
||||||
|
|
||||||
### 3. 文档更新
|
|
||||||
- 更新开发指南,说明使用`BindAndValidate`的最佳实践
|
|
||||||
- 更新API文档,说明验证规则和错误格式
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
通过这次更新,我们成功统一了handler中的请求绑定方式,使用`h.validator.BindAndValidate`替代了`ShouldBindJSON`。这样的更改带来了更好的代码一致性、更友好的错误处理和更强的验证能力,同时保持了API的向后兼容性。
|
|
||||||
|
|
||||||
所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# 发票功能外部服务集成 TODO
|
|
||||||
|
|
||||||
## 1. 短信服务集成
|
|
||||||
|
|
||||||
### 位置
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/events/invoice_events.go`
|
|
||||||
|
|
||||||
### 需要实现的功能
|
|
||||||
- [ ] 发票申请创建时发送短信通知管理员
|
|
||||||
- [ ] 配置管理员手机号
|
|
||||||
- [ ] 短信内容模板
|
|
||||||
|
|
||||||
### 示例代码
|
|
||||||
```go
|
|
||||||
// 在 InvoiceAggregateServiceImpl 中注入短信服务
|
|
||||||
type InvoiceAggregateServiceImpl struct {
|
|
||||||
// ... 其他依赖
|
|
||||||
smsService SMSService
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在事件处理器中发送短信
|
|
||||||
func (s *InvoiceEventHandler) HandleInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
|
|
||||||
// TODO: 发送短信通知管理员
|
|
||||||
// message := fmt.Sprintf("新的发票申请:用户%s申请开票%.2f元", event.UserID, event.Amount)
|
|
||||||
// return s.smsService.SendSMS(ctx, adminPhone, message)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 邮件服务集成
|
|
||||||
|
|
||||||
### 位置
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/events/invoice_events.go`
|
|
||||||
|
|
||||||
### 需要实现的功能
|
|
||||||
- [ ] 发票文件上传后发送邮件给用户
|
|
||||||
- [ ] 发票申请被拒绝时发送邮件通知用户
|
|
||||||
- [ ] 邮件模板设计
|
|
||||||
|
|
||||||
### 示例代码
|
|
||||||
```go
|
|
||||||
// 在 SendInvoiceToEmail 方法中
|
|
||||||
func (s *InvoiceAggregateServiceImpl) SendInvoiceToEmail(ctx context.Context, invoiceID string) error {
|
|
||||||
// ... 获取发票信息
|
|
||||||
|
|
||||||
// TODO: 调用邮件服务发送发票
|
|
||||||
// emailData := &EmailData{
|
|
||||||
// To: invoice.ReceivingEmail,
|
|
||||||
// Subject: "您的发票已开具",
|
|
||||||
// Template: "invoice_issued",
|
|
||||||
// Data: map[string]interface{}{
|
|
||||||
// "CompanyName": invoice.CompanyName,
|
|
||||||
// "Amount": invoice.Amount,
|
|
||||||
// "FileURL": invoice.FileURL,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// return s.emailService.SendEmail(ctx, emailData)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 文件存储服务集成
|
|
||||||
|
|
||||||
### 位置
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service_impl.go`
|
|
||||||
|
|
||||||
### 需要实现的功能
|
|
||||||
- [ ] 上传发票PDF文件
|
|
||||||
- [ ] 生成文件访问URL
|
|
||||||
- [ ] 文件存储配置
|
|
||||||
|
|
||||||
### 示例代码
|
|
||||||
```go
|
|
||||||
// 在 UploadInvoiceFile 方法中
|
|
||||||
func (s *InvoiceAggregateServiceImpl) UploadInvoiceFile(ctx context.Context, invoiceID string, file multipart.File) error {
|
|
||||||
// ... 获取发票信息
|
|
||||||
|
|
||||||
// TODO: 调用文件存储服务上传文件
|
|
||||||
// uploadResult, err := s.fileStorageService.UploadFile(ctx, &UploadRequest{
|
|
||||||
// File: file,
|
|
||||||
// Path: fmt.Sprintf("invoices/%s", invoiceID),
|
|
||||||
// Filename: fmt.Sprintf("invoice_%s.pdf", invoiceID),
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("上传文件失败: %w", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// invoice.SetFileInfo(uploadResult.FileID, uploadResult.FileName, uploadResult.FileURL, uploadResult.FileSize)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 事件处理器实现
|
|
||||||
|
|
||||||
### 需要创建的文件
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/events/invoice_event_handler.go`
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/events/invoice_event_publisher.go`
|
|
||||||
|
|
||||||
### 服务文件结构
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/services/invoice_domain_service.go` - 领域服务(接口+实现)
|
|
||||||
- `tyapi-server-gin/internal/domains/finance/services/invoice_aggregate_service.go` - 聚合服务(接口+实现)
|
|
||||||
|
|
||||||
### 需要实现的功能
|
|
||||||
- [ ] 事件发布器实现
|
|
||||||
- [ ] 事件处理器实现
|
|
||||||
- [ ] 事件订阅配置
|
|
||||||
|
|
||||||
### 示例代码
|
|
||||||
```go
|
|
||||||
// 事件发布器实现
|
|
||||||
type InvoiceEventPublisherImpl struct {
|
|
||||||
// 可以使用消息队列、Redis发布订阅等
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件处理器实现
|
|
||||||
type InvoiceEventHandlerImpl struct {
|
|
||||||
smsService SMSService
|
|
||||||
emailService EmailService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *InvoiceEventHandlerImpl) HandleInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
|
|
||||||
// 发送短信通知管理员
|
|
||||||
return h.smsService.SendSMS(ctx, adminPhone, "新的发票申请")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *InvoiceEventHandlerImpl) HandleInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
|
|
||||||
// 发送邮件给用户
|
|
||||||
return h.emailService.SendInvoiceEmail(ctx, event.ReceivingEmail, event.FileURL, event.FileName)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 数据库迁移
|
|
||||||
|
|
||||||
### 需要创建的表
|
|
||||||
- [ ] `invoice_applications` - 发票申请表(包含文件信息)
|
|
||||||
|
|
||||||
### 迁移文件位置
|
|
||||||
- `tyapi-server-gin/migrations/`
|
|
||||||
|
|
||||||
## 6. 仓储实现
|
|
||||||
|
|
||||||
### 需要实现的文件
|
|
||||||
- [ ] `tyapi-server-gin/internal/infrastructure/database/repositories/invoice_application_repository_impl.go`
|
|
||||||
|
|
||||||
## 7. HTTP接口实现
|
|
||||||
|
|
||||||
### 已完成的文件
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/invoice_handler.go` - 用户发票处理器
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/admin_invoice_handler.go` - 管理员发票处理器
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/http/routes/invoice_routes.go` - 发票路由配置
|
|
||||||
- [x] `tyapi-server-gin/docs/发票API接口文档.md` - API接口文档
|
|
||||||
|
|
||||||
### 用户接口
|
|
||||||
- [x] 申请开票 `POST /api/v1/invoices/apply`
|
|
||||||
- [x] 获取用户发票信息 `GET /api/v1/invoices/info`
|
|
||||||
- [x] 更新用户发票信息 `PUT /api/v1/invoices/info`
|
|
||||||
- [x] 获取用户开票记录 `GET /api/v1/invoices/records`
|
|
||||||
- [x] 获取可开票金额 `GET /api/v1/invoices/available-amount`
|
|
||||||
- [x] 下载发票文件 `GET /api/v1/invoices/{application_id}/download`
|
|
||||||
|
|
||||||
### 管理员接口
|
|
||||||
- [x] 获取待处理申请列表 `GET /api/v1/admin/invoices/pending`
|
|
||||||
- [x] 通过发票申请 `POST /api/v1/admin/invoices/{application_id}/approve`
|
|
||||||
- [x] 拒绝发票申请 `POST /api/v1/admin/invoices/{application_id}/reject`
|
|
||||||
|
|
||||||
## 8. 依赖注入配置
|
|
||||||
|
|
||||||
### 已完成的文件
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/http/handlers/finance_handler.go` - 合并发票相关handler方法
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/http/routes/finance_routes.go` - 合并发票相关路由
|
|
||||||
- [x] 删除多余文件:`invoice_handler.go`、`admin_invoice_handler.go`、`invoice_routes.go`
|
|
||||||
|
|
||||||
### 已完成的文件
|
|
||||||
- [x] `tyapi-server-gin/internal/infrastructure/database/repositories/finance/invoice_application_repository_impl.go` - 实现发票申请仓储
|
|
||||||
- [x] `tyapi-server-gin/internal/application/finance/invoice_application_service.go` - 实现发票应用服务(合并用户端和管理员端)
|
|
||||||
- [x] `tyapi-server-gin/internal/container/container.go` - 添加发票相关服务的依赖注入
|
|
||||||
|
|
||||||
### 已完成的工作
|
|
||||||
- [x] 删除 `tyapi-server-gin/internal/application/finance/admin_invoice_application_service.go` - 已合并到主服务文件
|
|
||||||
- [x] 修复 `tyapi-server-gin/internal/application/finance/invoice_application_service.go` - 所有编译错误已修复
|
|
||||||
- [x] 使用 `*storage.QiNiuStorageService` 替换 `interfaces.StorageService`
|
|
||||||
- [x] 更新仓储接口以包含所有必要的方法
|
|
||||||
- [x] 修复DTO字段映射和类型转换
|
|
||||||
- [x] 修复聚合服务调用参数
|
|
||||||
|
|
||||||
## 8. 前端页面
|
|
||||||
|
|
||||||
### 需要创建的前端页面
|
|
||||||
- [ ] 发票申请页面
|
|
||||||
- [ ] 发票信息编辑页面
|
|
||||||
- [ ] 发票记录列表页面
|
|
||||||
- [ ] 管理员发票申请处理页面
|
|
||||||
|
|
||||||
## 优先级
|
|
||||||
|
|
||||||
1. **高优先级**: 数据库迁移、仓储实现、依赖注入配置
|
|
||||||
2. **中优先级**: 事件处理器实现、基础API接口
|
|
||||||
3. **低优先级**: 外部服务集成(短信、邮件、文件存储)
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 所有外部服务调用都应该有适当的错误处理和重试机制
|
|
||||||
- 事件发布失败不应该影响主业务流程
|
|
||||||
- 文件上传需要验证文件类型和大小
|
|
||||||
- 邮件发送需要支持模板和国际化
|
|
||||||
BIN
cmd/worker/__debug_bin.exe1068760645
Normal file
BIN
cmd/worker/__debug_bin.exe1068760645
Normal file
Binary file not shown.
BIN
cmd/worker/__debug_bin.exe1835124629
Normal file
BIN
cmd/worker/__debug_bin.exe1835124629
Normal file
Binary file not shown.
BIN
cmd/worker/__debug_bin.exe4056734935
Normal file
BIN
cmd/worker/__debug_bin.exe4056734935
Normal file
Binary file not shown.
BIN
cmd/worker/__debug_bin.exe438186156
Normal file
BIN
cmd/worker/__debug_bin.exe438186156
Normal file
Binary file not shown.
193
cmd/worker/main.go
Normal file
193
cmd/worker/main.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"tyapi-server/internal/config"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
"tyapi-server/internal/infrastructure/database"
|
||||||
|
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskTypeArticlePublish = "article:publish"
|
||||||
|
TaskTypeAnnouncementPublish = "announcement_publish"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 加载配置
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("加载配置失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志器
|
||||||
|
logger, err := zap.NewProduction()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("创建日志器失败:", err)
|
||||||
|
}
|
||||||
|
defer logger.Sync()
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
// 使用配置文件中的数据库配置
|
||||||
|
dbCfg := database.Config{
|
||||||
|
Host: cfg.Database.Host,
|
||||||
|
Port: cfg.Database.Port,
|
||||||
|
User: cfg.Database.User,
|
||||||
|
Password: cfg.Database.Password,
|
||||||
|
Name: cfg.Database.Name,
|
||||||
|
SSLMode: cfg.Database.SSLMode,
|
||||||
|
Timezone: cfg.Database.Timezone,
|
||||||
|
MaxOpenConns: cfg.Database.MaxOpenConns,
|
||||||
|
MaxIdleConns: cfg.Database.MaxIdleConns,
|
||||||
|
ConnMaxLifetime: cfg.Database.ConnMaxLifetime,
|
||||||
|
}
|
||||||
|
logger.Info("数据库配置", zap.Any("dbCfg", dbCfg))
|
||||||
|
dbWrapper, err := database.NewConnection(dbCfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("连接数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
db := dbWrapper.DB
|
||||||
|
|
||||||
|
// 使用配置文件中的Redis配置
|
||||||
|
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
|
||||||
|
// 创建 Asynq Server
|
||||||
|
server := asynq.NewServer(
|
||||||
|
asynq.RedisClientOpt{Addr: redisAddr},
|
||||||
|
asynq.Config{
|
||||||
|
Concurrency: 10,
|
||||||
|
Queues: map[string]int{
|
||||||
|
"critical": 6,
|
||||||
|
"default": 3,
|
||||||
|
"low": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建任务处理器
|
||||||
|
mux := asynq.NewServeMux()
|
||||||
|
mux.HandleFunc(TaskTypeArticlePublish, func(ctx context.Context, t *asynq.Task) error {
|
||||||
|
return handleArticlePublish(ctx, t, db, logger)
|
||||||
|
})
|
||||||
|
mux.HandleFunc(TaskTypeAnnouncementPublish, func(ctx context.Context, t *asynq.Task) error {
|
||||||
|
return handleAnnouncementPublish(ctx, t, db, logger)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动 Worker
|
||||||
|
go func() {
|
||||||
|
logger.Info("启动 Asynq Worker", zap.String("redis_addr", redisAddr))
|
||||||
|
if err := server.Run(mux); err != nil {
|
||||||
|
logger.Fatal("启动 Worker 失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
|
logger.Info("正在关闭 Worker...")
|
||||||
|
server.Stop()
|
||||||
|
server.Shutdown()
|
||||||
|
logger.Info("Worker 已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleArticlePublish 处理文章定时发布任务
|
||||||
|
func handleArticlePublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logger *zap.Logger) error {
|
||||||
|
var payload map[string]interface{}
|
||||||
|
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||||
|
logger.Error("解析任务载荷失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("解析任务载荷失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
articleID, ok := payload["article_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("任务载荷中缺少文章ID")
|
||||||
|
return fmt.Errorf("任务载荷中缺少文章ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文章
|
||||||
|
var article entities.Article
|
||||||
|
if err := db.WithContext(ctx).First(&article, "id = ?", articleID).Error; err != nil {
|
||||||
|
logger.Error("获取文章失败", zap.String("article_id", articleID), zap.Error(err))
|
||||||
|
return fmt.Errorf("获取文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布文章
|
||||||
|
if err := article.Publish(); err != nil {
|
||||||
|
logger.Error("发布文章失败", zap.String("article_id", articleID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新
|
||||||
|
if err := db.WithContext(ctx).Save(&article).Error; err != nil {
|
||||||
|
logger.Error("保存文章失败", zap.String("article_id", articleID), zap.Error(err))
|
||||||
|
return fmt.Errorf("保存文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("定时发布文章成功", zap.String("article_id", articleID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAnnouncementPublish 处理公告定时发布任务
|
||||||
|
func handleAnnouncementPublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logger *zap.Logger) error {
|
||||||
|
var payload map[string]interface{}
|
||||||
|
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||||
|
logger.Error("解析任务载荷失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("解析任务载荷失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
announcementID, ok := payload["announcement_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
logger.Error("任务载荷中缺少公告ID")
|
||||||
|
return fmt.Errorf("任务载荷中缺少公告ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取公告
|
||||||
|
var announcement entities.Announcement
|
||||||
|
if err := db.WithContext(ctx).First(&announcement, "id = ?", announcementID).Error; err != nil {
|
||||||
|
logger.Error("获取公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||||
|
return fmt.Errorf("获取公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已取消定时发布
|
||||||
|
if !announcement.IsScheduled() {
|
||||||
|
logger.Info("公告定时发布已取消,跳过执行",
|
||||||
|
zap.String("announcement_id", announcementID),
|
||||||
|
zap.String("status", string(announcement.Status)))
|
||||||
|
return nil // 静默返回,不报错
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查定时发布时间是否匹配
|
||||||
|
if announcement.ScheduledAt == nil {
|
||||||
|
logger.Info("公告没有定时发布时间,跳过执行",
|
||||||
|
zap.String("announcement_id", announcementID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布公告
|
||||||
|
if err := announcement.Publish(); err != nil {
|
||||||
|
logger.Error("发布公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新
|
||||||
|
if err := db.WithContext(ctx).Save(&announcement).Error; err != nil {
|
||||||
|
logger.Error("保存公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||||
|
return fmt.Errorf("保存公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("定时发布公告成功", zap.String("announcement_id", announcementID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
332
config.yaml
332
config.yaml
@@ -22,8 +22,8 @@ database:
|
|||||||
name: "tyapi_dev"
|
name: "tyapi_dev"
|
||||||
sslmode: "disable"
|
sslmode: "disable"
|
||||||
timezone: "Asia/Shanghai"
|
timezone: "Asia/Shanghai"
|
||||||
max_open_conns: 25
|
max_open_conns: 50
|
||||||
max_idle_conns: 10
|
max_idle_conns: 20
|
||||||
conn_max_lifetime: 300s
|
conn_max_lifetime: 300s
|
||||||
auto_migrate: true
|
auto_migrate: true
|
||||||
|
|
||||||
@@ -44,17 +44,73 @@ cache:
|
|||||||
cleanup_interval: 600s
|
cleanup_interval: 600s
|
||||||
max_size: 1000
|
max_size: 1000
|
||||||
|
|
||||||
|
# 🚀 日志系统配置 - 基于 Zap 官方推荐
|
||||||
logger:
|
logger:
|
||||||
level: "info"
|
# 基础配置
|
||||||
format: "console"
|
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
|
||||||
output: "file"
|
format: "json" # 输出格式: json, console
|
||||||
log_dir: "logs"
|
output: "file" # 输出方式: stdout, stderr, file
|
||||||
max_size: 100
|
log_dir: "logs" # 日志目录
|
||||||
max_backups: 3
|
use_daily: true # 是否按日分包
|
||||||
max_age: 7
|
use_color: false # 是否使用彩色输出(仅console格式有效)
|
||||||
compress: true
|
# 文件配置
|
||||||
use_color: true
|
max_size: 100 # 单个文件最大大小(MB)
|
||||||
use_daily: false
|
max_backups: 5 # 最大备份文件数
|
||||||
|
max_age: 30 # 最大保留天数
|
||||||
|
compress: true # 是否压缩
|
||||||
|
|
||||||
|
# 高级功能
|
||||||
|
enable_level_separation: true # 是否启用按级别分文件
|
||||||
|
enable_request_logging: true # 是否启用请求日志
|
||||||
|
enable_performance_log: true # 是否启用性能日志
|
||||||
|
|
||||||
|
# 开发环境配置
|
||||||
|
development: true # 是否为开发环境
|
||||||
|
sampling: false # 是否启用采样
|
||||||
|
|
||||||
|
# 各级别配置(按级别分文件时使用)
|
||||||
|
level_configs:
|
||||||
|
debug:
|
||||||
|
max_size: 50 # 50MB
|
||||||
|
max_backups: 3
|
||||||
|
max_age: 7 # 7天
|
||||||
|
compress: true
|
||||||
|
info:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200 # 200MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90 # 90天
|
||||||
|
compress: true
|
||||||
|
fatal:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365 # 1年
|
||||||
|
compress: true
|
||||||
|
panic:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365 # 1年
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 全面日志配置
|
||||||
|
comprehensive_logging:
|
||||||
|
enable_request_logging: true
|
||||||
|
enable_response_logging: true
|
||||||
|
enable_request_body_logging: true # 开发环境记录请求体
|
||||||
|
enable_error_logging: true
|
||||||
|
enable_business_logging: true
|
||||||
|
enable_performance_logging: true
|
||||||
|
max_body_size: 10240 # 10KB
|
||||||
|
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
|
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
|
||||||
@@ -68,8 +124,8 @@ sms:
|
|||||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||||
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
|
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
|
||||||
endpoint_url: "dysmsapi.aliyuncs.com"
|
endpoint_url: "dysmsapi.aliyuncs.com"
|
||||||
sign_name: "天远数据"
|
sign_name: "天远查"
|
||||||
template_code: "SMS_474525324"
|
template_code: "SMS_302641455"
|
||||||
code_length: 6
|
code_length: 6
|
||||||
expire_time: 5m
|
expire_time: 5m
|
||||||
mock_enabled: false
|
mock_enabled: false
|
||||||
@@ -107,45 +163,45 @@ ratelimit:
|
|||||||
|
|
||||||
# 每日请求限制配置
|
# 每日请求限制配置
|
||||||
daily_ratelimit:
|
daily_ratelimit:
|
||||||
max_requests_per_day: 200 # 每日最大请求次数
|
max_requests_per_day: 200 # 每日最大请求次数
|
||||||
max_requests_per_ip: 10 # 每个IP每日最大请求次数
|
max_requests_per_ip: 10 # 每个IP每日最大请求次数
|
||||||
key_prefix: "daily_limit" # Redis键前缀
|
key_prefix: "daily_limit" # Redis键前缀
|
||||||
ttl: 24h # 键过期时间
|
ttl: 24h # 键过期时间
|
||||||
max_concurrent: 5 # 最大并发请求数
|
max_concurrent: 5 # 最大并发请求数
|
||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
enable_ip_whitelist: false # 是否启用IP白名单
|
enable_ip_whitelist: false # 是否启用IP白名单
|
||||||
ip_whitelist: # IP白名单列表
|
ip_whitelist: # IP白名单列表
|
||||||
- "192.168.1.*" # 内网IP段
|
- "192.168.1.*" # 内网IP段
|
||||||
- "10.0.0.*" # 内网IP段
|
- "10.0.0.*" # 内网IP段
|
||||||
- "127.0.0.1" # 本地回环
|
- "127.0.0.1" # 本地回环
|
||||||
|
|
||||||
enable_ip_blacklist: true # 是否启用IP黑名单
|
enable_ip_blacklist: true # 是否启用IP黑名单
|
||||||
ip_blacklist: # IP黑名单列表
|
ip_blacklist: # IP黑名单列表
|
||||||
- "0.0.0.0" # 无效IP
|
- "0.0.0.0" # 无效IP
|
||||||
- "255.255.255.255" # 广播IP
|
- "255.255.255.255" # 广播IP
|
||||||
|
|
||||||
enable_user_agent: true # 是否检查User-Agent
|
enable_user_agent: false # 是否检查User-Agent
|
||||||
blocked_user_agents: # 被阻止的User-Agent
|
blocked_user_agents: # 被阻止的User-Agent
|
||||||
- "bot" # 机器人
|
- "bot" # 机器人
|
||||||
- "crawler" # 爬虫
|
- "crawler" # 爬虫
|
||||||
- "spider" # 蜘蛛
|
- "spider" # 蜘蛛
|
||||||
- "scraper" # 抓取器
|
- "scraper" # 抓取器
|
||||||
- "curl" # curl工具
|
- "curl" # curl工具
|
||||||
- "wget" # wget工具
|
- "wget" # wget工具
|
||||||
- "python" # Python脚本
|
- "python" # Python脚本
|
||||||
- "java" # Java脚本
|
- "java" # Java脚本
|
||||||
- "go-http-client" # Go HTTP客户端
|
- "go-http-client" # Go HTTP客户端
|
||||||
|
|
||||||
enable_referer: true # 是否检查Referer
|
enable_referer: true # 是否检查Referer
|
||||||
allowed_referers: # 允许的Referer
|
allowed_referers: # 允许的Referer
|
||||||
- "https://console.tianyuanapi.com" # 天元API控制台
|
- "https://console.tianyuanapi.com" # 天元API控制台
|
||||||
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
|
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
|
||||||
|
|
||||||
enable_proxy_check: true # 是否检查代理
|
enable_proxy_check: false # 是否检查代理
|
||||||
enable_geo_block: false # 是否启用地理位置阻止
|
enable_geo_block: false # 是否启用地理位置阻止
|
||||||
blocked_countries: # 被阻止的国家/地区
|
blocked_countries: # 被阻止的国家/地区
|
||||||
- "XX" # 示例国家代码
|
- "XX" # 示例国家代码
|
||||||
|
|
||||||
monitoring:
|
monitoring:
|
||||||
metrics_enabled: true
|
metrics_enabled: true
|
||||||
@@ -171,14 +227,15 @@ development:
|
|||||||
debug: true
|
debug: true
|
||||||
enable_profiler: true
|
enable_profiler: true
|
||||||
enable_cors: true
|
enable_cors: true
|
||||||
cors_allowed_origins: "http://localhost:3000,http://localhost:3001"
|
cors_allowed_origins: "http://localhost:5173,https://consoletest.tianyuanapi.com,https://console.tianyuanapi.com"
|
||||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With"
|
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||||
|
|
||||||
# 企业微信配置
|
# 企业微信配置
|
||||||
wechat_work:
|
wechat_work:
|
||||||
webhook_url: ""
|
webhook_url: ""
|
||||||
secret: ""
|
secret: ""
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 📝 e签宝服务配置
|
# 📝 e签宝服务配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -186,7 +243,7 @@ esign:
|
|||||||
app_id: "7439073138"
|
app_id: "7439073138"
|
||||||
app_secret: "d76e27fdd169b391e09262a0959dac5c"
|
app_secret: "d76e27fdd169b391e09262a0959dac5c"
|
||||||
server_url: "https://smlopenapi.esign.cn"
|
server_url: "https://smlopenapi.esign.cn"
|
||||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
@@ -219,6 +276,12 @@ wallet:
|
|||||||
- recharge_amount: 10000.00 # 充值10000元
|
- recharge_amount: 10000.00 # 充值10000元
|
||||||
bonus_amount: 800.00 # 赠送800元
|
bonus_amount: 800.00 # 赠送800元
|
||||||
|
|
||||||
|
# 余额预警配置
|
||||||
|
balance_alert:
|
||||||
|
default_enabled: true # 默认启用余额预警
|
||||||
|
default_threshold: 200.00 # 默认预警阈值
|
||||||
|
alert_cooldown_hours: 24 # 预警冷却时间(小时)
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌍 西部数据配置
|
# 🌍 西部数据配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -228,6 +291,32 @@ westdex:
|
|||||||
secret_id: "449159"
|
secret_id: "449159"
|
||||||
secret_second_id: "296804"
|
secret_second_id: "296804"
|
||||||
|
|
||||||
|
# 西部数据日志配置
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "westdex"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
# 各级别配置
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌍 羽山配置
|
# 🌍 羽山配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -236,6 +325,32 @@ yushan:
|
|||||||
api_key: "4c566c4a4b543164535455685655316c"
|
api_key: "4c566c4a4b543164535455685655316c"
|
||||||
acct_id: "YSSJ843926726"
|
acct_id: "YSSJ843926726"
|
||||||
|
|
||||||
|
# 羽山日志配置
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "yushan"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
# 各级别配置
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 💰 支付宝支付配置
|
# 💰 支付宝支付配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -247,6 +362,28 @@ alipay:
|
|||||||
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
||||||
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 💰 微信支付配置
|
||||||
|
# ===========================================
|
||||||
|
Wxpay:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
mch_id: "1683589176"
|
||||||
|
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||||
|
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||||
|
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||||
|
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||||
|
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||||
|
notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback"
|
||||||
|
refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback"
|
||||||
|
|
||||||
|
# 微信小程序配置
|
||||||
|
WechatMini:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
|
# 微信H5配置
|
||||||
|
WechatH5:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🔍 天眼查配置
|
# 🔍 天眼查配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -260,3 +397,100 @@ tianyancha:
|
|||||||
alicloud:
|
alicloud:
|
||||||
host: "https://kzidcardv1.market.alicloudapi.com"
|
host: "https://kzidcardv1.market.alicloudapi.com"
|
||||||
app_code: "d55b58829efb41c8aa8e86769cba4844"
|
app_code: "d55b58829efb41c8aa8e86769cba4844"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🔍 智查金控配置
|
||||||
|
# ===========================================
|
||||||
|
zhicha:
|
||||||
|
url: "https://www.zhichajinkong.com/dataMiddle/api/handle"
|
||||||
|
app_id: "4b78fff61ab8426f"
|
||||||
|
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
|
||||||
|
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
|
||||||
|
|
||||||
|
# 智查金控日志配置
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "zhicha"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
# 各级别配置
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🌐 木子数据配置
|
||||||
|
# ===========================================
|
||||||
|
muzi:
|
||||||
|
url: "https://carv.m0101.com/magic/carv/pubin/service/academic"
|
||||||
|
app_id: "713014138179585"
|
||||||
|
app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d"
|
||||||
|
timeout: 60s
|
||||||
|
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "muzi"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 50
|
||||||
|
max_backups: 3
|
||||||
|
max_age: 7
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🎯 行为数据配置
|
||||||
|
# ===========================================
|
||||||
|
xingwei:
|
||||||
|
url: "https://sjztyh.chengdaoji.cn/dataCenterManageApi/manage/interface/doc/api/handle"
|
||||||
|
api_id: "jGtqla2FQv1zuXuH"
|
||||||
|
api_key: "iR1qS9725N4JA70gwlwohqT3ogl2zBf3"
|
||||||
|
|
||||||
|
# 行为数据日志配置
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "xingwei"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
# 各级别配置
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|||||||
@@ -15,19 +15,6 @@ database:
|
|||||||
name: "tyapi_dev"
|
name: "tyapi_dev"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 📝 日志配置
|
|
||||||
# ===========================================
|
|
||||||
logger:
|
|
||||||
level: info
|
|
||||||
format: json
|
|
||||||
output: "console"
|
|
||||||
log_dir: "logs"
|
|
||||||
max_size: 100
|
|
||||||
max_backups: 5
|
|
||||||
max_age: 30
|
|
||||||
compress: true
|
|
||||||
use_daily: true
|
|
||||||
# ===========================================
|
|
||||||
# 🔐 JWT配置
|
# 🔐 JWT配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
jwt:
|
jwt:
|
||||||
@@ -56,7 +43,7 @@ esign:
|
|||||||
app_id: "7439073713"
|
app_id: "7439073713"
|
||||||
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
|
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
|
||||||
server_url: "https://smlopenapi.esign.cn"
|
server_url: "https://smlopenapi.esign.cn"
|
||||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
@@ -80,6 +67,8 @@ westdex:
|
|||||||
key: "121a1e41fc1690dd6b90afbcacd80cf4"
|
key: "121a1e41fc1690dd6b90afbcacd80cf4"
|
||||||
secret_id: "449159"
|
secret_id: "449159"
|
||||||
secret_second_id: "296804"
|
secret_second_id: "296804"
|
||||||
|
yushan:
|
||||||
|
url: https://api2.yushanshuju.com/credit-gw/service
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 💰 支付宝支付配置
|
# 💰 支付宝支付配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -92,6 +81,27 @@ alipay:
|
|||||||
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
# 💰 微信支付配置
|
||||||
|
# ===========================================
|
||||||
|
Wxpay:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
mch_id: "1683589176"
|
||||||
|
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||||
|
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||||
|
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||||
|
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||||
|
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||||
|
notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback"
|
||||||
|
refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback"
|
||||||
|
|
||||||
|
# 微信小程序配置
|
||||||
|
WechatMini:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
|
# 微信H5配置
|
||||||
|
WechatH5:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
# ===========================================
|
||||||
# 💰 钱包配置
|
# 💰 钱包配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
wallet:
|
wallet:
|
||||||
@@ -113,3 +123,54 @@ wallet:
|
|||||||
tianyancha:
|
tianyancha:
|
||||||
base_url: http://open.api.tianyancha.com/services
|
base_url: http://open.api.tianyancha.com/services
|
||||||
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
||||||
|
# 智查金控配置示例
|
||||||
|
zhicha:
|
||||||
|
url: "http://proxy.tianyuanapi.com/dataMiddle/api/handle"
|
||||||
|
app_id: "4b78fff61ab8426f"
|
||||||
|
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
|
||||||
|
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
|
||||||
|
development:
|
||||||
|
enable_cors: true
|
||||||
|
cors_allowed_origins: "http://localhost:5173,http://localhost:8080"
|
||||||
|
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||||
|
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🚦 开发环境全局限流(放宽或近似关闭)
|
||||||
|
# ===========================================
|
||||||
|
ratelimit:
|
||||||
|
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
|
||||||
|
window: 1s # 时间窗口
|
||||||
|
burst: 1000000 # 令牌桶突发容量
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🚀 开发环境频率限制配置(放宽限制)
|
||||||
|
# ===========================================
|
||||||
|
daily_ratelimit:
|
||||||
|
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||||
|
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||||
|
max_concurrent: 50 # 开发环境最大并发请求数
|
||||||
|
|
||||||
|
# 排除频率限制的路径
|
||||||
|
exclude_paths:
|
||||||
|
- "/health" # 健康检查接口
|
||||||
|
- "/metrics" # 监控指标接口
|
||||||
|
|
||||||
|
# 排除频率限制的域名
|
||||||
|
exclude_domains:
|
||||||
|
- "api.*" # API二级域名不受频率限制
|
||||||
|
- "*.api.*" # 支持多级API域名
|
||||||
|
|
||||||
|
# 开发环境安全配置(放宽限制)
|
||||||
|
enable_ip_whitelist: true # 启用IP白名单
|
||||||
|
ip_whitelist: # 开发环境IP白名单
|
||||||
|
- "127.0.0.1" # 本地回环
|
||||||
|
- "localhost" # 本地主机
|
||||||
|
- "192.168.*" # 内网IP段
|
||||||
|
- "10.*" # 内网IP段
|
||||||
|
- "172.16.*" # 内网IP段
|
||||||
|
|
||||||
|
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
||||||
|
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||||
|
enable_referer: false # 开发环境禁用Referer检查
|
||||||
|
enable_proxy_check: false # 开发环境禁用代理检查
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ app:
|
|||||||
server:
|
server:
|
||||||
mode: release
|
mode: release
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🔒 CORS配置 - 生产环境
|
||||||
|
# ===========================================
|
||||||
|
development:
|
||||||
|
enable_cors: true
|
||||||
|
cors_allowed_origins: "http://localhost:5173,https://consoletest.tianyuanapi.com,https://console.tianyuanapi.com"
|
||||||
|
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||||
|
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🗄️ 数据库配置
|
# 🗄️ 数据库配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -34,63 +43,7 @@ redis:
|
|||||||
port: "6379"
|
port: "6379"
|
||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
# ===========================================
|
|
||||||
# 📝 日志配置
|
|
||||||
# ===========================================
|
|
||||||
logger:
|
|
||||||
level: info
|
|
||||||
format: json
|
|
||||||
output: "file"
|
|
||||||
log_dir: "/app/logs"
|
|
||||||
max_size: 100
|
|
||||||
max_backups: 5
|
|
||||||
max_age: 30
|
|
||||||
compress: true
|
|
||||||
use_daily: true
|
|
||||||
# 启用按级别分文件
|
|
||||||
enable_level_separation: true
|
|
||||||
# 各级别日志文件配置
|
|
||||||
level_configs:
|
|
||||||
debug:
|
|
||||||
max_size: 50 # 50MB
|
|
||||||
max_backups: 3
|
|
||||||
max_age: 7 # 7天
|
|
||||||
compress: true
|
|
||||||
info:
|
|
||||||
max_size: 100 # 100MB
|
|
||||||
max_backups: 5
|
|
||||||
max_age: 30 # 30天
|
|
||||||
compress: true
|
|
||||||
warn:
|
|
||||||
max_size: 100 # 100MB
|
|
||||||
max_backups: 5
|
|
||||||
max_age: 30 # 30天
|
|
||||||
compress: true
|
|
||||||
error:
|
|
||||||
max_size: 200 # 200MB
|
|
||||||
max_backups: 10
|
|
||||||
max_age: 90 # 90天
|
|
||||||
compress: true
|
|
||||||
fatal:
|
|
||||||
max_size: 100 # 100MB
|
|
||||||
max_backups: 10
|
|
||||||
max_age: 365 # 1年
|
|
||||||
compress: true
|
|
||||||
panic:
|
|
||||||
max_size: 100 # 100MB
|
|
||||||
max_backups: 10
|
|
||||||
max_age: 365 # 1年
|
|
||||||
compress: true
|
|
||||||
# 生产环境全面日志配置
|
|
||||||
comprehensive_logging:
|
|
||||||
enable_request_logging: true
|
|
||||||
enable_response_logging: true
|
|
||||||
enable_request_body_logging: false # 生产环境不记录请求体(安全考虑)
|
|
||||||
enable_error_logging: true
|
|
||||||
enable_business_logging: true
|
|
||||||
enable_performance_logging: true
|
|
||||||
max_body_size: 10240 # 10KB
|
|
||||||
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🔐 JWT配置
|
# 🔐 JWT配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -122,7 +75,7 @@ esign:
|
|||||||
app_id: "5112008003"
|
app_id: "5112008003"
|
||||||
app_secret: "d487672273e7aa70c800804a1d9499b9"
|
app_secret: "d487672273e7aa70c800804a1d9499b9"
|
||||||
server_url: "https://openapi.esign.cn"
|
server_url: "https://openapi.esign.cn"
|
||||||
template_id: "c82af4df2790430299c81321f309eef3"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
@@ -139,14 +92,6 @@ esign:
|
|||||||
client_type: "ALL"
|
client_type: "ALL"
|
||||||
redirect_url: "https://console.tianyuanapi.com/certification/callback/sign"
|
redirect_url: "https://console.tianyuanapi.com/certification/callback/sign"
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌍 西部数据配置
|
|
||||||
# ===========================================
|
|
||||||
westdex:
|
|
||||||
url: "http://proxy.tianyuanapi.com/api/invoke"
|
|
||||||
key: "121a1e41fc1690dd6b90afbcacd80cf4"
|
|
||||||
secret_id: "449159"
|
|
||||||
secret_second_id: "296804"
|
|
||||||
# ===========================================
|
|
||||||
# 💰 支付宝支付配置
|
# 💰 支付宝支付配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
alipay:
|
alipay:
|
||||||
@@ -173,3 +118,42 @@ wallet:
|
|||||||
- recharge_amount: 10000.00 # 充值10000元
|
- recharge_amount: 10000.00 # 充值10000元
|
||||||
bonus_amount: 800.00 # 赠送800元
|
bonus_amount: 800.00 # 赠送800元
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🚦 频率限制配置 - 生产环境
|
||||||
|
# ===========================================
|
||||||
|
daily_ratelimit:
|
||||||
|
max_requests_per_day: 50000 # 生产环境每日最大请求次数
|
||||||
|
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
|
||||||
|
max_concurrent: 200 # 生产环境最大并发请求数
|
||||||
|
|
||||||
|
# 排除频率限制的路径
|
||||||
|
exclude_paths:
|
||||||
|
- "/health" # 健康检查接口
|
||||||
|
- "/metrics" # 监控指标接口
|
||||||
|
|
||||||
|
# 排除频率限制的域名
|
||||||
|
exclude_domains:
|
||||||
|
- "api.*" # API二级域名不受频率限制
|
||||||
|
- "*.api.*" # 支持多级API域名
|
||||||
|
|
||||||
|
# 生产环境安全配置(严格限制)
|
||||||
|
enable_ip_whitelist: false # 生产环境不启用IP白名单
|
||||||
|
enable_ip_blacklist: true # 启用IP黑名单
|
||||||
|
ip_blacklist: # 生产环境IP黑名单
|
||||||
|
- "192.168.1.100" # 示例:被禁止的IP
|
||||||
|
- "10.0.0.50" # 示例:被禁止的IP
|
||||||
|
|
||||||
|
enable_user_agent: true # 启用User-Agent检查
|
||||||
|
blocked_user_agents: # 被阻止的User-Agent
|
||||||
|
- "curl" # 阻止curl请求
|
||||||
|
- "wget" # 阻止wget请求
|
||||||
|
- "python-requests" # 阻止Python requests
|
||||||
|
|
||||||
|
enable_referer: true # 启用Referer检查
|
||||||
|
allowed_referers: # 允许的Referer
|
||||||
|
- "https://console.tianyuanapi.com"
|
||||||
|
- "https://consoletest.tianyuanapi.com"
|
||||||
|
|
||||||
|
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||||
|
enable_proxy_check: true # 启用代理检查
|
||||||
|
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 测试发票申请通过的事件系统
|
|
||||||
func main() {
|
|
||||||
fmt.Println("🔍 开始测试发票事件系统...")
|
|
||||||
|
|
||||||
// 1. 首先获取待处理的发票申请列表
|
|
||||||
fmt.Println("\n📋 步骤1: 获取待处理的发票申请列表")
|
|
||||||
applications, err := getPendingApplications()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 获取申请列表失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(applications) == 0 {
|
|
||||||
fmt.Println("⚠️ 没有待处理的发票申请")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择第一个申请进行测试
|
|
||||||
application := applications[0]
|
|
||||||
fmt.Printf("✅ 找到申请: ID=%s, 公司=%s, 金额=%s\n",
|
|
||||||
application["id"], application["company_name"], application["amount"])
|
|
||||||
|
|
||||||
// 2. 创建一个测试PDF文件
|
|
||||||
fmt.Println("\n📄 步骤2: 创建测试PDF文件")
|
|
||||||
testFile, err := createTestPDF()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 创建测试文件失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(testFile)
|
|
||||||
|
|
||||||
// 3. 通过发票申请(上传文件)
|
|
||||||
fmt.Println("\n📤 步骤3: 通过发票申请并上传文件")
|
|
||||||
err = approveInvoiceApplication(application["id"].(string), testFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 通过申请失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("✅ 发票申请通过成功!")
|
|
||||||
fmt.Println("📧 请检查日志中的邮件发送情况...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取待处理的发票申请列表
|
|
||||||
func getPendingApplications() ([]map[string]interface{}, error) {
|
|
||||||
url := "http://localhost:8080/api/v1/admin/invoices/pending"
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result["code"] != float64(200) {
|
|
||||||
return nil, fmt.Errorf("API返回错误: %s", result["message"])
|
|
||||||
}
|
|
||||||
|
|
||||||
data := result["data"].(map[string]interface{})
|
|
||||||
applications := data["applications"].([]interface{})
|
|
||||||
|
|
||||||
applicationsList := make([]map[string]interface{}, len(applications))
|
|
||||||
for i, app := range applications {
|
|
||||||
applicationsList[i] = app.(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return applicationsList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建测试PDF文件
|
|
||||||
func createTestPDF() (string, error) {
|
|
||||||
// 创建一个简单的PDF内容(这里只是示例)
|
|
||||||
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 44\n>>\nstream\nBT\n/F1 12 Tf\n72 720 Td\n(Test Invoice) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n297\n%%EOF\n")
|
|
||||||
|
|
||||||
tempFile := filepath.Join(os.TempDir(), "test_invoice.pdf")
|
|
||||||
err := os.WriteFile(tempFile, pdfContent, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过发票申请
|
|
||||||
func approveInvoiceApplication(applicationID, filePath string) error {
|
|
||||||
url := fmt.Sprintf("http://localhost:8080/api/v1/admin/invoices/%s/approve", applicationID)
|
|
||||||
|
|
||||||
// 创建multipart表单
|
|
||||||
var buf bytes.Buffer
|
|
||||||
writer := multipart.NewWriter(&buf)
|
|
||||||
|
|
||||||
// 添加文件
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("file", "test_invoice.pdf")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(part, file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加备注
|
|
||||||
writer.WriteField("admin_notes", "测试通过 - 调试事件系统")
|
|
||||||
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
req, err := http.NewRequest("POST", url, &buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result["code"] != float64(200) {
|
|
||||||
return fmt.Errorf("API返回错误: %s", result["message"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -44,6 +44,23 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
# Asynq 任务监控
|
||||||
|
asynq-monitor:
|
||||||
|
image: hibiken/asynqmon:latest
|
||||||
|
container_name: tyapi-asynq-monitor
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
command: --redis-addr=tyapi-redis:6379
|
||||||
|
networks:
|
||||||
|
- tyapi-network
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
restart:
|
||||||
|
unless-stopped
|
||||||
|
|
||||||
# Jaeger 链路追踪
|
# Jaeger 链路追踪
|
||||||
jaeger:
|
jaeger:
|
||||||
image: jaegertracing/all-in-one:1.70.0
|
image: jaegertracing/all-in-one:1.70.0
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ services:
|
|||||||
# 生产环境不暴露端口到主机
|
# 生产环境不暴露端口到主机
|
||||||
# ports:
|
# ports:
|
||||||
# - "6379:6379"
|
# - "6379:6379"
|
||||||
|
|
||||||
# TYAPI 应用程序
|
# TYAPI 应用程序
|
||||||
tyapi-app:
|
tyapi-app:
|
||||||
build:
|
build:
|
||||||
@@ -113,6 +112,78 @@ services:
|
|||||||
memory: 256M
|
memory: 256M
|
||||||
cpus: "0.3"
|
cpus: "0.3"
|
||||||
|
|
||||||
|
# TYAPI Worker 服务
|
||||||
|
tyapi-worker:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.worker
|
||||||
|
args:
|
||||||
|
VERSION: 1.0.0
|
||||||
|
COMMIT: dev
|
||||||
|
BUILD_TIME: ""
|
||||||
|
container_name: tyapi-worker-prod
|
||||||
|
environment:
|
||||||
|
# 时区配置
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
|
||||||
|
# 环境设置
|
||||||
|
ENV: production
|
||||||
|
volumes:
|
||||||
|
- ./logs:/root/logs
|
||||||
|
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||||
|
networks:
|
||||||
|
- tyapi-network
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "ps", "aux", "|", "grep", "worker"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "0.5"
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
cpus: "0.1"
|
||||||
|
|
||||||
|
# Asynq 任务监控 (生产环境)
|
||||||
|
asynq-monitor:
|
||||||
|
image: hibiken/asynqmon:latest
|
||||||
|
container_name: tyapi-asynq-monitor-prod
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "25080:8080"
|
||||||
|
command: --redis-addr=tyapi-redis-prod:6379
|
||||||
|
networks:
|
||||||
|
- tyapi-network
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: "0.3"
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: "0.1"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
261
docs/IVYZ9K2L_WestDex_API文档.md
Normal file
261
docs/IVYZ9K2L_WestDex_API文档.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# IVYZ9K2L - 身份认证三要素(人脸图像版) WestDex API 文档
|
||||||
|
|
||||||
|
## 接口信息
|
||||||
|
|
||||||
|
- **接口名称**: 身份认证三要素(人脸图像版)
|
||||||
|
- **接口代码**: IVYZ9K2L
|
||||||
|
- **WestDex API Code**: `idCardThreeElements`
|
||||||
|
- **请求方式**: POST
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
|
||||||
|
## 请求URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://apimaster.westdex.com.cn/api/invoke/{secret_id}/{api_code}?timestamp={timestamp}
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL 参数说明
|
||||||
|
|
||||||
|
| 参数 | 说明 | 示例值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| secret_id | 西部数据 SecretID(从配置获取) | `449159` |
|
||||||
|
| api_code | API代码 | `idCardThreeElements` |
|
||||||
|
| timestamp | 毫秒级时间戳(URL参数) | `1713421668375` |
|
||||||
|
|
||||||
|
### 完整URL示例
|
||||||
|
|
||||||
|
```
|
||||||
|
https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=1713421668375
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求头
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求体
|
||||||
|
|
||||||
|
### 请求体结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"timeStamp": "1713421668375",
|
||||||
|
"customNumber": "449159",
|
||||||
|
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
|
||||||
|
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
|
||||||
|
},
|
||||||
|
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
#### data 对象(必填)
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|
||||||
|
|--------|------|------|------|--------|
|
||||||
|
| timeStamp | string | 是 | 毫秒级时间戳,与URL参数中的timestamp一致 | `"1713421668375"` |
|
||||||
|
| customNumber | string | 是 | 自定义编号,使用配置中的 secret_id | `"449159"` |
|
||||||
|
| xM | string | 是 | 加密后的姓名(使用AES加密,密钥为配置中的key) | `"fU4B3fR3Dw+UkHNkFsHIjA=="` |
|
||||||
|
| gMSFZHM | string | 是 | 加密后的身份证号(使用AES加密,密钥为配置中的key) | `"qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="` |
|
||||||
|
|
||||||
|
#### photoData(必填)
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|
||||||
|
|--------|------|------|------|--------|
|
||||||
|
| photoData | string | 是 | Base64编码的人脸图片数据,仅支持JPG、BMP、PNG格式 | `"Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."` |
|
||||||
|
|
||||||
|
## 加密说明
|
||||||
|
|
||||||
|
### 姓名和身份证号加密
|
||||||
|
|
||||||
|
使用 AES-ECB 模式加密,密钥为配置中的 `key`(示例:`121a1e41fc1690dd6b90afbcacd80cf4`)
|
||||||
|
|
||||||
|
**加密步骤**:
|
||||||
|
1. 使用密钥生成 AES 密钥
|
||||||
|
2. 使用 AES-ECB 模式加密原始数据
|
||||||
|
3. 将加密结果进行 Base64 编码
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
- 原始姓名:`"张三"`
|
||||||
|
- 加密后:`"fU4B3fR3Dw+UkHNkFsHIjA=="`
|
||||||
|
|
||||||
|
## 完整请求示例
|
||||||
|
|
||||||
|
### cURL 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=1713421668375" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"timeStamp": "1713421668375",
|
||||||
|
"customNumber": "449159",
|
||||||
|
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
|
||||||
|
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
|
||||||
|
},
|
||||||
|
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const timestamp = Date.now().toString();
|
||||||
|
const url = `https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements?timestamp=${timestamp}`;
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
data: {
|
||||||
|
timeStamp: timestamp,
|
||||||
|
customNumber: "449159",
|
||||||
|
xM: "fU4B3fR3Dw+UkHNkFsHIjA==", // 加密后的姓名
|
||||||
|
gMSFZHM: "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg=" // 加密后的身份证号
|
||||||
|
},
|
||||||
|
photoData: "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..." // Base64图片数据
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => console.log(data))
|
||||||
|
.catch(error => console.error('Error:', error));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应格式
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "00000",
|
||||||
|
"message": "成功",
|
||||||
|
"data": "加密后的响应数据(需要解密)",
|
||||||
|
"id": "响应ID",
|
||||||
|
"error_code": null,
|
||||||
|
"reason": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "错误码",
|
||||||
|
"message": "错误信息",
|
||||||
|
"data": "加密后的错误数据(需要解密)",
|
||||||
|
"id": "响应ID",
|
||||||
|
"error_code": 错误码,
|
||||||
|
"reason": "错误原因"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应状态码说明
|
||||||
|
|
||||||
|
| 状态码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| `00000` | 成功 |
|
||||||
|
| `200` | 成功 |
|
||||||
|
| `0` | 成功 |
|
||||||
|
| 其他 | 失败 |
|
||||||
|
|
||||||
|
## 响应数据解密
|
||||||
|
|
||||||
|
响应中的 `data` 字段是加密的,需要使用相同的密钥进行解密:
|
||||||
|
|
||||||
|
**解密步骤**:
|
||||||
|
1. 使用配置中的 `key` 作为密钥
|
||||||
|
2. 对 `data` 字段进行 Base64 解码
|
||||||
|
3. 使用 AES-ECB 模式解密
|
||||||
|
4. 得到原始 JSON 字符串
|
||||||
|
|
||||||
|
## Apifox 配置步骤
|
||||||
|
|
||||||
|
### 1. 创建新请求
|
||||||
|
|
||||||
|
- 方法:`POST`
|
||||||
|
- URL:`https://apimaster.westdex.com.cn/api/invoke/449159/idCardThreeElements`
|
||||||
|
|
||||||
|
### 2. 设置URL参数
|
||||||
|
|
||||||
|
在"Params"标签页添加:
|
||||||
|
- `timestamp`: `{{$timestamp}}` (使用Apifox变量生成当前时间戳)
|
||||||
|
|
||||||
|
### 3. 设置请求头
|
||||||
|
|
||||||
|
在"Headers"标签页添加:
|
||||||
|
- `Content-Type`: `application/json`
|
||||||
|
|
||||||
|
### 4. 设置请求体
|
||||||
|
|
||||||
|
在"Body"标签页选择 `raw` 类型,格式选择 `JSON`,内容如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"timeStamp": "{{$timestamp}}",
|
||||||
|
"customNumber": "449159",
|
||||||
|
"xM": "fU4B3fR3Dw+UkHNkFsHIjA==",
|
||||||
|
"gMSFZHM": "qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg="
|
||||||
|
},
|
||||||
|
"photoData": "Qk3OlwAAAAAAADYAAAAoAAAAZgAAAH4AAAABABgAAA..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 配置环境变量(可选)
|
||||||
|
|
||||||
|
在Apifox中创建环境变量:
|
||||||
|
- `westdex_secret_id`: `449159`
|
||||||
|
- `westdex_key`: `121a1e41fc1690dd6b90afbcacd80cf4`
|
||||||
|
- `westdex_url`: `https://apimaster.westdex.com.cn/api/invoke`
|
||||||
|
|
||||||
|
然后在URL中使用:`{{westdex_url}}/{{westdex_secret_id}}/idCardThreeElements?timestamp={{$timestamp}}`
|
||||||
|
|
||||||
|
### 6. 前置脚本(用于生成时间戳)
|
||||||
|
|
||||||
|
在"前置脚本"中添加:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 生成毫秒级时间戳
|
||||||
|
pm.environment.set("timestamp", Date.now().toString());
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在URL参数和请求体中使用 `{{timestamp}}`
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **时间戳同步**:URL参数中的 `timestamp` 和请求体 `data.timeStamp` 必须一致
|
||||||
|
2. **加密密钥**:姓名和身份证号必须使用配置中的 `key` 进行加密
|
||||||
|
3. **图片格式**:`photoData` 必须是纯Base64字符串(不包含 `data:image/xxx;base64,` 前缀)
|
||||||
|
4. **图片格式限制**:仅支持 JPG、BMP、PNG 三种格式
|
||||||
|
5. **请求超时**:建议设置60秒超时时间
|
||||||
|
6. **响应解密**:成功响应中的 `data` 字段需要解密后才能查看实际内容
|
||||||
|
|
||||||
|
## 配置信息
|
||||||
|
|
||||||
|
根据项目配置文件,当前使用的配置为:
|
||||||
|
|
||||||
|
- **URL**: `https://apimaster.westdex.com.cn/api/invoke`
|
||||||
|
- **Key**: `121a1e41fc1690dd6b90afbcacd80cf4`
|
||||||
|
- **SecretID**: `449159`
|
||||||
|
- **SecretSecondID**: `296804`
|
||||||
|
|
||||||
|
## 测试数据示例
|
||||||
|
|
||||||
|
### 原始数据
|
||||||
|
- 姓名:`张三`
|
||||||
|
- 身份证号:`110101199001011234`
|
||||||
|
- 人脸图片:需要转换为Base64格式
|
||||||
|
|
||||||
|
### 加密后的数据(示例)
|
||||||
|
- 加密姓名:`fU4B3fR3Dw+UkHNkFsHIjA==`
|
||||||
|
- 加密身份证号:`qL3GFeI7JO8txKDT25hjuXe5IhnGJ00Jg8+YYbnQ6wg=`
|
||||||
|
|
||||||
|
**注意**:实际加密结果会根据密钥和原始数据不同而变化,以上仅为示例格式。
|
||||||
|
|
||||||
210
docs/PDF缓存优化说明.md
Normal file
210
docs/PDF缓存优化说明.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# PDF接口文档下载缓存优化说明
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本次优化为PDF接口文档下载功能添加了本地文件缓存机制,显著提升了下载性能,减少了重复生成PDF的开销。
|
||||||
|
|
||||||
|
## 🔍 问题分析
|
||||||
|
|
||||||
|
### 原有问题
|
||||||
|
|
||||||
|
1. **性能问题**:
|
||||||
|
- 每次请求都重新生成PDF,没有缓存机制
|
||||||
|
- PDF生成涉及复杂的字体加载、页面构建、表格渲染等操作,耗时较长
|
||||||
|
- 同一产品的PDF被多次下载时,会重复执行相同的生成过程
|
||||||
|
|
||||||
|
2. **资源浪费**:
|
||||||
|
- CPU资源浪费在重复的PDF生成上
|
||||||
|
- 数据库查询重复执行
|
||||||
|
- 没有版本控制,即使产品文档没有变化,也会重新生成
|
||||||
|
|
||||||
|
## ✅ 解决方案
|
||||||
|
|
||||||
|
### 1. PDF缓存管理器 (`PDFCacheManager`)
|
||||||
|
|
||||||
|
创建了专门的PDF缓存管理器,提供以下功能:
|
||||||
|
|
||||||
|
- **本地文件缓存**:将生成的PDF文件保存到本地文件系统
|
||||||
|
- **版本控制**:基于产品ID和文档版本号生成缓存键,确保版本更新时自动失效
|
||||||
|
- **自动过期**:支持TTL(Time To Live)机制,自动清理过期缓存
|
||||||
|
- **大小限制**:支持最大缓存大小限制,防止磁盘空间耗尽
|
||||||
|
- **定期清理**:后台任务每小时自动清理过期文件
|
||||||
|
|
||||||
|
### 2. 缓存键生成策略
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 基于产品ID和文档版本号生成唯一的缓存键
|
||||||
|
cacheKey = MD5(productID + ":" + version)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 当产品文档版本更新时,自动生成新的缓存
|
||||||
|
- 旧版本的缓存会在过期后自动清理
|
||||||
|
|
||||||
|
### 3. 缓存流程
|
||||||
|
|
||||||
|
```
|
||||||
|
请求下载PDF
|
||||||
|
↓
|
||||||
|
检查缓存是否存在且有效
|
||||||
|
↓
|
||||||
|
├─ 缓存命中 → 直接返回缓存的PDF文件
|
||||||
|
└─ 缓存未命中 → 生成PDF → 保存到缓存 → 返回PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 集成到下载接口
|
||||||
|
|
||||||
|
修改了 `DownloadProductDocumentation` 方法:
|
||||||
|
|
||||||
|
- **缓存优先**:首先尝试从缓存获取PDF
|
||||||
|
- **异步保存**:生成新PDF后异步保存到缓存,不阻塞响应
|
||||||
|
- **缓存标识**:响应头中添加 `X-Cache: HIT/MISS` 标识,便于监控
|
||||||
|
|
||||||
|
## 🚀 性能提升
|
||||||
|
|
||||||
|
### 预期效果
|
||||||
|
|
||||||
|
1. **首次下载**:与之前相同,需要生成PDF(约1-3秒)
|
||||||
|
2. **后续下载**:直接从缓存读取(< 100ms),性能提升 **10-30倍**
|
||||||
|
3. **缓存命中率**:对于热门产品,缓存命中率可达 **80-90%**
|
||||||
|
|
||||||
|
### 响应时间对比
|
||||||
|
|
||||||
|
| 场景 | 优化前 | 优化后 | 提升 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| 首次下载 | 1-3秒 | 1-3秒 | - |
|
||||||
|
| 缓存命中 | 1-3秒 | < 100ms | **10-30倍** |
|
||||||
|
| 版本更新后首次 | 1-3秒 | 1-3秒 | - |
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
可以通过环境变量自定义缓存配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 缓存目录(默认:系统临时目录下的tyapi_pdf_cache)
|
||||||
|
export PDF_CACHE_DIR="/path/to/cache"
|
||||||
|
|
||||||
|
# 缓存过期时间(默认:24小时)
|
||||||
|
export PDF_CACHE_TTL="24h"
|
||||||
|
|
||||||
|
# 最大缓存大小(默认:500MB)
|
||||||
|
export PDF_CACHE_MAX_SIZE="524288000" # 字节
|
||||||
|
```
|
||||||
|
|
||||||
|
### 默认配置
|
||||||
|
|
||||||
|
- **缓存目录**:系统临时目录下的 `tyapi_pdf_cache`
|
||||||
|
- **TTL**:24小时
|
||||||
|
- **最大缓存大小**:500MB
|
||||||
|
|
||||||
|
## 📁 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tyapi-server/
|
||||||
|
├── internal/
|
||||||
|
│ └── shared/
|
||||||
|
│ └── pdf/
|
||||||
|
│ ├── pdf_cache_manager.go # 新增:PDF缓存管理器
|
||||||
|
│ ├── pdf_generator.go # 原有:PDF生成器
|
||||||
|
│ └── ...
|
||||||
|
├── internal/
|
||||||
|
│ └── infrastructure/
|
||||||
|
│ └── http/
|
||||||
|
│ └── handlers/
|
||||||
|
│ └── product_handler.go # 修改:集成缓存机制
|
||||||
|
└── internal/
|
||||||
|
└── container/
|
||||||
|
└── container.go # 修改:初始化缓存管理器
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 使用示例
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
缓存机制已自动集成,无需额外代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 用户请求下载PDF
|
||||||
|
GET /api/v1/products/{id}/documentation/download
|
||||||
|
|
||||||
|
// 系统自动:
|
||||||
|
// 1. 检查缓存
|
||||||
|
// 2. 缓存命中 → 直接返回
|
||||||
|
// 3. 缓存未命中 → 生成PDF → 保存缓存 → 返回
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动管理缓存
|
||||||
|
|
||||||
|
如果需要手动管理缓存(如产品更新后清除缓存):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使特定产品的缓存失效
|
||||||
|
cacheManager.InvalidateByProductID(productID)
|
||||||
|
|
||||||
|
// 使特定版本的缓存失效
|
||||||
|
cacheManager.Invalidate(productID, version)
|
||||||
|
|
||||||
|
// 清空所有缓存
|
||||||
|
cacheManager.Clear()
|
||||||
|
|
||||||
|
// 获取缓存统计信息
|
||||||
|
stats, _ := cacheManager.GetCacheStats()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 监控和日志
|
||||||
|
|
||||||
|
### 日志输出
|
||||||
|
|
||||||
|
系统会记录以下日志:
|
||||||
|
|
||||||
|
- **缓存命中**:`PDF缓存命中` - 包含产品ID、版本、文件大小
|
||||||
|
- **缓存未命中**:`PDF缓存未命中,开始生成PDF`
|
||||||
|
- **缓存保存**:`PDF已缓存` - 包含产品ID、缓存键、文件大小
|
||||||
|
- **缓存清理**:`已清理过期缓存文件` - 包含清理数量和释放空间
|
||||||
|
|
||||||
|
### 响应头标识
|
||||||
|
|
||||||
|
响应头中添加了缓存标识:
|
||||||
|
|
||||||
|
- `X-Cache: HIT` - 缓存命中
|
||||||
|
- `X-Cache: MISS` - 缓存未命中
|
||||||
|
|
||||||
|
## 🔒 安全考虑
|
||||||
|
|
||||||
|
1. **文件权限**:缓存文件权限设置为 `0644`,仅所有者可写
|
||||||
|
2. **目录隔离**:缓存文件存储在独立目录,不影响其他文件
|
||||||
|
3. **自动清理**:过期文件自动清理,防止磁盘空间耗尽
|
||||||
|
|
||||||
|
## 🐛 故障处理
|
||||||
|
|
||||||
|
### 缓存初始化失败
|
||||||
|
|
||||||
|
如果缓存管理器初始化失败,系统会:
|
||||||
|
|
||||||
|
- 记录警告日志
|
||||||
|
- 继续正常运行(禁用缓存功能)
|
||||||
|
- 所有请求都会重新生成PDF
|
||||||
|
|
||||||
|
### 缓存读取失败
|
||||||
|
|
||||||
|
如果缓存读取失败,系统会:
|
||||||
|
|
||||||
|
- 记录警告日志
|
||||||
|
- 自动降级为重新生成PDF
|
||||||
|
- 不影响用户体验
|
||||||
|
|
||||||
|
## 🔄 后续优化建议
|
||||||
|
|
||||||
|
1. **分布式缓存**:考虑使用Redis等分布式缓存,支持多实例部署
|
||||||
|
2. **缓存预热**:在系统启动时预生成热门产品的PDF
|
||||||
|
3. **压缩存储**:对PDF文件进行压缩存储,节省磁盘空间
|
||||||
|
4. **缓存统计**:添加更详细的缓存统计和监控指标
|
||||||
|
5. **智能清理**:基于LRU等算法,优先清理不常用的缓存
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
|
- **2024-12-XX**:初始版本,实现本地文件缓存机制
|
||||||
|
- 添加PDF缓存管理器
|
||||||
|
- 集成到下载接口
|
||||||
|
- 支持版本控制和自动过期
|
||||||
242
docs/Ubuntu服务器PDF字体配置指南.md
Normal file
242
docs/Ubuntu服务器PDF字体配置指南.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# Ubuntu服务器PDF字体配置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档说明如何在Ubuntu 24.04 LTS服务器上配置PDF生成功能所需的中文字体。
|
||||||
|
|
||||||
|
## 字体文件位置
|
||||||
|
|
||||||
|
确保字体文件存在于以下任一位置:
|
||||||
|
|
||||||
|
### 推荐路径(按优先级)
|
||||||
|
|
||||||
|
1. **工作目录相对路径**(最常用)
|
||||||
|
```
|
||||||
|
{工作目录}/internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
例如:如果工作目录是 `/www/tyapi-server`,则字体应在:
|
||||||
|
```
|
||||||
|
/www/tyapi-server/internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **可执行文件相对路径**
|
||||||
|
```
|
||||||
|
{可执行文件所在目录}/internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **环境变量指定路径**
|
||||||
|
```bash
|
||||||
|
export PDF_FONT_DIR=/path/to/fonts
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **硬编码路径**(后备方案)
|
||||||
|
- `/www/tyapi-server/internal/shared/pdf/fonts` ✅(已配置)
|
||||||
|
- `/app/internal/shared/pdf/fonts`(Docker)
|
||||||
|
- `/usr/local/tyapi-server/internal/shared/pdf/fonts`
|
||||||
|
- `/opt/tyapi-server/internal/shared/pdf/fonts`
|
||||||
|
- `/home/ubuntu/tyapi-server/internal/shared/pdf/fonts`
|
||||||
|
- `/root/tyapi-server/internal/shared/pdf/fonts`
|
||||||
|
- `/var/www/tyapi-server/internal/shared/pdf/fonts`
|
||||||
|
|
||||||
|
## 部署步骤
|
||||||
|
|
||||||
|
### 方法1:直接复制字体文件(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 创建字体目录
|
||||||
|
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
|
||||||
|
|
||||||
|
# 2. 复制字体文件(从本地或Git仓库)
|
||||||
|
# 需要以下字体文件:
|
||||||
|
# - simhei.ttf (黑体,必需)
|
||||||
|
# - simkai.ttf (楷体,可选)
|
||||||
|
# - simfang.ttf (仿宋,可选)
|
||||||
|
# - YunFengFeiYunTi-2.ttf (水印字体,可选)
|
||||||
|
|
||||||
|
# 3. 设置权限
|
||||||
|
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
|
||||||
|
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttc
|
||||||
|
|
||||||
|
# 4. 确保运行用户有读取权限
|
||||||
|
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法2:使用环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 设置字体目录环境变量
|
||||||
|
export PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts
|
||||||
|
|
||||||
|
# 或在 systemd 服务文件中添加
|
||||||
|
# Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法3:使用符号链接
|
||||||
|
|
||||||
|
如果字体文件在其他位置,可以创建符号链接:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
|
||||||
|
sudo ln -s /path/to/actual/fonts/*.ttf /www/tyapi-server/internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证字体文件
|
||||||
|
|
||||||
|
### 1. 检查文件是否存在
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lh /www/tyapi-server/internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到:
|
||||||
|
```
|
||||||
|
-rw-r--r-- 1 user user 9.5M Dec 3 18:00 simhei.ttf
|
||||||
|
-rw-r--r-- 1 user user 8.2M Dec 3 18:00 simkai.ttf
|
||||||
|
-rw-r--r-- 1 user user 7.8M Dec 3 18:00 simfang.ttf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查文件权限
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stat /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
|
||||||
|
```
|
||||||
|
|
||||||
|
确保有读取权限(至少 `-r--r--r--`)。
|
||||||
|
|
||||||
|
### 3. 检查文件类型
|
||||||
|
|
||||||
|
```bash
|
||||||
|
file /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
|
||||||
|
```
|
||||||
|
|
||||||
|
应该显示:`TrueType font data`
|
||||||
|
|
||||||
|
## 验证PDF生成功能
|
||||||
|
|
||||||
|
### 1. 查看日志
|
||||||
|
|
||||||
|
启动服务后,查看日志中是否有以下信息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"level":"INFO","msg":"找到字体文件","count":3,"paths":["/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf",...]}
|
||||||
|
{"level":"INFO","msg":"成功加载中文字体","font_path":"/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试PDF生成
|
||||||
|
|
||||||
|
调用PDF下载接口,检查:
|
||||||
|
- PDF文件能正常生成
|
||||||
|
- 中文文字正常显示(不是乱码或空白)
|
||||||
|
- 没有字体相关的错误日志
|
||||||
|
|
||||||
|
### 3. 调试信息
|
||||||
|
|
||||||
|
如果字体未找到,查看日志中的调试信息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"level":"DEBUG","msg":"查找字体文件","total_paths":20,"paths":[...]}
|
||||||
|
{"level":"DEBUG","msg":"字体文件不存在","font_path":"...","error":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 问题1:字体文件找不到
|
||||||
|
|
||||||
|
**症状**:日志显示 `"未找到中文字体文件"`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 确认字体文件路径是否正确
|
||||||
|
2. 检查文件权限:`chmod 644 *.ttf`
|
||||||
|
3. 检查文件所有者:`chown user:user *.ttf`
|
||||||
|
4. 查看日志中的 `"查找字体文件"` 调试信息,确认尝试的路径
|
||||||
|
|
||||||
|
### 问题2:字体文件无权限读取
|
||||||
|
|
||||||
|
**症状**:日志显示 `"字体文件无读取权限"`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```bash
|
||||||
|
sudo chmod 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
|
||||||
|
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题3:中文显示为乱码
|
||||||
|
|
||||||
|
**症状**:PDF中中文显示为乱码或空白
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 确认字体文件已成功加载(查看日志)
|
||||||
|
2. 确认字体文件是有效的TTF格式
|
||||||
|
3. 检查字体文件是否损坏:`file *.ttf`
|
||||||
|
|
||||||
|
### 问题4:Docker容器中找不到字体
|
||||||
|
|
||||||
|
**症状**:在Docker容器中运行时找不到字体
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 确保Dockerfile中已复制字体文件:
|
||||||
|
```dockerfile
|
||||||
|
COPY --from=builder /app/internal/shared/pdf/fonts/ ./internal/shared/pdf/fonts/
|
||||||
|
```
|
||||||
|
2. 或使用volume挂载:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /www/tyapi-server/internal/shared/pdf/fonts:/app/internal/shared/pdf/fonts:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
## Systemd服务配置示例
|
||||||
|
|
||||||
|
如果使用systemd管理服务,可以在服务文件中设置环境变量:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=TYAPI Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
WorkingDirectory=/www/tyapi-server
|
||||||
|
ExecStart=/www/tyapi-server/tyapi-server -env=production
|
||||||
|
Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## 字体文件获取
|
||||||
|
|
||||||
|
如果本地没有字体文件,可以从以下来源获取:
|
||||||
|
|
||||||
|
1. **Windows系统字体**(如果服务器是Windows迁移过来的)
|
||||||
|
- `C:\Windows\Fonts\simhei.ttf` → 复制到服务器
|
||||||
|
|
||||||
|
2. **Linux系统字体包**
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei
|
||||||
|
# 然后从系统字体目录复制或创建符号链接
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **从项目仓库**
|
||||||
|
- 确保字体文件已提交到Git仓库
|
||||||
|
- 使用 `git pull` 拉取最新代码
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **字体文件大小**:每个TTF文件约8-10MB,确保有足够磁盘空间
|
||||||
|
2. **文件权限**:确保运行服务的用户有读取权限
|
||||||
|
3. **路径一致性**:确保字体路径与代码中的查找路径一致
|
||||||
|
4. **日志级别**:生产环境建议将字体查找日志设为DEBUG级别,避免日志过多
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如果遇到问题,请提供以下信息:
|
||||||
|
1. 服务器操作系统版本:`lsb_release -a`
|
||||||
|
2. 字体文件位置和权限:`ls -lh /www/tyapi-server/internal/shared/pdf/fonts/`
|
||||||
|
3. 工作目录:`pwd`(服务运行时)
|
||||||
|
4. 可执行文件位置:`which tyapi-server` 或 `readlink -f $(which tyapi-server)`
|
||||||
|
5. 相关日志:包含 `"查找字体文件"` 和 `"字体文件"` 的日志条目
|
||||||
|
|
||||||
453
docs/Zap官方最佳实践日志系统指南.md
Normal file
453
docs/Zap官方最佳实践日志系统指南.md
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
# 🚀 Zap 官方最佳实践日志系统指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本日志系统完全基于 [Zap 官方最佳实践](https://betterstack.com/community/guides/logging/go/zap/) 设计,使用 `zap.NewProduction()` 和 `zap.NewDevelopment()` 预设,提供高性能、结构化的日志记录。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
### 1. **基于 Zap 官方预设**
|
||||||
|
- 使用 `zap.NewProduction()` 生产环境预设
|
||||||
|
- 使用 `zap.NewDevelopment()` 开发环境预设
|
||||||
|
- 自动添加调用者信息、堆栈跟踪等
|
||||||
|
|
||||||
|
### 2. **全局日志器支持**
|
||||||
|
- 支持 `zap.ReplaceGlobals()` 全局替换
|
||||||
|
- 提供 `logger.L()` 和 `logger.GetGlobalLogger()` 访问
|
||||||
|
- 符合 Zap 官方推荐的使用方式
|
||||||
|
|
||||||
|
### 3. **强类型字段支持**
|
||||||
|
- 使用 `zap.String()`, `zap.Int()`, `zap.Error()` 等强类型字段
|
||||||
|
- 避免运行时类型错误
|
||||||
|
- 提供最佳性能
|
||||||
|
|
||||||
|
### 4. **上下文日志记录**
|
||||||
|
- 自动从上下文提取 `request_id`, `user_id`, `trace_id`
|
||||||
|
- 支持 `WithContext()` 方法
|
||||||
|
- 便于分布式系统追踪
|
||||||
|
|
||||||
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
|
### 核心接口
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Logger interface {
|
||||||
|
// 基础日志方法
|
||||||
|
Debug(msg string, fields ...zapcore.Field)
|
||||||
|
Info(msg string, fields ...zapcore.Field)
|
||||||
|
Warn(msg string, fields ...zapcore.Field)
|
||||||
|
Error(msg string, fields ...zapcore.Field)
|
||||||
|
Fatal(msg string, fields ...zapcore.Field)
|
||||||
|
Panic(msg string, fields ...zapcore.Field)
|
||||||
|
|
||||||
|
// 结构化日志方法
|
||||||
|
With(fields ...zapcore.Field) Logger
|
||||||
|
WithContext(ctx context.Context) Logger
|
||||||
|
Named(name string) Logger
|
||||||
|
|
||||||
|
// 同步和清理
|
||||||
|
Sync() error
|
||||||
|
Core() zapcore.Core
|
||||||
|
|
||||||
|
// 获取原生 Zap Logger
|
||||||
|
GetZapLogger() *zap.Logger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实现类型
|
||||||
|
|
||||||
|
1. **ZapLogger**: 标准日志器,基于 Zap 官方预设
|
||||||
|
2. **LevelLogger**: 级别分文件日志器,支持按级别分离
|
||||||
|
3. **全局日志器**: 通过 `zap.ReplaceGlobals()` 提供全局访问
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 1. 基础使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"tyapi-server/internal/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 初始化全局日志器
|
||||||
|
config := logger.Config{
|
||||||
|
Development: true,
|
||||||
|
Output: "stdout",
|
||||||
|
Format: "console",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logger.InitGlobalLogger(config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用全局日志器
|
||||||
|
logger.L().Info("应用启动成功")
|
||||||
|
|
||||||
|
// 或者获取全局日志器
|
||||||
|
globalLogger := logger.GetGlobalLogger()
|
||||||
|
globalLogger.Info("使用全局日志器")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 依赖注入使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ProductService struct {
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductService(logger logger.Logger) *ProductService {
|
||||||
|
return &ProductService{logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) CreateProduct(ctx context.Context, product *Product) error {
|
||||||
|
// 记录操作日志
|
||||||
|
s.logger.Info("创建产品",
|
||||||
|
zap.String("product_id", product.ID),
|
||||||
|
zap.String("product_name", product.Name),
|
||||||
|
zap.String("user_id", product.CreatedBy),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务逻辑...
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 上下文日志记录
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *ProductService) GetProduct(ctx context.Context, id string) (*Product, error) {
|
||||||
|
// 自动从上下文提取字段
|
||||||
|
logger := s.logger.WithContext(ctx)
|
||||||
|
|
||||||
|
logger.Info("获取产品信息",
|
||||||
|
zap.String("product_id", id),
|
||||||
|
zap.String("operation", "get_product"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务逻辑...
|
||||||
|
return product, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 结构化字段
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用强类型字段
|
||||||
|
s.logger.Info("用户登录",
|
||||||
|
zap.String("username", "john_doe"),
|
||||||
|
zap.Int("user_id", 12345),
|
||||||
|
zap.String("ip_address", "192.168.1.100"),
|
||||||
|
zap.String("user_agent", r.UserAgent()),
|
||||||
|
zap.Time("login_time", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 记录错误
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("数据库操作失败",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("operation", "create_user"),
|
||||||
|
zap.String("table", "users"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 级别分文件日志
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 配置启用级别分文件
|
||||||
|
config := logger.Config{
|
||||||
|
EnableLevelSeparation: true,
|
||||||
|
Output: "file",
|
||||||
|
LogDir: "logs",
|
||||||
|
UseDaily: true,
|
||||||
|
LevelConfigs: map[string]interface{}{
|
||||||
|
"debug": map[string]interface{}{
|
||||||
|
"max_size": 50,
|
||||||
|
"max_backups": 3,
|
||||||
|
"max_age": 7,
|
||||||
|
},
|
||||||
|
"error": map[string]interface{}{
|
||||||
|
"max_size": 200,
|
||||||
|
"max_backups": 10,
|
||||||
|
"max_age": 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建级别分文件日志器
|
||||||
|
levelLogger, err := logger.NewLevelLogger(logger.LevelLoggerConfig{
|
||||||
|
BaseConfig: config,
|
||||||
|
EnableLevelSeparation: true,
|
||||||
|
LevelConfigs: convertLevelConfigs(config.LevelConfigs),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 日志文件结构
|
||||||
|
|
||||||
|
### 按级别分文件
|
||||||
|
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── 2024-01-01/
|
||||||
|
│ ├── debug.log # 调试日志
|
||||||
|
│ ├── info.log # 信息日志
|
||||||
|
│ ├── warn.log # 警告日志
|
||||||
|
│ ├── error.log # 错误日志
|
||||||
|
│ ├── fatal.log # 致命错误日志
|
||||||
|
│ └── panic.log # 恐慌错误日志
|
||||||
|
└── app.log # 主日志文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 按日期分包
|
||||||
|
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── 2024-01-01/
|
||||||
|
│ ├── app.log
|
||||||
|
│ └── error.log
|
||||||
|
├── 2024-01-02/
|
||||||
|
│ ├── app.log
|
||||||
|
│ └── error.log
|
||||||
|
└── app.log # 当前日期
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 配置选项
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
# 环境配置
|
||||||
|
development: true # 是否为开发环境
|
||||||
|
|
||||||
|
# 输出配置
|
||||||
|
output: "file" # 输出方式: stdout, stderr, file
|
||||||
|
format: "json" # 输出格式: json, console
|
||||||
|
log_dir: "logs" # 日志目录
|
||||||
|
|
||||||
|
# 文件配置
|
||||||
|
max_size: 100 # 单个文件最大大小(MB)
|
||||||
|
max_backups: 5 # 最大备份文件数
|
||||||
|
max_age: 30 # 最大保留天数
|
||||||
|
compress: true # 是否压缩
|
||||||
|
|
||||||
|
# 高级功能
|
||||||
|
use_daily: true # 是否按日分包
|
||||||
|
enable_level_separation: true # 是否启用按级别分文件
|
||||||
|
use_color: false # 是否使用彩色输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 级别配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
level_configs:
|
||||||
|
debug:
|
||||||
|
max_size: 50 # 50MB
|
||||||
|
max_backups: 3 # 3个备份
|
||||||
|
max_age: 7 # 7天
|
||||||
|
compress: true
|
||||||
|
info:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5 # 5个备份
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200 # 200MB
|
||||||
|
max_backups: 10 # 10个备份
|
||||||
|
max_age: 90 # 90天
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 最佳实践
|
||||||
|
|
||||||
|
### 1. **使用强类型字段**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 推荐:使用强类型字段
|
||||||
|
logger.Info("用户操作",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("action", "login"),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ❌ 避免:使用 Any 字段
|
||||||
|
logger.Info("用户操作",
|
||||||
|
zap.Any("user_id", userID),
|
||||||
|
zap.Any("action", "login"),
|
||||||
|
zap.Any("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **合理使用日志级别**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Debug: 详细的调试信息
|
||||||
|
logger.Debug("SQL查询", zap.String("query", sql))
|
||||||
|
|
||||||
|
// Info: 重要的业务事件
|
||||||
|
logger.Info("用户注册成功", zap.String("user_id", userID))
|
||||||
|
|
||||||
|
// Warn: 警告信息,不影响功能
|
||||||
|
logger.Warn("数据库连接池使用率过高", zap.Int("usage", 85))
|
||||||
|
|
||||||
|
// Error: 错误信息,功能受影响
|
||||||
|
logger.Error("数据库连接失败", zap.Error(err))
|
||||||
|
|
||||||
|
// Fatal: 致命错误,应用无法继续
|
||||||
|
logger.Fatal("配置文件加载失败", zap.Error(err))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **上下文信息提取**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在中间件中设置上下文
|
||||||
|
func LoggingMiddleware(logger logger.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 生成请求ID
|
||||||
|
requestID := uuid.New().String()
|
||||||
|
|
||||||
|
// 设置上下文
|
||||||
|
ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
|
||||||
|
ctx = context.WithValue(ctx, "user_id", getUserID(c))
|
||||||
|
ctx = context.WithValue(ctx, "trace_id", getTraceID(c))
|
||||||
|
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
|
||||||
|
// 记录请求日志
|
||||||
|
logger.WithContext(ctx).Info("收到请求",
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("client_ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **性能优化**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 推荐:延迟计算
|
||||||
|
if logger.Core().Enabled(zapcore.DebugLevel) {
|
||||||
|
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 避免:总是计算
|
||||||
|
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 错误处理
|
||||||
|
|
||||||
|
### 1. **Panic 恢复**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用 panic 恢复中间件
|
||||||
|
func PanicRecoveryMiddleware(logger *zap.Logger) gin.HandlerFunc {
|
||||||
|
return gin.RecoveryWithWriter(&panicLogger{logger: logger})
|
||||||
|
}
|
||||||
|
|
||||||
|
type panicLogger struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *panicLogger) Write(p []byte) (n int, err error) {
|
||||||
|
pl.logger.Error("系统发生严重错误",
|
||||||
|
zap.String("error_type", "panic"),
|
||||||
|
zap.String("stack_trace", string(p)),
|
||||||
|
zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")),
|
||||||
|
)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **错误日志记录**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 记录错误详情
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("操作失败",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("operation", "create_user"),
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("stack_trace", string(debug.Stack())),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能基准
|
||||||
|
|
||||||
|
基于 [Zap 官方基准测试](https://betterstack.com/community/guides/logging/go/zap/):
|
||||||
|
|
||||||
|
| 包 | 时间 | 相对于 Zap | 内存分配 |
|
||||||
|
|----|------|------------|----------|
|
||||||
|
| zap | 193 ns/op | +0% | 0 allocs/op |
|
||||||
|
| zap (sugared) | 227 ns/op | +18% | 1 allocs/op |
|
||||||
|
| zerolog | 81 ns/op | -58% | 0 allocs/op |
|
||||||
|
| slog | 322 ns/op | +67% | 0 allocs/op |
|
||||||
|
|
||||||
|
## 🔍 调试和故障排除
|
||||||
|
|
||||||
|
### 1. **检查日志级别**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 检查日志级别是否启用
|
||||||
|
if logger.Core().Enabled(zapcore.DebugLevel) {
|
||||||
|
logger.Debug("调试信息")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **同步日志**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 确保日志写入完成
|
||||||
|
defer logger.Sync()
|
||||||
|
|
||||||
|
// 或者在应用关闭时
|
||||||
|
func cleanup() {
|
||||||
|
logger.Sync()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **验证配置**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 验证日志器配置
|
||||||
|
config := logger.Config{
|
||||||
|
Development: true,
|
||||||
|
Output: "stdout",
|
||||||
|
Format: "console",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := logger.NewLogger(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("创建日志器失败: %v", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
本日志系统完全基于 Zap 官方最佳实践设计,具有以下优势:
|
||||||
|
|
||||||
|
1. **高性能**: 基于 Zap 的高性能实现
|
||||||
|
2. **官方推荐**: 使用 `zap.NewProduction()` 和 `zap.NewDevelopment()` 预设
|
||||||
|
3. **强类型**: 支持强类型字段,避免运行时错误
|
||||||
|
4. **结构化**: 支持结构化日志记录
|
||||||
|
5. **上下文**: 自动提取上下文信息
|
||||||
|
6. **灵活配置**: 支持文件输出、级别分离、按日分包等
|
||||||
|
7. **全局访问**: 支持全局日志器访问
|
||||||
|
|
||||||
|
通过合理使用,您将获得高性能、结构化的日志系统,满足生产环境的各种需求!
|
||||||
|
|
||||||
|
## 📚 参考资源
|
||||||
|
|
||||||
|
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
|
||||||
|
- [Zap 最佳实践指南](https://betterstack.com/community/guides/logging/go/zap/)
|
||||||
|
- [Zap GitHub 仓库](https://github.com/uber-go/zap)
|
||||||
603
docs/api/statistics/api_documentation.md
Normal file
603
docs/api/statistics/api_documentation.md
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
# 统计功能API文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
统计功能API提供了完整的统计数据分析和管理功能,包括指标管理、实时统计、历史统计、仪表板管理、报告生成、数据导出等功能。
|
||||||
|
|
||||||
|
## 基础信息
|
||||||
|
|
||||||
|
- **基础URL**: `/api/v1/statistics`
|
||||||
|
- **认证方式**: Bearer Token
|
||||||
|
- **内容类型**: `application/json`
|
||||||
|
- **字符编码**: `UTF-8`
|
||||||
|
|
||||||
|
## 认证和权限
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
所有API请求都需要在请求头中包含有效的JWT令牌:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your-jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限级别
|
||||||
|
- **公开访问**: 无需认证的接口
|
||||||
|
- **用户权限**: 需要用户或管理员权限
|
||||||
|
- **管理员权限**: 仅管理员可访问
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 1. 指标管理
|
||||||
|
|
||||||
|
#### 1.1 创建统计指标
|
||||||
|
- **URL**: `POST /api/v1/statistics/metrics`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 创建新的统计指标
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metric_type": "api_calls",
|
||||||
|
"metric_name": "total_count",
|
||||||
|
"dimension": "realtime",
|
||||||
|
"value": 100.0,
|
||||||
|
"metadata": "{\"source\": \"api_gateway\"}",
|
||||||
|
"date": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "指标创建成功",
|
||||||
|
"data": {
|
||||||
|
"id": "uuid",
|
||||||
|
"metric_type": "api_calls",
|
||||||
|
"metric_name": "total_count",
|
||||||
|
"dimension": "realtime",
|
||||||
|
"value": 100.0,
|
||||||
|
"metadata": "{\"source\": \"api_gateway\"}",
|
||||||
|
"date": "2024-01-01T00:00:00Z",
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 更新统计指标
|
||||||
|
- **URL**: `PUT /api/v1/statistics/metrics`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 更新现有统计指标的值
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"value": 150.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 删除统计指标
|
||||||
|
- **URL**: `DELETE /api/v1/statistics/metrics`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 删除指定的统计指标
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 获取单个指标
|
||||||
|
- **URL**: `GET /api/v1/statistics/metrics/{id}`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据ID获取指定的统计指标
|
||||||
|
|
||||||
|
#### 1.5 获取指标列表
|
||||||
|
- **URL**: `GET /api/v1/statistics/metrics`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据条件获取统计指标列表
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `metric_type` (string): 指标类型
|
||||||
|
- `metric_name` (string): 指标名称
|
||||||
|
- `dimension` (string): 统计维度
|
||||||
|
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||||
|
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||||
|
- `limit` (int): 限制数量 (默认20, 最大1000)
|
||||||
|
- `offset` (int): 偏移量 (默认0)
|
||||||
|
- `sort_by` (string): 排序字段 (默认created_at)
|
||||||
|
- `sort_order` (string): 排序顺序 (默认desc)
|
||||||
|
|
||||||
|
### 2. 实时统计
|
||||||
|
|
||||||
|
#### 2.1 获取实时指标
|
||||||
|
- **URL**: `GET /api/v1/statistics/realtime`
|
||||||
|
- **权限**: 公开
|
||||||
|
- **描述**: 获取指定类型的实时统计指标
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `metric_type` (string, 必需): 指标类型
|
||||||
|
- `time_range` (string): 时间范围 (last_hour, last_day, last_week)
|
||||||
|
- `dimension` (string): 统计维度
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "获取实时指标成功",
|
||||||
|
"data": {
|
||||||
|
"metric_type": "api_calls",
|
||||||
|
"metrics": {
|
||||||
|
"total_count": 1000,
|
||||||
|
"success_count": 950,
|
||||||
|
"failed_count": 50,
|
||||||
|
"success_rate": 95.0
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T12:00:00Z",
|
||||||
|
"metadata": {
|
||||||
|
"time_range": "last_hour",
|
||||||
|
"dimension": "realtime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 历史统计
|
||||||
|
|
||||||
|
#### 3.1 获取历史指标
|
||||||
|
- **URL**: `GET /api/v1/statistics/historical`
|
||||||
|
- **权限**: 公开
|
||||||
|
- **描述**: 获取指定时间范围的历史统计指标
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `metric_type` (string, 必需): 指标类型
|
||||||
|
- `metric_name` (string): 指标名称
|
||||||
|
- `dimension` (string): 统计维度
|
||||||
|
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||||
|
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||||
|
- `period` (string): 统计周期
|
||||||
|
- `limit` (int): 限制数量 (默认20)
|
||||||
|
- `offset` (int): 偏移量 (默认0)
|
||||||
|
- `aggregate_by` (string): 聚合维度
|
||||||
|
- `group_by` (string): 分组维度
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "获取历史指标成功",
|
||||||
|
"data": {
|
||||||
|
"metric_type": "api_calls",
|
||||||
|
"metric_name": "total_count",
|
||||||
|
"dimension": "daily",
|
||||||
|
"data_points": [
|
||||||
|
{
|
||||||
|
"date": "2024-01-01T00:00:00Z",
|
||||||
|
"value": 1000,
|
||||||
|
"label": "total_count"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total": 30000,
|
||||||
|
"average": 1000,
|
||||||
|
"max": 1500,
|
||||||
|
"min": 500,
|
||||||
|
"count": 30,
|
||||||
|
"growth_rate": 5.2,
|
||||||
|
"trend": "increasing"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"period": "daily",
|
||||||
|
"aggregate_by": "day",
|
||||||
|
"group_by": "metric_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 仪表板管理
|
||||||
|
|
||||||
|
#### 4.1 创建仪表板
|
||||||
|
- **URL**: `POST /api/v1/statistics/dashboards`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 创建新的统计仪表板
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "用户仪表板",
|
||||||
|
"description": "普通用户专用仪表板",
|
||||||
|
"user_role": "user",
|
||||||
|
"layout": "{\"columns\": 2, \"rows\": 3}",
|
||||||
|
"widgets": "[{\"type\": \"api_calls\", \"position\": {\"x\": 0, \"y\": 0}}]",
|
||||||
|
"settings": "{\"theme\": \"light\", \"auto_refresh\": false}",
|
||||||
|
"refresh_interval": 600,
|
||||||
|
"access_level": "private",
|
||||||
|
"created_by": "user_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 获取仪表板列表
|
||||||
|
- **URL**: `GET /api/v1/statistics/dashboards`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据条件获取统计仪表板列表
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `user_role` (string): 用户角色
|
||||||
|
- `is_default` (bool): 是否默认
|
||||||
|
- `is_active` (bool): 是否激活
|
||||||
|
- `access_level` (string): 访问级别
|
||||||
|
- `created_by` (string): 创建者ID
|
||||||
|
- `name` (string): 仪表板名称
|
||||||
|
- `limit` (int): 限制数量 (默认20)
|
||||||
|
- `offset` (int): 偏移量 (默认0)
|
||||||
|
- `sort_by` (string): 排序字段 (默认created_at)
|
||||||
|
- `sort_order` (string): 排序顺序 (默认desc)
|
||||||
|
|
||||||
|
#### 4.3 获取单个仪表板
|
||||||
|
- **URL**: `GET /api/v1/statistics/dashboards/{id}`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据ID获取指定的统计仪表板
|
||||||
|
|
||||||
|
#### 4.4 获取仪表板数据
|
||||||
|
- **URL**: `GET /api/v1/statistics/dashboards/data`
|
||||||
|
- **权限**: 公开
|
||||||
|
- **描述**: 获取指定角色的仪表板数据
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `user_role` (string, 必需): 用户角色
|
||||||
|
- `period` (string): 统计周期 (today, week, month)
|
||||||
|
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||||
|
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||||
|
- `metric_types` (string): 指标类型列表
|
||||||
|
- `dimensions` (string): 统计维度列表
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "获取仪表板数据成功",
|
||||||
|
"data": {
|
||||||
|
"api_calls": {
|
||||||
|
"total_count": 10000,
|
||||||
|
"success_count": 9500,
|
||||||
|
"failed_count": 500,
|
||||||
|
"success_rate": 95.0,
|
||||||
|
"avg_response_time": 150.5
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"total_count": 1000,
|
||||||
|
"certified_count": 800,
|
||||||
|
"active_count": 750,
|
||||||
|
"certification_rate": 80.0,
|
||||||
|
"retention_rate": 75.0
|
||||||
|
},
|
||||||
|
"finance": {
|
||||||
|
"total_amount": 50000.0,
|
||||||
|
"recharge_amount": 60000.0,
|
||||||
|
"deduct_amount": 10000.0,
|
||||||
|
"net_amount": 50000.0
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"end_date": "2024-01-01",
|
||||||
|
"period": "today"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"generated_at": "2024-01-01 12:00:00",
|
||||||
|
"user_role": "user",
|
||||||
|
"data_version": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 报告管理
|
||||||
|
|
||||||
|
#### 5.1 生成报告
|
||||||
|
- **URL**: `POST /api/v1/statistics/reports`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 生成指定类型的统计报告
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"report_type": "summary",
|
||||||
|
"title": "月度汇总报告",
|
||||||
|
"period": "month",
|
||||||
|
"user_role": "admin",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"end_date": "2024-01-31T23:59:59Z",
|
||||||
|
"filters": {
|
||||||
|
"metric_types": ["api_calls", "users"],
|
||||||
|
"dimensions": ["daily", "weekly"]
|
||||||
|
},
|
||||||
|
"generated_by": "admin_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2 获取报告列表
|
||||||
|
- **URL**: `GET /api/v1/statistics/reports`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据条件获取统计报告列表
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `report_type` (string): 报告类型
|
||||||
|
- `user_role` (string): 用户角色
|
||||||
|
- `status` (string): 报告状态
|
||||||
|
- `period` (string): 统计周期
|
||||||
|
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||||
|
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||||
|
- `limit` (int): 限制数量 (默认20)
|
||||||
|
- `offset` (int): 偏移量 (默认0)
|
||||||
|
- `sort_by` (string): 排序字段 (默认created_at)
|
||||||
|
- `sort_order` (string): 排序顺序 (默认desc)
|
||||||
|
- `generated_by` (string): 生成者ID
|
||||||
|
|
||||||
|
#### 5.3 获取单个报告
|
||||||
|
- **URL**: `GET /api/v1/statistics/reports/{id}`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 根据ID获取指定的统计报告
|
||||||
|
|
||||||
|
### 6. 统计分析
|
||||||
|
|
||||||
|
#### 6.1 计算增长率
|
||||||
|
- **URL**: `GET /api/v1/statistics/analysis/growth-rate`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 计算指定指标的增长率
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `metric_type` (string, 必需): 指标类型
|
||||||
|
- `metric_name` (string, 必需): 指标名称
|
||||||
|
- `current_period` (string, 必需): 当前周期 (YYYY-MM-DD)
|
||||||
|
- `previous_period` (string, 必需): 上一周期 (YYYY-MM-DD)
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "计算增长率成功",
|
||||||
|
"data": {
|
||||||
|
"growth_rate": 15.5,
|
||||||
|
"current_value": 1150,
|
||||||
|
"previous_value": 1000,
|
||||||
|
"period": "daily"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 计算趋势
|
||||||
|
- **URL**: `GET /api/v1/statistics/analysis/trend`
|
||||||
|
- **权限**: 用户
|
||||||
|
- **描述**: 计算指定指标的趋势
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `metric_type` (string, 必需): 指标类型
|
||||||
|
- `metric_name` (string, 必需): 指标名称
|
||||||
|
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||||
|
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "计算趋势成功",
|
||||||
|
"data": {
|
||||||
|
"trend": "increasing",
|
||||||
|
"trend_strength": 0.8,
|
||||||
|
"data_points": 30,
|
||||||
|
"correlation": 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 数据导出
|
||||||
|
|
||||||
|
#### 7.1 导出数据
|
||||||
|
- **URL**: `POST /api/v1/statistics/export`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 导出指定格式的统计数据
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format": "excel",
|
||||||
|
"metric_type": "api_calls",
|
||||||
|
"start_date": "2024-01-01T00:00:00Z",
|
||||||
|
"end_date": "2024-01-31T23:59:59Z",
|
||||||
|
"dimension": "daily",
|
||||||
|
"group_by": "metric_name",
|
||||||
|
"filters": {
|
||||||
|
"status": "success"
|
||||||
|
},
|
||||||
|
"columns": ["date", "metric_name", "value"],
|
||||||
|
"include_charts": true,
|
||||||
|
"exported_by": "admin_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "数据导出成功",
|
||||||
|
"data": {
|
||||||
|
"download_url": "https://api.example.com/downloads/export_123.xlsx",
|
||||||
|
"file_name": "api_calls_export_20240101_20240131.xlsx",
|
||||||
|
"file_size": 1024000,
|
||||||
|
"expires_at": "2024-01-02T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 定时任务管理
|
||||||
|
|
||||||
|
#### 8.1 手动触发小时聚合
|
||||||
|
- **URL**: `POST /api/v1/statistics/cron/hourly-aggregation`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 手动触发指定时间的小时级数据聚合
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `target_hour` (string, 必需): 目标小时 (YYYY-MM-DDTHH:MM:SSZ)
|
||||||
|
|
||||||
|
#### 8.2 手动触发日聚合
|
||||||
|
- **URL**: `POST /api/v1/statistics/cron/daily-aggregation`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 手动触发指定时间的日级数据聚合
|
||||||
|
|
||||||
|
**查询参数**:
|
||||||
|
- `target_date` (string, 必需): 目标日期 (YYYY-MM-DD)
|
||||||
|
|
||||||
|
#### 8.3 手动触发数据清理
|
||||||
|
- **URL**: `POST /api/v1/statistics/cron/data-cleanup`
|
||||||
|
- **权限**: 管理员
|
||||||
|
- **描述**: 手动触发过期数据清理任务
|
||||||
|
|
||||||
|
## 错误码
|
||||||
|
|
||||||
|
| 错误码 | HTTP状态码 | 描述 |
|
||||||
|
|--------|------------|------|
|
||||||
|
| 400 | 400 Bad Request | 请求参数错误 |
|
||||||
|
| 401 | 401 Unauthorized | 未认证或认证失败 |
|
||||||
|
| 403 | 403 Forbidden | 权限不足 |
|
||||||
|
| 404 | 404 Not Found | 资源不存在 |
|
||||||
|
| 422 | 422 Unprocessable Entity | 参数验证失败 |
|
||||||
|
| 429 | 429 Too Many Requests | 请求频率过高 |
|
||||||
|
| 500 | 500 Internal Server Error | 服务器内部错误 |
|
||||||
|
|
||||||
|
## 响应格式
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": {
|
||||||
|
// 响应数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "错误描述",
|
||||||
|
"error": "详细错误信息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列表响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": [
|
||||||
|
// 数据列表
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 100,
|
||||||
|
"pages": 5,
|
||||||
|
"has_next": true,
|
||||||
|
"has_prev": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### 统计指标 (StatisticsMetric)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"metric_type": "string",
|
||||||
|
"metric_name": "string",
|
||||||
|
"dimension": "string",
|
||||||
|
"value": "number",
|
||||||
|
"metadata": "string",
|
||||||
|
"date": "string",
|
||||||
|
"created_at": "string",
|
||||||
|
"updated_at": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 统计报告 (StatisticsReport)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"report_type": "string",
|
||||||
|
"title": "string",
|
||||||
|
"content": "string",
|
||||||
|
"period": "string",
|
||||||
|
"user_role": "string",
|
||||||
|
"status": "string",
|
||||||
|
"generated_by": "string",
|
||||||
|
"generated_at": "string",
|
||||||
|
"expires_at": "string",
|
||||||
|
"created_at": "string",
|
||||||
|
"updated_at": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 统计仪表板 (StatisticsDashboard)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"user_role": "string",
|
||||||
|
"is_default": "boolean",
|
||||||
|
"is_active": "boolean",
|
||||||
|
"layout": "string",
|
||||||
|
"widgets": "string",
|
||||||
|
"settings": "string",
|
||||||
|
"refresh_interval": "number",
|
||||||
|
"created_by": "string",
|
||||||
|
"access_level": "string",
|
||||||
|
"created_at": "string",
|
||||||
|
"updated_at": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 获取今日API调用统计
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://api.example.com/api/v1/statistics/realtime?metric_type=api_calls&time_range=last_hour" \
|
||||||
|
-H "Authorization: Bearer your-jwt-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取历史用户数据
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://api.example.com/api/v1/statistics/historical?metric_type=users&start_date=2024-01-01&end_date=2024-01-31" \
|
||||||
|
-H "Authorization: Bearer your-jwt-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成月度报告
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.example.com/api/v1/statistics/reports" \
|
||||||
|
-H "Authorization: Bearer your-jwt-token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"report_type": "summary",
|
||||||
|
"title": "月度汇总报告",
|
||||||
|
"period": "month",
|
||||||
|
"user_role": "admin",
|
||||||
|
"generated_by": "admin_id"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **日期格式**: 所有日期参数都使用 `YYYY-MM-DD` 格式
|
||||||
|
2. **时间戳**: 所有时间戳都使用 ISO 8601 格式
|
||||||
|
3. **分页**: 默认每页20条记录,最大1000条
|
||||||
|
4. **限流**: API有请求频率限制,超出限制会返回429错误
|
||||||
|
5. **缓存**: 部分接口支持缓存,响应头会包含缓存信息
|
||||||
|
6. **权限**: 不同接口需要不同的权限级别,请确保有相应权限
|
||||||
|
7. **数据量**: 查询大量数据时建议使用分页和日期范围限制
|
||||||
|
|
||||||
9261
docs/docs.go
9261
docs/docs.go
File diff suppressed because it is too large
Load Diff
9233
docs/swagger.json
Normal file
9233
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
6209
docs/swagger.yaml
Normal file
6209
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6616
docs/swagger/docs.go
6616
docs/swagger/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1001
docs/产品示例报告下载功能实现方案.md
Normal file
1001
docs/产品示例报告下载功能实现方案.md
Normal file
File diff suppressed because it is too large
Load Diff
340
docs/新日志系统使用指南.md
Normal file
340
docs/新日志系统使用指南.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# 🚀 新日志系统使用指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目已重新设计日志系统,完全基于 **Zap 官方最佳实践**,提供高性能、结构化的日志记录功能。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
### 🎯 **基于 Zap 官方推荐**
|
||||||
|
- 使用 `zap.Config` 和官方配置结构
|
||||||
|
- 支持 `zap.NewProductionConfig()` 和 `zap.NewDevelopmentConfig()`
|
||||||
|
- 完整的编码器配置和输出配置
|
||||||
|
|
||||||
|
### 📁 **灵活的日志输出**
|
||||||
|
- **控制台输出**: `stdout`, `stderr`
|
||||||
|
- **文件输出**: 支持日志轮转和压缩
|
||||||
|
- **按日分包**: 自动按日期创建目录
|
||||||
|
- **按级别分文件**: 不同级别写入不同文件
|
||||||
|
|
||||||
|
### 🔧 **智能配置**
|
||||||
|
- 根据环境自动选择最佳配置
|
||||||
|
- 支持选项模式配置
|
||||||
|
- 完整的默认值设置
|
||||||
|
|
||||||
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
应用代码 → Logger接口 → LoggerFactory → Zap核心 → 输出目标
|
||||||
|
```
|
||||||
|
|
||||||
|
### **核心组件**
|
||||||
|
|
||||||
|
1. **`Logger` 接口**: 统一的日志接口
|
||||||
|
2. **`ZapLogger`**: 基于 Zap 的日志实现
|
||||||
|
3. **`LevelLogger`**: 级别分文件日志器
|
||||||
|
4. **`LoggerFactory`**: 日志器工厂,支持多种创建方式
|
||||||
|
|
||||||
|
## 📖 使用方法
|
||||||
|
|
||||||
|
### **1. 基础使用**
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "tyapi-server/internal/shared/logger"
|
||||||
|
|
||||||
|
// 创建日志器
|
||||||
|
log, err := logger.NewLogger(logger.Config{
|
||||||
|
Level: "info",
|
||||||
|
Format: "json",
|
||||||
|
Output: "file",
|
||||||
|
LogDir: "logs",
|
||||||
|
UseDaily: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
log.Info("应用启动成功",
|
||||||
|
logger.String("version", "1.0.0"),
|
||||||
|
logger.String("environment", "production"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. 使用日志工厂**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建工厂
|
||||||
|
factory := logger.NewLoggerFactory(logger.Config{
|
||||||
|
Level: "info",
|
||||||
|
Format: "json",
|
||||||
|
Output: "file",
|
||||||
|
Development: false, // 生产环境
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建生产环境日志器
|
||||||
|
prodLogger, err := factory.CreateProductionLogger()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建开发环境日志器
|
||||||
|
devLogger, err := factory.CreateDevelopmentLogger()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据环境自动选择
|
||||||
|
autoLogger, err := factory.CreateLoggerByEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. 选项模式配置**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用选项模式
|
||||||
|
logger, err := factory.CreateLoggerWithOptions(
|
||||||
|
logger.WithLevel("debug"),
|
||||||
|
logger.WithFormat("console"),
|
||||||
|
logger.WithOutput("stdout"),
|
||||||
|
logger.WithDevelopment(true),
|
||||||
|
logger.WithColor(true),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. 级别分文件日志器**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建级别分文件日志器
|
||||||
|
levelConfig := logger.LevelLoggerConfig{
|
||||||
|
BaseConfig: logger.Config{
|
||||||
|
Level: "info",
|
||||||
|
Format: "json",
|
||||||
|
Output: "file",
|
||||||
|
LogDir: "logs",
|
||||||
|
UseDaily: true,
|
||||||
|
},
|
||||||
|
EnableLevelSeparation: true,
|
||||||
|
LevelConfigs: map[zapcore.Level]logger.LevelFileConfig{
|
||||||
|
zapcore.InfoLevel: {
|
||||||
|
MaxSize: 100,
|
||||||
|
MaxBackups: 5,
|
||||||
|
MaxAge: 30,
|
||||||
|
Compress: true,
|
||||||
|
},
|
||||||
|
zapcore.ErrorLevel: {
|
||||||
|
MaxSize: 200,
|
||||||
|
MaxBackups: 10,
|
||||||
|
MaxAge: 90,
|
||||||
|
Compress: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
levelLogger, err := logger.NewLevelLogger(levelConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同级别的日志会写入不同文件
|
||||||
|
levelLogger.Info("这是一条信息日志") // 写入 logs/2024-01-01/info.log
|
||||||
|
levelLogger.Error("这是一条错误日志") // 写入 logs/2024-01-01/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. 结构化日志**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用 With 添加字段
|
||||||
|
userLogger := log.With(
|
||||||
|
logger.String("user_id", "12345"),
|
||||||
|
logger.String("action", "login"),
|
||||||
|
)
|
||||||
|
|
||||||
|
userLogger.Info("用户登录成功",
|
||||||
|
logger.String("ip", "192.168.1.1"),
|
||||||
|
logger.String("user_agent", "Mozilla/5.0..."),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 使用 WithContext 从上下文提取字段
|
||||||
|
ctx := context.WithValue(context.Background(), "request_id", "req_123")
|
||||||
|
ctx = context.WithValue(ctx, "user_id", "user_456")
|
||||||
|
ctx = context.WithValue(ctx, "trace_id", "trace_789")
|
||||||
|
|
||||||
|
contextLogger := log.WithContext(ctx)
|
||||||
|
contextLogger.Info("处理请求",
|
||||||
|
logger.String("endpoint", "/api/users"),
|
||||||
|
logger.Int("status_code", 200),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **6. 命名日志器**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建命名日志器
|
||||||
|
dbLogger := log.Named("database")
|
||||||
|
dbLogger.Info("数据库连接成功",
|
||||||
|
logger.String("host", "localhost"),
|
||||||
|
logger.String("database", "tyapi"),
|
||||||
|
)
|
||||||
|
|
||||||
|
apiLogger := log.Named("api")
|
||||||
|
apiLogger.Info("API 请求处理",
|
||||||
|
logger.String("method", "GET"),
|
||||||
|
logger.String("path", "/api/v1/users"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
### **基础配置**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
# 基础配置
|
||||||
|
level: "info" # 日志级别
|
||||||
|
format: "json" # 输出格式
|
||||||
|
output: "file" # 输出方式
|
||||||
|
log_dir: "logs" # 日志目录
|
||||||
|
use_daily: true # 是否按日分包
|
||||||
|
use_color: false # 是否使用彩色输出
|
||||||
|
|
||||||
|
# 文件配置
|
||||||
|
max_size: 100 # 单个文件最大大小(MB)
|
||||||
|
max_backups: 5 # 最大备份文件数
|
||||||
|
max_age: 30 # 最大保留天数
|
||||||
|
compress: true # 是否压缩
|
||||||
|
|
||||||
|
# 高级功能
|
||||||
|
enable_level_separation: true # 是否启用按级别分文件
|
||||||
|
development: true # 是否为开发环境
|
||||||
|
```
|
||||||
|
|
||||||
|
### **级别配置**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
level_configs:
|
||||||
|
debug:
|
||||||
|
max_size: 50 # 50MB
|
||||||
|
max_backups: 3
|
||||||
|
max_age: 7 # 7天
|
||||||
|
compress: true
|
||||||
|
info:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200 # 200MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90 # 90天
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 日志级别
|
||||||
|
|
||||||
|
### **级别说明**
|
||||||
|
- **`debug`**: 调试信息,开发环境使用
|
||||||
|
- **`info`**: 一般信息,记录应用状态
|
||||||
|
- **`warn`**: 警告信息,需要注意但不影响运行
|
||||||
|
- **`error`**: 错误信息,操作失败但可恢复
|
||||||
|
- **`fatal`**: 致命错误,应用无法继续运行
|
||||||
|
- **`panic`**: 恐慌错误,程序崩溃
|
||||||
|
|
||||||
|
### **级别选择建议**
|
||||||
|
- **开发环境**: `debug` 或 `info`
|
||||||
|
- **测试环境**: `info` 或 `warn`
|
||||||
|
- **生产环境**: `warn` 或 `error`
|
||||||
|
|
||||||
|
## 📊 日志格式
|
||||||
|
|
||||||
|
### **JSON 格式(推荐)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||||
|
"logger": "main",
|
||||||
|
"caller": "main.go:25",
|
||||||
|
"message": "应用启动成功",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"environment": "production"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Console 格式(开发环境)**
|
||||||
|
```
|
||||||
|
2024-01-01T12:00:00.000Z INFO main/main.go:25 应用启动成功 {"version": "1.0.0", "environment": "production"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 性能优化
|
||||||
|
|
||||||
|
### **Zap 官方推荐**
|
||||||
|
- 使用结构化字段而不是字符串拼接
|
||||||
|
- 避免在日志记录时进行复杂计算
|
||||||
|
- 合理设置日志级别,避免过度记录
|
||||||
|
|
||||||
|
### **最佳实践**
|
||||||
|
```go
|
||||||
|
// ✅ 推荐:使用结构化字段
|
||||||
|
log.Info("用户操作",
|
||||||
|
logger.String("user_id", userID),
|
||||||
|
logger.String("action", action),
|
||||||
|
logger.String("resource", resource),
|
||||||
|
)
|
||||||
|
|
||||||
|
// ❌ 不推荐:字符串拼接
|
||||||
|
log.Info(fmt.Sprintf("用户 %s 执行了 %s 操作,资源: %s", userID, action, resource))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### **常见问题**
|
||||||
|
|
||||||
|
1. **日志文件未创建**
|
||||||
|
- 检查目录权限
|
||||||
|
- 确认配置中的 `log_dir` 路径
|
||||||
|
|
||||||
|
2. **日志级别不生效**
|
||||||
|
- 检查配置中的 `level` 值
|
||||||
|
- 确认日志器创建时使用了正确的配置
|
||||||
|
|
||||||
|
3. **按级别分文件不工作**
|
||||||
|
- 确认 `enable_level_separation: true`
|
||||||
|
- 检查 `level_configs` 配置
|
||||||
|
|
||||||
|
4. **按日分包不工作**
|
||||||
|
- 确认 `use_daily: true`
|
||||||
|
- 检查日期格式是否正确
|
||||||
|
|
||||||
|
### **调试技巧**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 启用调试模式
|
||||||
|
log.SetLevel(zapcore.DebugLevel)
|
||||||
|
|
||||||
|
// 检查日志器配置
|
||||||
|
if zapLogger, ok := log.(*logger.ZapLogger); ok {
|
||||||
|
config := zapLogger.GetConfig()
|
||||||
|
fmt.Printf("日志器配置: %+v\n", config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 更多资源
|
||||||
|
|
||||||
|
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
|
||||||
|
- [Zap 最佳实践](https://github.com/uber-go/zap/blob/master/FAQ.md)
|
||||||
|
- [结构化日志指南](https://github.com/uber-go/zap/blob/master/FAQ.md#q-how-do-i-choose-between-the-json-and-console-encoders)
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
新的日志系统完全基于 Zap 官方最佳实践,提供了:
|
||||||
|
|
||||||
|
1. **高性能**: Zap 是 Go 生态中性能最好的日志库
|
||||||
|
2. **结构化**: 支持结构化字段,便于日志分析
|
||||||
|
3. **灵活性**: 支持多种输出方式和配置选项
|
||||||
|
4. **生产就绪**: 支持日志轮转、压缩、清理等生产环境需求
|
||||||
|
5. **官方推荐**: 完全按照 Zap 官方文档实现,确保最佳实践
|
||||||
|
|
||||||
|
使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能!
|
||||||
321
docs/日志系统配置示例.md
Normal file
321
docs/日志系统配置示例.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# 📝 日志系统配置示例
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
新的日志系统完全基于配置文件,所有配置都在 `config.yaml` 中设置,容器代码保持简洁。
|
||||||
|
|
||||||
|
## 🎯 完整配置示例
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 🚀 日志系统配置 - 基于 Zap 官方推荐
|
||||||
|
logger:
|
||||||
|
# 基础配置
|
||||||
|
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
|
||||||
|
format: "json" # 输出格式: json, console
|
||||||
|
output: "file" # 输出方式: stdout, stderr, file
|
||||||
|
log_dir: "logs" # 日志目录
|
||||||
|
use_daily: true # 是否按日分包
|
||||||
|
use_color: false # 是否使用彩色输出(仅console格式有效)
|
||||||
|
|
||||||
|
# 文件配置
|
||||||
|
max_size: 100 # 单个文件最大大小(MB)
|
||||||
|
max_backups: 5 # 最大备份文件数
|
||||||
|
max_age: 30 # 最大保留天数
|
||||||
|
compress: true # 是否压缩
|
||||||
|
|
||||||
|
# 高级功能
|
||||||
|
enable_level_separation: true # 是否启用按级别分文件
|
||||||
|
enable_request_logging: true # 是否启用请求日志
|
||||||
|
enable_performance_log: true # 是否启用性能日志
|
||||||
|
|
||||||
|
# 各级别配置(按级别分文件时使用)
|
||||||
|
level_configs:
|
||||||
|
debug:
|
||||||
|
max_size: 50 # 50MB
|
||||||
|
max_backups: 3
|
||||||
|
max_age: 7 # 7天
|
||||||
|
compress: true
|
||||||
|
info:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200 # 200MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90 # 90天
|
||||||
|
compress: true
|
||||||
|
fatal:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365 # 1年
|
||||||
|
compress: true
|
||||||
|
panic:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365 # 1年
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 环境特定配置
|
||||||
|
|
||||||
|
### 开发环境配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 开发环境 - 详细日志,控制台输出
|
||||||
|
logger:
|
||||||
|
level: "debug"
|
||||||
|
format: "console"
|
||||||
|
output: "stdout"
|
||||||
|
use_daily: false
|
||||||
|
use_color: true
|
||||||
|
enable_level_separation: false
|
||||||
|
development: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 生产环境 - 精简日志,文件输出
|
||||||
|
logger:
|
||||||
|
level: "warn"
|
||||||
|
format: "json"
|
||||||
|
output: "file"
|
||||||
|
log_dir: "/app/logs"
|
||||||
|
use_daily: true
|
||||||
|
use_color: false
|
||||||
|
enable_level_separation: true
|
||||||
|
development: false
|
||||||
|
|
||||||
|
# 生产环境文件配置
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 生产环境级别配置
|
||||||
|
level_configs:
|
||||||
|
warn:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 500
|
||||||
|
max_backups: 20
|
||||||
|
max_age: 180
|
||||||
|
compress: true
|
||||||
|
fatal:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 配置字段说明
|
||||||
|
|
||||||
|
### 基础配置字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `level` | string | 日志级别 | "info" |
|
||||||
|
| `format` | string | 输出格式 | "json" |
|
||||||
|
| `output` | string | 输出方式 | "stdout" |
|
||||||
|
| `log_dir` | string | 日志目录 | "logs" |
|
||||||
|
| `use_daily` | bool | 是否按日分包 | false |
|
||||||
|
| `use_color` | bool | 是否使用彩色输出 | false |
|
||||||
|
|
||||||
|
### 文件配置字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `max_size` | int | 单个文件最大大小(MB) | 100 |
|
||||||
|
| `max_backups` | int | 最大备份文件数 | 5 |
|
||||||
|
| `max_age` | int | 最大保留天数 | 30 |
|
||||||
|
| `compress` | bool | 是否压缩 | true |
|
||||||
|
|
||||||
|
### 高级功能字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `enable_level_separation` | bool | 是否启用按级别分文件 | false |
|
||||||
|
| `enable_request_logging` | bool | 是否启用请求日志 | false |
|
||||||
|
| `enable_performance_log` | bool | 是否启用性能日志 | false |
|
||||||
|
|
||||||
|
### 级别配置字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `max_size` | int | 单个文件最大大小(MB) | 无 |
|
||||||
|
| `max_backups` | int | 最大备份文件数 | 无 |
|
||||||
|
| `max_age` | int | 最大保留天数 | 无 |
|
||||||
|
| `compress` | bool | 是否压缩 | 无 |
|
||||||
|
|
||||||
|
## 🚀 配置最佳实践
|
||||||
|
|
||||||
|
### 1. 按环境配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 开发环境
|
||||||
|
logger:
|
||||||
|
level: "debug"
|
||||||
|
format: "console"
|
||||||
|
output: "stdout"
|
||||||
|
use_color: true
|
||||||
|
enable_level_separation: false
|
||||||
|
|
||||||
|
# 测试环境
|
||||||
|
logger:
|
||||||
|
level: "info"
|
||||||
|
format: "json"
|
||||||
|
output: "file"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
|
||||||
|
# 生产环境
|
||||||
|
logger:
|
||||||
|
level: "warn"
|
||||||
|
format: "json"
|
||||||
|
output: "file"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 按级别配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
enable_level_separation: true
|
||||||
|
level_configs:
|
||||||
|
# 调试日志 - 小文件,短期保留
|
||||||
|
debug:
|
||||||
|
max_size: 50
|
||||||
|
max_backups: 3
|
||||||
|
max_age: 7
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 信息日志 - 中等文件,中期保留
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 警告日志 - 中等文件,中期保留
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 错误日志 - 大文件,长期保留
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
# 致命错误 - 中等文件,长期保留
|
||||||
|
fatal:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 365
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 性能优化配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
# 生产环境性能优化
|
||||||
|
level: "warn" # 只记录警告及以上级别
|
||||||
|
format: "json" # JSON格式便于日志分析
|
||||||
|
output: "file" # 文件输出避免控制台性能影响
|
||||||
|
use_daily: true # 按日分包便于管理
|
||||||
|
enable_level_separation: true # 按级别分文件便于分析
|
||||||
|
|
||||||
|
# 文件轮转优化
|
||||||
|
max_size: 200 # 较大的文件大小减少轮转频率
|
||||||
|
max_backups: 10 # 适中的备份数量
|
||||||
|
max_age: 90 # 90天保留期平衡存储和分析需求
|
||||||
|
compress: true # 启用压缩节省存储空间
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 配置验证
|
||||||
|
|
||||||
|
### 1. 必需字段
|
||||||
|
|
||||||
|
以下字段是必需的,如果未设置将使用默认值:
|
||||||
|
|
||||||
|
- `level`: 日志级别
|
||||||
|
- `format`: 输出格式
|
||||||
|
- `output`: 输出方式
|
||||||
|
|
||||||
|
### 2. 可选字段
|
||||||
|
|
||||||
|
以下字段是可选的,如果未设置将使用系统默认值:
|
||||||
|
|
||||||
|
- `log_dir`: 日志目录
|
||||||
|
- `use_daily`: 是否按日分包
|
||||||
|
- `use_color`: 是否使用彩色输出
|
||||||
|
- `max_size`: 文件大小限制
|
||||||
|
- `max_backups`: 备份文件数量
|
||||||
|
- `max_age`: 文件保留天数
|
||||||
|
- `compress`: 是否压缩
|
||||||
|
|
||||||
|
### 3. 条件字段
|
||||||
|
|
||||||
|
以下字段在特定条件下是必需的:
|
||||||
|
|
||||||
|
- 当 `output: "file"` 时,建议设置 `log_dir`
|
||||||
|
- 当 `enable_level_separation: true` 时,建议设置 `level_configs`
|
||||||
|
- 当 `format: "console"` 时,`use_color` 才有效
|
||||||
|
|
||||||
|
## 📝 配置迁移指南
|
||||||
|
|
||||||
|
### 从旧配置迁移
|
||||||
|
|
||||||
|
如果您之前使用的是旧版本的日志配置,可以按照以下方式迁移:
|
||||||
|
|
||||||
|
#### 旧配置
|
||||||
|
```yaml
|
||||||
|
# 旧版本配置
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
file: "logs/app.log"
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 新配置
|
||||||
|
```yaml
|
||||||
|
# 新版本配置
|
||||||
|
logger:
|
||||||
|
level: "info"
|
||||||
|
output: "file"
|
||||||
|
log_dir: "logs"
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 3
|
||||||
|
format: "json"
|
||||||
|
use_daily: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
新的日志系统配置完全基于 `config.yaml` 文件,具有以下特点:
|
||||||
|
|
||||||
|
1. **配置集中**: 所有日志配置都在一个地方管理
|
||||||
|
2. **环境友好**: 支持不同环境的配置
|
||||||
|
3. **灵活性强**: 支持按级别分文件、按日分包等高级功能
|
||||||
|
4. **性能优化**: 基于 Zap 官方最佳实践
|
||||||
|
5. **易于维护**: 配置结构清晰,易于理解和修改
|
||||||
|
|
||||||
|
通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求!
|
||||||
176
docs/组合包动态处理机制说明.md
Normal file
176
docs/组合包动态处理机制说明.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# 组合包动态处理机制说明
|
||||||
|
|
||||||
|
## 🎉 重大更新
|
||||||
|
|
||||||
|
组合包系统现在支持**动态处理机制**!这意味着:
|
||||||
|
- ✅ **零编码**:大部分组合包无需编写任何Go代码
|
||||||
|
- ✅ **配置驱动**:只需在数据库配置即可立即使用
|
||||||
|
- ✅ **灵活扩展**:特殊需求仍可通过自定义处理器实现
|
||||||
|
|
||||||
|
## 🔧 工作原理
|
||||||
|
|
||||||
|
### 处理优先级
|
||||||
|
1. **自定义处理器优先**:如果注册了专门的处理器,优先使用
|
||||||
|
2. **通用处理器兜底**:COMB开头的API自动使用通用组合包处理器
|
||||||
|
3. **数据库驱动**:根据数据库配置自动调用子产品处理器
|
||||||
|
|
||||||
|
### 系统架构
|
||||||
|
```
|
||||||
|
API请求 (COMBXXXX)
|
||||||
|
↓
|
||||||
|
优先查找自定义处理器
|
||||||
|
↓ (未找到)
|
||||||
|
检查是否COMB开头
|
||||||
|
↓ (是)
|
||||||
|
通用组合包处理器
|
||||||
|
↓
|
||||||
|
查询数据库获取子产品配置
|
||||||
|
↓
|
||||||
|
并发调用子产品处理器
|
||||||
|
↓
|
||||||
|
聚合结果并返回
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 使用方法
|
||||||
|
|
||||||
|
### 方案1:纯配置组合包(推荐)
|
||||||
|
|
||||||
|
**步骤1:创建组合包产品**
|
||||||
|
```sql
|
||||||
|
INSERT INTO products (
|
||||||
|
id, code, name, description,
|
||||||
|
is_package, is_enabled, is_visible,
|
||||||
|
price, category_id
|
||||||
|
) VALUES (
|
||||||
|
'uuid1', 'COMB1234', '身份验证组合包', '包含身份证二要素和手机三要素验证',
|
||||||
|
true, true, true,
|
||||||
|
5.00, 'category_id'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤2:配置子产品**
|
||||||
|
```sql
|
||||||
|
INSERT INTO product_package_items (package_id, product_id, sort_order) VALUES
|
||||||
|
('uuid1', 'product_id_1', 1), -- FLXG162A 身份证二要素
|
||||||
|
('uuid1', 'product_id_2', 2); -- FLXG54F5 手机三要素
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤3:直接使用**
|
||||||
|
```bash
|
||||||
|
# 立即可用,无需任何代码编写!
|
||||||
|
POST /api/v1/COMB1234
|
||||||
|
{
|
||||||
|
"id_card": "123456789012345678",
|
||||||
|
"name": "张三",
|
||||||
|
"mobile_no": "13800138000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案2:自定义逻辑组合包
|
||||||
|
|
||||||
|
如果需要对结果进行后处理,才需要编写代码:
|
||||||
|
|
||||||
|
**步骤1:创建处理器文件**
|
||||||
|
```go
|
||||||
|
// internal/domains/api/services/processors/comb/comb1234_processor.go
|
||||||
|
func ProcessCOMB1234Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
// 参数验证
|
||||||
|
var paramsDto dto.COMB1234Req
|
||||||
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用组合包服务
|
||||||
|
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB1234")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义后处理逻辑
|
||||||
|
for _, resp := range combinedResult.Responses {
|
||||||
|
if resp.ApiCode == "FLXG162A" && resp.Success {
|
||||||
|
// 添加自定义字段
|
||||||
|
if data, ok := resp.Data.(map[string]interface{}); ok {
|
||||||
|
data["processed_by"] = "COMB1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(combinedResult)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤2:注册处理器**
|
||||||
|
```go
|
||||||
|
// api_request_service.go 中添加
|
||||||
|
"COMB1234": comb.ProcessCOMB1234Request, // 有自定义逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤3:添加DTO(如需要)**
|
||||||
|
```go
|
||||||
|
// dto/api_request_dto.go 中添加
|
||||||
|
type COMB1234Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 现有组合包状态
|
||||||
|
|
||||||
|
### ✅ 已迁移到动态处理
|
||||||
|
- **COMB298Y**:删除了专门的处理器文件,现在使用通用处理器
|
||||||
|
|
||||||
|
### 🔧 保留自定义处理器
|
||||||
|
- **COMB86PM**:有特殊逻辑(重命名ApiCode),保留自定义处理器
|
||||||
|
|
||||||
|
## 🎯 响应格式
|
||||||
|
|
||||||
|
所有组合包都返回统一的响应格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"responses": [
|
||||||
|
{
|
||||||
|
"api_code": "FLXG162A",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// 子产品的响应数据
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "FLXG54F5",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// 子产品的响应数据
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "YYSY4B37",
|
||||||
|
"success": false,
|
||||||
|
"error": "数据源异常"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 优势总结
|
||||||
|
|
||||||
|
1. **开发效率**:90%的组合包无需编写代码
|
||||||
|
2. **维护成本**:减少重复代码,统一处理逻辑
|
||||||
|
3. **业务灵活**:数据库配置即时生效
|
||||||
|
4. **向后兼容**:现有自定义处理器继续工作
|
||||||
|
5. **扩展性强**:特殊需求仍可通过自定义处理器实现
|
||||||
|
|
||||||
|
## ⚡ 性能特性
|
||||||
|
|
||||||
|
- **并发处理**:所有子产品并发调用
|
||||||
|
- **独立失败**:单个子产品失败不影响其他
|
||||||
|
- **智能排序**:通过sort_order控制响应顺序
|
||||||
|
- **错误隔离**:每个子产品的错误独立处理
|
||||||
|
|
||||||
|
现在,创建一个新的组合包就像配置数据库一样简单!🎉
|
||||||
383
docs/西部数据日志系统使用指南.md
Normal file
383
docs/西部数据日志系统使用指南.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# 🚀 西部数据日志系统使用指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
西部数据服务现在集成了完整的日志记录系统,支持请求、响应、错误和性能四种类型的日志记录,每种类型都有独立的日志文件。
|
||||||
|
|
||||||
|
## 📝 日志记录范围
|
||||||
|
|
||||||
|
### ✅ **会记录日志的操作**
|
||||||
|
- `CallAPI()` - 调用西部数据API
|
||||||
|
- `G05HZ01CallAPI()` - 调用G05HZ01 API
|
||||||
|
|
||||||
|
### ❌ **不会记录日志的操作**
|
||||||
|
- `Encrypt()` - 内部加密方法
|
||||||
|
- `Md5Encrypt()` - 内部MD5加密方法
|
||||||
|
|
||||||
|
**说明**: 加密解密是内部工具方法,调用频率高且不涉及外部API,因此不记录日志以提高性能。日志系统专注于记录外部API调用的完整生命周期。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
### 1. **四种日志类型**
|
||||||
|
- **请求日志**: 记录所有API请求的详细信息
|
||||||
|
- **响应日志**: 记录所有API响应的详细信息
|
||||||
|
- **错误日志**: 记录所有错误和异常情况
|
||||||
|
- **性能日志**: 记录请求耗时和性能指标
|
||||||
|
|
||||||
|
### 2. **日志文件分离**
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── westdex/
|
||||||
|
│ ├── 2024-01-01/
|
||||||
|
│ │ ├── request.log # 请求日志
|
||||||
|
│ │ ├── response.log # 响应日志
|
||||||
|
│ │ ├── error.log # 错误日志
|
||||||
|
│ │ └── performance.log # 性能日志
|
||||||
|
│ └── 2024-01-02/
|
||||||
|
│ ├── request.log
|
||||||
|
│ ├── response.log
|
||||||
|
│ ├── error.log
|
||||||
|
│ └── performance.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **请求追踪**
|
||||||
|
- 每个请求都有唯一的请求ID
|
||||||
|
- 请求和响应日志通过请求ID关联
|
||||||
|
- 支持完整的请求链路追踪
|
||||||
|
|
||||||
|
### 4. **性能监控**
|
||||||
|
- 记录每个API调用的耗时
|
||||||
|
- 区分成功和失败的请求
|
||||||
|
- 提供性能分析数据
|
||||||
|
|
||||||
|
## 🏗️ 配置说明
|
||||||
|
|
||||||
|
### 配置文件设置
|
||||||
|
|
||||||
|
在 `config.yaml` 中添加西部数据日志配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
westdex:
|
||||||
|
url: "https://apimaster.westdex.com.cn/api/invoke"
|
||||||
|
key: "your-key"
|
||||||
|
secret_id: "your-secret-id"
|
||||||
|
secret_second_id: "your-secret-second-id"
|
||||||
|
|
||||||
|
# 西部数据日志配置
|
||||||
|
logging:
|
||||||
|
enabled: true # 启用日志记录
|
||||||
|
log_dir: "logs/westdex" # 日志目录
|
||||||
|
use_daily: true # 按日分包
|
||||||
|
enable_level_separation: true # 启用级别分离
|
||||||
|
|
||||||
|
# 各级别配置
|
||||||
|
level_configs:
|
||||||
|
request:
|
||||||
|
max_size: 100 # 100MB
|
||||||
|
max_backups: 5 # 5个备份
|
||||||
|
max_age: 30 # 30天
|
||||||
|
compress: true # 启用压缩
|
||||||
|
response:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200 # 错误日志文件更大
|
||||||
|
max_backups: 10 # 更多备份
|
||||||
|
max_age: 90 # 保留更久
|
||||||
|
compress: true
|
||||||
|
performance:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 1. 使用配置创建服务
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tyapi-server/internal/config"
|
||||||
|
"tyapi-server/internal/infrastructure/external/westdex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 加载配置
|
||||||
|
cfg := &config.Config{
|
||||||
|
// ... 配置内容
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用配置创建西部数据服务
|
||||||
|
service, err := westdex.NewWestDexServiceWithConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用服务
|
||||||
|
resp, err := service.CallAPI("G05HZ01", map[string]interface{}{
|
||||||
|
"param1": "value1",
|
||||||
|
"param2": "value2",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// 错误会自动记录到错误日志
|
||||||
|
log.Printf("API调用失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应会自动记录到响应日志
|
||||||
|
log.Printf("API调用成功: %s", string(resp))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用自定义日志配置
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建自定义日志配置
|
||||||
|
loggingConfig := westdex.WestDexLoggingConfig{
|
||||||
|
Enabled: true,
|
||||||
|
LogDir: "logs/custom_westdex",
|
||||||
|
UseDaily: true,
|
||||||
|
EnableLevelSeparation: true,
|
||||||
|
LevelConfigs: map[string]westdex.WestDexLevelFileConfig{
|
||||||
|
"request": {
|
||||||
|
MaxSize: 50,
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 7,
|
||||||
|
Compress: true,
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
MaxSize: 100,
|
||||||
|
MaxBackups: 5,
|
||||||
|
MaxAge: 30,
|
||||||
|
Compress: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用自定义配置创建服务
|
||||||
|
service, err := westdex.NewWestDexServiceWithLogging(
|
||||||
|
"https://api.example.com",
|
||||||
|
"your-key",
|
||||||
|
"your-secret-id",
|
||||||
|
"your-secret-second-id",
|
||||||
|
loggingConfig,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 日志示例
|
||||||
|
|
||||||
|
### 请求日志示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"msg": "西部数据API请求",
|
||||||
|
"request_id": "westdex_a1b2c3d4",
|
||||||
|
"api_code": "G05HZ01",
|
||||||
|
"url": "https://apimaster.westdex.com.cn/api/invoke/449159/G05HZ01?timestamp=1704096000000",
|
||||||
|
"request_data": {
|
||||||
|
"param1": "value1",
|
||||||
|
"param2": "value2"
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应日志示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"msg": "西部数据API响应",
|
||||||
|
"request_id": "westdex_a1b2c3d4",
|
||||||
|
"api_code": "G05HZ01",
|
||||||
|
"status_code": 200,
|
||||||
|
"response_data": "{\"code\":\"0000\",\"data\":\"...\",\"message\":\"success\"}",
|
||||||
|
"duration": "150ms",
|
||||||
|
"timestamp": "2024-01-01T12:00:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误日志示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "error",
|
||||||
|
"msg": "西部数据API错误",
|
||||||
|
"request_id": "westdex_a1b2c3d4",
|
||||||
|
"api_code": "G05HZ01",
|
||||||
|
"error": "数据源异常: 查询失败",
|
||||||
|
"request_data": {
|
||||||
|
"param1": "value1",
|
||||||
|
"param2": "value2"
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-01T12:00:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能日志示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"msg": "西部数据API性能",
|
||||||
|
"request_id": "westdex_a1b2c3d4",
|
||||||
|
"api_code": "G05HZ01",
|
||||||
|
"duration": "150ms",
|
||||||
|
"success": true,
|
||||||
|
"timestamp": "2024-01-01T12:00:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 日志字段说明
|
||||||
|
|
||||||
|
### 通用字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `request_id` | string | 请求唯一标识符 |
|
||||||
|
| `api_code` | string | API代码 |
|
||||||
|
| `timestamp` | string | 时间戳(ISO8601格式) |
|
||||||
|
|
||||||
|
### 请求日志字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `url` | string | 完整的请求URL |
|
||||||
|
| `request_data` | object | 请求数据 |
|
||||||
|
|
||||||
|
### 响应日志字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `status_code` | int | HTTP状态码 |
|
||||||
|
| `response_data` | string | 响应数据(JSON字符串) |
|
||||||
|
| `duration` | duration | 请求耗时 |
|
||||||
|
|
||||||
|
### 错误日志字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `error` | error | 错误信息 |
|
||||||
|
| `request_data` | object | 原始请求数据 |
|
||||||
|
|
||||||
|
### 性能日志字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `duration` | duration | 请求耗时 |
|
||||||
|
| `success` | bool | 是否成功 |
|
||||||
|
|
||||||
|
## 🎯 最佳实践
|
||||||
|
|
||||||
|
### 1. **日志配置优化**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 生产环境配置
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "/var/log/westdex"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
level_configs:
|
||||||
|
request:
|
||||||
|
max_size: 200 # 更大的文件大小
|
||||||
|
max_backups: 10 # 更多备份
|
||||||
|
max_age: 90 # 保留更久
|
||||||
|
error:
|
||||||
|
max_size: 500 # 错误日志文件更大
|
||||||
|
max_backups: 20 # 更多备份
|
||||||
|
max_age: 180 # 保留更久
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **日志分析**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看请求日志
|
||||||
|
tail -f logs/westdex/2024-01-01/request.log
|
||||||
|
|
||||||
|
# 查看错误日志
|
||||||
|
tail -f logs/westdex/2024-01-01/error.log
|
||||||
|
|
||||||
|
# 查看性能日志
|
||||||
|
tail -f logs/westdex/2024-01-01/performance.log
|
||||||
|
|
||||||
|
# 统计API调用次数
|
||||||
|
grep -c "G05HZ01" logs/westdex/2024-01-01/request.log
|
||||||
|
|
||||||
|
# 统计错误率
|
||||||
|
grep -c "error" logs/westdex/2024-01-01/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **性能监控**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看平均响应时间
|
||||||
|
grep "duration" logs/westdex/2024-01-01/performance.log | \
|
||||||
|
awk -F'"duration":"' '{print $2}' | \
|
||||||
|
awk -F'"' '{print $1}' | \
|
||||||
|
awk -F'ms' '{sum+=$1; count++} END {print "平均响应时间: " sum/count "ms"}'
|
||||||
|
|
||||||
|
# 查看成功率
|
||||||
|
total=$(grep -c "success" logs/westdex/2024-01-01/performance.log)
|
||||||
|
success=$(grep -c '"success":true' logs/westdex/2024-01-01/performance.log)
|
||||||
|
echo "成功率: $((success * 100 / total))%"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 注意事项
|
||||||
|
|
||||||
|
### 1. **日志文件管理**
|
||||||
|
- 定期清理旧日志文件
|
||||||
|
- 监控磁盘空间使用
|
||||||
|
- 配置合适的日志轮转策略
|
||||||
|
|
||||||
|
### 2. **敏感信息处理**
|
||||||
|
- 日志中可能包含敏感数据
|
||||||
|
- 确保日志文件访问权限
|
||||||
|
- 考虑日志脱敏需求
|
||||||
|
|
||||||
|
### 3. **性能影响**
|
||||||
|
- 日志记录会增加少量性能开销
|
||||||
|
- 异步日志记录可减少性能影响
|
||||||
|
- 合理配置日志级别
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### 1. **日志文件未创建**
|
||||||
|
- 检查日志目录权限
|
||||||
|
- 确认日志配置已启用
|
||||||
|
- 验证文件路径配置
|
||||||
|
|
||||||
|
### 2. **日志记录不完整**
|
||||||
|
- 检查日志器是否正确初始化
|
||||||
|
- 确认请求ID生成逻辑
|
||||||
|
- 验证错误处理流程
|
||||||
|
|
||||||
|
### 3. **性能问题**
|
||||||
|
- 检查日志文件大小和数量
|
||||||
|
- 确认日志轮转配置
|
||||||
|
- 监控磁盘I/O性能
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
西部数据日志系统提供了完整的API调用追踪能力:
|
||||||
|
|
||||||
|
1. **请求追踪**: 通过唯一请求ID关联请求和响应
|
||||||
|
2. **错误监控**: 记录所有错误和异常情况
|
||||||
|
3. **性能分析**: 提供详细的性能指标
|
||||||
|
4. **文件管理**: 支持按日期分包和级别分离
|
||||||
|
5. **配置灵活**: 支持自定义日志配置
|
||||||
|
|
||||||
|
通过合理使用这个日志系统,您可以:
|
||||||
|
- 快速定位API调用问题
|
||||||
|
- 监控系统性能和稳定性
|
||||||
|
- 分析API使用模式和趋势
|
||||||
|
- 提高问题排查效率
|
||||||
|
|
||||||
|
现在您的西部数据服务已经具备了完整的日志记录能力!🚀
|
||||||
11
go.mod
11
go.mod
@@ -11,10 +11,14 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.26.0
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/hibiken/asynq v0.25.1
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/qiniu/go-sdk/v7 v7.25.4
|
github.com/qiniu/go-sdk/v7 v7.25.4
|
||||||
github.com/redis/go-redis/v9 v9.11.0
|
github.com/redis/go-redis/v9 v9.11.0
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25
|
github.com/smartwalle/alipay/v3 v3.2.25
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -22,6 +26,8 @@ require (
|
|||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.37.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0
|
go.opentelemetry.io/otel/sdk v1.37.0
|
||||||
@@ -84,6 +90,8 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.62.0 // indirect
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||||
github.com/smartwalle/ngx v1.0.9 // indirect
|
github.com/smartwalle/ngx v1.0.9 // indirect
|
||||||
@@ -95,8 +103,11 @@ require (
|
|||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
|||||||
27
go.sum
27
go.sum
@@ -9,6 +9,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
|
|||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||||
|
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
||||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
||||||
@@ -108,6 +110,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||||
|
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||||
|
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
||||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
@@ -131,6 +135,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
|||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
@@ -189,6 +195,13 @@ github.com/qiniu/go-sdk/v7 v7.25.4/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peq
|
|||||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||||
|
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||||
|
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
@@ -197,6 +210,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
|||||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
|
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
||||||
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||||
@@ -241,6 +256,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
|||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||||
@@ -249,6 +266,14 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
|||||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||||
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||||
|
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||||
|
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
@@ -296,6 +321,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
|
|||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||||
|
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
|||||||
@@ -22,8 +22,15 @@ import (
|
|||||||
// 产品域实体
|
// 产品域实体
|
||||||
productEntities "tyapi-server/internal/domains/product/entities"
|
productEntities "tyapi-server/internal/domains/product/entities"
|
||||||
|
|
||||||
|
// 文章域实体
|
||||||
|
articleEntities "tyapi-server/internal/domains/article/entities"
|
||||||
|
|
||||||
|
// 统计域实体
|
||||||
|
statisticsEntities "tyapi-server/internal/domains/statistics/entities"
|
||||||
|
|
||||||
apiEntities "tyapi-server/internal/domains/api/entities"
|
apiEntities "tyapi-server/internal/domains/api/entities"
|
||||||
"tyapi-server/internal/infrastructure/database"
|
"tyapi-server/internal/infrastructure/database"
|
||||||
|
taskEntities "tyapi-server/internal/infrastructure/task/entities"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Application 应用程序结构
|
// Application 应用程序结构
|
||||||
@@ -224,6 +231,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
|||||||
&financeEntities.AlipayOrder{},
|
&financeEntities.AlipayOrder{},
|
||||||
&financeEntities.InvoiceApplication{},
|
&financeEntities.InvoiceApplication{},
|
||||||
&financeEntities.UserInvoiceInfo{},
|
&financeEntities.UserInvoiceInfo{},
|
||||||
|
|
||||||
// 产品域
|
// 产品域
|
||||||
&productEntities.Product{},
|
&productEntities.Product{},
|
||||||
&productEntities.ProductPackageItem{},
|
&productEntities.ProductPackageItem{},
|
||||||
@@ -231,9 +239,29 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
|||||||
&productEntities.Subscription{},
|
&productEntities.Subscription{},
|
||||||
&productEntities.ProductDocumentation{},
|
&productEntities.ProductDocumentation{},
|
||||||
&productEntities.ProductApiConfig{},
|
&productEntities.ProductApiConfig{},
|
||||||
|
&productEntities.ComponentReportDownload{},
|
||||||
|
&productEntities.UIComponent{},
|
||||||
|
&productEntities.ProductUIComponent{},
|
||||||
|
|
||||||
|
// 文章域
|
||||||
|
&articleEntities.Article{},
|
||||||
|
&articleEntities.Category{},
|
||||||
|
&articleEntities.Tag{},
|
||||||
|
&articleEntities.ScheduledTask{},
|
||||||
|
// 公告
|
||||||
|
&articleEntities.Announcement{},
|
||||||
|
|
||||||
|
// 统计域
|
||||||
|
&statisticsEntities.StatisticsMetric{},
|
||||||
|
&statisticsEntities.StatisticsDashboard{},
|
||||||
|
&statisticsEntities.StatisticsReport{},
|
||||||
|
|
||||||
// api
|
// api
|
||||||
&apiEntities.ApiUser{},
|
&apiEntities.ApiUser{},
|
||||||
&apiEntities.ApiCall{},
|
&apiEntities.ApiCall{},
|
||||||
|
|
||||||
|
// 任务域
|
||||||
|
&taskEntities.AsyncTask{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +284,7 @@ func createLogger(cfg *config.Config) (*zap.Logger, error) {
|
|||||||
if cfg.Logger.Format == "" {
|
if cfg.Logger.Format == "" {
|
||||||
config.Encoding = "json"
|
config.Encoding = "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Logger.Output == "" {
|
if cfg.Logger.Output == "" {
|
||||||
config.OutputPaths = []string{"stdout"}
|
config.OutputPaths = []string{"stdout"}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,3 +24,48 @@ type DecryptCommand struct {
|
|||||||
EncryptedData string `json:"encrypted_data" binding:"required"`
|
EncryptedData string `json:"encrypted_data" binding:"required"`
|
||||||
SecretKey string `json:"secret_key" binding:"required"`
|
SecretKey string `json:"secret_key" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveApiCallCommand 保存API调用命令
|
||||||
|
type SaveApiCallCommand struct {
|
||||||
|
ApiCallID string `json:"api_call_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
ProductID string `json:"product_id"`
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
ErrorType string `json:"error_type"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessDeductionCommand 处理扣款命令
|
||||||
|
type ProcessDeductionCommand struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
ApiCallID string `json:"api_call_id"`
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
ProductID string `json:"product_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUsageStatsCommand 更新使用统计命令
|
||||||
|
type UpdateUsageStatsCommand struct {
|
||||||
|
SubscriptionID string `json:"subscription_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
ProductID string `json:"product_id"`
|
||||||
|
Increment int `json:"increment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordApiLogCommand 记录API日志命令
|
||||||
|
type RecordApiLogCommand struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
ApiName string `json:"api_name"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
ResponseSize int64 `json:"response_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessCompensationCommand 处理补偿命令
|
||||||
|
type ProcessCompensationCommand struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
104
internal/application/api/dto/api_call_validation.go
Normal file
104
internal/application/api/dto/api_call_validation.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
api_entities "tyapi-server/internal/domains/api/entities"
|
||||||
|
product_entities "tyapi-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
|
||||||
|
}
|
||||||
@@ -26,11 +26,13 @@ type WhiteListResponse struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IPAddress string `json:"ip_address"`
|
IPAddress string `json:"ip_address"`
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhiteListRequest struct {
|
type WhiteListRequest struct {
|
||||||
IPAddress string `json:"ip_address" binding:"required,ip"`
|
IPAddress string `json:"ip_address" binding:"required,ip"`
|
||||||
|
Remark string `json:"remark"` // 备注(可选)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhiteListListResponse struct {
|
type WhiteListListResponse struct {
|
||||||
|
|||||||
19
internal/application/api/dto/form_config_dto.go
Normal file
19
internal/application/api/dto/form_config_dto.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// FormField 表单字段配置
|
||||||
|
type FormField struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Validation string `json:"validation"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Example string `json:"example"`
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormConfigResponse 表单配置响应
|
||||||
|
type FormConfigResponse struct {
|
||||||
|
ApiCode string `json:"api_code"`
|
||||||
|
Fields []FormField `json:"fields"`
|
||||||
|
}
|
||||||
@@ -4,38 +4,46 @@ import "errors"
|
|||||||
|
|
||||||
// API调用相关错误类型
|
// API调用相关错误类型
|
||||||
var (
|
var (
|
||||||
ErrQueryEmpty = errors.New("查询为空")
|
ErrQueryEmpty = errors.New("查询为空")
|
||||||
ErrSystem = errors.New("接口异常")
|
ErrSystem = errors.New("接口异常")
|
||||||
ErrDecryptFail = errors.New("解密失败")
|
ErrDecryptFail = errors.New("解密失败")
|
||||||
ErrRequestParam = errors.New("请求参数结构不正确")
|
ErrRequestParam = errors.New("请求参数结构不正确")
|
||||||
ErrInvalidParam = errors.New("参数校验不正确")
|
ErrInvalidParam = errors.New("参数校验不正确")
|
||||||
ErrInvalidIP = errors.New("未经授权的IP")
|
ErrInvalidIP = errors.New("未经授权的IP")
|
||||||
ErrMissingAccessId = errors.New("缺少Access-Id")
|
ErrMissingAccessId = errors.New("缺少Access-Id")
|
||||||
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
ErrInvalidAccessId = errors.New("未经授权的AccessId")
|
||||||
ErrFrozenAccount = errors.New("账户已冻结")
|
ErrFrozenAccount = errors.New("账户已冻结")
|
||||||
ErrArrears = errors.New("账户余额不足,无法请求")
|
ErrArrears = errors.New("账户余额不足,无法请求")
|
||||||
ErrProductNotFound = errors.New("产品不存在")
|
ErrInsufficientBalance = errors.New("钱包余额不足")
|
||||||
ErrProductDisabled = errors.New("产品已停用")
|
ErrProductNotFound = errors.New("产品不存在")
|
||||||
ErrNotSubscribed = errors.New("未订阅此产品")
|
ErrProductDisabled = errors.New("产品已停用")
|
||||||
ErrBusiness = errors.New("业务失败")
|
ErrNotSubscribed = errors.New("未订阅此产品")
|
||||||
|
ErrProductNotSubscribed = errors.New("未订阅此产品")
|
||||||
|
ErrSubscriptionExpired = errors.New("订阅已过期")
|
||||||
|
ErrSubscriptionSuspended = errors.New("订阅已暂停")
|
||||||
|
ErrBusiness = errors.New("业务失败")
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误码映射 - 严格按照用户要求
|
// 错误码映射 - 严格按照用户要求
|
||||||
var ErrorCodeMap = map[error]int{
|
var ErrorCodeMap = map[error]int{
|
||||||
ErrQueryEmpty: 1000,
|
ErrQueryEmpty: 1000,
|
||||||
ErrSystem: 1001,
|
ErrSystem: 1001,
|
||||||
ErrDecryptFail: 1002,
|
ErrDecryptFail: 1002,
|
||||||
ErrRequestParam: 1003,
|
ErrRequestParam: 1003,
|
||||||
ErrInvalidParam: 1003,
|
ErrInvalidParam: 1003,
|
||||||
ErrInvalidIP: 1004,
|
ErrInvalidIP: 1004,
|
||||||
ErrMissingAccessId: 1005,
|
ErrMissingAccessId: 1005,
|
||||||
ErrInvalidAccessId: 1006,
|
ErrInvalidAccessId: 1006,
|
||||||
ErrFrozenAccount: 1007,
|
ErrFrozenAccount: 1007,
|
||||||
ErrArrears: 1007,
|
ErrArrears: 1007,
|
||||||
ErrProductNotFound: 1008,
|
ErrInsufficientBalance: 1007,
|
||||||
ErrProductDisabled: 1008,
|
ErrProductNotFound: 1008,
|
||||||
ErrNotSubscribed: 1008,
|
ErrProductDisabled: 1008,
|
||||||
ErrBusiness: 2001,
|
ErrNotSubscribed: 1008,
|
||||||
|
ErrProductNotSubscribed: 1008,
|
||||||
|
ErrSubscriptionExpired: 1008,
|
||||||
|
ErrSubscriptionSuspended: 1008,
|
||||||
|
ErrBusiness: 2001,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetErrorCode 获取错误对应的错误码
|
// GetErrorCode 获取错误对应的错误码
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tyapi-server/internal/application/article/dto/commands"
|
||||||
|
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||||
|
"tyapi-server/internal/application/article/dto/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnouncementApplicationService 公告应用服务接口
|
||||||
|
type AnnouncementApplicationService interface {
|
||||||
|
// 公告管理
|
||||||
|
CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error
|
||||||
|
UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error
|
||||||
|
DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error
|
||||||
|
GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error)
|
||||||
|
ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error)
|
||||||
|
|
||||||
|
// 公告状态管理
|
||||||
|
PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error
|
||||||
|
PublishAnnouncementByID(ctx context.Context, announcementID string) error // 通过ID发布公告 (用于定时任务)
|
||||||
|
WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error
|
||||||
|
ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error
|
||||||
|
SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error
|
||||||
|
UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error
|
||||||
|
CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"tyapi-server/internal/application/article/dto/commands"
|
||||||
|
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||||
|
"tyapi-server/internal/application/article/dto/responses"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
"tyapi-server/internal/domains/article/repositories"
|
||||||
|
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||||
|
"tyapi-server/internal/domains/article/services"
|
||||||
|
task_entities "tyapi-server/internal/infrastructure/task/entities"
|
||||||
|
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnouncementApplicationServiceImpl 公告应用服务实现
|
||||||
|
type AnnouncementApplicationServiceImpl struct {
|
||||||
|
announcementRepo repositories.AnnouncementRepository
|
||||||
|
announcementService *services.AnnouncementService
|
||||||
|
taskManager task_interfaces.TaskManager
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnouncementApplicationService 创建公告应用服务
|
||||||
|
func NewAnnouncementApplicationService(
|
||||||
|
announcementRepo repositories.AnnouncementRepository,
|
||||||
|
announcementService *services.AnnouncementService,
|
||||||
|
taskManager task_interfaces.TaskManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) AnnouncementApplicationService {
|
||||||
|
return &AnnouncementApplicationServiceImpl{
|
||||||
|
announcementRepo: announcementRepo,
|
||||||
|
announcementService: announcementService,
|
||||||
|
taskManager: taskManager,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAnnouncement 创建公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error {
|
||||||
|
// 1. 创建公告实体
|
||||||
|
announcement := &entities.Announcement{
|
||||||
|
Title: cmd.Title,
|
||||||
|
Content: cmd.Content,
|
||||||
|
Status: entities.AnnouncementStatusDraft,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用领域服务验证
|
||||||
|
if err := s.announcementService.ValidateAnnouncement(announcement); err != nil {
|
||||||
|
return fmt.Errorf("业务验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存公告
|
||||||
|
_, err := s.announcementRepo.Create(ctx, *announcement)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("创建公告失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAnnouncement 更新公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error {
|
||||||
|
// 1. 获取原公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否可以编辑
|
||||||
|
if err := s.announcementService.CanEdit(&announcement); err != nil {
|
||||||
|
return fmt.Errorf("公告状态不允许编辑: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新字段
|
||||||
|
if cmd.Title != "" {
|
||||||
|
announcement.Title = cmd.Title
|
||||||
|
}
|
||||||
|
if cmd.Content != "" {
|
||||||
|
announcement.Content = cmd.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证更新后的公告
|
||||||
|
if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil {
|
||||||
|
return fmt.Errorf("业务验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("更新公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("更新公告成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAnnouncement 删除公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error {
|
||||||
|
// 1. 检查公告是否存在
|
||||||
|
_, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 删除公告
|
||||||
|
if err := s.announcementRepo.Delete(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Error("删除公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("删除公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("删除公告成功", zap.String("id", cmd.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncementByID 获取公告详情
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, query.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", query.ID), zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
response := responses.FromAnnouncementEntity(&announcement)
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAnnouncements 获取公告列表
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) {
|
||||||
|
// 1. 构建仓储查询
|
||||||
|
repoQuery := &repoQueries.ListAnnouncementQuery{
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
Status: query.Status,
|
||||||
|
Title: query.Title,
|
||||||
|
OrderBy: query.OrderBy,
|
||||||
|
OrderDir: query.OrderDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用仓储
|
||||||
|
announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告列表失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取公告列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为响应对象
|
||||||
|
items := responses.FromAnnouncementEntityList(announcements)
|
||||||
|
|
||||||
|
response := &responses.AnnouncementListResponse{
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
Size: query.PageSize,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("获取公告列表成功", zap.Int64("total", total))
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishAnnouncement 发布公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否可以发布
|
||||||
|
if err := s.announcementService.CanPublish(&announcement); err != nil {
|
||||||
|
return fmt.Errorf("无法发布公告: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 发布公告
|
||||||
|
if err := announcement.Publish(); err != nil {
|
||||||
|
return fmt.Errorf("发布公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("发布公告成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishAnnouncementByID 通过ID发布公告 (用于定时任务)
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, announcementID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否已取消定时发布
|
||||||
|
if !announcement.IsScheduled() {
|
||||||
|
s.logger.Info("公告定时发布已取消,跳过执行",
|
||||||
|
zap.String("id", announcementID),
|
||||||
|
zap.String("status", string(announcement.Status)))
|
||||||
|
return nil // 静默返回,不报错
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查定时发布时间是否匹配
|
||||||
|
if announcement.ScheduledAt == nil {
|
||||||
|
s.logger.Info("公告没有定时发布时间,跳过执行",
|
||||||
|
zap.String("id", announcementID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 发布公告
|
||||||
|
if err := announcement.Publish(); err != nil {
|
||||||
|
return fmt.Errorf("发布公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithdrawAnnouncement 撤回公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否可以撤回
|
||||||
|
if err := s.announcementService.CanWithdraw(&announcement); err != nil {
|
||||||
|
return fmt.Errorf("无法撤回公告: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 撤回公告
|
||||||
|
if err := announcement.Withdraw(); err != nil {
|
||||||
|
return fmt.Errorf("撤回公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("撤回公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("撤回公告成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveAnnouncement 归档公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否可以归档
|
||||||
|
if err := s.announcementService.CanArchive(&announcement); err != nil {
|
||||||
|
return fmt.Errorf("无法归档公告: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 归档公告
|
||||||
|
announcement.Status = entities.AnnouncementStatusArchived
|
||||||
|
|
||||||
|
// 4. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("归档公告失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("归档公告成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchedulePublishAnnouncement 定时发布公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error {
|
||||||
|
// 1. 解析定时发布时间
|
||||||
|
scheduledTime, err := cmd.GetScheduledTime()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||||
|
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否可以定时发布
|
||||||
|
if err := s.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil {
|
||||||
|
return fmt.Errorf("无法设置定时发布: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 取消旧任务(如果存在)
|
||||||
|
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 创建任务工厂
|
||||||
|
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||||
|
|
||||||
|
// 6. 创建并异步入队公告发布任务
|
||||||
|
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||||
|
ctx,
|
||||||
|
cmd.ID,
|
||||||
|
scheduledTime,
|
||||||
|
"system", // 暂时使用系统用户ID
|
||||||
|
); err != nil {
|
||||||
|
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 设置定时发布
|
||||||
|
if err := announcement.SchedulePublish(scheduledTime); err != nil {
|
||||||
|
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSchedulePublishAnnouncement 更新定时发布公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error {
|
||||||
|
// 1. 解析定时发布时间
|
||||||
|
scheduledTime, err := cmd.GetScheduledTime()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||||
|
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否已设置定时发布
|
||||||
|
if !announcement.IsScheduled() {
|
||||||
|
return fmt.Errorf("公告未设置定时发布,无法修改时间")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 取消旧任务
|
||||||
|
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 创建任务工厂
|
||||||
|
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||||
|
|
||||||
|
// 6. 创建并异步入队新的公告发布任务
|
||||||
|
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||||
|
ctx,
|
||||||
|
cmd.ID,
|
||||||
|
scheduledTime,
|
||||||
|
"system", // 暂时使用系统用户ID
|
||||||
|
); err != nil {
|
||||||
|
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 更新定时发布时间
|
||||||
|
if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||||
|
return fmt.Errorf("更新定时发布时间失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSchedulePublishAnnouncement 取消定时发布公告
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error {
|
||||||
|
// 1. 获取公告
|
||||||
|
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("公告不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否已设置定时发布
|
||||||
|
if !announcement.IsScheduled() {
|
||||||
|
return fmt.Errorf("公告未设置定时发布,无需取消")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 取消任务
|
||||||
|
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||||
|
// 继续执行,即使取消任务失败也尝试取消定时发布状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 取消定时发布
|
||||||
|
if err := announcement.CancelSchedulePublish(); err != nil {
|
||||||
|
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||||
|
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncementStats 获取公告统计信息
|
||||||
|
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) {
|
||||||
|
// 1. 统计总数
|
||||||
|
total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("统计公告总数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 统计各状态数量
|
||||||
|
published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("统计已发布公告数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("统计草稿公告数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("统计归档公告数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 统计定时发布数量(需要查询有scheduled_at的草稿)
|
||||||
|
scheduled, err := s.announcementRepo.FindScheduled(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("统计定时发布公告数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &responses.AnnouncementStatsResponse{
|
||||||
|
TotalAnnouncements: total + published + archived,
|
||||||
|
PublishedAnnouncements: published,
|
||||||
|
DraftAnnouncements: draft,
|
||||||
|
ArchivedAnnouncements: archived,
|
||||||
|
ScheduledAnnouncements: int64(len(scheduled)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
48
internal/application/article/article_application_service.go
Normal file
48
internal/application/article/article_application_service.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tyapi-server/internal/application/article/dto/commands"
|
||||||
|
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||||
|
"tyapi-server/internal/application/article/dto/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArticleApplicationService 文章应用服务接口
|
||||||
|
type ArticleApplicationService interface {
|
||||||
|
// 文章管理
|
||||||
|
CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error
|
||||||
|
UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error
|
||||||
|
DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error
|
||||||
|
GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error)
|
||||||
|
ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
|
||||||
|
ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
|
||||||
|
|
||||||
|
// 文章状态管理
|
||||||
|
PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error
|
||||||
|
PublishArticleByID(ctx context.Context, articleID string) error
|
||||||
|
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||||
|
UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
|
||||||
|
CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error
|
||||||
|
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
|
||||||
|
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
|
||||||
|
|
||||||
|
// 文章交互
|
||||||
|
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
|
||||||
|
|
||||||
|
// 分类管理
|
||||||
|
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
|
||||||
|
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
|
||||||
|
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
|
||||||
|
GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
|
||||||
|
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
|
||||||
|
|
||||||
|
// 标签管理
|
||||||
|
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
|
||||||
|
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
|
||||||
|
DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error
|
||||||
|
GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error)
|
||||||
|
ListTags(ctx context.Context) (*responses.TagListResponse, error)
|
||||||
|
}
|
||||||
836
internal/application/article/article_application_service_impl.go
Normal file
836
internal/application/article/article_application_service_impl.go
Normal file
@@ -0,0 +1,836 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"tyapi-server/internal/application/article/dto/commands"
|
||||||
|
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||||
|
"tyapi-server/internal/application/article/dto/responses"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
"tyapi-server/internal/domains/article/repositories"
|
||||||
|
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||||
|
"tyapi-server/internal/domains/article/services"
|
||||||
|
task_entities "tyapi-server/internal/infrastructure/task/entities"
|
||||||
|
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||||
|
shared_interfaces "tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArticleApplicationServiceImpl 文章应用服务实现
|
||||||
|
type ArticleApplicationServiceImpl struct {
|
||||||
|
articleRepo repositories.ArticleRepository
|
||||||
|
categoryRepo repositories.CategoryRepository
|
||||||
|
tagRepo repositories.TagRepository
|
||||||
|
articleService *services.ArticleService
|
||||||
|
taskManager task_interfaces.TaskManager
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArticleApplicationService 创建文章应用服务
|
||||||
|
func NewArticleApplicationService(
|
||||||
|
articleRepo repositories.ArticleRepository,
|
||||||
|
categoryRepo repositories.CategoryRepository,
|
||||||
|
tagRepo repositories.TagRepository,
|
||||||
|
articleService *services.ArticleService,
|
||||||
|
taskManager task_interfaces.TaskManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) ArticleApplicationService {
|
||||||
|
return &ArticleApplicationServiceImpl{
|
||||||
|
articleRepo: articleRepo,
|
||||||
|
categoryRepo: categoryRepo,
|
||||||
|
tagRepo: tagRepo,
|
||||||
|
articleService: articleService,
|
||||||
|
taskManager: taskManager,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateArticle 创建文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error {
|
||||||
|
// 1. 参数验证
|
||||||
|
if err := s.validateCreateArticle(cmd); err != nil {
|
||||||
|
return fmt.Errorf("参数验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建文章实体
|
||||||
|
article := &entities.Article{
|
||||||
|
Title: cmd.Title,
|
||||||
|
Content: cmd.Content,
|
||||||
|
Summary: cmd.Summary,
|
||||||
|
CoverImage: cmd.CoverImage,
|
||||||
|
CategoryID: cmd.CategoryID,
|
||||||
|
IsFeatured: cmd.IsFeatured,
|
||||||
|
Status: entities.ArticleStatusDraft,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 调用领域服务验证
|
||||||
|
if err := s.articleService.ValidateArticle(article); err != nil {
|
||||||
|
return fmt.Errorf("业务验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 保存文章
|
||||||
|
_, err := s.articleRepo.Create(ctx, *article)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("创建文章失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 处理标签关联
|
||||||
|
if len(cmd.TagIDs) > 0 {
|
||||||
|
for _, tagID := range cmd.TagIDs {
|
||||||
|
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||||
|
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateArticle 更新文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error {
|
||||||
|
// 1. 获取原文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否可以编辑
|
||||||
|
if !article.CanEdit() {
|
||||||
|
return fmt.Errorf("文章状态不允许编辑")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新字段
|
||||||
|
if cmd.Title != "" {
|
||||||
|
article.Title = cmd.Title
|
||||||
|
}
|
||||||
|
if cmd.Content != "" {
|
||||||
|
article.Content = cmd.Content
|
||||||
|
}
|
||||||
|
if cmd.Summary != "" {
|
||||||
|
article.Summary = cmd.Summary
|
||||||
|
}
|
||||||
|
if cmd.CoverImage != "" {
|
||||||
|
article.CoverImage = cmd.CoverImage
|
||||||
|
}
|
||||||
|
if cmd.CategoryID != "" {
|
||||||
|
article.CategoryID = cmd.CategoryID
|
||||||
|
}
|
||||||
|
article.IsFeatured = cmd.IsFeatured
|
||||||
|
|
||||||
|
// 4. 验证更新后的文章
|
||||||
|
if err := s.articleService.ValidateArticle(&article); err != nil {
|
||||||
|
return fmt.Errorf("业务验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("更新文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 处理标签关联
|
||||||
|
// 先清除现有标签
|
||||||
|
existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID)
|
||||||
|
for _, tag := range existingTags {
|
||||||
|
s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新标签
|
||||||
|
for _, tagID := range cmd.TagIDs {
|
||||||
|
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
|
||||||
|
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("更新文章成功", zap.String("id", article.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteArticle 删除文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error {
|
||||||
|
// 1. 检查文章是否存在
|
||||||
|
_, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 删除文章
|
||||||
|
if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("删除文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("删除文章成功", zap.String("id", cmd.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArticleByID 根据ID获取文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, query.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
response := responses.FromArticleEntity(&article)
|
||||||
|
|
||||||
|
s.logger.Info("获取文章成功", zap.String("id", article.ID))
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListArticles 获取文章列表
|
||||||
|
func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
|
||||||
|
// 1. 构建仓储查询
|
||||||
|
repoQuery := &repoQueries.ListArticleQuery{
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
Status: query.Status,
|
||||||
|
CategoryID: query.CategoryID,
|
||||||
|
TagID: query.TagID,
|
||||||
|
Title: query.Title,
|
||||||
|
Summary: query.Summary,
|
||||||
|
IsFeatured: query.IsFeatured,
|
||||||
|
OrderBy: query.OrderBy,
|
||||||
|
OrderDir: query.OrderDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用仓储
|
||||||
|
articles, total, err := s.articleRepo.ListArticles(ctx, repoQuery)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章列表失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取文章列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为响应对象
|
||||||
|
items := responses.FromArticleEntitiesToListItemList(articles)
|
||||||
|
|
||||||
|
response := &responses.ArticleListResponse{
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
Size: query.PageSize,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListArticlesForAdmin 获取文章列表(管理员端)
|
||||||
|
func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
|
||||||
|
// 1. 构建仓储查询
|
||||||
|
repoQuery := &repoQueries.ListArticleQuery{
|
||||||
|
Page: query.Page,
|
||||||
|
PageSize: query.PageSize,
|
||||||
|
Status: query.Status,
|
||||||
|
CategoryID: query.CategoryID,
|
||||||
|
TagID: query.TagID,
|
||||||
|
Title: query.Title,
|
||||||
|
Summary: query.Summary,
|
||||||
|
IsFeatured: query.IsFeatured,
|
||||||
|
OrderBy: query.OrderBy,
|
||||||
|
OrderDir: query.OrderDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用仓储
|
||||||
|
articles, total, err := s.articleRepo.ListArticlesForAdmin(ctx, repoQuery)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章列表失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取文章列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为响应对象
|
||||||
|
items := responses.FromArticleEntitiesToListItemList(articles)
|
||||||
|
|
||||||
|
response := &responses.ArticleListResponse{
|
||||||
|
Total: total,
|
||||||
|
Page: query.Page,
|
||||||
|
Size: query.PageSize,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishArticle 发布文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 发布文章
|
||||||
|
if err := article.Publish(); err != nil {
|
||||||
|
return fmt.Errorf("发布文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("发布文章成功", zap.String("id", article.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishArticleByID 通过ID发布文章 (用于定时任务)
|
||||||
|
func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, articleID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否已取消定时发布
|
||||||
|
if !article.IsScheduled() {
|
||||||
|
s.logger.Info("文章定时发布已取消,跳过执行",
|
||||||
|
zap.String("id", articleID),
|
||||||
|
zap.String("status", string(article.Status)))
|
||||||
|
return nil // 静默返回,不报错
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查定时发布时间是否匹配
|
||||||
|
if article.ScheduledAt == nil {
|
||||||
|
s.logger.Info("文章没有定时发布时间,跳过执行",
|
||||||
|
zap.String("id", articleID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 发布文章
|
||||||
|
if err := article.Publish(); err != nil {
|
||||||
|
return fmt.Errorf("发布文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("发布文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchedulePublishArticle 定时发布文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
|
||||||
|
// 1. 解析定时发布时间
|
||||||
|
scheduledTime, err := cmd.GetScheduledTime()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||||
|
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 取消旧任务
|
||||||
|
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 创建任务工厂
|
||||||
|
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||||
|
|
||||||
|
// 5. 创建并异步入队文章发布任务
|
||||||
|
if err := taskFactory.CreateAndEnqueueArticlePublishTask(
|
||||||
|
ctx,
|
||||||
|
cmd.ID,
|
||||||
|
scheduledTime,
|
||||||
|
"system", // 暂时使用系统用户ID
|
||||||
|
); err != nil {
|
||||||
|
s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 设置定时发布
|
||||||
|
if err := article.SchedulePublish(scheduledTime); err != nil {
|
||||||
|
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSchedulePublishArticle 取消定时发布文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否已设置定时发布
|
||||||
|
if !article.IsScheduled() {
|
||||||
|
return fmt.Errorf("文章未设置定时发布")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 取消定时任务
|
||||||
|
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
|
||||||
|
// 不返回错误,继续执行取消定时发布
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 取消定时发布
|
||||||
|
if err := article.CancelSchedulePublish(); err != nil {
|
||||||
|
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveArticle 归档文章
|
||||||
|
func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 归档文章
|
||||||
|
if err := article.Archive(); err != nil {
|
||||||
|
return fmt.Errorf("归档文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("归档文章失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("归档文章成功", zap.String("id", article.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeatured 设置推荐状态
|
||||||
|
func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error {
|
||||||
|
// 1. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置推荐状态
|
||||||
|
article.SetFeatured(cmd.IsFeatured)
|
||||||
|
|
||||||
|
// 3. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("设置推荐状态失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordView 记录阅读
|
||||||
|
func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error {
|
||||||
|
// 1. 增加阅读量
|
||||||
|
if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil {
|
||||||
|
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
|
||||||
|
return fmt.Errorf("记录阅读失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("记录阅读成功", zap.String("id", articleID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArticleStats 获取文章统计
|
||||||
|
func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) {
|
||||||
|
// 1. 获取各种统计
|
||||||
|
totalArticles, err := s.articleRepo.CountByStatus(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章总数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取已发布文章数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取草稿文章数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取归档文章数失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &responses.ArticleStatsResponse{
|
||||||
|
TotalArticles: totalArticles,
|
||||||
|
PublishedArticles: publishedArticles,
|
||||||
|
DraftArticles: draftArticles,
|
||||||
|
ArchivedArticles: archivedArticles,
|
||||||
|
TotalViews: 0, // TODO: 实现总阅读量统计
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("获取文章统计成功")
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCreateArticle 验证创建文章参数
|
||||||
|
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
|
||||||
|
if cmd.Title == "" {
|
||||||
|
return fmt.Errorf("文章标题不能为空")
|
||||||
|
}
|
||||||
|
if cmd.Content == "" {
|
||||||
|
return fmt.Errorf("文章内容不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 分类相关方法 ====================
|
||||||
|
|
||||||
|
// CreateCategory 创建分类
|
||||||
|
func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
|
||||||
|
// 1. 参数验证
|
||||||
|
if err := s.validateCreateCategory(cmd); err != nil {
|
||||||
|
return fmt.Errorf("参数验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建分类实体
|
||||||
|
category := &entities.Category{
|
||||||
|
Name: cmd.Name,
|
||||||
|
Description: cmd.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存分类
|
||||||
|
_, err := s.categoryRepo.Create(ctx, *category)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("创建分类失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建分类失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory 更新分类
|
||||||
|
func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
|
||||||
|
// 1. 获取原分类
|
||||||
|
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("分类不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新字段
|
||||||
|
category.Name = cmd.Name
|
||||||
|
category.Description = cmd.Description
|
||||||
|
|
||||||
|
// 3. 保存更新
|
||||||
|
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||||
|
s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("更新分类失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("更新分类成功", zap.String("id", category.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategory 删除分类
|
||||||
|
func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
|
||||||
|
// 1. 检查分类是否存在
|
||||||
|
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("分类不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否有文章使用此分类
|
||||||
|
count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("删除分类失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 删除分类
|
||||||
|
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("删除分类失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryByID 获取分类详情
|
||||||
|
func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
|
||||||
|
// 1. 获取分类
|
||||||
|
category, err := s.categoryRepo.GetByID(ctx, query.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("分类不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
response := &responses.CategoryInfoResponse{
|
||||||
|
ID: category.ID,
|
||||||
|
Name: category.Name,
|
||||||
|
Description: category.Description,
|
||||||
|
SortOrder: category.SortOrder,
|
||||||
|
CreatedAt: category.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCategories 获取分类列表
|
||||||
|
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
|
||||||
|
// 1. 获取分类列表
|
||||||
|
categories, err := s.categoryRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取分类列表失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取分类列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
items := make([]responses.CategoryInfoResponse, len(categories))
|
||||||
|
for i, category := range categories {
|
||||||
|
items[i] = responses.CategoryInfoResponse{
|
||||||
|
ID: category.ID,
|
||||||
|
Name: category.Name,
|
||||||
|
Description: category.Description,
|
||||||
|
SortOrder: category.SortOrder,
|
||||||
|
CreatedAt: category.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &responses.CategoryListResponse{
|
||||||
|
Items: items,
|
||||||
|
Total: len(items),
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 标签相关方法 ====================
|
||||||
|
|
||||||
|
// CreateTag 创建标签
|
||||||
|
func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error {
|
||||||
|
// 1. 参数验证
|
||||||
|
if err := s.validateCreateTag(cmd); err != nil {
|
||||||
|
return fmt.Errorf("参数验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建标签实体
|
||||||
|
tag := &entities.Tag{
|
||||||
|
Name: cmd.Name,
|
||||||
|
Color: cmd.Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保存标签
|
||||||
|
_, err := s.tagRepo.Create(ctx, *tag)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("创建标签失败", zap.Error(err))
|
||||||
|
return fmt.Errorf("创建标签失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTag 更新标签
|
||||||
|
func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error {
|
||||||
|
// 1. 获取原标签
|
||||||
|
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("标签不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新字段
|
||||||
|
tag.Name = cmd.Name
|
||||||
|
tag.Color = cmd.Color
|
||||||
|
|
||||||
|
// 3. 保存更新
|
||||||
|
if err := s.tagRepo.Update(ctx, tag); err != nil {
|
||||||
|
s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("更新标签失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("更新标签成功", zap.String("id", tag.ID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTag 删除标签
|
||||||
|
func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error {
|
||||||
|
// 1. 检查标签是否存在
|
||||||
|
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("标签不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 删除标签
|
||||||
|
if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil {
|
||||||
|
s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("删除标签失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagByID 获取标签详情
|
||||||
|
func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) {
|
||||||
|
// 1. 获取标签
|
||||||
|
tag, err := s.tagRepo.GetByID(ctx, query.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("标签不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
response := &responses.TagInfoResponse{
|
||||||
|
ID: tag.ID,
|
||||||
|
Name: tag.Name,
|
||||||
|
Color: tag.Color,
|
||||||
|
CreatedAt: tag.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTags 获取标签列表
|
||||||
|
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
|
||||||
|
// 1. 获取标签列表
|
||||||
|
tags, err := s.tagRepo.List(ctx, shared_interfaces.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取标签列表失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("获取标签列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为响应对象
|
||||||
|
items := make([]responses.TagInfoResponse, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
items[i] = responses.TagInfoResponse{
|
||||||
|
ID: tag.ID,
|
||||||
|
Name: tag.Name,
|
||||||
|
Color: tag.Color,
|
||||||
|
CreatedAt: tag.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &responses.TagListResponse{
|
||||||
|
Items: items,
|
||||||
|
Total: len(items),
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSchedulePublishArticle 修改定时发布时间
|
||||||
|
func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
|
||||||
|
// 1. 解析定时发布时间
|
||||||
|
scheduledTime, err := cmd.GetScheduledTime()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||||
|
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取文章
|
||||||
|
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("文章不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否已设置定时发布
|
||||||
|
if !article.IsScheduled() {
|
||||||
|
return fmt.Errorf("文章未设置定时发布,无法修改时间")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新数据库中的任务调度时间
|
||||||
|
if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
|
||||||
|
s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新定时发布
|
||||||
|
if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||||
|
return fmt.Errorf("更新定时发布失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 保存更新
|
||||||
|
if err := s.articleRepo.Update(ctx, article); err != nil {
|
||||||
|
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
|
||||||
|
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("修改定时发布时间成功",
|
||||||
|
zap.String("id", article.ID),
|
||||||
|
zap.Time("new_scheduled_time", scheduledTime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 验证方法 ====================
|
||||||
|
|
||||||
|
// validateCreateCategory 验证创建分类参数
|
||||||
|
func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
|
||||||
|
if cmd.Name == "" {
|
||||||
|
return fmt.Errorf("分类名称不能为空")
|
||||||
|
}
|
||||||
|
if len(cmd.Name) > 50 {
|
||||||
|
return fmt.Errorf("分类名称长度不能超过50个字符")
|
||||||
|
}
|
||||||
|
if len(cmd.Description) > 200 {
|
||||||
|
return fmt.Errorf("分类描述长度不能超过200个字符")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCreateTag 验证创建标签参数
|
||||||
|
func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error {
|
||||||
|
if cmd.Name == "" {
|
||||||
|
return fmt.Errorf("标签名称不能为空")
|
||||||
|
}
|
||||||
|
if len(cmd.Name) > 30 {
|
||||||
|
return fmt.Errorf("标签名称长度不能超过30个字符")
|
||||||
|
}
|
||||||
|
if cmd.Color == "" {
|
||||||
|
return fmt.Errorf("标签颜色不能为空")
|
||||||
|
}
|
||||||
|
// TODO: 添加十六进制颜色格式验证
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAnnouncementCommand 创建公告命令
|
||||||
|
type CreateAnnouncementCommand struct {
|
||||||
|
Title string `json:"title" binding:"required" comment:"公告标题"`
|
||||||
|
Content string `json:"content" binding:"required" comment:"公告内容"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAnnouncementCommand 更新公告命令
|
||||||
|
type UpdateAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
Title string `json:"title" comment:"公告标题"`
|
||||||
|
Content string `json:"content" comment:"公告内容"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAnnouncementCommand 删除公告命令
|
||||||
|
type DeleteAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishAnnouncementCommand 发布公告命令
|
||||||
|
type PublishAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithdrawAnnouncementCommand 撤回公告命令
|
||||||
|
type WithdrawAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveAnnouncementCommand 归档公告命令
|
||||||
|
type ArchiveAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchedulePublishAnnouncementCommand 定时发布公告命令
|
||||||
|
type SchedulePublishAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScheduledTime 获取解析后的定时发布时间
|
||||||
|
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||||
|
// 定义中国东八区时区
|
||||||
|
cst := time.FixedZone("CST", 8*3600)
|
||||||
|
|
||||||
|
// 支持多种时间格式
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||||
|
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||||
|
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||||
|
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||||
|
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||||
|
// 确保返回的时间是东八区时区
|
||||||
|
return t.In(cst), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
|
||||||
|
type UpdateSchedulePublishAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScheduledTime 获取解析后的定时发布时间
|
||||||
|
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||||
|
// 定义中国东八区时区
|
||||||
|
cst := time.FixedZone("CST", 8*3600)
|
||||||
|
|
||||||
|
// 支持多种时间格式
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||||
|
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||||
|
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||||
|
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||||
|
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||||
|
// 确保返回的时间是东八区时区
|
||||||
|
return t.In(cst), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
|
||||||
|
type CancelSchedulePublishAnnouncementCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
// CreateArticleCommand 创建文章命令
|
||||||
|
type CreateArticleCommand struct {
|
||||||
|
Title string `json:"title" binding:"required" comment:"文章标题"`
|
||||||
|
Content string `json:"content" binding:"required" comment:"文章内容"`
|
||||||
|
Summary string `json:"summary" comment:"文章摘要"`
|
||||||
|
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||||
|
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||||
|
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||||
|
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateArticleCommand 更新文章命令
|
||||||
|
type UpdateArticleCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
Title string `json:"title" comment:"文章标题"`
|
||||||
|
Content string `json:"content" comment:"文章内容"`
|
||||||
|
Summary string `json:"summary" comment:"文章摘要"`
|
||||||
|
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||||
|
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||||
|
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||||
|
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteArticleCommand 删除文章命令
|
||||||
|
type DeleteArticleCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishArticleCommand 发布文章命令
|
||||||
|
type PublishArticleCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveArticleCommand 归档文章命令
|
||||||
|
type ArchiveArticleCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeaturedCommand 设置推荐状态命令
|
||||||
|
type SetFeaturedCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
// CancelScheduleCommand 取消定时发布命令
|
||||||
|
type CancelScheduleCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
// CreateCategoryCommand 创建分类命令
|
||||||
|
type CreateCategoryCommand struct {
|
||||||
|
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||||
|
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategoryCommand 更新分类命令
|
||||||
|
type UpdateCategoryCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||||
|
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||||
|
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategoryCommand 删除分类命令
|
||||||
|
type DeleteCategoryCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchedulePublishCommand 定时发布文章命令
|
||||||
|
type SchedulePublishCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScheduledTime 获取解析后的定时发布时间
|
||||||
|
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
|
||||||
|
// 定义中国东八区时区
|
||||||
|
cst := time.FixedZone("CST", 8*3600)
|
||||||
|
|
||||||
|
// 支持多种时间格式
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||||
|
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||||
|
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||||
|
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||||
|
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||||
|
// 确保返回的时间是东八区时区
|
||||||
|
return t.In(cst), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||||
|
}
|
||||||
19
internal/application/article/dto/commands/tag_commands.go
Normal file
19
internal/application/article/dto/commands/tag_commands.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
// CreateTagCommand 创建标签命令
|
||||||
|
type CreateTagCommand struct {
|
||||||
|
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||||
|
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTagCommand 更新标签命令
|
||||||
|
type UpdateTagCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||||
|
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||||
|
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTagCommand 删除标签命令
|
||||||
|
type DeleteTagCommand struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import "tyapi-server/internal/domains/article/entities"
|
||||||
|
|
||||||
|
// ListAnnouncementQuery 公告列表查询
|
||||||
|
type ListAnnouncementQuery struct {
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
Status entities.AnnouncementStatus `form:"status" comment:"公告状态"`
|
||||||
|
Title string `form:"title" comment:"标题关键词"`
|
||||||
|
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||||
|
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncementQuery 获取公告详情查询
|
||||||
|
type GetAnnouncementQuery struct {
|
||||||
|
ID string `uri:"id" binding:"required" comment:"公告ID"`
|
||||||
|
}
|
||||||
54
internal/application/article/dto/queries/article_queries.go
Normal file
54
internal/application/article/dto/queries/article_queries.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import "tyapi-server/internal/domains/article/entities"
|
||||||
|
|
||||||
|
// ListArticleQuery 文章列表查询
|
||||||
|
type ListArticleQuery struct {
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||||
|
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||||
|
TagID string `form:"tag_id" comment:"标签ID"`
|
||||||
|
Title string `form:"title" comment:"标题关键词"`
|
||||||
|
Summary string `form:"summary" comment:"摘要关键词"`
|
||||||
|
IsFeatured *bool `form:"is_featured" comment:"是否推荐"`
|
||||||
|
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||||
|
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchArticleQuery 文章搜索查询
|
||||||
|
type SearchArticleQuery struct {
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
Keyword string `form:"keyword" comment:"搜索关键词"`
|
||||||
|
CategoryID string `form:"category_id" comment:"分类ID"`
|
||||||
|
AuthorID string `form:"author_id" comment:"作者ID"`
|
||||||
|
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
|
||||||
|
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||||
|
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArticleQuery 获取文章详情查询
|
||||||
|
type GetArticleQuery struct {
|
||||||
|
ID string `uri:"id" binding:"required" comment:"文章ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArticlesByAuthorQuery 获取作者文章查询
|
||||||
|
type GetArticlesByAuthorQuery struct {
|
||||||
|
AuthorID string `uri:"author_id" binding:"required" comment:"作者ID"`
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArticlesByCategoryQuery 获取分类文章查询
|
||||||
|
type GetArticlesByCategoryQuery struct {
|
||||||
|
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeaturedArticlesQuery 获取推荐文章查询
|
||||||
|
type GetFeaturedArticlesQuery struct {
|
||||||
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||||
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
// GetCategoryQuery 获取分类详情查询
|
||||||
|
type GetCategoryQuery struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||||
|
}
|
||||||
6
internal/application/article/dto/queries/tag_queries.go
Normal file
6
internal/application/article/dto/queries/tag_queries.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
// GetTagQuery 获取标签详情查询
|
||||||
|
type GetTagQuery struct {
|
||||||
|
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnouncementInfoResponse 公告详情响应
|
||||||
|
type AnnouncementInfoResponse struct {
|
||||||
|
ID string `json:"id" comment:"公告ID"`
|
||||||
|
Title string `json:"title" comment:"公告标题"`
|
||||||
|
Content string `json:"content" comment:"公告内容"`
|
||||||
|
Status string `json:"status" comment:"公告状态"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnouncementListItemResponse 公告列表项响应
|
||||||
|
type AnnouncementListItemResponse struct {
|
||||||
|
ID string `json:"id" comment:"公告ID"`
|
||||||
|
Title string `json:"title" comment:"公告标题"`
|
||||||
|
Content string `json:"content" comment:"公告内容"`
|
||||||
|
Status string `json:"status" comment:"公告状态"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnouncementListResponse 公告列表响应
|
||||||
|
type AnnouncementListResponse struct {
|
||||||
|
Total int64 `json:"total" comment:"总数"`
|
||||||
|
Page int `json:"page" comment:"页码"`
|
||||||
|
Size int `json:"size" comment:"每页数量"`
|
||||||
|
Items []AnnouncementListItemResponse `json:"items" comment:"公告列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnouncementStatsResponse 公告统计响应
|
||||||
|
type AnnouncementStatsResponse struct {
|
||||||
|
TotalAnnouncements int64 `json:"total_announcements" comment:"公告总数"`
|
||||||
|
PublishedAnnouncements int64 `json:"published_announcements" comment:"已发布公告数"`
|
||||||
|
DraftAnnouncements int64 `json:"draft_announcements" comment:"草稿公告数"`
|
||||||
|
ArchivedAnnouncements int64 `json:"archived_announcements" comment:"归档公告数"`
|
||||||
|
ScheduledAnnouncements int64 `json:"scheduled_announcements" comment:"定时发布公告数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromAnnouncementEntity 从公告实体转换为响应对象
|
||||||
|
func FromAnnouncementEntity(announcement *entities.Announcement) *AnnouncementInfoResponse {
|
||||||
|
if announcement == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AnnouncementInfoResponse{
|
||||||
|
ID: announcement.ID,
|
||||||
|
Title: announcement.Title,
|
||||||
|
Content: announcement.Content,
|
||||||
|
Status: string(announcement.Status),
|
||||||
|
ScheduledAt: announcement.ScheduledAt,
|
||||||
|
CreatedAt: announcement.CreatedAt,
|
||||||
|
UpdatedAt: announcement.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromAnnouncementEntityList 从公告实体列表转换为列表项响应
|
||||||
|
func FromAnnouncementEntityList(announcements []*entities.Announcement) []AnnouncementListItemResponse {
|
||||||
|
items := make([]AnnouncementListItemResponse, 0, len(announcements))
|
||||||
|
for _, announcement := range announcements {
|
||||||
|
items = append(items, AnnouncementListItemResponse{
|
||||||
|
ID: announcement.ID,
|
||||||
|
Title: announcement.Title,
|
||||||
|
Content: announcement.Content,
|
||||||
|
Status: string(announcement.Status),
|
||||||
|
ScheduledAt: announcement.ScheduledAt,
|
||||||
|
CreatedAt: announcement.CreatedAt,
|
||||||
|
UpdatedAt: announcement.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
219
internal/application/article/dto/responses/article_responses.go
Normal file
219
internal/application/article/dto/responses/article_responses.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArticleInfoResponse 文章详情响应
|
||||||
|
type ArticleInfoResponse struct {
|
||||||
|
ID string `json:"id" comment:"文章ID"`
|
||||||
|
Title string `json:"title" comment:"文章标题"`
|
||||||
|
Content string `json:"content" comment:"文章内容"`
|
||||||
|
Summary string `json:"summary" comment:"文章摘要"`
|
||||||
|
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||||
|
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||||
|
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||||
|
Status string `json:"status" comment:"文章状态"`
|
||||||
|
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||||
|
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||||
|
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleListItemResponse 文章列表项响应(不包含content)
|
||||||
|
type ArticleListItemResponse struct {
|
||||||
|
ID string `json:"id" comment:"文章ID"`
|
||||||
|
Title string `json:"title" comment:"文章标题"`
|
||||||
|
Summary string `json:"summary" comment:"文章摘要"`
|
||||||
|
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||||
|
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||||
|
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
|
||||||
|
Status string `json:"status" comment:"文章状态"`
|
||||||
|
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||||
|
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||||
|
ViewCount int `json:"view_count" comment:"阅读量"`
|
||||||
|
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArticleListResponse 文章列表响应
|
||||||
|
type ArticleListResponse struct {
|
||||||
|
Total int64 `json:"total" comment:"总数"`
|
||||||
|
Page int `json:"page" comment:"页码"`
|
||||||
|
Size int `json:"size" comment:"每页数量"`
|
||||||
|
Items []ArticleListItemResponse `json:"items" comment:"文章列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryInfoResponse 分类信息响应
|
||||||
|
type CategoryInfoResponse struct {
|
||||||
|
ID string `json:"id" comment:"分类ID"`
|
||||||
|
Name string `json:"name" comment:"分类名称"`
|
||||||
|
Description string `json:"description" comment:"分类描述"`
|
||||||
|
SortOrder int `json:"sort_order" comment:"排序"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagInfoResponse 标签信息响应
|
||||||
|
type TagInfoResponse struct {
|
||||||
|
ID string `json:"id" comment:"标签ID"`
|
||||||
|
Name string `json:"name" comment:"标签名称"`
|
||||||
|
Color string `json:"color" comment:"标签颜色"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryListResponse 分类列表响应
|
||||||
|
type CategoryListResponse struct {
|
||||||
|
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
|
||||||
|
Total int `json:"total" comment:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagListResponse 标签列表响应
|
||||||
|
type TagListResponse struct {
|
||||||
|
Items []TagInfoResponse `json:"items" comment:"标签列表"`
|
||||||
|
Total int `json:"total" comment:"总数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ArticleStatsResponse 文章统计响应
|
||||||
|
type ArticleStatsResponse struct {
|
||||||
|
TotalArticles int64 `json:"total_articles" comment:"文章总数"`
|
||||||
|
PublishedArticles int64 `json:"published_articles" comment:"已发布文章数"`
|
||||||
|
DraftArticles int64 `json:"draft_articles" comment:"草稿文章数"`
|
||||||
|
ArchivedArticles int64 `json:"archived_articles" comment:"归档文章数"`
|
||||||
|
TotalViews int64 `json:"total_views" comment:"总阅读量"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromArticleEntity 从文章实体转换为响应对象
|
||||||
|
func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
|
||||||
|
if article == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &ArticleInfoResponse{
|
||||||
|
ID: article.ID,
|
||||||
|
Title: article.Title,
|
||||||
|
Content: article.Content,
|
||||||
|
Summary: article.Summary,
|
||||||
|
CoverImage: article.CoverImage,
|
||||||
|
CategoryID: article.CategoryID,
|
||||||
|
Status: string(article.Status),
|
||||||
|
IsFeatured: article.IsFeatured,
|
||||||
|
PublishedAt: article.PublishedAt,
|
||||||
|
ScheduledAt: article.ScheduledAt,
|
||||||
|
ViewCount: article.ViewCount,
|
||||||
|
CreatedAt: article.CreatedAt,
|
||||||
|
UpdatedAt: article.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换分类信息
|
||||||
|
if article.Category != nil {
|
||||||
|
response.Category = &CategoryInfoResponse{
|
||||||
|
ID: article.Category.ID,
|
||||||
|
Name: article.Category.Name,
|
||||||
|
Description: article.Category.Description,
|
||||||
|
SortOrder: article.Category.SortOrder,
|
||||||
|
CreatedAt: article.Category.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换标签信息
|
||||||
|
if len(article.Tags) > 0 {
|
||||||
|
response.Tags = make([]TagInfoResponse, len(article.Tags))
|
||||||
|
for i, tag := range article.Tags {
|
||||||
|
response.Tags[i] = TagInfoResponse{
|
||||||
|
ID: tag.ID,
|
||||||
|
Name: tag.Name,
|
||||||
|
Color: tag.Color,
|
||||||
|
CreatedAt: tag.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromArticleEntityToListItem 从文章实体转换为列表项响应对象(不包含content)
|
||||||
|
func FromArticleEntityToListItem(article *entities.Article) *ArticleListItemResponse {
|
||||||
|
if article == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &ArticleListItemResponse{
|
||||||
|
ID: article.ID,
|
||||||
|
Title: article.Title,
|
||||||
|
Summary: article.Summary,
|
||||||
|
CoverImage: article.CoverImage,
|
||||||
|
CategoryID: article.CategoryID,
|
||||||
|
Status: string(article.Status),
|
||||||
|
IsFeatured: article.IsFeatured,
|
||||||
|
PublishedAt: article.PublishedAt,
|
||||||
|
ScheduledAt: article.ScheduledAt,
|
||||||
|
ViewCount: article.ViewCount,
|
||||||
|
CreatedAt: article.CreatedAt,
|
||||||
|
UpdatedAt: article.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换分类信息
|
||||||
|
if article.Category != nil {
|
||||||
|
response.Category = &CategoryInfoResponse{
|
||||||
|
ID: article.Category.ID,
|
||||||
|
Name: article.Category.Name,
|
||||||
|
Description: article.Category.Description,
|
||||||
|
SortOrder: article.Category.SortOrder,
|
||||||
|
CreatedAt: article.Category.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换标签信息
|
||||||
|
if len(article.Tags) > 0 {
|
||||||
|
response.Tags = make([]TagInfoResponse, len(article.Tags))
|
||||||
|
for i, tag := range article.Tags {
|
||||||
|
response.Tags[i] = TagInfoResponse{
|
||||||
|
ID: tag.ID,
|
||||||
|
Name: tag.Name,
|
||||||
|
Color: tag.Color,
|
||||||
|
CreatedAt: tag.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromArticleEntities 从文章实体列表转换为响应对象列表
|
||||||
|
func FromArticleEntities(articles []*entities.Article) []ArticleInfoResponse {
|
||||||
|
if len(articles) == 0 {
|
||||||
|
return []ArticleInfoResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]ArticleInfoResponse, len(articles))
|
||||||
|
for i, article := range articles {
|
||||||
|
if response := FromArticleEntity(article); response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromArticleEntitiesToListItemList 从文章实体列表转换为列表项响应对象列表(不包含content)
|
||||||
|
func FromArticleEntitiesToListItemList(articles []*entities.Article) []ArticleListItemResponse {
|
||||||
|
if len(articles) == 0 {
|
||||||
|
return []ArticleListItemResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]ArticleListItemResponse, len(articles))
|
||||||
|
for i, article := range articles {
|
||||||
|
if response := FromArticleEntityToListItem(article); response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses
|
||||||
|
}
|
||||||
126
internal/application/article/task_management_service.go
Normal file
126
internal/application/article/task_management_service.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
"tyapi-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
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ type CertificationApplicationService interface {
|
|||||||
// 申请合同签署
|
// 申请合同签署
|
||||||
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
|
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
|
||||||
|
|
||||||
|
// OCR营业执照识别
|
||||||
|
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
|
||||||
|
|
||||||
// ================ 查询用例 ================
|
// ================ 查询用例 ================
|
||||||
|
|
||||||
// 获取认证详情
|
// 获取认证详情
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"tyapi-server/internal/infrastructure/external/storage"
|
"tyapi-server/internal/infrastructure/external/storage"
|
||||||
"tyapi-server/internal/shared/database"
|
"tyapi-server/internal/shared/database"
|
||||||
"tyapi-server/internal/shared/esign"
|
"tyapi-server/internal/shared/esign"
|
||||||
|
sharedOCR "tyapi-server/internal/shared/ocr"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -40,6 +41,7 @@ type CertificationApplicationServiceImpl struct {
|
|||||||
walletAggregateService finance_service.WalletAggregateService
|
walletAggregateService finance_service.WalletAggregateService
|
||||||
apiUserAggregateService api_service.ApiUserAggregateService
|
apiUserAggregateService api_service.ApiUserAggregateService
|
||||||
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService
|
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService
|
||||||
|
ocrService sharedOCR.OCRService
|
||||||
// 仓储依赖
|
// 仓储依赖
|
||||||
queryRepository repositories.CertificationQueryRepository
|
queryRepository repositories.CertificationQueryRepository
|
||||||
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
||||||
@@ -62,6 +64,7 @@ func NewCertificationApplicationService(
|
|||||||
walletAggregateService finance_service.WalletAggregateService,
|
walletAggregateService finance_service.WalletAggregateService,
|
||||||
apiUserAggregateService api_service.ApiUserAggregateService,
|
apiUserAggregateService api_service.ApiUserAggregateService,
|
||||||
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService,
|
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService,
|
||||||
|
ocrService sharedOCR.OCRService,
|
||||||
txManager *database.TransactionManager,
|
txManager *database.TransactionManager,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) CertificationApplicationService {
|
) CertificationApplicationService {
|
||||||
@@ -78,6 +81,7 @@ func NewCertificationApplicationService(
|
|||||||
walletAggregateService: walletAggregateService,
|
walletAggregateService: walletAggregateService,
|
||||||
apiUserAggregateService: apiUserAggregateService,
|
apiUserAggregateService: apiUserAggregateService,
|
||||||
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
||||||
|
ocrService: ocrService,
|
||||||
txManager: txManager,
|
txManager: txManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
@@ -987,3 +991,33 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
|
|||||||
|
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecognizeBusinessLicense OCR识别营业执照
|
||||||
|
func (s *CertificationApplicationServiceImpl) RecognizeBusinessLicense(
|
||||||
|
ctx context.Context,
|
||||||
|
imageBytes []byte,
|
||||||
|
) (*responses.BusinessLicenseResult, error) {
|
||||||
|
s.logger.Info("开始OCR识别营业执照", zap.Int("image_size", len(imageBytes)))
|
||||||
|
|
||||||
|
// 调用OCR服务识别营业执照
|
||||||
|
result, err := s.ocrService.RecognizeBusinessLicense(ctx, imageBytes)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("OCR识别营业执照失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("营业执照识别失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证识别结果
|
||||||
|
if err := s.ocrService.ValidateBusinessLicense(result); err != nil {
|
||||||
|
s.logger.Error("营业执照识别结果验证失败", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("营业执照识别结果不完整: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("营业执照OCR识别成功",
|
||||||
|
zap.String("company_name", result.CompanyName),
|
||||||
|
zap.String("unified_social_code", result.UnifiedSocialCode),
|
||||||
|
zap.String("legal_person_name", result.LegalPersonName),
|
||||||
|
zap.Float64("confidence", result.Confidence),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
|
|||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TransferRechargeCommand 对公转账充值命令
|
// TransferRechargeCommand 对公转账充值命令
|
||||||
type TransferRechargeCommand struct {
|
type TransferRechargeCommand struct {
|
||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
@@ -16,16 +15,24 @@ type TransferRechargeCommand struct {
|
|||||||
|
|
||||||
// GiftRechargeCommand 赠送充值命令
|
// GiftRechargeCommand 赠送充值命令
|
||||||
type GiftRechargeCommand struct {
|
type GiftRechargeCommand struct {
|
||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
Amount string `json:"amount" binding:"required"`
|
Amount string `json:"amount" binding:"required"`
|
||||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
||||||
type CreateAlipayRechargeCommand struct {
|
type CreateAlipayRechargeCommand struct {
|
||||||
UserID string `json:"-"` // 用户ID(从token获取)
|
UserID string `json:"-"` // 用户ID(从token获取)
|
||||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||||
Subject string `json:"-"` // 订单标题
|
Subject string `json:"-"` // 订单标题
|
||||||
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import (
|
|||||||
|
|
||||||
// WalletResponse 钱包响应
|
// WalletResponse 钱包响应
|
||||||
type WalletResponse struct {
|
type WalletResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Balance decimal.Decimal `json:"balance"`
|
Balance decimal.Decimal `json:"balance"`
|
||||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionResponse 交易响应
|
// TransactionResponse 交易响应
|
||||||
@@ -49,34 +49,36 @@ type WalletStatsResponse struct {
|
|||||||
|
|
||||||
// RechargeRecordResponse 充值记录响应
|
// RechargeRecordResponse 充值记录响应
|
||||||
type RechargeRecordResponse struct {
|
type RechargeRecordResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Amount decimal.Decimal `json:"amount"`
|
Amount decimal.Decimal `json:"amount"`
|
||||||
RechargeType string `json:"recharge_type"`
|
RechargeType string `json:"recharge_type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
WechatOrderID string `json:"wechat_order_id,omitempty"`
|
||||||
Notes string `json:"notes,omitempty"`
|
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||||
OperatorID string `json:"operator_id,omitempty"`
|
Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等
|
||||||
CompanyName string `json:"company_name,omitempty"`
|
Notes string `json:"notes,omitempty"`
|
||||||
User *UserSimpleResponse `json:"user,omitempty"`
|
OperatorID string `json:"operator_id,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
User *UserSimpleResponse `json:"user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletTransactionResponse 钱包交易记录响应
|
// WalletTransactionResponse 钱包交易记录响应
|
||||||
type WalletTransactionResponse struct {
|
type WalletTransactionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ApiCallID string `json:"api_call_id"`
|
ApiCallID string `json:"api_call_id"`
|
||||||
TransactionID string `json:"transaction_id"`
|
TransactionID string `json:"transaction_id"`
|
||||||
ProductID string `json:"product_id"`
|
ProductID string `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
Amount decimal.Decimal `json:"amount"`
|
Amount decimal.Decimal `json:"amount"`
|
||||||
CompanyName string `json:"company_name,omitempty"`
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
User *UserSimpleResponse `json:"user,omitempty"`
|
User *UserSimpleResponse `json:"user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletTransactionListResponse 钱包交易记录列表响应
|
// WalletTransactionListResponse 钱包交易记录列表响应
|
||||||
@@ -97,17 +99,17 @@ type RechargeRecordListResponse struct {
|
|||||||
|
|
||||||
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
||||||
type AlipayRechargeOrderResponse struct {
|
type AlipayRechargeOrderResponse struct {
|
||||||
PayURL string `json:"pay_url"` // 支付链接
|
PayURL string `json:"pay_url"` // 支付链接
|
||||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||||
Platform string `json:"platform"` // 支付平台
|
Platform string `json:"platform"` // 支付平台
|
||||||
Subject string `json:"subject"` // 订单标题
|
Subject string `json:"subject"` // 订单标题
|
||||||
}
|
}
|
||||||
|
|
||||||
// RechargeConfigResponse 充值配置响应
|
// RechargeConfigResponse 充值配置响应
|
||||||
type RechargeConfigResponse struct {
|
type RechargeConfigResponse struct {
|
||||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WechatOrderStatusResponse 微信订单状态响应
|
||||||
|
type WechatOrderStatusResponse struct {
|
||||||
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
|
TransactionID *string `json:"transaction_id"` // 微信支付交易号
|
||||||
|
Status string `json:"status"` // 订单状态
|
||||||
|
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||||
|
Subject string `json:"subject"` // 订单标题
|
||||||
|
Platform string `json:"platform"` // 支付平台
|
||||||
|
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||||
|
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||||
|
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||||
|
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||||
|
ErrorCode *string `json:"error_code"` // 错误码
|
||||||
|
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||||
|
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||||
|
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import "github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
// WechatRechargeOrderResponse 微信充值下单响应
|
||||||
|
type WechatRechargeOrderResponse struct {
|
||||||
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
|
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||||
|
Platform string `json:"platform"` // 支付平台
|
||||||
|
Subject string `json:"subject"` // 订单标题
|
||||||
|
PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数)
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package finance
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"tyapi-server/internal/application/finance/dto/commands"
|
"tyapi-server/internal/application/finance/dto/commands"
|
||||||
"tyapi-server/internal/application/finance/dto/queries"
|
"tyapi-server/internal/application/finance/dto/queries"
|
||||||
"tyapi-server/internal/application/finance/dto/responses"
|
"tyapi-server/internal/application/finance/dto/responses"
|
||||||
@@ -12,27 +11,36 @@ import (
|
|||||||
|
|
||||||
// FinanceApplicationService 财务应用服务接口
|
// FinanceApplicationService 财务应用服务接口
|
||||||
type FinanceApplicationService interface {
|
type FinanceApplicationService interface {
|
||||||
|
// 钱包管理
|
||||||
|
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
|
||||||
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
|
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
|
||||||
|
|
||||||
|
// 充值管理
|
||||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, 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
|
HandleAlipayCallback(ctx context.Context, r *http.Request) error
|
||||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||||
|
|
||||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
// 微信支付回调处理
|
||||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, 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)
|
||||||
|
|
||||||
// 获取用户钱包交易记录
|
// 充值记录
|
||||||
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)
|
|
||||||
|
|
||||||
// 获取用户充值记录
|
|
||||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, 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)
|
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||||
|
|
||||||
// 获取充值配置
|
// 获取充值配置
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -393,7 +393,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
|||||||
// 5. 构建响应DTO
|
// 5. 构建响应DTO
|
||||||
return &dto.AvailableAmountResponse{
|
return &dto.AvailableAmountResponse{
|
||||||
AvailableAmount: availableAmount,
|
AvailableAmount: availableAmount,
|
||||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
|
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
|
||||||
TotalGifted: totalGifted,
|
TotalGifted: totalGifted,
|
||||||
TotalInvoiced: totalInvoiced,
|
TotalInvoiced: totalInvoiced,
|
||||||
PendingApplications: pendingAmount,
|
PendingApplications: pendingAmount,
|
||||||
@@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
|||||||
|
|
||||||
// calculateAvailableAmount 计算可开票金额(私有方法)
|
// calculateAvailableAmount 计算可开票金额(私有方法)
|
||||||
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||||
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decimal.Zero, err
|
return decimal.Zero, err
|
||||||
@@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
|
|||||||
fmt.Println("totalInvoiced", totalInvoiced)
|
fmt.Println("totalInvoiced", totalInvoiced)
|
||||||
fmt.Println("pendingAmount", pendingAmount)
|
fmt.Println("pendingAmount", pendingAmount)
|
||||||
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
||||||
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
|
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||||
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
||||||
fmt.Println("availableAmount", availableAmount)
|
fmt.Println("availableAmount", availableAmount)
|
||||||
// 确保可开票金额不为负数
|
// 确保可开票金额不为负数
|
||||||
@@ -452,16 +452,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us
|
|||||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
|
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
|
||||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
|
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
|
||||||
var totalGifted decimal.Decimal // 总赠送金额
|
var totalGifted decimal.Decimal // 总赠送金额
|
||||||
for _, record := range rechargeRecords {
|
for _, record := range rechargeRecords {
|
||||||
if record.IsSuccess() {
|
if record.IsSuccess() {
|
||||||
if record.RechargeType == entities.RechargeTypeGift {
|
if record.RechargeType == entities.RechargeTypeGift {
|
||||||
// 赠送金额不计入可开票金额
|
// 赠送金额不计入可开票金额
|
||||||
totalGifted = totalGifted.Add(record.Amount)
|
totalGifted = totalGifted.Add(record.Amount)
|
||||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
|
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
|
||||||
// 只有支付宝充值和对公转账计入可开票金额
|
// 支付宝充值、微信充值和对公转账计入可开票金额
|
||||||
realRecharged = realRecharged.Add(record.Amount)
|
realRecharged = realRecharged.Add(record.Amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
internal/application/product/category_application_service.go
Normal file
19
internal/application/product/category_application_service.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package product
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
|
"tyapi-server/internal/application/product/dto/queries"
|
||||||
|
"tyapi-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)
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package product
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
@@ -28,6 +30,9 @@ type DocumentationApplicationServiceInterface interface {
|
|||||||
|
|
||||||
// GetDocumentationsByProductIDs 批量获取文档
|
// GetDocumentationsByProductIDs 批量获取文档
|
||||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||||
|
|
||||||
|
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||||
|
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentationApplicationService 文档应用服务
|
// DocumentationApplicationService 文档应用服务
|
||||||
@@ -53,6 +58,7 @@ func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Contex
|
|||||||
ResponseFields: cmd.ResponseFields,
|
ResponseFields: cmd.ResponseFields,
|
||||||
ResponseExample: cmd.ResponseExample,
|
ResponseExample: cmd.ResponseExample,
|
||||||
ErrorCodes: cmd.ErrorCodes,
|
ErrorCodes: cmd.ErrorCodes,
|
||||||
|
PDFFilePath: cmd.PDFFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用领域服务创建文档
|
// 调用领域服务创建文档
|
||||||
@@ -88,6 +94,20 @@ func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Contex
|
|||||||
return nil, err
|
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)
|
resp := responses.NewDocumentationResponse(doc)
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
@@ -136,3 +156,93 @@ func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx cont
|
|||||||
|
|
||||||
return docResponses, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type CreateDocumentationCommand struct {
|
|||||||
ResponseFields string `json:"response_fields"`
|
ResponseFields string `json:"response_fields"`
|
||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDocumentationCommand 更新文档命令
|
// UpdateDocumentationCommand 更新文档命令
|
||||||
@@ -21,4 +22,5 @@ type UpdateDocumentationCommand struct {
|
|||||||
ResponseFields string `json:"response_fields"`
|
ResponseFields string `json:"response_fields"`
|
||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type CreateProductCommand struct {
|
|||||||
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
||||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
||||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
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:"是否启用"`
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||||
@@ -27,6 +29,8 @@ type UpdateProductCommand struct {
|
|||||||
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
|
||||||
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
CategoryID string `json:"category_id" binding:"required,uuid" comment:"产品分类ID"`
|
||||||
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
|
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:"是否启用"`
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||||
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
IsVisible bool `json:"is_visible" comment:"是否展示"`
|
||||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||||
|
|||||||
@@ -11,3 +11,12 @@ type UpdateSubscriptionPriceCommand struct {
|
|||||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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:所有)"`
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type DocumentationResponse struct {
|
|||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationR
|
|||||||
ResponseExample: doc.ResponseExample,
|
ResponseExample: doc.ResponseExample,
|
||||||
ErrorCodes: doc.ErrorCodes,
|
ErrorCodes: doc.ErrorCodes,
|
||||||
Version: doc.Version,
|
Version: doc.Version,
|
||||||
|
PDFFilePath: doc.PDFFilePath,
|
||||||
CreatedAt: doc.CreatedAt,
|
CreatedAt: doc.CreatedAt,
|
||||||
UpdatedAt: doc.UpdatedAt,
|
UpdatedAt: doc.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type PackageItemResponse struct {
|
|||||||
ProductName string `json:"product_name" comment:"子产品名称"`
|
ProductName string `json:"product_name" comment:"子产品名称"`
|
||||||
SortOrder int `json:"sort_order" comment:"排序"`
|
SortOrder int `json:"sort_order" comment:"排序"`
|
||||||
Price float64 `json:"price" comment:"子产品价格"`
|
Price float64 `json:"price" comment:"子产品价格"`
|
||||||
|
CostPrice float64 `json:"cost_price" comment:"子产品成本价"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductInfoResponse 产品详情响应
|
// ProductInfoResponse 产品详情响应
|
||||||
@@ -70,6 +71,12 @@ type ProductSimpleResponse struct {
|
|||||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||||
|
type ProductSimpleAdminResponse struct {
|
||||||
|
ProductSimpleResponse
|
||||||
|
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||||
|
}
|
||||||
|
|
||||||
// ProductStatsResponse 产品统计响应
|
// ProductStatsResponse 产品统计响应
|
||||||
type ProductStatsResponse struct {
|
type ProductStatsResponse struct {
|
||||||
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
||||||
@@ -88,6 +95,8 @@ type ProductAdminInfoResponse struct {
|
|||||||
Content string `json:"content" comment:"产品内容"`
|
Content string `json:"content" comment:"产品内容"`
|
||||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||||
Price float64 `json:"price" comment:"产品价格"`
|
Price float64 `json:"price" comment:"产品价格"`
|
||||||
|
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||||
|
Remark string `json:"remark" comment:"备注"`
|
||||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||||
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
||||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type SubscriptionInfoResponse struct {
|
|||||||
// 关联信息
|
// 关联信息
|
||||||
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
||||||
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
||||||
|
// 管理员端使用,包含成本价的产品信息
|
||||||
|
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
// ProductApplicationService 产品应用服务接口
|
// ProductApplicationService 产品应用服务接口
|
||||||
type ProductApplicationService interface {
|
type ProductApplicationService interface {
|
||||||
// 产品管理
|
// 产品管理
|
||||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error
|
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error)
|
||||||
|
|
||||||
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
||||||
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
||||||
|
|
||||||
@@ -38,8 +39,8 @@ type ProductApplicationService interface {
|
|||||||
ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error
|
ReorderPackageItems(ctx context.Context, packageID string, cmd *commands.ReorderPackageItemsCommand) error
|
||||||
UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error
|
UpdatePackageItems(ctx context.Context, packageID string, cmd *commands.UpdatePackageItemsCommand) error
|
||||||
|
|
||||||
// 可选子产品查询
|
// 可选子产品查询(管理员端,返回包含成本价的数据)
|
||||||
GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductListResponse, error)
|
GetAvailableProducts(ctx context.Context, query *queries.GetAvailableProductsQuery) (*responses.ProductAdminListResponse, error)
|
||||||
|
|
||||||
// API配置管理
|
// API配置管理
|
||||||
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
GetProductApiConfig(ctx context.Context, productID string) (*responses.ProductApiConfigResponse, error)
|
||||||
@@ -47,37 +48,3 @@ type ProductApplicationService interface {
|
|||||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// 业务查询
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package product
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -10,6 +12,8 @@ import (
|
|||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
|
"tyapi-server/internal/domains/api/dto"
|
||||||
|
api_services "tyapi-server/internal/domains/api/services"
|
||||||
"tyapi-server/internal/domains/product/entities"
|
"tyapi-server/internal/domains/product/entities"
|
||||||
product_service "tyapi-server/internal/domains/product/services"
|
product_service "tyapi-server/internal/domains/product/services"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
@@ -22,6 +26,7 @@ type ProductApplicationServiceImpl struct {
|
|||||||
productSubscriptionService *product_service.ProductSubscriptionService
|
productSubscriptionService *product_service.ProductSubscriptionService
|
||||||
productApiConfigAppService ProductApiConfigApplicationService
|
productApiConfigAppService ProductApiConfigApplicationService
|
||||||
documentationAppService DocumentationApplicationServiceInterface
|
documentationAppService DocumentationApplicationServiceInterface
|
||||||
|
formConfigService api_services.FormConfigService
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ func NewProductApplicationService(
|
|||||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||||
productApiConfigAppService ProductApiConfigApplicationService,
|
productApiConfigAppService ProductApiConfigApplicationService,
|
||||||
documentationAppService DocumentationApplicationServiceInterface,
|
documentationAppService DocumentationApplicationServiceInterface,
|
||||||
|
formConfigService api_services.FormConfigService,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) ProductApplicationService {
|
) ProductApplicationService {
|
||||||
return &ProductApplicationServiceImpl{
|
return &ProductApplicationServiceImpl{
|
||||||
@@ -38,13 +44,14 @@ func NewProductApplicationService(
|
|||||||
productSubscriptionService: productSubscriptionService,
|
productSubscriptionService: productSubscriptionService,
|
||||||
productApiConfigAppService: productApiConfigAppService,
|
productApiConfigAppService: productApiConfigAppService,
|
||||||
documentationAppService: documentationAppService,
|
documentationAppService: documentationAppService,
|
||||||
|
formConfigService: formConfigService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateProduct 创建产品
|
// CreateProduct 创建产品
|
||||||
// 业务流程:1. 构建产品实体 2. 创建产品
|
// 业务流程<EFBFBD>?. 构建产品实体 2. 创建产品
|
||||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error {
|
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
|
||||||
// 1. 构建产品实体
|
// 1. 构建产品实体
|
||||||
product := &entities.Product{
|
product := &entities.Product{
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
@@ -53,6 +60,8 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *
|
|||||||
Content: cmd.Content,
|
Content: cmd.Content,
|
||||||
CategoryID: cmd.CategoryID,
|
CategoryID: cmd.CategoryID,
|
||||||
Price: decimal.NewFromFloat(cmd.Price),
|
Price: decimal.NewFromFloat(cmd.Price),
|
||||||
|
CostPrice: decimal.NewFromFloat(cmd.CostPrice),
|
||||||
|
Remark: cmd.Remark,
|
||||||
IsEnabled: cmd.IsEnabled,
|
IsEnabled: cmd.IsEnabled,
|
||||||
IsVisible: cmd.IsVisible,
|
IsVisible: cmd.IsVisible,
|
||||||
IsPackage: cmd.IsPackage,
|
IsPackage: cmd.IsPackage,
|
||||||
@@ -62,12 +71,17 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 创建产品
|
// 2. 创建产品
|
||||||
_, err := s.productManagementService.CreateProduct(ctx, product)
|
createdProduct, err := s.productManagementService.CreateProduct(ctx, product)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为响应对象
|
||||||
|
return s.convertToProductAdminInfoResponse(createdProduct), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProduct 更新产品
|
// UpdateProduct 更新产品
|
||||||
// 业务流程:1. 获取现有产品 2. 更新产品信息 3. 保存产品
|
// 业务流程<EFBFBD>?. 获取现有产品 2. 更新产品信息 3. 保存产品
|
||||||
func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error {
|
func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error {
|
||||||
// 1. 获取现有产品
|
// 1. 获取现有产品
|
||||||
existingProduct, err := s.productManagementService.GetProductByID(ctx, cmd.ID)
|
existingProduct, err := s.productManagementService.GetProductByID(ctx, cmd.ID)
|
||||||
@@ -82,6 +96,8 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
|
|||||||
existingProduct.Content = cmd.Content
|
existingProduct.Content = cmd.Content
|
||||||
existingProduct.CategoryID = cmd.CategoryID
|
existingProduct.CategoryID = cmd.CategoryID
|
||||||
existingProduct.Price = decimal.NewFromFloat(cmd.Price)
|
existingProduct.Price = decimal.NewFromFloat(cmd.Price)
|
||||||
|
existingProduct.CostPrice = decimal.NewFromFloat(cmd.CostPrice)
|
||||||
|
existingProduct.Remark = cmd.Remark
|
||||||
existingProduct.IsEnabled = cmd.IsEnabled
|
existingProduct.IsEnabled = cmd.IsEnabled
|
||||||
existingProduct.IsVisible = cmd.IsVisible
|
existingProduct.IsVisible = cmd.IsVisible
|
||||||
existingProduct.IsPackage = cmd.IsPackage
|
existingProduct.IsPackage = cmd.IsPackage
|
||||||
@@ -94,13 +110,13 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProduct 删除产品
|
// DeleteProduct 删除产品
|
||||||
// 业务流程:1. 删除产品
|
// 业务流程<EFBFBD>?. 删除产品
|
||||||
func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error {
|
func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error {
|
||||||
return s.productManagementService.DeleteProduct(ctx, cmd.ID)
|
return s.productManagementService.DeleteProduct(ctx, cmd.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListProducts 获取产品列表
|
// ListProducts 获取产品列表
|
||||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
// 业务流程<EFBFBD>?. 获取产品列表 2. 构建响应数据
|
||||||
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
||||||
// 检查是否有用户ID,如果有则使用带订阅状态的方法
|
// 检查是否有用户ID,如果有则使用带订阅状态的方法
|
||||||
if userID, ok := filters["user_id"].(string); ok && userID != "" {
|
if userID, ok := filters["user_id"].(string); ok && userID != "" {
|
||||||
@@ -128,7 +144,7 @@ func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态)
|
// ListProductsWithSubscriptionStatus 获取产品列表(包含订阅状态)
|
||||||
// 业务流程:1. 获取产品列表和订阅状态 2. 构建响应数据
|
// 业务流程<EFBFBD>?. 获取产品列表和订阅状<EFBFBD>?2. 构建响应数据
|
||||||
func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
|
||||||
// 调用领域服务获取产品列表(包含订阅状态)
|
// 调用领域服务获取产品列表(包含订阅状态)
|
||||||
products, subscriptionStatusMap, total, err := s.productManagementService.ListProductsWithSubscriptionStatus(ctx, filters, options)
|
products, subscriptionStatusMap, total, err := s.productManagementService.ListProductsWithSubscriptionStatus(ctx, filters, options)
|
||||||
@@ -158,7 +174,7 @@ func (s *ProductApplicationServiceImpl) ListProductsWithSubscriptionStatus(ctx c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProductsByIDs 根据ID列表获取产品
|
// GetProductsByIDs 根据ID列表获取产品
|
||||||
// 业务流程:1. 获取产品列表 2. 构建响应数据
|
// 业务流程<EFBFBD>?. 获取产品列表 2. 构建响应数据
|
||||||
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
|
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
|
||||||
// 这里需要扩展领域服务来支持批量获取
|
// 这里需要扩展领域服务来支持批量获取
|
||||||
// 暂时返回空列表
|
// 暂时返回空列表
|
||||||
@@ -191,7 +207,7 @@ func (s *ProductApplicationServiceImpl) GetSubscribableProducts(ctx context.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProductByID 根据ID获取产品
|
// GetProductByID 根据ID获取产品
|
||||||
// 业务流程:1. 获取产品信息 2. 构建响应数据
|
// 业务流程<EFBFBD>?. 获取产品信息 2. 构建响应数据
|
||||||
func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
|
func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
|
||||||
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -202,7 +218,7 @@ func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, quer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProductStats 获取产品统计信息
|
// GetProductStats 获取产品统计信息
|
||||||
// 业务流程:1. 获取产品统计 2. 构建响应数据
|
// 业务流程<EFBFBD>?. 获取产品统计 2. 构建响应数据
|
||||||
func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) {
|
func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) {
|
||||||
stats, err := s.productSubscriptionService.GetProductStats(ctx)
|
stats, err := s.productSubscriptionService.GetProductStats(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -346,14 +362,13 @@ func (s *ProductApplicationServiceImpl) UpdatePackageItems(ctx context.Context,
|
|||||||
return s.productManagementService.UpdatePackageItemsBatch(ctx, packageID, cmd.Items)
|
return s.productManagementService.UpdatePackageItemsBatch(ctx, packageID, cmd.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAvailableProducts 获取可选子产品列表
|
// GetAvailableProducts 获取可选子产品列表(管理员端,返回包含成本价的数据)
|
||||||
// 业务流程:1. 获取启用产品 2. 过滤可订阅产品 3. 构建响应数据
|
// 业务流程:1. 获取启用产品 2. 过滤可订阅产品 3. 构建管理员响应数据
|
||||||
func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context, query *appQueries.GetAvailableProductsQuery) (*responses.ProductListResponse, error) {
|
func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context, query *appQueries.GetAvailableProductsQuery) (*responses.ProductAdminListResponse, error) {
|
||||||
// 构建筛选条件
|
// 构建筛选条件
|
||||||
filters := make(map[string]interface{})
|
filters := make(map[string]interface{})
|
||||||
filters["is_package"] = false // 只获取非组合包产品
|
filters["is_package"] = false // 只获取非组合包产品
|
||||||
filters["is_enabled"] = true // 只获取启用产品
|
filters["is_enabled"] = true // 只获取启用产品
|
||||||
|
|
||||||
if query.Keyword != "" {
|
if query.Keyword != "" {
|
||||||
filters["keyword"] = query.Keyword
|
filters["keyword"] = query.Keyword
|
||||||
}
|
}
|
||||||
@@ -375,13 +390,13 @@ func (s *ProductApplicationServiceImpl) GetAvailableProducts(ctx context.Context
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为响应对象
|
// 转换为管理员响应对象(包含成本价,用于组合包配置)
|
||||||
items := make([]responses.ProductInfoResponse, len(products))
|
items := make([]responses.ProductAdminInfoResponse, len(products))
|
||||||
for i := range products {
|
for i := range products {
|
||||||
items[i] = *s.convertToProductInfoResponse(products[i])
|
items[i] = *s.convertToProductAdminInfoResponse(products[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
return &responses.ProductListResponse{
|
return &responses.ProductAdminListResponse{
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: options.Page,
|
Page: options.Page,
|
||||||
Size: options.PageSize,
|
Size: options.PageSize,
|
||||||
@@ -434,7 +449,8 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProductByIDForUser 根据ID获取产品(用户端专用)
|
// GetProductByIDForUser 根据ID获取产品(用户端专用)
|
||||||
// 业务流程:1. 获取产品信息 2. 验证产品可见性 3. 构建用户响应数据
|
// 业务流程:1. 获取产品信息 2. 构建用户响应数据
|
||||||
|
// 注意:详情接口不受 is_visible 字段影响,可通过直接访问详情接口查看任何产品
|
||||||
func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error) {
|
func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error) {
|
||||||
// 首先尝试通过新ID查找产品
|
// 首先尝试通过新ID查找产品
|
||||||
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
||||||
@@ -446,11 +462,6 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户端只能查看可见的产品
|
|
||||||
if !product.IsVisible {
|
|
||||||
return nil, fmt.Errorf("产品不存在或不可见")
|
|
||||||
}
|
|
||||||
|
|
||||||
response := &responses.ProductInfoWithDocumentResponse{
|
response := &responses.ProductInfoWithDocumentResponse{
|
||||||
ProductInfoResponse: *s.convertToProductInfoResponse(product),
|
ProductInfoResponse: *s.convertToProductInfoResponse(product),
|
||||||
}
|
}
|
||||||
@@ -460,6 +471,12 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
|
|||||||
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, product.ID)
|
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, product.ID)
|
||||||
if err == nil && doc != nil {
|
if err == nil && doc != nil {
|
||||||
response.Documentation = doc
|
response.Documentation = doc
|
||||||
|
} else if product.IsPackage && len(response.PackageItems) > 0 {
|
||||||
|
// 如果是组合包且没有自己的文档,尝试合并子产品的文档
|
||||||
|
mergedDoc := s.mergePackageItemsDocumentation(ctx, product.Code, response.PackageItems)
|
||||||
|
if mergedDoc != nil {
|
||||||
|
response.Documentation = mergedDoc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +519,7 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
|
|||||||
ProductName: item.Product.Name,
|
ProductName: item.Product.Name,
|
||||||
SortOrder: item.SortOrder,
|
SortOrder: item.SortOrder,
|
||||||
Price: item.Product.Price.InexactFloat64(),
|
Price: item.Product.Price.InexactFloat64(),
|
||||||
|
CostPrice: item.Product.CostPrice.InexactFloat64(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,6 +538,8 @@ func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(produc
|
|||||||
Content: product.Content,
|
Content: product.Content,
|
||||||
CategoryID: product.CategoryID,
|
CategoryID: product.CategoryID,
|
||||||
Price: product.Price.InexactFloat64(),
|
Price: product.Price.InexactFloat64(),
|
||||||
|
CostPrice: product.CostPrice.InexactFloat64(),
|
||||||
|
Remark: product.Remark,
|
||||||
IsEnabled: product.IsEnabled,
|
IsEnabled: product.IsEnabled,
|
||||||
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
||||||
IsPackage: product.IsPackage,
|
IsPackage: product.IsPackage,
|
||||||
@@ -546,6 +566,7 @@ func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(produc
|
|||||||
ProductName: item.Product.Name,
|
ProductName: item.Product.Name,
|
||||||
SortOrder: item.SortOrder,
|
SortOrder: item.SortOrder,
|
||||||
Price: item.Product.Price.InexactFloat64(),
|
Price: item.Product.Price.InexactFloat64(),
|
||||||
|
CostPrice: item.Product.CostPrice.InexactFloat64(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,3 +605,592 @@ func (s *ProductApplicationServiceImpl) UpdateProductApiConfig(ctx context.Conte
|
|||||||
func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
|
||||||
return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID)
|
return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergePackageItemsDocumentation 合并组合包子产品的文档
|
||||||
|
// packageCode: 组合包的产品编号,用于生成请求地址
|
||||||
|
func (s *ProductApplicationServiceImpl) mergePackageItemsDocumentation(ctx context.Context, packageCode string, packageItems []*responses.PackageItemResponse) *responses.DocumentationResponse {
|
||||||
|
if len(packageItems) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有子产品的ID
|
||||||
|
productIDs := make([]string, 0, len(packageItems))
|
||||||
|
for _, item := range packageItems {
|
||||||
|
productIDs = append(productIDs, item.ProductID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量获取子产品的文档
|
||||||
|
docs, err := s.documentationAppService.GetDocumentationsByProductIDs(ctx, productIDs)
|
||||||
|
if err != nil || len(docs) == 0 {
|
||||||
|
s.logger.Debug("组合包子产品文档获取失败或为空", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文档映射,方便按产品ID查找
|
||||||
|
docMap := make(map[string]*responses.DocumentationResponse)
|
||||||
|
for i := range docs {
|
||||||
|
docMap[docs[i].ProductID] = &docs[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并文档内容
|
||||||
|
mergedDoc := &responses.DocumentationResponse{
|
||||||
|
ProductID: packageItems[0].ProductID, // 使用第一个子产品的ID作为标识
|
||||||
|
RequestMethod: "POST", // 默认方法
|
||||||
|
Version: "1.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集子产品的文档数据,用于构建组合包响应结构
|
||||||
|
subProductDocs := make([]subProductDocInfo, 0)
|
||||||
|
|
||||||
|
// 按packageItems的顺序合并文档
|
||||||
|
for _, item := range packageItems {
|
||||||
|
doc, exists := docMap[item.ProductID]
|
||||||
|
if !exists || doc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存子产品文档信息,用于构建组合包响应结构
|
||||||
|
subProductDocs = append(subProductDocs, subProductDocInfo{
|
||||||
|
item: item,
|
||||||
|
doc: doc,
|
||||||
|
responseFields: doc.ResponseFields,
|
||||||
|
responseExample: doc.ResponseExample,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建组合包的返回字段说明
|
||||||
|
if len(subProductDocs) > 0 {
|
||||||
|
mergedDoc.ResponseFields = s.buildCombPackageResponseFields(subProductDocs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建组合包的响应示例
|
||||||
|
if len(subProductDocs) > 0 {
|
||||||
|
mergedDoc.ResponseExample = s.buildCombPackageResponseExample(subProductDocs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求参数(从DTO结构体中提取所有子产品的参数,去重后生成统一文档)
|
||||||
|
if len(packageItems) > 0 {
|
||||||
|
mergedDoc.RequestParams = s.mergeRequestParamsFromDTOs(ctx, packageItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求地址和方法(使用组合包的产品编号)
|
||||||
|
mergedDoc.RequestURL = fmt.Sprintf("https://api.tianyuanapi.com/api/v1/%s?t=13位时间戳", packageCode)
|
||||||
|
mergedDoc.RequestMethod = "POST"
|
||||||
|
|
||||||
|
// 错误代码和请求方式部分设置为空,前端会显示默认内容
|
||||||
|
mergedDoc.ErrorCodes = ""
|
||||||
|
mergedDoc.BasicInfo = ""
|
||||||
|
|
||||||
|
// 如果没有任何有意义的内容(请求参数、返回字段、响应示例都没有),返回nil
|
||||||
|
if mergedDoc.RequestParams == "" && mergedDoc.ResponseFields == "" && mergedDoc.ResponseExample == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinParts 使用分隔符连接文档部分
|
||||||
|
func joinParts(parts []string) string {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用分隔线分隔不同产品的文档
|
||||||
|
return strings.Join(parts, "\n\n---\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// subProductDocInfo 子产品文档信息
|
||||||
|
type subProductDocInfo struct {
|
||||||
|
item *responses.PackageItemResponse
|
||||||
|
doc *responses.DocumentationResponse
|
||||||
|
responseFields string
|
||||||
|
responseExample string
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCombPackageResponseFields 构建组合包的返回字段说明
|
||||||
|
func (s *ProductApplicationServiceImpl) buildCombPackageResponseFields(subProductDocs []subProductDocInfo) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// 组合包响应结构说明
|
||||||
|
builder.WriteString("## 组合包响应结构\n\n")
|
||||||
|
builder.WriteString("组合包API返回的是一个包含所有子产品响应结果的数组。\n\n")
|
||||||
|
builder.WriteString("| 字段名 | 类型 | 说明 |\n")
|
||||||
|
builder.WriteString("|--------|------|------|\n")
|
||||||
|
builder.WriteString("| responses | array | 子产品响应列表 |\n")
|
||||||
|
builder.WriteString("| responses[].api_code | string | 子产品代码 |\n")
|
||||||
|
builder.WriteString("| responses[].success | boolean | 该子产品调用是否成功 |\n")
|
||||||
|
builder.WriteString("| responses[].data | object/null | 子产品的响应数据(成功时返回数据,失败时为null) |\n")
|
||||||
|
builder.WriteString("| responses[].error | string | 错误信息(仅在失败时返回) |\n\n")
|
||||||
|
|
||||||
|
// 各个子产品的data字段说明
|
||||||
|
builder.WriteString("## 子产品响应数据说明\n\n")
|
||||||
|
for i, spDoc := range subProductDocs {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString("\n---\n\n")
|
||||||
|
}
|
||||||
|
productTitle := fmt.Sprintf("### %s (%s) 的 data 字段", spDoc.item.ProductName, spDoc.item.ProductCode)
|
||||||
|
builder.WriteString(productTitle + "\n\n")
|
||||||
|
if spDoc.responseFields != "" {
|
||||||
|
// 移除子产品文档中可能存在的标题,只保留字段说明
|
||||||
|
fields := spDoc.responseFields
|
||||||
|
// 如果包含标题,尝试提取data字段相关的部分
|
||||||
|
if strings.Contains(fields, "data") || strings.Contains(fields, "返回字段") {
|
||||||
|
builder.WriteString(fields)
|
||||||
|
} else {
|
||||||
|
builder.WriteString(fields)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.WriteString("该子产品的 data 字段结构请参考该产品的单独文档。")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCombPackageResponseExample 构建组合包的响应示例
|
||||||
|
func (s *ProductApplicationServiceImpl) buildCombPackageResponseExample(subProductDocs []subProductDocInfo) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("## 组合包响应示例\n\n")
|
||||||
|
builder.WriteString("### 成功响应(部分子产品成功)\n\n")
|
||||||
|
builder.WriteString("```json\n")
|
||||||
|
builder.WriteString("{\n")
|
||||||
|
builder.WriteString(" \"responses\": [\n")
|
||||||
|
|
||||||
|
// 构建示例JSON - 第一个成功,第二个可能失败
|
||||||
|
for i, spDoc := range subProductDocs {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString(",\n")
|
||||||
|
}
|
||||||
|
builder.WriteString(" {\n")
|
||||||
|
builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", spDoc.item.ProductCode))
|
||||||
|
|
||||||
|
// 第一个示例显示成功,其他可能显示部分失败
|
||||||
|
if i == 0 {
|
||||||
|
builder.WriteString(" \"success\": true,\n")
|
||||||
|
builder.WriteString(" \"data\": {\n")
|
||||||
|
builder.WriteString(fmt.Sprintf(" // %s 的实际响应数据\n", spDoc.item.ProductName))
|
||||||
|
builder.WriteString(" // 具体字段结构请参考下方该子产品的详细响应示例\n")
|
||||||
|
builder.WriteString(" }\n")
|
||||||
|
} else {
|
||||||
|
// 第二个可能成功也可能失败,展示成功的情况
|
||||||
|
builder.WriteString(" \"success\": true,\n")
|
||||||
|
builder.WriteString(" \"data\": {}\n")
|
||||||
|
}
|
||||||
|
builder.WriteString(" }")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n ]\n")
|
||||||
|
builder.WriteString("}\n")
|
||||||
|
builder.WriteString("```\n\n")
|
||||||
|
|
||||||
|
// 添加失败示例
|
||||||
|
builder.WriteString("### 部分失败响应示例\n\n")
|
||||||
|
builder.WriteString("```json\n")
|
||||||
|
builder.WriteString("{\n")
|
||||||
|
builder.WriteString(" \"responses\": [\n")
|
||||||
|
builder.WriteString(" {\n")
|
||||||
|
if len(subProductDocs) > 0 {
|
||||||
|
builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", subProductDocs[0].item.ProductCode))
|
||||||
|
}
|
||||||
|
builder.WriteString(" \"success\": false,\n")
|
||||||
|
builder.WriteString(" \"data\": null,\n")
|
||||||
|
builder.WriteString(" \"error\": \"参数校验不正确\"\n")
|
||||||
|
builder.WriteString(" }")
|
||||||
|
if len(subProductDocs) > 1 {
|
||||||
|
builder.WriteString(",\n")
|
||||||
|
builder.WriteString(" {\n")
|
||||||
|
builder.WriteString(fmt.Sprintf(" \"api_code\": \"%s\",\n", subProductDocs[1].item.ProductCode))
|
||||||
|
builder.WriteString(" \"success\": true,\n")
|
||||||
|
builder.WriteString(" \"data\": {}\n")
|
||||||
|
builder.WriteString(" }")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n ]\n")
|
||||||
|
builder.WriteString("}\n")
|
||||||
|
builder.WriteString("```\n\n")
|
||||||
|
|
||||||
|
// 添加各子产品的详细响应示例说明
|
||||||
|
if len(subProductDocs) > 0 {
|
||||||
|
builder.WriteString("## 各子产品详细响应示例\n\n")
|
||||||
|
builder.WriteString("以下为各个子产品的详细响应示例,组合包响应中的 `data` 字段即为对应子产品的完整响应数据。\n\n")
|
||||||
|
|
||||||
|
for i, spDoc := range subProductDocs {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString("\n---\n\n")
|
||||||
|
}
|
||||||
|
productTitle := fmt.Sprintf("### %s (%s)", spDoc.item.ProductName, spDoc.item.ProductCode)
|
||||||
|
builder.WriteString(productTitle + "\n\n")
|
||||||
|
if spDoc.responseExample != "" {
|
||||||
|
builder.WriteString(spDoc.responseExample)
|
||||||
|
} else {
|
||||||
|
builder.WriteString("该子产品的响应示例请参考该产品的单独文档。")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// paramField 参数字段信息
|
||||||
|
type paramField struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Required string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeRequestParamsFromDTOs 从DTO结构体中提取并合并组合包的请求参数
|
||||||
|
func (s *ProductApplicationServiceImpl) mergeRequestParamsFromDTOs(ctx context.Context, packageItems []*responses.PackageItemResponse) string {
|
||||||
|
if len(packageItems) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于存储所有参数字段,key为字段名(json tag)
|
||||||
|
paramMap := make(map[string]*paramField)
|
||||||
|
|
||||||
|
// 获取DTO映射(复用form_config_service的逻辑)
|
||||||
|
dtoMap := s.getDTOMap()
|
||||||
|
|
||||||
|
// 遍历每个子产品,从DTO中提取字段
|
||||||
|
for _, item := range packageItems {
|
||||||
|
// 从DTO映射中获取子产品的DTO结构体
|
||||||
|
if dtoStruct, exists := dtoMap[item.ProductCode]; exists {
|
||||||
|
// 通过反射解析DTO字段
|
||||||
|
s.extractFieldsFromDTO(dtoStruct, paramMap)
|
||||||
|
} else {
|
||||||
|
// 如果没有找到DTO,尝试通过FormConfigService获取
|
||||||
|
if s.formConfigService != nil {
|
||||||
|
formConfig, err := s.formConfigService.GetFormConfig(ctx, item.ProductCode)
|
||||||
|
if err == nil && formConfig != nil {
|
||||||
|
// 从表单配置中提取字段
|
||||||
|
for _, field := range formConfig.Fields {
|
||||||
|
if _, exists := paramMap[field.Name]; !exists {
|
||||||
|
requiredStr := "否"
|
||||||
|
if field.Required {
|
||||||
|
requiredStr = "是"
|
||||||
|
}
|
||||||
|
paramMap[field.Name] = ¶mField{
|
||||||
|
Name: field.Name,
|
||||||
|
Type: s.mapFieldTypeToDocType(field.Type),
|
||||||
|
Required: requiredStr,
|
||||||
|
Description: field.Description,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果字段已存在,保留更详细的描述,如果新字段是必填则更新
|
||||||
|
existing := paramMap[field.Name]
|
||||||
|
if field.Description != "" && existing.Description == "" {
|
||||||
|
existing.Description = field.Description
|
||||||
|
}
|
||||||
|
if field.Required && existing.Required != "是" {
|
||||||
|
existing.Required = "是"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有提取到任何参数,返回空字符串
|
||||||
|
if len(paramMap) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建合并后的请求参数文档
|
||||||
|
var result strings.Builder
|
||||||
|
result.WriteString("## 请求参数\n\n")
|
||||||
|
|
||||||
|
// 构建JSON示例
|
||||||
|
result.WriteString("```json\n{\n")
|
||||||
|
first := true
|
||||||
|
for fieldName := range paramMap {
|
||||||
|
if !first {
|
||||||
|
result.WriteString(",\n")
|
||||||
|
}
|
||||||
|
result.WriteString(fmt.Sprintf(` "%s": "string"`, fieldName))
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
result.WriteString("\n}\n```\n\n")
|
||||||
|
|
||||||
|
// 构建表格
|
||||||
|
result.WriteString("| 字段名 | 类型 | 必填 | 描述 |\n")
|
||||||
|
result.WriteString("|--------|------|------|------|\n")
|
||||||
|
|
||||||
|
// 按字段名排序输出
|
||||||
|
fieldNames := make([]string, 0, len(paramMap))
|
||||||
|
for fieldName := range paramMap {
|
||||||
|
fieldNames = append(fieldNames, fieldName)
|
||||||
|
}
|
||||||
|
// 简单排序
|
||||||
|
for i := 0; i < len(fieldNames)-1; i++ {
|
||||||
|
for j := i + 1; j < len(fieldNames); j++ {
|
||||||
|
if fieldNames[i] > fieldNames[j] {
|
||||||
|
fieldNames[i], fieldNames[j] = fieldNames[j], fieldNames[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fieldName := range fieldNames {
|
||||||
|
field := paramMap[fieldName]
|
||||||
|
result.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n",
|
||||||
|
field.Name, field.Type, field.Required, field.Description))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.WriteString("\n通过加密后得到 Base64 字符串,将其放入到请求体中,字段名为 `data`。\n\n")
|
||||||
|
result.WriteString("```json\n{\n \"data\": \"xxxx(base64)\"\n}\n```")
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDTOMap 获取API代码到DTO结构体的映射(复用form_config_service的逻辑)
|
||||||
|
func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"IVYZ9363": &dto.IVYZ9363Req{},
|
||||||
|
"IVYZ385E": &dto.IVYZ385EReq{},
|
||||||
|
"IVYZ5733": &dto.IVYZ5733Req{},
|
||||||
|
"FLXG3D56": &dto.FLXG3D56Req{},
|
||||||
|
"FLXG75FE": &dto.FLXG75FEReq{},
|
||||||
|
"FLXG0V3B": &dto.FLXG0V3BReq{},
|
||||||
|
"FLXG0V4B": &dto.FLXG0V4BReq{},
|
||||||
|
"FLXG54F5": &dto.FLXG54F5Req{},
|
||||||
|
"FLXG162A": &dto.FLXG162AReq{},
|
||||||
|
"FLXG0687": &dto.FLXG0687Req{},
|
||||||
|
"FLXGBC21": &dto.FLXGBC21Req{},
|
||||||
|
"FLXG970F": &dto.FLXG970FReq{},
|
||||||
|
"FLXG5876": &dto.FLXG5876Req{},
|
||||||
|
"FLXG9687": &dto.FLXG9687Req{},
|
||||||
|
"FLXGC9D1": &dto.FLXGC9D1Req{},
|
||||||
|
"FLXGCA3D": &dto.FLXGCA3DReq{},
|
||||||
|
"FLXGDEC7": &dto.FLXGDEC7Req{},
|
||||||
|
"JRZQ0A03": &dto.JRZQ0A03Req{},
|
||||||
|
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
|
||||||
|
"JRZQ8203": &dto.JRZQ8203Req{},
|
||||||
|
"JRZQDCBE": &dto.JRZQDCBEReq{},
|
||||||
|
"QYGL2ACD": &dto.QYGL2ACDReq{},
|
||||||
|
"QYGL6F2D": &dto.QYGL6F2DReq{},
|
||||||
|
"QYGL45BD": &dto.QYGL45BDReq{},
|
||||||
|
"QYGL8261": &dto.QYGL8261Req{},
|
||||||
|
"QYGL8271": &dto.QYGL8271Req{},
|
||||||
|
"QYGLB4C0": &dto.QYGLB4C0Req{},
|
||||||
|
"QYGL23T7": &dto.QYGL23T7Req{},
|
||||||
|
"QYGL5A3C": &dto.QYGL5A3CReq{},
|
||||||
|
"QYGL8B4D": &dto.QYGL8B4DReq{},
|
||||||
|
"QYGL9E2F": &dto.QYGL9E2FReq{},
|
||||||
|
"QYGL7C1A": &dto.QYGL7C1AReq{},
|
||||||
|
"QYGL3F8E": &dto.QYGL3F8EReq{},
|
||||||
|
"YYSY4B37": &dto.YYSY4B37Req{},
|
||||||
|
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||||
|
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||||
|
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||||
|
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||||
|
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||||
|
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||||
|
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||||
|
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
|
||||||
|
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
|
||||||
|
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
|
||||||
|
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
|
||||||
|
"IVYZGZ08": &dto.IVYZGZ08Req{},
|
||||||
|
"FLXG8A3F": &dto.FLXG8A3FReq{},
|
||||||
|
"FLXG5B2E": &dto.FLXG5B2EReq{},
|
||||||
|
"COMB298Y": &dto.COMB298YReq{},
|
||||||
|
"COMB86PM": &dto.COMB86PMReq{},
|
||||||
|
"QCXG7A2B": &dto.QCXG7A2BReq{},
|
||||||
|
"COMENT01": &dto.COMENT01Req{},
|
||||||
|
"JRZQ09J8": &dto.JRZQ09J8Req{},
|
||||||
|
"FLXGDEA8": &dto.FLXGDEA8Req{},
|
||||||
|
"FLXGDEA9": &dto.FLXGDEA9Req{},
|
||||||
|
"JRZQ1D09": &dto.JRZQ1D09Req{},
|
||||||
|
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
|
||||||
|
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
|
||||||
|
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
|
||||||
|
"YYSY4F2E": &dto.YYSY4F2EReq{},
|
||||||
|
"YYSY8B1C": &dto.YYSY8B1CReq{},
|
||||||
|
"YYSY6D9A": &dto.YYSY6D9AReq{},
|
||||||
|
"YYSY3E7F": &dto.YYSY3E7FReq{},
|
||||||
|
"FLXG5A3B": &dto.FLXG5A3BReq{},
|
||||||
|
"FLXG9C1D": &dto.FLXG9C1DReq{},
|
||||||
|
"FLXG2E8F": &dto.FLXG2E8FReq{},
|
||||||
|
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
|
||||||
|
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
|
||||||
|
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
|
||||||
|
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
|
||||||
|
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
|
||||||
|
"DWBG6A2C": &dto.DWBG6A2CReq{},
|
||||||
|
"DWBG8B4D": &dto.DWBG8B4DReq{},
|
||||||
|
"FLXG8B4D": &dto.FLXG8B4DReq{},
|
||||||
|
"IVYZ81NC": &dto.IVYZ81NCReq{},
|
||||||
|
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
|
||||||
|
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
|
||||||
|
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
|
||||||
|
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
|
||||||
|
"DWBG7F3A": &dto.DWBG7F3AReq{},
|
||||||
|
"YYSY8F3A": &dto.YYSY8F3AReq{},
|
||||||
|
"QCXG9P1C": &dto.QCXG9P1CReq{},
|
||||||
|
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
|
||||||
|
"YYSY9A1B": &dto.YYSY9A1BReq{},
|
||||||
|
"YYSY8C2D": &dto.YYSY8C2DReq{},
|
||||||
|
"YYSY7D3E": &dto.YYSY7D3EReq{},
|
||||||
|
"YYSY9E4A": &dto.YYSY9E4AReq{},
|
||||||
|
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
|
||||||
|
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
|
||||||
|
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
|
||||||
|
"FLXG7E8F": &dto.FLXG7E8FReq{},
|
||||||
|
"QYGL5F6A": &dto.QYGL5F6AReq{},
|
||||||
|
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||||
|
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||||
|
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFieldsFromDTO 从DTO结构体中提取字段信息
|
||||||
|
func (s *ProductApplicationServiceImpl) extractFieldsFromDTO(dtoStruct interface{}, paramMap map[string]*paramField) {
|
||||||
|
if dtoStruct == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t := reflect.TypeOf(dtoStruct).Elem()
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
// 获取JSON标签
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除omitempty等选项
|
||||||
|
jsonTag = strings.Split(jsonTag, ",")[0]
|
||||||
|
|
||||||
|
// 跳过加密相关的字段(不应该出现在业务参数文档中)
|
||||||
|
if jsonTag == "data" || jsonTag == "encrypt" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取验证标签
|
||||||
|
validateTag := field.Tag.Get("validate")
|
||||||
|
required := strings.Contains(validateTag, "required")
|
||||||
|
requiredStr := "否"
|
||||||
|
if required {
|
||||||
|
requiredStr = "是"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段类型
|
||||||
|
fieldType := s.getGoTypeName(field.Type)
|
||||||
|
|
||||||
|
// 生成字段描述
|
||||||
|
description := s.generateFieldDescription(jsonTag, validateTag)
|
||||||
|
|
||||||
|
// 如果字段已存在,保留更详细的描述,如果新字段是必填则更新
|
||||||
|
if existing, exists := paramMap[jsonTag]; exists {
|
||||||
|
if description != "" && existing.Description == "" {
|
||||||
|
existing.Description = description
|
||||||
|
}
|
||||||
|
if required && existing.Required != "是" {
|
||||||
|
existing.Required = "是"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paramMap[jsonTag] = ¶mField{
|
||||||
|
Name: jsonTag,
|
||||||
|
Type: fieldType,
|
||||||
|
Required: requiredStr,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGoTypeName 获取Go类型的名称(转换为文档中的类型描述)
|
||||||
|
func (s *ProductApplicationServiceImpl) getGoTypeName(fieldType reflect.Type) string {
|
||||||
|
switch fieldType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return "string"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return "int"
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return "int"
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return "float"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
return "array"
|
||||||
|
case reflect.Map:
|
||||||
|
return "object"
|
||||||
|
default:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFieldDescription 生成字段描述
|
||||||
|
func (s *ProductApplicationServiceImpl) generateFieldDescription(jsonTag string, validateTag string) string {
|
||||||
|
// 基础字段描述映射
|
||||||
|
descMap := map[string]string{
|
||||||
|
"mobile_no": "手机号码(11位)",
|
||||||
|
"id_card": "身份证号码(18位)",
|
||||||
|
"name": "姓名",
|
||||||
|
"man_name": "男方姓名",
|
||||||
|
"woman_name": "女方姓名",
|
||||||
|
"man_id_card": "男方身份证号码",
|
||||||
|
"woman_id_card": "女方身份证号码",
|
||||||
|
"ent_name": "企业名称",
|
||||||
|
"legal_person": "法人姓名",
|
||||||
|
"ent_code": "统一社会信用代码",
|
||||||
|
"auth_date": "授权日期范围(格式:YYYYMMDD-YYYYMMDD)",
|
||||||
|
"time_range": "时间范围(格式:HH:MM-HH:MM)",
|
||||||
|
"authorized": "是否授权(0-未授权,1-已授权)",
|
||||||
|
"authorization_url": "授权书URL地址(支持格式:pdf/jpg/jpeg/png/bmp)",
|
||||||
|
"unique_id": "唯一标识",
|
||||||
|
"return_url": "返回链接",
|
||||||
|
"mobile_type": "手机类型",
|
||||||
|
"start_date": "开始日期",
|
||||||
|
"years": "查询年数(0-100)",
|
||||||
|
"bank_card": "银行卡号",
|
||||||
|
"user_type": "关系类型(1-ETC开户人;2-车辆所有人;3-ETC经办人)",
|
||||||
|
"vehicle_type": "车辆类型(0-客车;1-货车;2-全部)",
|
||||||
|
"page_num": "页码(从1开始)",
|
||||||
|
"page_size": "每页数量(1-100)",
|
||||||
|
"use_scenario": "使用场景(1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他)",
|
||||||
|
"wo": "我的",
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc, exists := descMap[jsonTag]; exists {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有预定义,根据验证规则生成描述
|
||||||
|
if strings.Contains(validateTag, "required") {
|
||||||
|
return "必填字段"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapFieldTypeToDocType 将前端字段类型映射为文档类型
|
||||||
|
func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType string) string {
|
||||||
|
switch frontendType {
|
||||||
|
case "tel", "text":
|
||||||
|
return "string"
|
||||||
|
case "number":
|
||||||
|
return "int"
|
||||||
|
case "checkbox":
|
||||||
|
return "boolean"
|
||||||
|
case "select":
|
||||||
|
return "string"
|
||||||
|
case "date":
|
||||||
|
return "string"
|
||||||
|
case "url":
|
||||||
|
return "string"
|
||||||
|
default:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package product
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
|
"tyapi-server/internal/application/product/dto/queries"
|
||||||
|
"tyapi-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
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@ package product
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
|
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||||
"tyapi-server/internal/domains/product/entities"
|
"tyapi-server/internal/domains/product/entities"
|
||||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||||
product_service "tyapi-server/internal/domains/product/services"
|
product_service "tyapi-server/internal/domains/product/services"
|
||||||
@@ -19,6 +22,7 @@ import (
|
|||||||
type SubscriptionApplicationServiceImpl struct {
|
type SubscriptionApplicationServiceImpl struct {
|
||||||
productSubscriptionService *product_service.ProductSubscriptionService
|
productSubscriptionService *product_service.ProductSubscriptionService
|
||||||
userRepo user_repositories.UserRepository
|
userRepo user_repositories.UserRepository
|
||||||
|
apiCallRepository domain_api_repo.ApiCallRepository
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +30,13 @@ type SubscriptionApplicationServiceImpl struct {
|
|||||||
func NewSubscriptionApplicationService(
|
func NewSubscriptionApplicationService(
|
||||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||||
userRepo user_repositories.UserRepository,
|
userRepo user_repositories.UserRepository,
|
||||||
|
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) SubscriptionApplicationService {
|
) SubscriptionApplicationService {
|
||||||
return &SubscriptionApplicationServiceImpl{
|
return &SubscriptionApplicationServiceImpl{
|
||||||
productSubscriptionService: productSubscriptionService,
|
productSubscriptionService: productSubscriptionService,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
|
apiCallRepository: apiCallRepository,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +47,113 @@ func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context
|
|||||||
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price)
|
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 创建订阅
|
// CreateSubscription 创建订阅
|
||||||
// 业务流程:1. 创建订阅
|
// 业务流程:1. 创建订阅
|
||||||
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
|
||||||
@@ -80,7 +193,7 @@ func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Conte
|
|||||||
}
|
}
|
||||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||||
for i := range subscriptions {
|
for i := range subscriptions {
|
||||||
resp := s.convertToSubscriptionInfoResponse(subscriptions[i])
|
resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
items[i] = *resp // 解引用指针
|
items[i] = *resp // 解引用指针
|
||||||
}
|
}
|
||||||
@@ -153,17 +266,30 @@ func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSubscriptionUsage 获取订阅使用情况
|
// GetSubscriptionUsage 获取订阅使用情况
|
||||||
// 业务流程:1. 获取订阅使用情况 2. 构建响应数据
|
// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
|
||||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||||
|
// 获取订阅信息
|
||||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
return &responses.SubscriptionUsageResponse{
|
||||||
ID: subscription.ID,
|
ID: subscription.ID,
|
||||||
ProductID: subscription.ProductID,
|
ProductID: subscription.ProductID,
|
||||||
APIUsed: subscription.APIUsed,
|
APIUsed: apiCallCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +321,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
|
|||||||
}, nil
|
}, 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 转换为订阅信息响应
|
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
@@ -251,6 +409,65 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(prod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &responses.SubscriptionInfoResponse{
|
||||||
|
ID: subscription.ID,
|
||||||
|
UserID: subscription.UserID,
|
||||||
|
ProductID: subscription.ProductID,
|
||||||
|
Price: subscription.Price.InexactFloat64(),
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||||
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||||
if category == nil {
|
if category == nil {
|
||||||
|
|||||||
686
internal/application/product/ui_component_application_service.go
Normal file
686
internal/application/product/ui_component_application_service.go
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
package product
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tyapi-server/internal/domains/product/entities"
|
||||||
|
"tyapi-server/internal/domains/product/repositories"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
) UIComponentApplicationService {
|
||||||
|
return &UIComponentApplicationServiceImpl{
|
||||||
|
uiComponentRepo: uiComponentRepo,
|
||||||
|
productUIComponentRepo: productUIComponentRepo,
|
||||||
|
fileStorageService: fileStorageService,
|
||||||
|
fileService: fileService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 仅对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
|
||||||
|
|
||||||
|
// 检查是否有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
|
||||||
|
|
||||||
|
// 检查是否有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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return ErrComponentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除关联的文件
|
||||||
|
if component.FilePath != nil {
|
||||||
|
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.uiComponentRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 仅对ZIP文件设置已解压标记
|
||||||
|
if fileType == ".zip" {
|
||||||
|
component.IsExtracted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.uiComponentRepo.Update(ctx, *component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUIComponentFolderContent 获取UI组件文件夹内容
|
||||||
|
func (s *UIComponentApplicationServiceImpl) GetUIComponentFolderContent(ctx context.Context, id string) ([]FileInfo, error) {
|
||||||
|
// 获取组件信息
|
||||||
|
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return nil, ErrComponentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有文件夹路径,返回空
|
||||||
|
if component.FolderPath == nil {
|
||||||
|
return []FileInfo{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件夹内容
|
||||||
|
return s.fileService.GetFolderContent(*component.FolderPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUIComponentFolder 删除UI组件文件夹
|
||||||
|
func (s *UIComponentApplicationServiceImpl) DeleteUIComponentFolder(ctx context.Context, id string) error {
|
||||||
|
// 获取组件信息
|
||||||
|
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return ErrComponentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:我们不再删除整个UI目录,因为所有组件共享同一个目录
|
||||||
|
// 这里只更新组件信息,标记为未上传状态
|
||||||
|
// 更新组件信息
|
||||||
|
component.FolderPath = nil
|
||||||
|
component.IsExtracted = false
|
||||||
|
return s.uiComponentRepo.Update(ctx, *component)
|
||||||
|
}
|
||||||
21
internal/application/product/ui_component_errors.go
Normal file
21
internal/application/product/ui_component_errors.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package product
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// UI组件相关错误定义
|
||||||
|
var (
|
||||||
|
// ErrComponentNotFound UI组件不存在
|
||||||
|
ErrComponentNotFound = errors.New("UI组件不存在")
|
||||||
|
|
||||||
|
// ErrComponentCodeAlreadyExists UI组件编码已存在
|
||||||
|
ErrComponentCodeAlreadyExists = errors.New("UI组件编码已存在")
|
||||||
|
|
||||||
|
// ErrComponentFileNotFound UI组件文件不存在
|
||||||
|
ErrComponentFileNotFound = errors.New("UI组件文件不存在")
|
||||||
|
|
||||||
|
// ErrInvalidFileType 无效的文件类型
|
||||||
|
ErrInvalidFileType = errors.New("无效的文件类型,仅支持ZIP文件")
|
||||||
|
|
||||||
|
// ErrProductComponentRelationNotFound 产品UI组件关联不存在
|
||||||
|
ErrProductComponentRelationNotFound = errors.New("产品UI组件关联不存在")
|
||||||
|
)
|
||||||
341
internal/application/product/ui_component_file_service.go
Normal file
341
internal/application/product/ui_component_file_service.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if !s.FolderExists(folderPath) {
|
||||||
|
return nil // 文件夹不存在,不视为错误
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(folderPath); err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
412
internal/application/statistics/commands_queries.go
Normal file
412
internal/application/statistics/commands_queries.go
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================ 命令对象 ================
|
||||||
|
|
||||||
|
// CreateMetricCommand 创建指标命令
|
||||||
|
type CreateMetricCommand struct {
|
||||||
|
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" validate:"required" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" comment:"统计维度"`
|
||||||
|
Value float64 `json:"value" validate:"min=0" comment:"指标值"`
|
||||||
|
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||||
|
Date time.Time `json:"date" validate:"required" comment:"统计日期"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMetricCommand 更新指标命令
|
||||||
|
type UpdateMetricCommand struct {
|
||||||
|
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||||
|
Value float64 `json:"value" validate:"min=0" comment:"新指标值"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMetricCommand 删除指标命令
|
||||||
|
type DeleteMetricCommand struct {
|
||||||
|
ID string `json:"id" validate:"required" comment:"指标ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateReportCommand 生成报告命令
|
||||||
|
type GenerateReportCommand struct {
|
||||||
|
ReportType string `json:"report_type" validate:"required" comment:"报告类型"`
|
||||||
|
Title string `json:"title" validate:"required" comment:"报告标题"`
|
||||||
|
Period string `json:"period" validate:"required" comment:"统计周期"`
|
||||||
|
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||||
|
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||||
|
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||||
|
GeneratedBy string `json:"generated_by" validate:"required" comment:"生成者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDashboardCommand 创建仪表板命令
|
||||||
|
type CreateDashboardCommand struct {
|
||||||
|
Name string `json:"name" validate:"required" comment:"仪表板名称"`
|
||||||
|
Description string `json:"description" comment:"仪表板描述"`
|
||||||
|
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||||
|
Layout string `json:"layout" comment:"布局配置"`
|
||||||
|
Widgets string `json:"widgets" comment:"组件配置"`
|
||||||
|
Settings string `json:"settings" comment:"设置配置"`
|
||||||
|
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||||
|
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||||
|
CreatedBy string `json:"created_by" validate:"required" comment:"创建者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDashboardCommand 更新仪表板命令
|
||||||
|
type UpdateDashboardCommand struct {
|
||||||
|
ID string `json:"id" validate:"required" comment:"仪表板ID"`
|
||||||
|
Name string `json:"name" comment:"仪表板名称"`
|
||||||
|
Description string `json:"description" comment:"仪表板描述"`
|
||||||
|
Layout string `json:"layout" comment:"布局配置"`
|
||||||
|
Widgets string `json:"widgets" comment:"组件配置"`
|
||||||
|
Settings string `json:"settings" comment:"设置配置"`
|
||||||
|
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
|
||||||
|
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||||
|
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultDashboardCommand 设置默认仪表板命令
|
||||||
|
type SetDefaultDashboardCommand struct {
|
||||||
|
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||||
|
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
|
||||||
|
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivateDashboardCommand 激活仪表板命令
|
||||||
|
type ActivateDashboardCommand struct {
|
||||||
|
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||||
|
ActivatedBy string `json:"activated_by" validate:"required" comment:"激活者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeactivateDashboardCommand 停用仪表板命令
|
||||||
|
type DeactivateDashboardCommand struct {
|
||||||
|
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||||
|
DeactivatedBy string `json:"deactivated_by" validate:"required" comment:"停用者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDashboardCommand 删除仪表板命令
|
||||||
|
type DeleteDashboardCommand struct {
|
||||||
|
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||||
|
DeletedBy string `json:"deleted_by" validate:"required" comment:"删除者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportDataCommand 导出数据命令
|
||||||
|
type ExportDataCommand struct {
|
||||||
|
Format string `json:"format" validate:"required" comment:"导出格式"`
|
||||||
|
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
StartDate time.Time `json:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
Dimension string `json:"dimension" comment:"统计维度"`
|
||||||
|
GroupBy string `json:"group_by" comment:"分组维度"`
|
||||||
|
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||||
|
Columns []string `json:"columns" comment:"导出列"`
|
||||||
|
IncludeCharts bool `json:"include_charts" comment:"是否包含图表"`
|
||||||
|
ExportedBy string `json:"exported_by" validate:"required" comment:"导出者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerAggregationCommand 触发数据聚合命令
|
||||||
|
type TriggerAggregationCommand struct {
|
||||||
|
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
Period string `json:"period" validate:"required" comment:"聚合周期"`
|
||||||
|
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||||
|
Force bool `json:"force" comment:"是否强制重新聚合"`
|
||||||
|
TriggeredBy string `json:"triggered_by" validate:"required" comment:"触发者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证触发聚合命令
|
||||||
|
func (c *TriggerAggregationCommand) Validate() error {
|
||||||
|
if c.MetricType == "" {
|
||||||
|
return fmt.Errorf("指标类型不能为空")
|
||||||
|
}
|
||||||
|
if c.Period == "" {
|
||||||
|
return fmt.Errorf("聚合周期不能为空")
|
||||||
|
}
|
||||||
|
if c.TriggeredBy == "" {
|
||||||
|
return fmt.Errorf("触发者ID不能为空")
|
||||||
|
}
|
||||||
|
// 验证周期类型
|
||||||
|
validPeriods := []string{"hourly", "daily", "weekly", "monthly"}
|
||||||
|
isValidPeriod := false
|
||||||
|
for _, period := range validPeriods {
|
||||||
|
if c.Period == period {
|
||||||
|
isValidPeriod = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValidPeriod {
|
||||||
|
return fmt.Errorf("不支持的聚合周期: %s", c.Period)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ 查询对象 ================
|
||||||
|
|
||||||
|
// GetMetricsQuery 获取指标查询
|
||||||
|
type GetMetricsQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||||
|
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||||
|
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||||
|
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||||
|
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRealtimeMetricsQuery 获取实时指标查询
|
||||||
|
type GetRealtimeMetricsQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
TimeRange string `json:"time_range" form:"time_range" comment:"时间范围"`
|
||||||
|
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHistoricalMetricsQuery 获取历史指标查询
|
||||||
|
type GetHistoricalMetricsQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||||
|
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||||
|
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||||
|
AggregateBy string `json:"aggregate_by" form:"aggregate_by" comment:"聚合维度"`
|
||||||
|
GroupBy string `json:"group_by" form:"group_by" comment:"分组维度"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardDataQuery 获取仪表板数据查询
|
||||||
|
type GetDashboardDataQuery struct {
|
||||||
|
UserRole string `json:"user_role" form:"user_role" validate:"required" comment:"用户角色"`
|
||||||
|
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||||
|
MetricTypes []string `json:"metric_types" form:"metric_types" comment:"指标类型列表"`
|
||||||
|
Dimensions []string `json:"dimensions" form:"dimensions" comment:"统计维度列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReportsQuery 获取报告查询
|
||||||
|
type GetReportsQuery struct {
|
||||||
|
ReportType string `json:"report_type" form:"report_type" comment:"报告类型"`
|
||||||
|
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||||
|
Status string `json:"status" form:"status" comment:"报告状态"`
|
||||||
|
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||||
|
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||||
|
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||||
|
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||||
|
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||||
|
GeneratedBy string `json:"generated_by" form:"generated_by" comment:"生成者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardsQuery 获取仪表板查询
|
||||||
|
type GetDashboardsQuery struct {
|
||||||
|
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||||
|
IsDefault *bool `json:"is_default" form:"is_default" comment:"是否默认"`
|
||||||
|
IsActive *bool `json:"is_active" form:"is_active" comment:"是否激活"`
|
||||||
|
AccessLevel string `json:"access_level" form:"access_level" comment:"访问级别"`
|
||||||
|
CreatedBy string `json:"created_by" form:"created_by" comment:"创建者ID"`
|
||||||
|
Name string `json:"name" form:"name" comment:"仪表板名称"`
|
||||||
|
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||||
|
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||||
|
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||||
|
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReportQuery 获取单个报告查询
|
||||||
|
type GetReportQuery struct {
|
||||||
|
ReportID string `json:"report_id" form:"report_id" validate:"required" comment:"报告ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardQuery 获取单个仪表板查询
|
||||||
|
type GetDashboardQuery struct {
|
||||||
|
DashboardID string `json:"dashboard_id" form:"dashboard_id" validate:"required" comment:"仪表板ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricQuery 获取单个指标查询
|
||||||
|
type GetMetricQuery struct {
|
||||||
|
MetricID string `json:"metric_id" form:"metric_id" validate:"required" comment:"指标ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateGrowthRateQuery 计算增长率查询
|
||||||
|
type CalculateGrowthRateQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||||
|
CurrentPeriod time.Time `json:"current_period" form:"current_period" validate:"required" comment:"当前周期"`
|
||||||
|
PreviousPeriod time.Time `json:"previous_period" form:"previous_period" validate:"required" comment:"上一周期"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateTrendQuery 计算趋势查询
|
||||||
|
type CalculateTrendQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateCorrelationQuery 计算相关性查询
|
||||||
|
type CalculateCorrelationQuery struct {
|
||||||
|
MetricType1 string `json:"metric_type1" form:"metric_type1" validate:"required" comment:"指标类型1"`
|
||||||
|
MetricName1 string `json:"metric_name1" form:"metric_name1" validate:"required" comment:"指标名称1"`
|
||||||
|
MetricType2 string `json:"metric_type2" form:"metric_type2" validate:"required" comment:"指标类型2"`
|
||||||
|
MetricName2 string `json:"metric_name2" form:"metric_name2" validate:"required" comment:"指标名称2"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateMovingAverageQuery 计算移动平均查询
|
||||||
|
type CalculateMovingAverageQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
WindowSize int `json:"window_size" form:"window_size" validate:"min=1" comment:"窗口大小"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateSeasonalityQuery 计算季节性查询
|
||||||
|
type CalculateSeasonalityQuery struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ 响应对象 ================
|
||||||
|
|
||||||
|
// CommandResponse 命令响应
|
||||||
|
type CommandResponse struct {
|
||||||
|
Success bool `json:"success" comment:"是否成功"`
|
||||||
|
Message string `json:"message" comment:"响应消息"`
|
||||||
|
Data interface{} `json:"data" comment:"响应数据"`
|
||||||
|
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryResponse 查询响应
|
||||||
|
type QueryResponse struct {
|
||||||
|
Success bool `json:"success" comment:"是否成功"`
|
||||||
|
Message string `json:"message" comment:"响应消息"`
|
||||||
|
Data interface{} `json:"data" comment:"响应数据"`
|
||||||
|
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||||
|
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResponse 列表响应
|
||||||
|
type ListResponse struct {
|
||||||
|
Success bool `json:"success" comment:"是否成功"`
|
||||||
|
Message string `json:"message" comment:"响应消息"`
|
||||||
|
Data ListDataDTO `json:"data" comment:"数据列表"`
|
||||||
|
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||||
|
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDataDTO 列表数据DTO
|
||||||
|
type ListDataDTO struct {
|
||||||
|
Total int64 `json:"total" comment:"总数"`
|
||||||
|
Page int `json:"page" comment:"页码"`
|
||||||
|
Size int `json:"size" comment:"每页数量"`
|
||||||
|
Items []interface{} `json:"items" comment:"数据列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ 验证方法 ================
|
||||||
|
|
||||||
|
// Validate 验证创建指标命令
|
||||||
|
func (c *CreateMetricCommand) Validate() error {
|
||||||
|
if c.MetricType == "" {
|
||||||
|
return fmt.Errorf("指标类型不能为空")
|
||||||
|
}
|
||||||
|
if c.MetricName == "" {
|
||||||
|
return fmt.Errorf("指标名称不能为空")
|
||||||
|
}
|
||||||
|
if c.Value < 0 {
|
||||||
|
return fmt.Errorf("指标值不能为负数")
|
||||||
|
}
|
||||||
|
if c.Date.IsZero() {
|
||||||
|
return fmt.Errorf("统计日期不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证更新指标命令
|
||||||
|
func (c *UpdateMetricCommand) Validate() error {
|
||||||
|
if c.ID == "" {
|
||||||
|
return fmt.Errorf("指标ID不能为空")
|
||||||
|
}
|
||||||
|
if c.Value < 0 {
|
||||||
|
return fmt.Errorf("指标值不能为负数")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证生成报告命令
|
||||||
|
func (c *GenerateReportCommand) Validate() error {
|
||||||
|
if c.ReportType == "" {
|
||||||
|
return fmt.Errorf("报告类型不能为空")
|
||||||
|
}
|
||||||
|
if c.Title == "" {
|
||||||
|
return fmt.Errorf("报告标题不能为空")
|
||||||
|
}
|
||||||
|
if c.Period == "" {
|
||||||
|
return fmt.Errorf("统计周期不能为空")
|
||||||
|
}
|
||||||
|
if c.UserRole == "" {
|
||||||
|
return fmt.Errorf("用户角色不能为空")
|
||||||
|
}
|
||||||
|
if c.GeneratedBy == "" {
|
||||||
|
return fmt.Errorf("生成者ID不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证创建仪表板命令
|
||||||
|
func (c *CreateDashboardCommand) Validate() error {
|
||||||
|
if c.Name == "" {
|
||||||
|
return fmt.Errorf("仪表板名称不能为空")
|
||||||
|
}
|
||||||
|
if c.UserRole == "" {
|
||||||
|
return fmt.Errorf("用户角色不能为空")
|
||||||
|
}
|
||||||
|
if c.CreatedBy == "" {
|
||||||
|
return fmt.Errorf("创建者ID不能为空")
|
||||||
|
}
|
||||||
|
if c.RefreshInterval < 30 {
|
||||||
|
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证更新仪表板命令
|
||||||
|
func (c *UpdateDashboardCommand) Validate() error {
|
||||||
|
if c.ID == "" {
|
||||||
|
return fmt.Errorf("仪表板ID不能为空")
|
||||||
|
}
|
||||||
|
if c.UpdatedBy == "" {
|
||||||
|
return fmt.Errorf("更新者ID不能为空")
|
||||||
|
}
|
||||||
|
if c.RefreshInterval < 30 {
|
||||||
|
return fmt.Errorf("刷新间隔不能少于30秒")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 验证导出数据命令
|
||||||
|
func (c *ExportDataCommand) Validate() error {
|
||||||
|
if c.Format == "" {
|
||||||
|
return fmt.Errorf("导出格式不能为空")
|
||||||
|
}
|
||||||
|
if c.MetricType == "" {
|
||||||
|
return fmt.Errorf("指标类型不能为空")
|
||||||
|
}
|
||||||
|
if c.StartDate.IsZero() || c.EndDate.IsZero() {
|
||||||
|
return fmt.Errorf("开始日期和结束日期不能为空")
|
||||||
|
}
|
||||||
|
if c.StartDate.After(c.EndDate) {
|
||||||
|
return fmt.Errorf("开始日期不能晚于结束日期")
|
||||||
|
}
|
||||||
|
if c.ExportedBy == "" {
|
||||||
|
return fmt.Errorf("导出者ID不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
258
internal/application/statistics/dtos.go
Normal file
258
internal/application/statistics/dtos.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatisticsMetricDTO 统计指标DTO
|
||||||
|
type StatisticsMetricDTO struct {
|
||||||
|
ID string `json:"id" comment:"统计指标唯一标识"`
|
||||||
|
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" comment:"统计维度"`
|
||||||
|
Value float64 `json:"value" comment:"指标值"`
|
||||||
|
Metadata string `json:"metadata" comment:"额外维度信息"`
|
||||||
|
Date time.Time `json:"date" comment:"统计日期"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsReportDTO 统计报告DTO
|
||||||
|
type StatisticsReportDTO struct {
|
||||||
|
ID string `json:"id" comment:"报告唯一标识"`
|
||||||
|
ReportType string `json:"report_type" comment:"报告类型"`
|
||||||
|
Title string `json:"title" comment:"报告标题"`
|
||||||
|
Content string `json:"content" comment:"报告内容"`
|
||||||
|
Period string `json:"period" comment:"统计周期"`
|
||||||
|
UserRole string `json:"user_role" comment:"用户角色"`
|
||||||
|
Status string `json:"status" comment:"报告状态"`
|
||||||
|
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||||
|
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsDashboardDTO 统计仪表板DTO
|
||||||
|
type StatisticsDashboardDTO struct {
|
||||||
|
ID string `json:"id" comment:"仪表板唯一标识"`
|
||||||
|
Name string `json:"name" comment:"仪表板名称"`
|
||||||
|
Description string `json:"description" comment:"仪表板描述"`
|
||||||
|
UserRole string `json:"user_role" comment:"用户角色"`
|
||||||
|
IsDefault bool `json:"is_default" comment:"是否为默认仪表板"`
|
||||||
|
IsActive bool `json:"is_active" comment:"是否激活"`
|
||||||
|
Layout string `json:"layout" comment:"布局配置"`
|
||||||
|
Widgets string `json:"widgets" comment:"组件配置"`
|
||||||
|
Settings string `json:"settings" comment:"设置配置"`
|
||||||
|
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||||
|
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||||
|
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||||
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardDataDTO 仪表板数据DTO
|
||||||
|
type DashboardDataDTO struct {
|
||||||
|
// API调用统计
|
||||||
|
APICalls struct {
|
||||||
|
TotalCount int64 `json:"total_count" comment:"总调用次数"`
|
||||||
|
SuccessCount int64 `json:"success_count" comment:"成功调用次数"`
|
||||||
|
FailedCount int64 `json:"failed_count" comment:"失败调用次数"`
|
||||||
|
SuccessRate float64 `json:"success_rate" comment:"成功率"`
|
||||||
|
AvgResponseTime float64 `json:"avg_response_time" comment:"平均响应时间"`
|
||||||
|
} `json:"api_calls"`
|
||||||
|
|
||||||
|
// 用户统计
|
||||||
|
Users struct {
|
||||||
|
TotalCount int64 `json:"total_count" comment:"总用户数"`
|
||||||
|
CertifiedCount int64 `json:"certified_count" comment:"认证用户数"`
|
||||||
|
ActiveCount int64 `json:"active_count" comment:"活跃用户数"`
|
||||||
|
CertificationRate float64 `json:"certification_rate" comment:"认证完成率"`
|
||||||
|
RetentionRate float64 `json:"retention_rate" comment:"留存率"`
|
||||||
|
} `json:"users"`
|
||||||
|
|
||||||
|
// 财务统计
|
||||||
|
Finance struct {
|
||||||
|
TotalAmount float64 `json:"total_amount" comment:"总金额"`
|
||||||
|
RechargeAmount float64 `json:"recharge_amount" comment:"充值金额"`
|
||||||
|
DeductAmount float64 `json:"deduct_amount" comment:"扣款金额"`
|
||||||
|
NetAmount float64 `json:"net_amount" comment:"净金额"`
|
||||||
|
} `json:"finance"`
|
||||||
|
|
||||||
|
// 产品统计
|
||||||
|
Products struct {
|
||||||
|
TotalProducts int64 `json:"total_products" comment:"总产品数"`
|
||||||
|
ActiveProducts int64 `json:"active_products" comment:"活跃产品数"`
|
||||||
|
TotalSubscriptions int64 `json:"total_subscriptions" comment:"总订阅数"`
|
||||||
|
ActiveSubscriptions int64 `json:"active_subscriptions" comment:"活跃订阅数"`
|
||||||
|
} `json:"products"`
|
||||||
|
|
||||||
|
// 认证统计
|
||||||
|
Certification struct {
|
||||||
|
TotalCertifications int64 `json:"total_certifications" comment:"总认证数"`
|
||||||
|
CompletedCertifications int64 `json:"completed_certifications" comment:"完成认证数"`
|
||||||
|
PendingCertifications int64 `json:"pending_certifications" comment:"待处理认证数"`
|
||||||
|
FailedCertifications int64 `json:"failed_certifications" comment:"失败认证数"`
|
||||||
|
CompletionRate float64 `json:"completion_rate" comment:"完成率"`
|
||||||
|
} `json:"certification"`
|
||||||
|
|
||||||
|
// 时间信息
|
||||||
|
Period struct {
|
||||||
|
StartDate string `json:"start_date" comment:"开始日期"`
|
||||||
|
EndDate string `json:"end_date" comment:"结束日期"`
|
||||||
|
Period string `json:"period" comment:"统计周期"`
|
||||||
|
} `json:"period"`
|
||||||
|
|
||||||
|
// 元数据
|
||||||
|
Metadata struct {
|
||||||
|
GeneratedAt string `json:"generated_at" comment:"生成时间"`
|
||||||
|
UserRole string `json:"user_role" comment:"用户角色"`
|
||||||
|
DataVersion string `json:"data_version" comment:"数据版本"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RealtimeMetricsDTO 实时指标DTO
|
||||||
|
type RealtimeMetricsDTO struct {
|
||||||
|
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||||
|
Metrics map[string]float64 `json:"metrics" comment:"指标数据"`
|
||||||
|
Timestamp time.Time `json:"timestamp" comment:"时间戳"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoricalMetricsDTO 历史指标DTO
|
||||||
|
type HistoricalMetricsDTO struct {
|
||||||
|
MetricType string `json:"metric_type" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" comment:"统计维度"`
|
||||||
|
DataPoints []DataPointDTO `json:"data_points" comment:"数据点"`
|
||||||
|
Summary MetricsSummaryDTO `json:"summary" comment:"汇总信息"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataPointDTO 数据点DTO
|
||||||
|
type DataPointDTO struct {
|
||||||
|
Date time.Time `json:"date" comment:"日期"`
|
||||||
|
Value float64 `json:"value" comment:"值"`
|
||||||
|
Label string `json:"label" comment:"标签"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricsSummaryDTO 指标汇总DTO
|
||||||
|
type MetricsSummaryDTO struct {
|
||||||
|
Total float64 `json:"total" comment:"总值"`
|
||||||
|
Average float64 `json:"average" comment:"平均值"`
|
||||||
|
Max float64 `json:"max" comment:"最大值"`
|
||||||
|
Min float64 `json:"min" comment:"最小值"`
|
||||||
|
Count int64 `json:"count" comment:"数据点数量"`
|
||||||
|
GrowthRate float64 `json:"growth_rate" comment:"增长率"`
|
||||||
|
Trend string `json:"trend" comment:"趋势"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportContentDTO 报告内容DTO
|
||||||
|
type ReportContentDTO struct {
|
||||||
|
ReportType string `json:"report_type" comment:"报告类型"`
|
||||||
|
Title string `json:"title" comment:"报告标题"`
|
||||||
|
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||||
|
Details map[string]interface{} `json:"details" comment:"详细信息"`
|
||||||
|
Charts []ChartDTO `json:"charts" comment:"图表数据"`
|
||||||
|
Tables []TableDTO `json:"tables" comment:"表格数据"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChartDTO 图表DTO
|
||||||
|
type ChartDTO struct {
|
||||||
|
Type string `json:"type" comment:"图表类型"`
|
||||||
|
Title string `json:"title" comment:"图表标题"`
|
||||||
|
Data map[string]interface{} `json:"data" comment:"图表数据"`
|
||||||
|
Options map[string]interface{} `json:"options" comment:"图表选项"`
|
||||||
|
Description string `json:"description" comment:"图表描述"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableDTO 表格DTO
|
||||||
|
type TableDTO struct {
|
||||||
|
Title string `json:"title" comment:"表格标题"`
|
||||||
|
Headers []string `json:"headers" comment:"表头"`
|
||||||
|
Rows [][]interface{} `json:"rows" comment:"表格行数据"`
|
||||||
|
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
|
||||||
|
Description string `json:"description" comment:"表格描述"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportDataDTO 导出数据DTO
|
||||||
|
type ExportDataDTO struct {
|
||||||
|
Format string `json:"format" comment:"导出格式"`
|
||||||
|
FileName string `json:"file_name" comment:"文件名"`
|
||||||
|
Data []map[string]interface{} `json:"data" comment:"导出数据"`
|
||||||
|
Headers []string `json:"headers" comment:"表头"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
|
||||||
|
DownloadURL string `json:"download_url" comment:"下载链接"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsQueryDTO 统计查询DTO
|
||||||
|
type StatisticsQueryDTO struct {
|
||||||
|
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
|
||||||
|
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
|
||||||
|
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
|
||||||
|
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
|
||||||
|
Period string `json:"period" form:"period" comment:"统计周期"`
|
||||||
|
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
|
||||||
|
Limit int `json:"limit" form:"limit" comment:"限制数量"`
|
||||||
|
Offset int `json:"offset" form:"offset" comment:"偏移量"`
|
||||||
|
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
|
||||||
|
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportGenerationDTO 报告生成DTO
|
||||||
|
type ReportGenerationDTO struct {
|
||||||
|
ReportType string `json:"report_type" comment:"报告类型"`
|
||||||
|
Title string `json:"title" comment:"报告标题"`
|
||||||
|
Period string `json:"period" comment:"统计周期"`
|
||||||
|
UserRole string `json:"user_role" comment:"用户角色"`
|
||||||
|
StartDate time.Time `json:"start_date" comment:"开始日期"`
|
||||||
|
EndDate time.Time `json:"end_date" comment:"结束日期"`
|
||||||
|
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
|
||||||
|
Format string `json:"format" comment:"输出格式"`
|
||||||
|
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardConfigDTO 仪表板配置DTO
|
||||||
|
type DashboardConfigDTO struct {
|
||||||
|
Name string `json:"name" comment:"仪表板名称"`
|
||||||
|
Description string `json:"description" comment:"仪表板描述"`
|
||||||
|
UserRole string `json:"user_role" comment:"用户角色"`
|
||||||
|
Layout string `json:"layout" comment:"布局配置"`
|
||||||
|
Widgets string `json:"widgets" comment:"组件配置"`
|
||||||
|
Settings string `json:"settings" comment:"设置配置"`
|
||||||
|
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
|
||||||
|
AccessLevel string `json:"access_level" comment:"访问级别"`
|
||||||
|
CreatedBy string `json:"created_by" comment:"创建者ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsResponseDTO 统计响应DTO
|
||||||
|
type StatisticsResponseDTO struct {
|
||||||
|
Success bool `json:"success" comment:"是否成功"`
|
||||||
|
Message string `json:"message" comment:"响应消息"`
|
||||||
|
Data interface{} `json:"data" comment:"响应数据"`
|
||||||
|
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||||
|
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationDTO 分页DTO
|
||||||
|
type PaginationDTO struct {
|
||||||
|
Page int `json:"page" comment:"当前页"`
|
||||||
|
PageSize int `json:"page_size" comment:"每页大小"`
|
||||||
|
Total int64 `json:"total" comment:"总数量"`
|
||||||
|
Pages int `json:"pages" comment:"总页数"`
|
||||||
|
HasNext bool `json:"has_next" comment:"是否有下一页"`
|
||||||
|
HasPrev bool `json:"has_prev" comment:"是否有上一页"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsListResponseDTO 统计列表响应DTO
|
||||||
|
type StatisticsListResponseDTO struct {
|
||||||
|
Success bool `json:"success" comment:"是否成功"`
|
||||||
|
Message string `json:"message" comment:"响应消息"`
|
||||||
|
Data []interface{} `json:"data" comment:"数据列表"`
|
||||||
|
Pagination PaginationDTO `json:"pagination" comment:"分页信息"`
|
||||||
|
Meta map[string]interface{} `json:"meta" comment:"元数据"`
|
||||||
|
Error string `json:"error,omitempty" comment:"错误信息"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatisticsApplicationService 统计应用服务接口
|
||||||
|
// 负责统计功能的业务逻辑编排和协调
|
||||||
|
type StatisticsApplicationService interface {
|
||||||
|
// ================ 指标管理 ================
|
||||||
|
|
||||||
|
// CreateMetric 创建统计指标
|
||||||
|
CreateMetric(ctx context.Context, cmd *CreateMetricCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// UpdateMetric 更新统计指标
|
||||||
|
UpdateMetric(ctx context.Context, cmd *UpdateMetricCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// DeleteMetric 删除统计指标
|
||||||
|
DeleteMetric(ctx context.Context, cmd *DeleteMetricCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// GetMetric 获取单个指标
|
||||||
|
GetMetric(ctx context.Context, query *GetMetricQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetMetrics 获取指标列表
|
||||||
|
GetMetrics(ctx context.Context, query *GetMetricsQuery) (*ListResponse, error)
|
||||||
|
|
||||||
|
// ================ 实时统计 ================
|
||||||
|
|
||||||
|
// GetRealtimeMetrics 获取实时指标
|
||||||
|
GetRealtimeMetrics(ctx context.Context, query *GetRealtimeMetricsQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// UpdateRealtimeMetric 更新实时指标
|
||||||
|
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
|
||||||
|
|
||||||
|
// ================ 历史统计 ================
|
||||||
|
|
||||||
|
// GetHistoricalMetrics 获取历史指标
|
||||||
|
GetHistoricalMetrics(ctx context.Context, query *GetHistoricalMetricsQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AggregateMetrics 聚合指标
|
||||||
|
AggregateMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) error
|
||||||
|
|
||||||
|
// ================ 仪表板管理 ================
|
||||||
|
|
||||||
|
// CreateDashboard 创建仪表板
|
||||||
|
CreateDashboard(ctx context.Context, cmd *CreateDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// UpdateDashboard 更新仪表板
|
||||||
|
UpdateDashboard(ctx context.Context, cmd *UpdateDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// DeleteDashboard 删除仪表板
|
||||||
|
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// GetDashboard 获取单个仪表板
|
||||||
|
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetDashboards 获取仪表板列表
|
||||||
|
GetDashboards(ctx context.Context, query *GetDashboardsQuery) (*ListResponse, error)
|
||||||
|
|
||||||
|
// SetDefaultDashboard 设置默认仪表板
|
||||||
|
SetDefaultDashboard(ctx context.Context, cmd *SetDefaultDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// ActivateDashboard 激活仪表板
|
||||||
|
ActivateDashboard(ctx context.Context, cmd *ActivateDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// DeactivateDashboard 停用仪表板
|
||||||
|
DeactivateDashboard(ctx context.Context, cmd *DeactivateDashboardCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// GetDashboardData 获取仪表板数据
|
||||||
|
GetDashboardData(ctx context.Context, query *GetDashboardDataQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 报告管理 ================
|
||||||
|
|
||||||
|
// GenerateReport 生成报告
|
||||||
|
GenerateReport(ctx context.Context, cmd *GenerateReportCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// GetReport 获取单个报告
|
||||||
|
GetReport(ctx context.Context, query *GetReportQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetReports 获取报告列表
|
||||||
|
GetReports(ctx context.Context, query *GetReportsQuery) (*ListResponse, error)
|
||||||
|
|
||||||
|
// DeleteReport 删除报告
|
||||||
|
DeleteReport(ctx context.Context, reportID string) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// ================ 统计分析 ================
|
||||||
|
|
||||||
|
// CalculateGrowthRate 计算增长率
|
||||||
|
CalculateGrowthRate(ctx context.Context, query *CalculateGrowthRateQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// CalculateTrend 计算趋势
|
||||||
|
CalculateTrend(ctx context.Context, query *CalculateTrendQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// CalculateCorrelation 计算相关性
|
||||||
|
CalculateCorrelation(ctx context.Context, query *CalculateCorrelationQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// CalculateMovingAverage 计算移动平均
|
||||||
|
CalculateMovingAverage(ctx context.Context, query *CalculateMovingAverageQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// CalculateSeasonality 计算季节性
|
||||||
|
CalculateSeasonality(ctx context.Context, query *CalculateSeasonalityQuery) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 数据导出 ================
|
||||||
|
|
||||||
|
// ExportData 导出数据
|
||||||
|
ExportData(ctx context.Context, cmd *ExportDataCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// ================ 定时任务 ================
|
||||||
|
|
||||||
|
// ProcessHourlyAggregation 处理小时级聚合
|
||||||
|
ProcessHourlyAggregation(ctx context.Context, date time.Time) error
|
||||||
|
|
||||||
|
// ProcessDailyAggregation 处理日级聚合
|
||||||
|
ProcessDailyAggregation(ctx context.Context, date time.Time) error
|
||||||
|
|
||||||
|
// ProcessWeeklyAggregation 处理周级聚合
|
||||||
|
ProcessWeeklyAggregation(ctx context.Context, date time.Time) error
|
||||||
|
|
||||||
|
// ProcessMonthlyAggregation 处理月级聚合
|
||||||
|
ProcessMonthlyAggregation(ctx context.Context, date time.Time) error
|
||||||
|
|
||||||
|
// CleanupExpiredData 清理过期数据
|
||||||
|
CleanupExpiredData(ctx context.Context) error
|
||||||
|
|
||||||
|
// ================ 管理员专用方法 ================
|
||||||
|
|
||||||
|
// AdminGetSystemStatistics 管理员获取系统统计
|
||||||
|
AdminGetSystemStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminTriggerAggregation 管理员触发数据聚合
|
||||||
|
AdminTriggerAggregation(ctx context.Context, cmd *TriggerAggregationCommand) (*CommandResponse, error)
|
||||||
|
|
||||||
|
// AdminGetUserStatistics 管理员获取单个用户统计
|
||||||
|
AdminGetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 管理员独立域统计接口 ================
|
||||||
|
|
||||||
|
// AdminGetUserDomainStatistics 管理员获取用户域统计
|
||||||
|
AdminGetUserDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetApiDomainStatistics 管理员获取API域统计
|
||||||
|
AdminGetApiDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetConsumptionDomainStatistics 管理员获取消费域统计
|
||||||
|
AdminGetConsumptionDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetRechargeDomainStatistics 管理员获取充值域统计
|
||||||
|
AdminGetRechargeDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 公开和用户统计方法 ================
|
||||||
|
|
||||||
|
// GetPublicStatistics 获取公开统计信息
|
||||||
|
GetPublicStatistics(ctx context.Context) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetUserStatistics 获取用户统计信息
|
||||||
|
GetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 独立统计接口 ================
|
||||||
|
|
||||||
|
// GetApiCallsStatistics 获取API调用统计
|
||||||
|
GetApiCallsStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetConsumptionStatistics 获取消费统计
|
||||||
|
GetConsumptionStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetRechargeStatistics 获取充值统计
|
||||||
|
GetRechargeStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// GetLatestProducts 获取最新产品推荐
|
||||||
|
GetLatestProducts(ctx context.Context, limit int) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// ================ 管理员排行榜接口 ================
|
||||||
|
|
||||||
|
// AdminGetUserCallRanking 获取用户调用排行榜
|
||||||
|
AdminGetUserCallRanking(ctx context.Context, rankingType, period string, limit int) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetRechargeRanking 获取充值排行榜
|
||||||
|
AdminGetRechargeRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetApiPopularityRanking 获取API受欢迎程度排行榜
|
||||||
|
AdminGetApiPopularityRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
|
||||||
|
|
||||||
|
// AdminGetTodayCertifiedEnterprises 获取今日认证企业列表
|
||||||
|
AdminGetTodayCertifiedEnterprises(ctx context.Context, limit int) (*QueryResponse, error)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,32 +6,38 @@ import (
|
|||||||
|
|
||||||
// Config 应用程序总配置
|
// Config 应用程序总配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
Redis RedisConfig `mapstructure:"redis"`
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
Cache CacheConfig `mapstructure:"cache"`
|
Cache CacheConfig `mapstructure:"cache"`
|
||||||
Logger LoggerConfig `mapstructure:"logger"`
|
Logger LoggerConfig `mapstructure:"logger"`
|
||||||
JWT JWTConfig `mapstructure:"jwt"`
|
JWT JWTConfig `mapstructure:"jwt"`
|
||||||
API APIConfig `mapstructure:"api"`
|
API APIConfig `mapstructure:"api"`
|
||||||
SMS SMSConfig `mapstructure:"sms"`
|
SMS SMSConfig `mapstructure:"sms"`
|
||||||
Email EmailConfig `mapstructure:"email"`
|
Email EmailConfig `mapstructure:"email"`
|
||||||
Storage StorageConfig `mapstructure:"storage"`
|
Storage StorageConfig `mapstructure:"storage"`
|
||||||
OCR OCRConfig `mapstructure:"ocr"`
|
OCR OCRConfig `mapstructure:"ocr"`
|
||||||
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
|
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
|
||||||
DailyRateLimit DailyRateLimitConfig `mapstructure:"daily_ratelimit"`
|
DailyRateLimit DailyRateLimitConfig `mapstructure:"daily_ratelimit"`
|
||||||
Monitoring MonitoringConfig `mapstructure:"monitoring"`
|
Monitoring MonitoringConfig `mapstructure:"monitoring"`
|
||||||
Health HealthConfig `mapstructure:"health"`
|
Health HealthConfig `mapstructure:"health"`
|
||||||
Resilience ResilienceConfig `mapstructure:"resilience"`
|
Resilience ResilienceConfig `mapstructure:"resilience"`
|
||||||
Development DevelopmentConfig `mapstructure:"development"`
|
Development DevelopmentConfig `mapstructure:"development"`
|
||||||
App AppConfig `mapstructure:"app"`
|
App AppConfig `mapstructure:"app"`
|
||||||
WechatWork WechatWorkConfig `mapstructure:"wechat_work"`
|
WechatWork WechatWorkConfig `mapstructure:"wechat_work"`
|
||||||
Esign EsignConfig `mapstructure:"esign"`
|
Esign EsignConfig `mapstructure:"esign"`
|
||||||
Wallet WalletConfig `mapstructure:"wallet"`
|
Wallet WalletConfig `mapstructure:"wallet"`
|
||||||
WestDex WestDexConfig `mapstructure:"westdex"`
|
WestDex WestDexConfig `mapstructure:"westdex"`
|
||||||
AliPay AliPayConfig `mapstructure:"alipay"`
|
Zhicha ZhichaConfig `mapstructure:"zhicha"`
|
||||||
Yushan YushanConfig `mapstructure:"yushan"`
|
Muzi MuziConfig `mapstructure:"muzi"`
|
||||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
AliPay AliPayConfig `mapstructure:"alipay"`
|
||||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
Wxpay WxpayConfig `mapstructure:"wxpay"`
|
||||||
|
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
|
||||||
|
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
|
||||||
|
Yushan YushanConfig `mapstructure:"yushan"`
|
||||||
|
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||||
|
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||||
|
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig HTTP服务器配置
|
// ServerConfig HTTP服务器配置
|
||||||
@@ -124,20 +130,24 @@ type DailyRateLimitConfig struct {
|
|||||||
MaxRequestsPerDay int `mapstructure:"max_requests_per_day"` // 每日最大请求次数
|
MaxRequestsPerDay int `mapstructure:"max_requests_per_day"` // 每日最大请求次数
|
||||||
MaxRequestsPerIP int `mapstructure:"max_requests_per_ip"` // 每个IP每日最大请求次数
|
MaxRequestsPerIP int `mapstructure:"max_requests_per_ip"` // 每个IP每日最大请求次数
|
||||||
KeyPrefix string `mapstructure:"key_prefix"` // Redis键前缀
|
KeyPrefix string `mapstructure:"key_prefix"` // Redis键前缀
|
||||||
TTL time.Duration `mapstructure:"ttl"` // 键过期时间
|
TTL time.Duration `mapstructure:"ttl"` // 键过期时间
|
||||||
// 新增安全配置
|
// 新增安全配置
|
||||||
EnableIPWhitelist bool `mapstructure:"enable_ip_whitelist"` // 是否启用IP白名单
|
EnableIPWhitelist bool `mapstructure:"enable_ip_whitelist"` // 是否启用IP白名单
|
||||||
IPWhitelist []string `mapstructure:"ip_whitelist"` // IP白名单
|
IPWhitelist []string `mapstructure:"ip_whitelist"` // IP白名单
|
||||||
EnableIPBlacklist bool `mapstructure:"enable_ip_blacklist"` // 是否启用IP黑名单
|
EnableIPBlacklist bool `mapstructure:"enable_ip_blacklist"` // 是否启用IP黑名单
|
||||||
IPBlacklist []string `mapstructure:"ip_blacklist"` // IP黑名单
|
IPBlacklist []string `mapstructure:"ip_blacklist"` // IP黑名单
|
||||||
EnableUserAgent bool `mapstructure:"enable_user_agent"` // 是否检查User-Agent
|
EnableUserAgent bool `mapstructure:"enable_user_agent"` // 是否检查User-Agent
|
||||||
BlockedUserAgents []string `mapstructure:"blocked_user_agents"` // 被阻止的User-Agent
|
BlockedUserAgents []string `mapstructure:"blocked_user_agents"` // 被阻止的User-Agent
|
||||||
EnableReferer bool `mapstructure:"enable_referer"` // 是否检查Referer
|
EnableReferer bool `mapstructure:"enable_referer"` // 是否检查Referer
|
||||||
AllowedReferers []string `mapstructure:"allowed_referers"` // 允许的Referer
|
AllowedReferers []string `mapstructure:"allowed_referers"` // 允许的Referer
|
||||||
EnableGeoBlock bool `mapstructure:"enable_geo_block"` // 是否启用地理位置阻止
|
EnableGeoBlock bool `mapstructure:"enable_geo_block"` // 是否启用地理位置阻止
|
||||||
BlockedCountries []string `mapstructure:"blocked_countries"` // 被阻止的国家/地区
|
BlockedCountries []string `mapstructure:"blocked_countries"` // 被阻止的国家/地区
|
||||||
EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理
|
EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理
|
||||||
MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数
|
MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数
|
||||||
|
// 路径排除配置
|
||||||
|
ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径
|
||||||
|
// 域名排除配置
|
||||||
|
ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonitoringConfig 监控配置
|
// MonitoringConfig 监控配置
|
||||||
@@ -210,14 +220,14 @@ type SMSRateLimit struct {
|
|||||||
|
|
||||||
// EmailConfig 邮件服务配置
|
// EmailConfig 邮件服务配置
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
Host string `mapstructure:"host"` // SMTP服务器地址
|
Host string `mapstructure:"host"` // SMTP服务器地址
|
||||||
Port int `mapstructure:"port"` // SMTP服务器端口
|
Port int `mapstructure:"port"` // SMTP服务器端口
|
||||||
Username string `mapstructure:"username"` // 邮箱用户名
|
Username string `mapstructure:"username"` // 邮箱用户名
|
||||||
Password string `mapstructure:"password"` // 邮箱密码/授权码
|
Password string `mapstructure:"password"` // 邮箱密码/授权码
|
||||||
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
|
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
|
||||||
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
|
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
|
||||||
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
|
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
|
||||||
Domain string `mapstructure:"domain"` // 控制台域名
|
Domain string `mapstructure:"domain"` // 控制台域名
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDSN 获取数据库DSN连接字符串
|
// GetDSN 获取数据库DSN连接字符串
|
||||||
@@ -313,6 +323,14 @@ type WalletConfig struct {
|
|||||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||||
|
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceAlertConfig 余额预警配置
|
||||||
|
type BalanceAlertConfig struct {
|
||||||
|
DefaultEnabled bool `mapstructure:"default_enabled"` // 默认启用余额预警
|
||||||
|
DefaultThreshold float64 `mapstructure:"default_threshold"` // 默认预警阈值
|
||||||
|
AlertCooldownHours int `mapstructure:"alert_cooldown_hours"` // 预警冷却时间(小时)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AliPayRechargeBonusRule 支付宝充值赠送规则
|
// AliPayRechargeBonusRule 支付宝充值赠送规则
|
||||||
@@ -321,12 +339,87 @@ type AliPayRechargeBonusRule struct {
|
|||||||
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
|
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
|
||||||
}
|
}
|
||||||
|
|
||||||
// WestDexConfig WestDex配置
|
// WestDexConfig 西部数据配置
|
||||||
type WestDexConfig struct {
|
type WestDexConfig struct {
|
||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
Key string `mapstructure:"key"`
|
Key string `mapstructure:"key"`
|
||||||
SecretId string `mapstructure:"secret_id"`
|
SecretID string `mapstructure:"secret_id"`
|
||||||
SecretSecondId string `mapstructure:"secret_second_id"`
|
SecretSecondID string `mapstructure:"secret_second_id"`
|
||||||
|
|
||||||
|
// 西部数据日志配置
|
||||||
|
Logging WestDexLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WestDexLoggingConfig 西部数据日志配置
|
||||||
|
type WestDexLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]WestDexLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WestDexLevelFileConfig 西部数据级别文件配置
|
||||||
|
type WestDexLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZhichaConfig 智查金控配置
|
||||||
|
type ZhichaConfig struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
AppSecret string `mapstructure:"app_secret"`
|
||||||
|
EncryptKey string `mapstructure:"encrypt_key"`
|
||||||
|
|
||||||
|
// 智查金控日志配置
|
||||||
|
Logging ZhichaLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZhichaLoggingConfig 智查金控日志配置
|
||||||
|
type ZhichaLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]ZhichaLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZhichaLevelFileConfig 智查金控级别文件配置
|
||||||
|
type ZhichaLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuziConfig 木子数据配置
|
||||||
|
type MuziConfig struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
AppSecret string `mapstructure:"app_secret"`
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
|
||||||
|
Logging MuziLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuziLoggingConfig 木子数据日志配置
|
||||||
|
type MuziLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]MuziLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuziLevelFileConfig 木子数据日志级别配置
|
||||||
|
type MuziLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AliPayConfig 支付宝配置
|
// AliPayConfig 支付宝配置
|
||||||
@@ -339,11 +432,54 @@ type AliPayConfig struct {
|
|||||||
ReturnURL string `mapstructure:"return_url"`
|
ReturnURL string `mapstructure:"return_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WxpayConfig 微信支付配置
|
||||||
|
type WxpayConfig struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
MchID string `mapstructure:"mch_id"`
|
||||||
|
MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"`
|
||||||
|
MchApiv3Key string `mapstructure:"mch_apiv3_key"`
|
||||||
|
MchPrivateKeyPath string `mapstructure:"mch_private_key_path"`
|
||||||
|
MchPublicKeyID string `mapstructure:"mch_public_key_id"`
|
||||||
|
MchPublicKeyPath string `mapstructure:"mch_public_key_path"`
|
||||||
|
NotifyUrl string `mapstructure:"notify_url"`
|
||||||
|
RefundNotifyUrl string `mapstructure:"refund_notify_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WechatMiniConfig 微信小程序配置
|
||||||
|
type WechatMiniConfig struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WechatH5Config 微信H5配置
|
||||||
|
type WechatH5Config struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// YushanConfig 羽山配置
|
// YushanConfig 羽山配置
|
||||||
type YushanConfig struct {
|
type YushanConfig struct {
|
||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
AcctID string `mapstructure:"acct_id"`
|
AcctID string `mapstructure:"acct_id"`
|
||||||
|
|
||||||
|
// 羽山日志配置
|
||||||
|
Logging YushanLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// YushanLoggingConfig 羽山日志配置
|
||||||
|
type YushanLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]YushanLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// YushanLevelFileConfig 羽山级别文件配置
|
||||||
|
type YushanLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TianYanChaConfig 天眼查配置
|
// TianYanChaConfig 天眼查配置
|
||||||
@@ -357,6 +493,33 @@ type AlicloudConfig struct {
|
|||||||
AppCode string `mapstructure:"app_code"`
|
AppCode string `mapstructure:"app_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XingweiConfig 行为数据配置
|
||||||
|
type XingweiConfig struct {
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
ApiID string `mapstructure:"api_id"`
|
||||||
|
ApiKey string `mapstructure:"api_key"`
|
||||||
|
|
||||||
|
// 行为数据日志配置
|
||||||
|
Logging XingweiLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XingweiLoggingConfig 行为数据日志配置
|
||||||
|
type XingweiLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]XingweiLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XingweiLevelFileConfig 行为数据级别文件配置
|
||||||
|
type XingweiLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
// DomainConfig 域名配置
|
// DomainConfig 域名配置
|
||||||
type DomainConfig struct {
|
type DomainConfig struct {
|
||||||
API string `mapstructure:"api"` // API域名
|
API string `mapstructure:"api"` // API域名
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -9,37 +12,54 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"tyapi-server/internal/application/article"
|
||||||
"tyapi-server/internal/application/certification"
|
"tyapi-server/internal/application/certification"
|
||||||
"tyapi-server/internal/application/finance"
|
"tyapi-server/internal/application/finance"
|
||||||
"tyapi-server/internal/application/product"
|
"tyapi-server/internal/application/product"
|
||||||
|
"tyapi-server/internal/application/statistics"
|
||||||
"tyapi-server/internal/application/user"
|
"tyapi-server/internal/application/user"
|
||||||
"tyapi-server/internal/config"
|
"tyapi-server/internal/config"
|
||||||
|
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||||
|
domain_article_repo "tyapi-server/internal/domains/article/repositories"
|
||||||
|
article_service "tyapi-server/internal/domains/article/services"
|
||||||
domain_certification_repo "tyapi-server/internal/domains/certification/repositories"
|
domain_certification_repo "tyapi-server/internal/domains/certification/repositories"
|
||||||
certification_service "tyapi-server/internal/domains/certification/services"
|
certification_service "tyapi-server/internal/domains/certification/services"
|
||||||
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||||
finance_service "tyapi-server/internal/domains/finance/services"
|
finance_service "tyapi-server/internal/domains/finance/services"
|
||||||
domain_product_repo "tyapi-server/internal/domains/product/repositories"
|
domain_product_repo "tyapi-server/internal/domains/product/repositories"
|
||||||
product_service "tyapi-server/internal/domains/product/services"
|
product_service "tyapi-server/internal/domains/product/services"
|
||||||
|
statistics_service "tyapi-server/internal/domains/statistics/services"
|
||||||
user_service "tyapi-server/internal/domains/user/services"
|
user_service "tyapi-server/internal/domains/user/services"
|
||||||
"tyapi-server/internal/infrastructure/cache"
|
"tyapi-server/internal/infrastructure/cache"
|
||||||
"tyapi-server/internal/infrastructure/database"
|
"tyapi-server/internal/infrastructure/database"
|
||||||
|
article_repo "tyapi-server/internal/infrastructure/database/repositories/article"
|
||||||
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
|
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
|
||||||
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
|
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
|
||||||
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
|
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
|
||||||
infra_events "tyapi-server/internal/infrastructure/events"
|
infra_events "tyapi-server/internal/infrastructure/events"
|
||||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||||
"tyapi-server/internal/infrastructure/external/email"
|
"tyapi-server/internal/infrastructure/external/email"
|
||||||
|
"tyapi-server/internal/infrastructure/external/muzi"
|
||||||
"tyapi-server/internal/infrastructure/external/ocr"
|
"tyapi-server/internal/infrastructure/external/ocr"
|
||||||
"tyapi-server/internal/infrastructure/external/sms"
|
"tyapi-server/internal/infrastructure/external/sms"
|
||||||
"tyapi-server/internal/infrastructure/external/storage"
|
"tyapi-server/internal/infrastructure/external/storage"
|
||||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||||
"tyapi-server/internal/infrastructure/external/westdex"
|
"tyapi-server/internal/infrastructure/external/westdex"
|
||||||
|
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||||
"tyapi-server/internal/infrastructure/external/yushan"
|
"tyapi-server/internal/infrastructure/external/yushan"
|
||||||
|
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||||
"tyapi-server/internal/infrastructure/http/handlers"
|
"tyapi-server/internal/infrastructure/http/handlers"
|
||||||
"tyapi-server/internal/infrastructure/http/routes"
|
"tyapi-server/internal/infrastructure/http/routes"
|
||||||
|
"tyapi-server/internal/infrastructure/task"
|
||||||
|
task_implementations "tyapi-server/internal/infrastructure/task/implementations"
|
||||||
|
asynq "tyapi-server/internal/infrastructure/task/implementations/asynq"
|
||||||
|
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||||
|
task_repositories "tyapi-server/internal/infrastructure/task/repositories"
|
||||||
|
component_report "tyapi-server/internal/shared/component_report"
|
||||||
shared_database "tyapi-server/internal/shared/database"
|
shared_database "tyapi-server/internal/shared/database"
|
||||||
"tyapi-server/internal/shared/esign"
|
"tyapi-server/internal/shared/esign"
|
||||||
"tyapi-server/internal/shared/events"
|
shared_events "tyapi-server/internal/shared/events"
|
||||||
|
"tyapi-server/internal/shared/export"
|
||||||
"tyapi-server/internal/shared/health"
|
"tyapi-server/internal/shared/health"
|
||||||
"tyapi-server/internal/shared/hooks"
|
"tyapi-server/internal/shared/hooks"
|
||||||
sharedhttp "tyapi-server/internal/shared/http"
|
sharedhttp "tyapi-server/internal/shared/http"
|
||||||
@@ -49,6 +69,7 @@ import (
|
|||||||
"tyapi-server/internal/shared/middleware"
|
"tyapi-server/internal/shared/middleware"
|
||||||
sharedOCR "tyapi-server/internal/shared/ocr"
|
sharedOCR "tyapi-server/internal/shared/ocr"
|
||||||
"tyapi-server/internal/shared/payment"
|
"tyapi-server/internal/shared/payment"
|
||||||
|
"tyapi-server/internal/shared/pdf"
|
||||||
"tyapi-server/internal/shared/resilience"
|
"tyapi-server/internal/shared/resilience"
|
||||||
"tyapi-server/internal/shared/saga"
|
"tyapi-server/internal/shared/saga"
|
||||||
"tyapi-server/internal/shared/tracing"
|
"tyapi-server/internal/shared/tracing"
|
||||||
@@ -57,12 +78,18 @@ import (
|
|||||||
domain_user_repo "tyapi-server/internal/domains/user/repositories"
|
domain_user_repo "tyapi-server/internal/domains/user/repositories"
|
||||||
user_repo "tyapi-server/internal/infrastructure/database/repositories/user"
|
user_repo "tyapi-server/internal/infrastructure/database/repositories/user"
|
||||||
|
|
||||||
|
hibiken_asynq "github.com/hibiken/asynq"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
api_app "tyapi-server/internal/application/api"
|
api_app "tyapi-server/internal/application/api"
|
||||||
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||||
api_service "tyapi-server/internal/domains/api/services"
|
api_services "tyapi-server/internal/domains/api/services"
|
||||||
|
finance_services "tyapi-server/internal/domains/finance/services"
|
||||||
|
product_services "tyapi-server/internal/domains/product/services"
|
||||||
|
domain_statistics_repo "tyapi-server/internal/domains/statistics/repositories"
|
||||||
|
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||||
api_repo "tyapi-server/internal/infrastructure/database/repositories/api"
|
api_repo "tyapi-server/internal/infrastructure/database/repositories/api"
|
||||||
|
statistics_repo "tyapi-server/internal/infrastructure/database/repositories/statistics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Container 应用容器
|
// Container 应用容器
|
||||||
@@ -82,37 +109,44 @@ func NewContainer() *Container {
|
|||||||
fx.Provide(
|
fx.Provide(
|
||||||
// 日志器 - 提供自定义Logger和*zap.Logger
|
// 日志器 - 提供自定义Logger和*zap.Logger
|
||||||
func(cfg *config.Config) (logger.Logger, error) {
|
func(cfg *config.Config) (logger.Logger, error) {
|
||||||
|
// 将 config.LoggerConfig 转换为 logger.Config
|
||||||
|
// 转换 LevelConfigs 类型
|
||||||
|
levelConfigs := make(map[string]interface{})
|
||||||
|
for key, value := range cfg.Logger.LevelConfigs {
|
||||||
|
levelConfigs[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
logCfg := logger.Config{
|
||||||
|
Level: cfg.Logger.Level,
|
||||||
|
Format: cfg.Logger.Format,
|
||||||
|
Output: cfg.Logger.Output,
|
||||||
|
LogDir: cfg.Logger.LogDir,
|
||||||
|
MaxSize: cfg.Logger.MaxSize,
|
||||||
|
MaxBackups: cfg.Logger.MaxBackups,
|
||||||
|
MaxAge: cfg.Logger.MaxAge,
|
||||||
|
Compress: cfg.Logger.Compress,
|
||||||
|
UseDaily: cfg.Logger.UseDaily,
|
||||||
|
UseColor: cfg.Logger.UseColor,
|
||||||
|
EnableLevelSeparation: cfg.Logger.EnableLevelSeparation,
|
||||||
|
LevelConfigs: levelConfigs,
|
||||||
|
Development: cfg.App.Env == "development",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化全局日志器
|
||||||
|
if err := logger.InitGlobalLogger(logCfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Logger.EnableLevelSeparation {
|
if cfg.Logger.EnableLevelSeparation {
|
||||||
// 使用按级别分文件的日志器
|
// 使用按级别分文件的日志器
|
||||||
levelConfig := logger.LevelLoggerConfig{
|
levelConfig := logger.LevelLoggerConfig{
|
||||||
BaseConfig: logger.Config{
|
BaseConfig: logCfg,
|
||||||
Level: cfg.Logger.Level,
|
|
||||||
Format: cfg.Logger.Format,
|
|
||||||
Output: cfg.Logger.Output,
|
|
||||||
LogDir: cfg.Logger.LogDir,
|
|
||||||
MaxSize: cfg.Logger.MaxSize,
|
|
||||||
MaxBackups: cfg.Logger.MaxBackups,
|
|
||||||
MaxAge: cfg.Logger.MaxAge,
|
|
||||||
Compress: cfg.Logger.Compress,
|
|
||||||
UseDaily: cfg.Logger.UseDaily,
|
|
||||||
},
|
|
||||||
EnableLevelSeparation: true,
|
EnableLevelSeparation: true,
|
||||||
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
|
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
|
||||||
}
|
}
|
||||||
return logger.NewLevelLogger(levelConfig)
|
return logger.NewLevelLogger(levelConfig)
|
||||||
} else {
|
} else {
|
||||||
// 使用普通日志器
|
// 使用普通日志器
|
||||||
logCfg := logger.Config{
|
|
||||||
Level: cfg.Logger.Level,
|
|
||||||
Format: cfg.Logger.Format,
|
|
||||||
Output: cfg.Logger.Output,
|
|
||||||
LogDir: cfg.Logger.LogDir,
|
|
||||||
MaxSize: cfg.Logger.MaxSize,
|
|
||||||
MaxBackups: cfg.Logger.MaxBackups,
|
|
||||||
MaxAge: cfg.Logger.MaxAge,
|
|
||||||
Compress: cfg.Logger.Compress,
|
|
||||||
UseDaily: cfg.Logger.UseDaily,
|
|
||||||
}
|
|
||||||
return logger.NewLogger(logCfg)
|
return logger.NewLogger(logCfg)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -130,9 +164,8 @@ func NewContainer() *Container {
|
|||||||
return infoLogger
|
return infoLogger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果类型转换失败,创建一个默认的zap logger
|
// 如果类型转换失败,使用全局日志器
|
||||||
defaultLogger, _ := zap.NewProduction()
|
return logger.GetGlobalLogger()
|
||||||
return defaultLogger
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -184,7 +217,7 @@ func NewContainer() *Container {
|
|||||||
return 5 // 默认5个工作协程
|
return 5 // 默认5个工作协程
|
||||||
},
|
},
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
events.NewMemoryEventBus,
|
shared_events.NewMemoryEventBus,
|
||||||
fx.As(new(interfaces.EventBus)),
|
fx.As(new(interfaces.EventBus)),
|
||||||
),
|
),
|
||||||
// 健康检查
|
// 健康检查
|
||||||
@@ -275,6 +308,20 @@ func NewContainer() *Container {
|
|||||||
}
|
}
|
||||||
return payment.NewAliPayService(config)
|
return payment.NewAliPayService(config)
|
||||||
},
|
},
|
||||||
|
// 微信支付服务
|
||||||
|
func(cfg *config.Config, logger *zap.Logger) *payment.WechatPayService {
|
||||||
|
// 根据配置选择初始化方式,默认使用平台证书方式
|
||||||
|
initType := payment.InitTypePlatformCert
|
||||||
|
// 如果配置了公钥ID,使用公钥方式
|
||||||
|
if cfg.Wxpay.MchPublicKeyID != "" {
|
||||||
|
initType = payment.InitTypeWxPayPubKey
|
||||||
|
}
|
||||||
|
return payment.NewWechatPayService(*cfg, initType, logger)
|
||||||
|
},
|
||||||
|
// 导出管理器
|
||||||
|
func(logger *zap.Logger) *export.ExportManager {
|
||||||
|
return export.NewExportManager(logger)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 高级特性模块
|
// 高级特性模块
|
||||||
@@ -304,19 +351,27 @@ func NewContainer() *Container {
|
|||||||
sharedhttp.NewResponseBuilder,
|
sharedhttp.NewResponseBuilder,
|
||||||
validator.NewRequestValidator,
|
validator.NewRequestValidator,
|
||||||
// WestDexService - 需要从配置中获取参数
|
// WestDexService - 需要从配置中获取参数
|
||||||
func(cfg *config.Config) *westdex.WestDexService {
|
func(cfg *config.Config) (*westdex.WestDexService, error) {
|
||||||
return westdex.NewWestDexService(
|
return westdex.NewWestDexServiceWithConfig(cfg)
|
||||||
cfg.WestDex.URL,
|
},
|
||||||
cfg.WestDex.Key,
|
// MuziService - 木子数据服务
|
||||||
cfg.WestDex.SecretId,
|
func(cfg *config.Config) (*muzi.MuziService, error) {
|
||||||
cfg.WestDex.SecretSecondId,
|
return muzi.NewMuziServiceWithConfig(cfg)
|
||||||
)
|
},
|
||||||
|
// ZhichaService - 智查金控服务
|
||||||
|
func(cfg *config.Config) (*zhicha.ZhichaService, error) {
|
||||||
|
return zhicha.NewZhichaServiceWithConfig(cfg)
|
||||||
|
},
|
||||||
|
// XingweiService - 行为数据服务
|
||||||
|
func(cfg *config.Config) (*xingwei.XingweiService, error) {
|
||||||
|
return xingwei.NewXingweiServiceWithConfig(cfg)
|
||||||
},
|
},
|
||||||
func(cfg *config.Config) *yushan.YushanService {
|
func(cfg *config.Config) *yushan.YushanService {
|
||||||
return yushan.NewYushanService(
|
return yushan.NewYushanService(
|
||||||
cfg.Yushan.URL,
|
cfg.Yushan.URL,
|
||||||
cfg.Yushan.APIKey,
|
cfg.Yushan.APIKey,
|
||||||
cfg.Yushan.AcctID,
|
cfg.Yushan.AcctID,
|
||||||
|
nil, // 暂时不传入logger,使用无日志版本
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// TianYanChaService - 天眼查服务
|
// TianYanChaService - 天眼查服务
|
||||||
@@ -350,8 +405,8 @@ func NewContainer() *Container {
|
|||||||
MaxRequestsPerDay: cfg.DailyRateLimit.MaxRequestsPerDay,
|
MaxRequestsPerDay: cfg.DailyRateLimit.MaxRequestsPerDay,
|
||||||
MaxRequestsPerIP: cfg.DailyRateLimit.MaxRequestsPerIP,
|
MaxRequestsPerIP: cfg.DailyRateLimit.MaxRequestsPerIP,
|
||||||
KeyPrefix: cfg.DailyRateLimit.KeyPrefix,
|
KeyPrefix: cfg.DailyRateLimit.KeyPrefix,
|
||||||
TTL: cfg.DailyRateLimit.TTL,
|
TTL: cfg.DailyRateLimit.TTL,
|
||||||
MaxConcurrent: cfg.DailyRateLimit.MaxConcurrent,
|
MaxConcurrent: cfg.DailyRateLimit.MaxConcurrent,
|
||||||
// 安全配置
|
// 安全配置
|
||||||
EnableIPWhitelist: cfg.DailyRateLimit.EnableIPWhitelist,
|
EnableIPWhitelist: cfg.DailyRateLimit.EnableIPWhitelist,
|
||||||
IPWhitelist: cfg.DailyRateLimit.IPWhitelist,
|
IPWhitelist: cfg.DailyRateLimit.IPWhitelist,
|
||||||
@@ -364,6 +419,10 @@ func NewContainer() *Container {
|
|||||||
EnableGeoBlock: cfg.DailyRateLimit.EnableGeoBlock,
|
EnableGeoBlock: cfg.DailyRateLimit.EnableGeoBlock,
|
||||||
BlockedCountries: cfg.DailyRateLimit.BlockedCountries,
|
BlockedCountries: cfg.DailyRateLimit.BlockedCountries,
|
||||||
EnableProxyCheck: cfg.DailyRateLimit.EnableProxyCheck,
|
EnableProxyCheck: cfg.DailyRateLimit.EnableProxyCheck,
|
||||||
|
// 排除路径配置
|
||||||
|
ExcludePaths: cfg.DailyRateLimit.ExcludePaths,
|
||||||
|
// 排除域名配置
|
||||||
|
ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains,
|
||||||
}
|
}
|
||||||
return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig)
|
return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig)
|
||||||
},
|
},
|
||||||
@@ -464,6 +523,11 @@ func NewContainer() *Container {
|
|||||||
finance_repo.NewGormAlipayOrderRepository,
|
finance_repo.NewGormAlipayOrderRepository,
|
||||||
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
||||||
),
|
),
|
||||||
|
// 微信订单仓储
|
||||||
|
fx.Annotate(
|
||||||
|
finance_repo.NewGormWechatOrderRepository,
|
||||||
|
fx.As(new(domain_finance_repo.WechatOrderRepository)),
|
||||||
|
),
|
||||||
// 发票申请仓储
|
// 发票申请仓储
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
finance_repo.NewGormInvoiceApplicationRepository,
|
finance_repo.NewGormInvoiceApplicationRepository,
|
||||||
@@ -502,6 +566,50 @@ func NewContainer() *Container {
|
|||||||
product_repo.NewGormProductDocumentationRepository,
|
product_repo.NewGormProductDocumentationRepository,
|
||||||
fx.As(new(domain_product_repo.ProductDocumentationRepository)),
|
fx.As(new(domain_product_repo.ProductDocumentationRepository)),
|
||||||
),
|
),
|
||||||
|
// 组件报告下载记录仓储
|
||||||
|
fx.Annotate(
|
||||||
|
product_repo.NewGormComponentReportRepository,
|
||||||
|
fx.As(new(domain_product_repo.ComponentReportRepository)),
|
||||||
|
),
|
||||||
|
// UI组件仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
product_repo.NewGormUIComponentRepository,
|
||||||
|
fx.As(new(domain_product_repo.UIComponentRepository)),
|
||||||
|
),
|
||||||
|
// 产品UI组件关联仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
product_repo.NewGormProductUIComponentRepository,
|
||||||
|
fx.As(new(domain_product_repo.ProductUIComponentRepository)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 仓储层 - 文章域
|
||||||
|
fx.Provide(
|
||||||
|
// 文章仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormArticleRepository,
|
||||||
|
fx.As(new(domain_article_repo.ArticleRepository)),
|
||||||
|
),
|
||||||
|
// 分类仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormCategoryRepository,
|
||||||
|
fx.As(new(domain_article_repo.CategoryRepository)),
|
||||||
|
),
|
||||||
|
// 标签仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormTagRepository,
|
||||||
|
fx.As(new(domain_article_repo.TagRepository)),
|
||||||
|
),
|
||||||
|
// 定时任务仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormScheduledTaskRepository,
|
||||||
|
fx.As(new(domain_article_repo.ScheduledTaskRepository)),
|
||||||
|
),
|
||||||
|
// 公告仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormAnnouncementRepository,
|
||||||
|
fx.As(new(domain_article_repo.AnnouncementRepository)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// API域仓储层
|
// API域仓储层
|
||||||
@@ -516,6 +624,22 @@ func NewContainer() *Container {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 统计域仓储层
|
||||||
|
fx.Provide(
|
||||||
|
fx.Annotate(
|
||||||
|
statistics_repo.NewGormStatisticsRepository,
|
||||||
|
fx.As(new(domain_statistics_repo.StatisticsRepository)),
|
||||||
|
),
|
||||||
|
fx.Annotate(
|
||||||
|
statistics_repo.NewGormStatisticsReportRepository,
|
||||||
|
fx.As(new(domain_statistics_repo.StatisticsReportRepository)),
|
||||||
|
),
|
||||||
|
fx.Annotate(
|
||||||
|
statistics_repo.NewGormStatisticsDashboardRepository,
|
||||||
|
fx.As(new(domain_statistics_repo.StatisticsDashboardRepository)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// 领域服务
|
// 领域服务
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
@@ -528,6 +652,25 @@ func NewContainer() *Container {
|
|||||||
product_service.NewProductSubscriptionService,
|
product_service.NewProductSubscriptionService,
|
||||||
product_service.NewProductApiConfigService,
|
product_service.NewProductApiConfigService,
|
||||||
product_service.NewProductDocumentationService,
|
product_service.NewProductDocumentationService,
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
apiUserRepo api_repositories.ApiUserRepository,
|
||||||
|
userRepo user_repositories.UserRepository,
|
||||||
|
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||||
|
smsService *sms.AliSMSService,
|
||||||
|
config *config.Config,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) finance_service.BalanceAlertService {
|
||||||
|
return finance_service.NewBalanceAlertService(
|
||||||
|
apiUserRepo,
|
||||||
|
userRepo,
|
||||||
|
enterpriseInfoRepo,
|
||||||
|
smsService,
|
||||||
|
config,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
finance_service.NewWalletAggregateService,
|
finance_service.NewWalletAggregateService,
|
||||||
finance_service.NewRechargeRecordService,
|
finance_service.NewRechargeRecordService,
|
||||||
// 发票领域服务
|
// 发票领域服务
|
||||||
@@ -569,18 +712,128 @@ func NewContainer() *Container {
|
|||||||
infra_events.NewInvoiceEventHandler,
|
infra_events.NewInvoiceEventHandler,
|
||||||
certification_service.NewCertificationAggregateService,
|
certification_service.NewCertificationAggregateService,
|
||||||
certification_service.NewEnterpriseInfoSubmitRecordService,
|
certification_service.NewEnterpriseInfoSubmitRecordService,
|
||||||
|
// 文章领域服务
|
||||||
|
article_service.NewArticleService,
|
||||||
|
// 公告领域服务
|
||||||
|
article_service.NewAnnouncementService,
|
||||||
|
// 统计领域服务
|
||||||
|
statistics_service.NewStatisticsAggregateService,
|
||||||
|
statistics_service.NewStatisticsCalculationService,
|
||||||
|
statistics_service.NewStatisticsReportService,
|
||||||
),
|
),
|
||||||
|
|
||||||
// API域服务层
|
// API域服务层
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
api_service.NewApiUserAggregateService,
|
fx.Annotate(
|
||||||
api_service.NewApiCallAggregateService,
|
api_services.NewApiUserAggregateService,
|
||||||
api_service.NewApiRequestService,
|
),
|
||||||
|
api_services.NewApiCallAggregateService,
|
||||||
|
api_services.NewApiRequestService,
|
||||||
|
api_services.NewFormConfigService,
|
||||||
),
|
),
|
||||||
|
|
||||||
// API域应用服务
|
// API域应用服务
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
api_app.NewApiApplicationService,
|
// API应用服务 - 绑定到接口
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
apiCallService api_services.ApiCallAggregateService,
|
||||||
|
apiUserService api_services.ApiUserAggregateService,
|
||||||
|
apiRequestService *api_services.ApiRequestService,
|
||||||
|
formConfigService api_services.FormConfigService,
|
||||||
|
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||||
|
productManagementService *product_services.ProductManagementService,
|
||||||
|
userRepo user_repositories.UserRepository,
|
||||||
|
txManager *shared_database.TransactionManager,
|
||||||
|
config *config.Config,
|
||||||
|
logger *zap.Logger,
|
||||||
|
contractInfoService user_repositories.ContractInfoRepository,
|
||||||
|
taskManager task_interfaces.TaskManager,
|
||||||
|
walletService finance_services.WalletAggregateService,
|
||||||
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
|
exportManager *export.ExportManager,
|
||||||
|
balanceAlertService finance_services.BalanceAlertService,
|
||||||
|
) api_app.ApiApplicationService {
|
||||||
|
return api_app.NewApiApplicationService(
|
||||||
|
apiCallService,
|
||||||
|
apiUserService,
|
||||||
|
apiRequestService,
|
||||||
|
formConfigService,
|
||||||
|
apiCallRepository,
|
||||||
|
productManagementService,
|
||||||
|
userRepo,
|
||||||
|
txManager,
|
||||||
|
config,
|
||||||
|
logger,
|
||||||
|
contractInfoService,
|
||||||
|
taskManager,
|
||||||
|
walletService,
|
||||||
|
subscriptionService,
|
||||||
|
exportManager,
|
||||||
|
balanceAlertService,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fx.As(new(api_app.ApiApplicationService)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 任务系统
|
||||||
|
fx.Provide(
|
||||||
|
// Asynq 客户端 (github.com/hibiken/asynq)
|
||||||
|
func(cfg *config.Config) *hibiken_asynq.Client {
|
||||||
|
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
return hibiken_asynq.NewClient(hibiken_asynq.RedisClientOpt{Addr: redisAddr})
|
||||||
|
},
|
||||||
|
// 自定义Asynq客户端 (用于文章任务)
|
||||||
|
func(cfg *config.Config, scheduledTaskRepo domain_article_repo.ScheduledTaskRepository, logger *zap.Logger) *asynq.AsynqClient {
|
||||||
|
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
return task.NewAsynqClient(redisAddr, scheduledTaskRepo, logger)
|
||||||
|
},
|
||||||
|
// 文章任务队列
|
||||||
|
func(cfg *config.Config, logger *zap.Logger) task_interfaces.ArticleTaskQueue {
|
||||||
|
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
return task.NewArticleTaskQueue(redisAddr, logger)
|
||||||
|
},
|
||||||
|
// AsyncTask 仓库
|
||||||
|
task_repositories.NewAsyncTaskRepository,
|
||||||
|
// TaskManager - 统一任务管理器
|
||||||
|
func(
|
||||||
|
asynqClient *hibiken_asynq.Client,
|
||||||
|
asyncTaskRepo task_repositories.AsyncTaskRepository,
|
||||||
|
logger *zap.Logger,
|
||||||
|
config *config.Config,
|
||||||
|
) task_interfaces.TaskManager {
|
||||||
|
taskConfig := &task_interfaces.TaskManagerConfig{
|
||||||
|
RedisAddr: fmt.Sprintf("%s:%s", config.Redis.Host, config.Redis.Port),
|
||||||
|
MaxRetries: 5,
|
||||||
|
RetryInterval: 5 * time.Minute,
|
||||||
|
CleanupDays: 30,
|
||||||
|
}
|
||||||
|
return task_implementations.NewTaskManager(asynqClient, asyncTaskRepo, logger, taskConfig)
|
||||||
|
},
|
||||||
|
// AsynqWorker - 任务处理器
|
||||||
|
func(
|
||||||
|
cfg *config.Config,
|
||||||
|
logger *zap.Logger,
|
||||||
|
articleApplicationService article.ArticleApplicationService,
|
||||||
|
announcementApplicationService article.AnnouncementApplicationService,
|
||||||
|
apiApplicationService api_app.ApiApplicationService,
|
||||||
|
walletService finance_services.WalletAggregateService,
|
||||||
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
|
asyncTaskRepo task_repositories.AsyncTaskRepository,
|
||||||
|
) *asynq.AsynqWorker {
|
||||||
|
redisAddr := fmt.Sprintf("%s:%s", cfg.Redis.Host, cfg.Redis.Port)
|
||||||
|
return asynq.NewAsynqWorker(
|
||||||
|
redisAddr,
|
||||||
|
logger,
|
||||||
|
articleApplicationService,
|
||||||
|
announcementApplicationService,
|
||||||
|
apiApplicationService,
|
||||||
|
walletService,
|
||||||
|
subscriptionService,
|
||||||
|
asyncTaskRepo,
|
||||||
|
)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 应用服务
|
// 应用服务
|
||||||
@@ -592,12 +845,78 @@ func NewContainer() *Container {
|
|||||||
),
|
),
|
||||||
// 认证应用服务 - 绑定到接口
|
// 认证应用服务 - 绑定到接口
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
certification.NewCertificationApplicationService,
|
func(
|
||||||
|
aggregateService certification_service.CertificationAggregateService,
|
||||||
|
userAggregateService user_service.UserAggregateService,
|
||||||
|
queryRepository domain_certification_repo.CertificationQueryRepository,
|
||||||
|
enterpriseInfoSubmitRecordRepo domain_certification_repo.EnterpriseInfoSubmitRecordRepository,
|
||||||
|
smsCodeService *user_service.SMSCodeService,
|
||||||
|
esignClient *esign.Client,
|
||||||
|
esignConfig *esign.Config,
|
||||||
|
qiniuStorageService *storage.QiNiuStorageService,
|
||||||
|
contractAggregateService user_service.ContractAggregateService,
|
||||||
|
walletAggregateService finance_services.WalletAggregateService,
|
||||||
|
apiUserAggregateService api_services.ApiUserAggregateService,
|
||||||
|
enterpriseInfoSubmitRecordService *certification_service.EnterpriseInfoSubmitRecordService,
|
||||||
|
ocrService sharedOCR.OCRService,
|
||||||
|
txManager *shared_database.TransactionManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) certification.CertificationApplicationService {
|
||||||
|
return certification.NewCertificationApplicationService(
|
||||||
|
aggregateService,
|
||||||
|
userAggregateService,
|
||||||
|
queryRepository,
|
||||||
|
enterpriseInfoSubmitRecordRepo,
|
||||||
|
smsCodeService,
|
||||||
|
esignClient,
|
||||||
|
esignConfig,
|
||||||
|
qiniuStorageService,
|
||||||
|
contractAggregateService,
|
||||||
|
walletAggregateService,
|
||||||
|
apiUserAggregateService,
|
||||||
|
enterpriseInfoSubmitRecordService,
|
||||||
|
ocrService,
|
||||||
|
txManager,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
fx.As(new(certification.CertificationApplicationService)),
|
fx.As(new(certification.CertificationApplicationService)),
|
||||||
),
|
),
|
||||||
// 财务应用服务 - 绑定到接口
|
// 财务应用服务 - 绑定到接口
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
finance.NewFinanceApplicationService,
|
func(
|
||||||
|
aliPayClient *payment.AliPayService,
|
||||||
|
wechatPayService *payment.WechatPayService,
|
||||||
|
walletService finance_services.WalletAggregateService,
|
||||||
|
rechargeRecordService finance_services.RechargeRecordService,
|
||||||
|
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||||
|
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||||
|
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||||
|
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||||
|
userRepo domain_user_repo.UserRepository,
|
||||||
|
txManager *shared_database.TransactionManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
config *config.Config,
|
||||||
|
exportManager *export.ExportManager,
|
||||||
|
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||||
|
) finance.FinanceApplicationService {
|
||||||
|
return finance.NewFinanceApplicationService(
|
||||||
|
aliPayClient,
|
||||||
|
wechatPayService,
|
||||||
|
walletService,
|
||||||
|
rechargeRecordService,
|
||||||
|
walletTransactionRepo,
|
||||||
|
alipayOrderRepo,
|
||||||
|
wechatOrderRepo,
|
||||||
|
rechargeRecordRepo,
|
||||||
|
componentReportRepo,
|
||||||
|
userRepo,
|
||||||
|
txManager,
|
||||||
|
logger,
|
||||||
|
config,
|
||||||
|
exportManager,
|
||||||
|
)
|
||||||
|
},
|
||||||
fx.As(new(finance.FinanceApplicationService)),
|
fx.As(new(finance.FinanceApplicationService)),
|
||||||
),
|
),
|
||||||
// 发票应用服务 - 绑定到接口
|
// 发票应用服务 - 绑定到接口
|
||||||
@@ -612,7 +931,23 @@ func NewContainer() *Container {
|
|||||||
),
|
),
|
||||||
// 产品应用服务 - 绑定到接口
|
// 产品应用服务 - 绑定到接口
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
product.NewProductApplicationService,
|
func(
|
||||||
|
productManagementService *product_services.ProductManagementService,
|
||||||
|
productSubscriptionService *product_services.ProductSubscriptionService,
|
||||||
|
productApiConfigAppService product.ProductApiConfigApplicationService,
|
||||||
|
documentationAppService product.DocumentationApplicationServiceInterface,
|
||||||
|
formConfigService api_services.FormConfigService,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) product.ProductApplicationService {
|
||||||
|
return product.NewProductApplicationService(
|
||||||
|
productManagementService,
|
||||||
|
productSubscriptionService,
|
||||||
|
productApiConfigAppService,
|
||||||
|
documentationAppService,
|
||||||
|
formConfigService,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
fx.As(new(product.ProductApplicationService)),
|
fx.As(new(product.ProductApplicationService)),
|
||||||
),
|
),
|
||||||
// 产品API配置应用服务 - 绑定到接口
|
// 产品API配置应用服务 - 绑定到接口
|
||||||
@@ -634,8 +969,180 @@ func NewContainer() *Container {
|
|||||||
product.NewSubscriptionApplicationService,
|
product.NewSubscriptionApplicationService,
|
||||||
fx.As(new(product.SubscriptionApplicationService)),
|
fx.As(new(product.SubscriptionApplicationService)),
|
||||||
),
|
),
|
||||||
|
// 任务管理服务
|
||||||
|
article.NewTaskManagementService,
|
||||||
|
// 文章应用服务 - 绑定到接口
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
articleRepo domain_article_repo.ArticleRepository,
|
||||||
|
categoryRepo domain_article_repo.CategoryRepository,
|
||||||
|
tagRepo domain_article_repo.TagRepository,
|
||||||
|
articleService *article_service.ArticleService,
|
||||||
|
taskManager task_interfaces.TaskManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) article.ArticleApplicationService {
|
||||||
|
return article.NewArticleApplicationService(
|
||||||
|
articleRepo,
|
||||||
|
categoryRepo,
|
||||||
|
tagRepo,
|
||||||
|
articleService,
|
||||||
|
taskManager,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fx.As(new(article.ArticleApplicationService)),
|
||||||
|
),
|
||||||
|
// 公告应用服务 - 绑定到接口
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
announcementRepo domain_article_repo.AnnouncementRepository,
|
||||||
|
announcementService *article_service.AnnouncementService,
|
||||||
|
taskManager task_interfaces.TaskManager,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) article.AnnouncementApplicationService {
|
||||||
|
return article.NewAnnouncementApplicationService(
|
||||||
|
announcementRepo,
|
||||||
|
announcementService,
|
||||||
|
taskManager,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fx.As(new(article.AnnouncementApplicationService)),
|
||||||
|
),
|
||||||
|
// 统计应用服务 - 绑定到接口
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
aggregateService statistics_service.StatisticsAggregateService,
|
||||||
|
calculationService statistics_service.StatisticsCalculationService,
|
||||||
|
reportService statistics_service.StatisticsReportService,
|
||||||
|
metricRepo domain_statistics_repo.StatisticsRepository,
|
||||||
|
reportRepo domain_statistics_repo.StatisticsReportRepository,
|
||||||
|
dashboardRepo domain_statistics_repo.StatisticsDashboardRepository,
|
||||||
|
userRepo domain_user_repo.UserRepository,
|
||||||
|
enterpriseInfoRepo domain_user_repo.EnterpriseInfoRepository,
|
||||||
|
apiCallRepo domain_api_repo.ApiCallRepository,
|
||||||
|
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||||
|
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||||
|
productRepo domain_product_repo.ProductRepository,
|
||||||
|
certificationRepo domain_certification_repo.CertificationQueryRepository,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) statistics.StatisticsApplicationService {
|
||||||
|
return statistics.NewStatisticsApplicationService(
|
||||||
|
aggregateService,
|
||||||
|
calculationService,
|
||||||
|
reportService,
|
||||||
|
metricRepo,
|
||||||
|
reportRepo,
|
||||||
|
dashboardRepo,
|
||||||
|
userRepo,
|
||||||
|
enterpriseInfoRepo,
|
||||||
|
apiCallRepo,
|
||||||
|
walletTransactionRepo,
|
||||||
|
rechargeRecordRepo,
|
||||||
|
productRepo,
|
||||||
|
certificationRepo,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fx.As(new(statistics.StatisticsApplicationService)),
|
||||||
|
),
|
||||||
|
// UI组件应用服务 - 绑定到接口
|
||||||
|
fx.Annotate(
|
||||||
|
func(
|
||||||
|
uiComponentRepo domain_product_repo.UIComponentRepository,
|
||||||
|
productUIComponentRepo domain_product_repo.ProductUIComponentRepository,
|
||||||
|
fileStorageService *storage.LocalFileStorageService,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) product.UIComponentApplicationService {
|
||||||
|
// 创建UI组件文件服务
|
||||||
|
basePath := "resources/Pure Component/src/ui"
|
||||||
|
fileService := product.NewUIComponentFileService(basePath, logger)
|
||||||
|
|
||||||
|
return product.NewUIComponentApplicationService(
|
||||||
|
uiComponentRepo,
|
||||||
|
productUIComponentRepo,
|
||||||
|
fileStorageService,
|
||||||
|
fileService,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fx.As(new(product.UIComponentApplicationService)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// PDF查找服务
|
||||||
|
fx.Provide(
|
||||||
|
func(logger *zap.Logger) (*pdf.PDFFinder, error) {
|
||||||
|
docDir, err := pdf.GetDocumentationDir()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("未找到接口文档文件夹,PDF自动查找功能将不可用", zap.Error(err))
|
||||||
|
return nil, nil // 返回nil,handler中会检查
|
||||||
|
}
|
||||||
|
logger.Info("PDF查找服务已初始化", zap.String("documentation_dir", docDir))
|
||||||
|
return pdf.NewPDFFinder(docDir, logger), nil
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// PDF生成器
|
||||||
|
fx.Provide(
|
||||||
|
func(logger *zap.Logger) *pdf.PDFGenerator {
|
||||||
|
return pdf.NewPDFGenerator(logger)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// PDF缓存管理器
|
||||||
|
fx.Provide(
|
||||||
|
func(logger *zap.Logger) (*pdf.PDFCacheManager, error) {
|
||||||
|
// 使用默认配置:缓存目录在临时目录,TTL为24小时,最大缓存大小为500MB
|
||||||
|
cacheDir := "" // 使用默认目录(临时目录下的tyapi_pdf_cache)
|
||||||
|
ttl := 24 * time.Hour
|
||||||
|
maxSize := int64(500 * 1024 * 1024) // 500MB
|
||||||
|
|
||||||
|
// 可以通过环境变量覆盖
|
||||||
|
if envCacheDir := os.Getenv("PDF_CACHE_DIR"); envCacheDir != "" {
|
||||||
|
cacheDir = envCacheDir
|
||||||
|
}
|
||||||
|
if envTTL := os.Getenv("PDF_CACHE_TTL"); envTTL != "" {
|
||||||
|
if parsedTTL, err := time.ParseDuration(envTTL); err == nil {
|
||||||
|
ttl = parsedTTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if envMaxSize := os.Getenv("PDF_CACHE_MAX_SIZE"); envMaxSize != "" {
|
||||||
|
if parsedMaxSize, err := strconv.ParseInt(envMaxSize, 10, 64); err == nil {
|
||||||
|
maxSize = parsedMaxSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheManager, err := pdf.NewPDFCacheManager(logger, cacheDir, ttl, maxSize)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("PDF缓存管理器初始化失败,将禁用缓存功能", zap.Error(err))
|
||||||
|
return nil, nil // 返回nil,handler中会检查
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("PDF缓存管理器已初始化",
|
||||||
|
zap.String("cache_dir", cacheDir),
|
||||||
|
zap.Duration("ttl", ttl),
|
||||||
|
zap.Int64("max_size", maxSize),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cacheManager, nil
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// 本地文件存储服务
|
||||||
|
fx.Provide(
|
||||||
|
func(logger *zap.Logger) *storage.LocalFileStorageService {
|
||||||
|
// 使用默认配置:基础存储目录在项目根目录下的storage目录
|
||||||
|
basePath := "storage"
|
||||||
|
|
||||||
|
// 可以通过环境变量覆盖
|
||||||
|
if envBasePath := os.Getenv("FILE_STORAGE_BASE_PATH"); envBasePath != "" {
|
||||||
|
basePath = envBasePath
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("本地文件存储服务已初始化",
|
||||||
|
zap.String("base_path", basePath),
|
||||||
|
)
|
||||||
|
|
||||||
|
return storage.NewLocalFileStorageService(basePath, logger)
|
||||||
|
},
|
||||||
|
),
|
||||||
// HTTP处理器
|
// HTTP处理器
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
// 用户HTTP处理器
|
// 用户HTTP处理器
|
||||||
@@ -650,6 +1157,50 @@ func NewContainer() *Container {
|
|||||||
handlers.NewProductAdminHandler,
|
handlers.NewProductAdminHandler,
|
||||||
// API Handler
|
// API Handler
|
||||||
handlers.NewApiHandler,
|
handlers.NewApiHandler,
|
||||||
|
// 统计HTTP处理器
|
||||||
|
handlers.NewStatisticsHandler,
|
||||||
|
// 文章HTTP处理器
|
||||||
|
func(
|
||||||
|
appService article.ArticleApplicationService,
|
||||||
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
|
validator interfaces.RequestValidator,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *handlers.ArticleHandler {
|
||||||
|
return handlers.NewArticleHandler(appService, responseBuilder, validator, logger)
|
||||||
|
},
|
||||||
|
// 公告HTTP处理器
|
||||||
|
func(
|
||||||
|
appService article.AnnouncementApplicationService,
|
||||||
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
|
validator interfaces.RequestValidator,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *handlers.AnnouncementHandler {
|
||||||
|
return handlers.NewAnnouncementHandler(appService, responseBuilder, validator, logger)
|
||||||
|
},
|
||||||
|
// 组件报告处理器
|
||||||
|
func(
|
||||||
|
productRepo domain_product_repo.ProductRepository,
|
||||||
|
docRepo domain_product_repo.ProductDocumentationRepository,
|
||||||
|
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
|
||||||
|
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||||
|
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||||
|
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||||
|
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||||
|
aliPayService *payment.AliPayService,
|
||||||
|
wechatPayService *payment.WechatPayService,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *component_report.ComponentReportHandler {
|
||||||
|
return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger)
|
||||||
|
},
|
||||||
|
// UI组件HTTP处理器
|
||||||
|
func(
|
||||||
|
uiComponentAppService product.UIComponentApplicationService,
|
||||||
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
|
validator interfaces.RequestValidator,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *handlers.UIComponentHandler {
|
||||||
|
return handlers.NewUIComponentHandler(uiComponentAppService, responseBuilder, validator, logger)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 路由注册
|
// 路由注册
|
||||||
@@ -664,8 +1215,16 @@ func NewContainer() *Container {
|
|||||||
routes.NewProductRoutes,
|
routes.NewProductRoutes,
|
||||||
// 产品管理员路由
|
// 产品管理员路由
|
||||||
routes.NewProductAdminRoutes,
|
routes.NewProductAdminRoutes,
|
||||||
|
// UI组件路由
|
||||||
|
routes.NewUIComponentRoutes,
|
||||||
|
// 文章路由
|
||||||
|
routes.NewArticleRoutes,
|
||||||
|
// 公告路由
|
||||||
|
routes.NewAnnouncementRoutes,
|
||||||
// API路由
|
// API路由
|
||||||
routes.NewApiRoutes,
|
routes.NewApiRoutes,
|
||||||
|
// 统计路由
|
||||||
|
routes.NewStatisticsRoutes,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 应用生命周期
|
// 应用生命周期
|
||||||
@@ -700,15 +1259,34 @@ func (c *Container) Stop() error {
|
|||||||
func RegisterLifecycleHooks(
|
func RegisterLifecycleHooks(
|
||||||
lifecycle fx.Lifecycle,
|
lifecycle fx.Lifecycle,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
|
asynqWorker *asynq.AsynqWorker,
|
||||||
) {
|
) {
|
||||||
lifecycle.Append(fx.Hook{
|
lifecycle.Append(fx.Hook{
|
||||||
OnStart: func(context.Context) error {
|
OnStart: func(context.Context) error {
|
||||||
logger.Info("应用启动中...")
|
logger.Info("应用启动中...")
|
||||||
logger.Info("所有依赖注入完成,开始启动应用服务")
|
logger.Info("所有依赖注入完成,开始启动应用服务")
|
||||||
|
|
||||||
|
// 确保校验器最先初始化
|
||||||
|
validator.InitGlobalValidator()
|
||||||
|
logger.Info("全局校验器初始化完成")
|
||||||
|
|
||||||
|
// 启动AsynqWorker
|
||||||
|
if err := asynqWorker.Start(); err != nil {
|
||||||
|
logger.Error("启动AsynqWorker失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("AsynqWorker启动成功")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
OnStop: func(context.Context) error {
|
OnStop: func(context.Context) error {
|
||||||
logger.Info("应用关闭中...")
|
logger.Info("应用关闭中...")
|
||||||
|
|
||||||
|
// 停止AsynqWorker
|
||||||
|
asynqWorker.Stop()
|
||||||
|
asynqWorker.Shutdown()
|
||||||
|
logger.Info("AsynqWorker已停止")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -753,7 +1331,13 @@ func RegisterRoutes(
|
|||||||
financeRoutes *routes.FinanceRoutes,
|
financeRoutes *routes.FinanceRoutes,
|
||||||
productRoutes *routes.ProductRoutes,
|
productRoutes *routes.ProductRoutes,
|
||||||
productAdminRoutes *routes.ProductAdminRoutes,
|
productAdminRoutes *routes.ProductAdminRoutes,
|
||||||
|
uiComponentRoutes *routes.UIComponentRoutes,
|
||||||
|
articleRoutes *routes.ArticleRoutes,
|
||||||
|
announcementRoutes *routes.AnnouncementRoutes,
|
||||||
apiRoutes *routes.ApiRoutes,
|
apiRoutes *routes.ApiRoutes,
|
||||||
|
statisticsRoutes *routes.StatisticsRoutes,
|
||||||
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
|
adminAuth *middleware.AdminAuthMiddleware,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) {
|
) {
|
||||||
@@ -769,6 +1353,16 @@ func RegisterRoutes(
|
|||||||
productRoutes.Register(router)
|
productRoutes.Register(router)
|
||||||
productAdminRoutes.Register(router)
|
productAdminRoutes.Register(router)
|
||||||
|
|
||||||
|
// UI组件路由需要特殊处理,因为它需要管理员中间件
|
||||||
|
engine := router.GetEngine()
|
||||||
|
adminGroup := engine.Group("/api/v1/admin")
|
||||||
|
adminGroup.Use(adminAuth.Handle())
|
||||||
|
uiComponentRoutes.RegisterRoutes(adminGroup, adminAuth)
|
||||||
|
|
||||||
|
articleRoutes.Register(router)
|
||||||
|
announcementRoutes.Register(router)
|
||||||
|
statisticsRoutes.Register(router)
|
||||||
|
|
||||||
// 打印注册的路由信息
|
// 打印注册的路由信息
|
||||||
router.PrintRoutes()
|
router.PrintRoutes()
|
||||||
|
|
||||||
@@ -815,6 +1409,7 @@ func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.
|
|||||||
"panic": zapcore.PanicLevel,
|
"panic": zapcore.PanicLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只转换配置文件中存在的级别
|
||||||
for levelStr, config := range configs {
|
for levelStr, config := range configs {
|
||||||
if level, exists := levelMap[levelStr]; exists {
|
if level, exists := levelMap[levelStr]; exists {
|
||||||
result[level] = logger.LevelFileConfig{
|
result[level] = logger.LevelFileConfig{
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type FLXG162AReq struct {
|
|||||||
type FLXG0687Req struct {
|
type FLXG0687Req struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
type FLXG21Req struct {
|
type FLXGBC21Req struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
}
|
}
|
||||||
type FLXG970FReq struct {
|
type FLXG970FReq struct {
|
||||||
@@ -68,6 +68,10 @@ type IVYZ5733Req struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
|
type IVYZ81NCReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
type IVYZ9363Req struct {
|
type IVYZ9363Req struct {
|
||||||
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
ManName string `json:"man_name" validate:"required,min=1,validName"`
|
||||||
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
|
||||||
@@ -90,14 +94,14 @@ type JRZQ8203Req struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
type JRZQDBCEReq struct {
|
type JRZQDCBEReq struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
type QYGL2ACDReq struct {
|
type QYGL2ACDReq struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
}
|
}
|
||||||
@@ -105,13 +109,13 @@ type QYGL6F2DReq struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
type QYGL45BDReq struct {
|
type QYGL45BDReq struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
type QYGL8261Req struct {
|
type QYGL8261Req struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
}
|
}
|
||||||
type QYGL8271Req struct {
|
type QYGL8271Req struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
||||||
@@ -123,12 +127,19 @@ type QYGLB4C0Req struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QYGL23T7Req struct {
|
type QYGL23T7Req struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QYGL5CMPReq struct {
|
||||||
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
type YYSY4B37Req struct {
|
type YYSY4B37Req struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
}
|
}
|
||||||
@@ -147,7 +158,7 @@ type YYSY09CDReq struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
type IVYZ0b03Req struct {
|
type IVYZ0B03Req struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
@@ -191,6 +202,18 @@ type IVYZGZ08Req struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IVYZ2B2TReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
QueryReasonId int64 `json:"query_reason_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ5A9OReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type FLXG8A3FReq struct {
|
type FLXG8A3FReq struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
@@ -209,6 +232,13 @@ type COMB298YReq struct {
|
|||||||
TimeRange string `json:"time_range" validate:"omitempty,validTimeRange"` // 非必填字段
|
TimeRange string `json:"time_range" validate:"omitempty,validTimeRange"` // 非必填字段
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type COMBHZY2Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||||
|
}
|
||||||
|
|
||||||
type COMB86PMReq struct {
|
type COMB86PMReq struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
@@ -221,6 +251,456 @@ type QCXG7A2BReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type COMENT01Req struct {
|
type COMENT01Req struct {
|
||||||
EntName string `json:"ent_name" validate:"required,min=1,validName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JRZQ09J8Req struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXGDEA8Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXGDEA9Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ1D09Req struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,min=1,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增的处理器DTO
|
||||||
|
type IVYZ2A8BReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ7C9DReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
UniqueID string `json:"unique_id" validate:"required,validUniqueID"`
|
||||||
|
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ5E3FReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ7F3AReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ3P9MReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
ReturnType string `json:"return_type" validate:"omitempty,oneof=1 2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ3A7FReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ9K2LReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||||
|
}
|
||||||
|
type IVYZP2Q6Req struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ1W4XReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ9D2EReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
UseScenario string `json:"use_scenario" validate:"required,oneof=1 2 3 4 99"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ2C1PReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DWBG7F3AReq 行为数据查询请求参数
|
||||||
|
type DWBG7F3AReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增的QYGL处理器DTO
|
||||||
|
type QYGL5A3CReq struct {
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||||
|
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL8B4DReq struct {
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||||
|
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL9E2FReq struct {
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||||
|
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL7C1AReq struct {
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||||
|
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL3F8EReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY4F2EReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY9F1BReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
type YYSY6F2BReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY8B1CReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY6D9AReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY3E7FReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXG5A3BReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXG9C1DReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 法院被执行人限高版
|
||||||
|
type FLXG3A9BReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 法院被执行人高级版
|
||||||
|
type FLXGK5D2Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 综合多头
|
||||||
|
|
||||||
|
type JRZQ8F7CReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXG2E8FReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ3C7BReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
type JRZQ3C9RReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ3P01Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JRZQ3AG6Req JRZQ3AG6 轻松查公积API处理方法
|
||||||
|
type JRZQ3AG6Req struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
|
||||||
|
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||||
|
}
|
||||||
|
type JRZQ8A2DReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// YYSY8F3AReq 行为数据查询请求参数
|
||||||
|
type YYSY8F3AReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"cardNo" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
CardId string `json:"cardId" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 銀行卡黑名單
|
||||||
|
type JRZQ0B6YReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 银行卡鉴权
|
||||||
|
type JRZQ9A1WReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
|
||||||
|
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业管理董监高司法综合信息核验
|
||||||
|
type QYGL6S1BReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ5E9FReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ4B6CReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ7F1AReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DWBG6A2CReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DWBG8B4DReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXG8B4DReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||||
|
BankCard string `json:"bank_card" validate:"omitempty,validBankCard"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QCXG9P1CReq struct {
|
||||||
|
VehicleType string `json:"vehicle_type" validate:"omitempty,oneof=0 1 2"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"omitempty,min=1,validName"`
|
||||||
|
UserType string `json:"user_type" validate:"omitempty,oneof=1 2 3"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QCXG8A3DReq struct {
|
||||||
|
PlateNo string `json:"plate_no" validate:"required"`
|
||||||
|
PlateType string `json:"plate_type" validate:"omitempty,oneof=01 02"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QCXG6B4EReq struct {
|
||||||
|
VINCode string `json:"vin_code" validate:"required"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL2B5CReq struct {
|
||||||
|
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||||
|
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全国企业借贷意向验证查询_V1
|
||||||
|
type QYGL9T1QReq struct {
|
||||||
|
OwnerType string `json:"owner_type" validate:"required,oneof=1 2 3 4 5"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全国企业各类工商风险统计数量查询
|
||||||
|
type QYGL5A9TReq struct {
|
||||||
|
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||||
|
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失信被执行企业或个人查询
|
||||||
|
type QYGL2S0WReq struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=per ent"`
|
||||||
|
Name string `json:"name" validate:"omitempty,min=1,validName"`
|
||||||
|
EntName string `json:"ent_name" validate:"omitempty,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||||
|
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ2F8AReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ1E7BReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ9E2AReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY9A1BReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY8C2DReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY7D3EReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ6F2AReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ8B3CReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ9D4EReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JRZQ0L85Req struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FLXG7E8FReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QYGL5F6AReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ6G7HReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ8I9JReq struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ6M8PReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YYSY9E4AReq struct {
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -56,7 +52,7 @@ type ApiCall struct {
|
|||||||
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
|
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
|
||||||
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
|
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
|
||||||
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
|
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
|
||||||
TransactionId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id"`
|
TransactionId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"transaction_id"`
|
||||||
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
|
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
|
||||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||||
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
|
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
|
||||||
@@ -145,40 +141,9 @@ func (a *ApiCall) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局计数器,用于确保TransactionID的唯一性
|
// GenerateTransactionID 生成UUID格式的交易单号
|
||||||
var (
|
|
||||||
transactionCounter int64
|
|
||||||
counterMutex sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// GenerateTransactionID 生成16位数的交易单号
|
|
||||||
func GenerateTransactionID() string {
|
func GenerateTransactionID() string {
|
||||||
// 使用互斥锁确保计数器的线程安全
|
return uuid.New().String()
|
||||||
counterMutex.Lock()
|
|
||||||
transactionCounter++
|
|
||||||
currentCounter := transactionCounter
|
|
||||||
counterMutex.Unlock()
|
|
||||||
|
|
||||||
// 获取当前时间戳(微秒精度)
|
|
||||||
timestamp := time.Now().UnixMicro()
|
|
||||||
|
|
||||||
// 组合时间戳和计数器,确保唯一性
|
|
||||||
combined := fmt.Sprintf("%d%06d", timestamp, currentCounter%1000000)
|
|
||||||
|
|
||||||
// 如果长度超出16位,截断;如果不够,填充随机字符
|
|
||||||
if len(combined) >= 16 {
|
|
||||||
return combined[:16]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果长度不够,使用随机字节填充
|
|
||||||
if len(combined) < 16 {
|
|
||||||
randomBytes := make([]byte, 8)
|
|
||||||
rand.Read(randomBytes)
|
|
||||||
randomHex := hex.EncodeToString(randomBytes)
|
|
||||||
combined += randomHex[:16-len(combined)]
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定数据库表名
|
// TableName 指定数据库表名
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package entities
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -18,6 +20,78 @@ const (
|
|||||||
ApiUserStatusFrozen = "frozen"
|
ApiUserStatusFrozen = "frozen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WhiteListItem 白名单项,包含IP地址、添加时间和备注
|
||||||
|
type WhiteListItem struct {
|
||||||
|
IPAddress string `json:"ip_address"` // IP地址
|
||||||
|
AddedAt time.Time `json:"added_at"` // 添加时间
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhiteList 白名单类型,支持向后兼容(旧的字符串数组格式)
|
||||||
|
type WhiteList []WhiteListItem
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||||
|
func (w WhiteList) Value() (driver.Value, error) {
|
||||||
|
if w == nil {
|
||||||
|
return "[]", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口,用于数据库读取(支持向后兼容)
|
||||||
|
func (w *WhiteList) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*w = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes []byte
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
bytes = v
|
||||||
|
case string:
|
||||||
|
bytes = []byte(v)
|
||||||
|
default:
|
||||||
|
return errors.New("无法扫描 WhiteList 类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bytes) == 0 || string(bytes) == "[]" || string(bytes) == "null" {
|
||||||
|
*w = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先尝试解析为新格式(结构体数组)
|
||||||
|
var items []WhiteListItem
|
||||||
|
if err := json.Unmarshal(bytes, &items); err == nil {
|
||||||
|
// 成功解析为新格式
|
||||||
|
*w = WhiteList(items)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果失败,尝试解析为旧格式(字符串数组)
|
||||||
|
var oldFormat []string
|
||||||
|
if err := json.Unmarshal(bytes, &oldFormat); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将旧格式转换为新格式
|
||||||
|
now := time.Now()
|
||||||
|
items = make([]WhiteListItem, 0, len(oldFormat))
|
||||||
|
for _, ip := range oldFormat {
|
||||||
|
items = append(items, WhiteListItem{
|
||||||
|
IPAddress: ip,
|
||||||
|
AddedAt: now, // 使用当前时间作为添加时间(因为旧数据没有时间信息)
|
||||||
|
Remark: "", // 旧数据没有备注信息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*w = WhiteList(items)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ApiUser API用户(聚合根)
|
// ApiUser API用户(聚合根)
|
||||||
type ApiUser struct {
|
type ApiUser struct {
|
||||||
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
||||||
@@ -25,7 +99,15 @@ type ApiUser struct {
|
|||||||
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
||||||
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
||||||
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
||||||
WhiteList []string `gorm:"type:json;serializer:json;default:'[]'" json:"white_list"` // 支持多个白名单
|
WhiteList WhiteList `gorm:"type:json;default:'[]'" json:"white_list"` // 支持多个白名单,包含IP和添加时间,支持向后兼容
|
||||||
|
|
||||||
|
// 余额预警配置
|
||||||
|
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
||||||
|
BalanceAlertThreshold float64 `gorm:"default:200.00" json:"balance_alert_threshold" comment:"余额预警阈值"`
|
||||||
|
AlertPhone string `gorm:"type:varchar(20)" json:"alert_phone" comment:"预警手机号"`
|
||||||
|
LastLowBalanceAlert *time.Time `json:"last_low_balance_alert" comment:"最后低余额预警时间"`
|
||||||
|
LastArrearsAlert *time.Time `json:"last_arrears_alert" comment:"最后欠费预警时间"`
|
||||||
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -33,7 +115,7 @@ type ApiUser struct {
|
|||||||
// IsWhiteListed 校验IP/域名是否在白名单
|
// IsWhiteListed 校验IP/域名是否在白名单
|
||||||
func (u *ApiUser) IsWhiteListed(target string) bool {
|
func (u *ApiUser) IsWhiteListed(target string) bool {
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w == target {
|
if w.IPAddress == target {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +133,7 @@ func (u *ApiUser) IsFrozen() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewApiUser 工厂方法
|
// NewApiUser 工厂方法
|
||||||
func NewApiUser(userId string) (*ApiUser, error) {
|
func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold float64) (*ApiUser, error) {
|
||||||
if userId == "" {
|
if userId == "" {
|
||||||
return nil, errors.New("用户ID不能为空")
|
return nil, errors.New("用户ID不能为空")
|
||||||
}
|
}
|
||||||
@@ -64,12 +146,14 @@ func NewApiUser(userId string) (*ApiUser, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ApiUser{
|
return &ApiUser{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
AccessId: accessId,
|
AccessId: accessId,
|
||||||
SecretKey: secretKey,
|
SecretKey: secretKey,
|
||||||
Status: ApiUserStatusNormal,
|
Status: ApiUserStatusNormal,
|
||||||
WhiteList: []string{},
|
WhiteList: WhiteList{},
|
||||||
|
BalanceAlertEnabled: defaultAlertEnabled,
|
||||||
|
BalanceAlertThreshold: defaultAlertThreshold,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +164,12 @@ func (u *ApiUser) Freeze() {
|
|||||||
func (u *ApiUser) Unfreeze() {
|
func (u *ApiUser) Unfreeze() {
|
||||||
u.Status = ApiUserStatusNormal
|
u.Status = ApiUserStatusNormal
|
||||||
}
|
}
|
||||||
func (u *ApiUser) UpdateWhiteList(list []string) {
|
func (u *ApiUser) UpdateWhiteList(list []WhiteListItem) {
|
||||||
u.WhiteList = list
|
u.WhiteList = WhiteList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddToWhiteList 新增白名单项(防御性校验)
|
// AddToWhiteList 新增白名单项(防御性校验)
|
||||||
func (u *ApiUser) AddToWhiteList(entry string) error {
|
func (u *ApiUser) AddToWhiteList(entry string, remark string) error {
|
||||||
if len(u.WhiteList) >= 10 {
|
if len(u.WhiteList) >= 10 {
|
||||||
return errors.New("白名单最多只能有10个")
|
return errors.New("白名单最多只能有10个")
|
||||||
}
|
}
|
||||||
@@ -93,27 +177,31 @@ func (u *ApiUser) AddToWhiteList(entry string) error {
|
|||||||
return errors.New("非法IP")
|
return errors.New("非法IP")
|
||||||
}
|
}
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w == entry {
|
if w.IPAddress == entry {
|
||||||
return errors.New("白名单已存在")
|
return errors.New("白名单已存在")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u.WhiteList = append(u.WhiteList, entry)
|
u.WhiteList = append(u.WhiteList, WhiteListItem{
|
||||||
|
IPAddress: entry,
|
||||||
|
AddedAt: time.Now(),
|
||||||
|
Remark: remark,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
||||||
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
||||||
if u.WhiteList == nil {
|
if u.WhiteList == nil {
|
||||||
u.WhiteList = []string{}
|
u.WhiteList = WhiteList{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFromWhiteList 删除白名单项
|
// RemoveFromWhiteList 删除白名单项
|
||||||
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||||
newList := make([]string, 0, len(u.WhiteList))
|
newList := make([]WhiteListItem, 0, len(u.WhiteList))
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w != entry {
|
if w.IPAddress != entry {
|
||||||
newList = append(newList, w)
|
newList = append(newList, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +212,68 @@ func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 余额预警相关方法
|
||||||
|
|
||||||
|
// UpdateBalanceAlertSettings 更新余额预警设置
|
||||||
|
func (u *ApiUser) UpdateBalanceAlertSettings(enabled bool, threshold float64, phone string) error {
|
||||||
|
if threshold < 0 {
|
||||||
|
return errors.New("预警阈值不能为负数")
|
||||||
|
}
|
||||||
|
if phone != "" && len(phone) != 11 {
|
||||||
|
return errors.New("手机号格式不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.BalanceAlertEnabled = enabled
|
||||||
|
u.BalanceAlertThreshold = threshold
|
||||||
|
u.AlertPhone = phone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSendLowBalanceAlert 是否应该发送低余额预警(24小时冷却期)
|
||||||
|
func (u *ApiUser) ShouldSendLowBalanceAlert(balance float64) bool {
|
||||||
|
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 余额低于阈值
|
||||||
|
if balance < u.BalanceAlertThreshold {
|
||||||
|
// 检查是否已经发送过预警(避免频繁发送)
|
||||||
|
if u.LastLowBalanceAlert != nil {
|
||||||
|
// 如果距离上次预警不足24小时,不发送
|
||||||
|
if time.Since(*u.LastLowBalanceAlert) < 24*time.Hour {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSendArrearsAlert 是否应该发送欠费预警(不受冷却期限制)
|
||||||
|
func (u *ApiUser) ShouldSendArrearsAlert(balance float64) bool {
|
||||||
|
if !u.BalanceAlertEnabled || u.AlertPhone == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 余额为负数(欠费)- 欠费预警不受冷却期限制
|
||||||
|
if balance < 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLowBalanceAlertSent 标记低余额预警已发送
|
||||||
|
func (u *ApiUser) MarkLowBalanceAlertSent() {
|
||||||
|
now := time.Now()
|
||||||
|
u.LastLowBalanceAlert = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkArrearsAlertSent 标记欠费预警已发送
|
||||||
|
func (u *ApiUser) MarkArrearsAlertSent() {
|
||||||
|
now := time.Now()
|
||||||
|
u.LastArrearsAlert = &now
|
||||||
|
}
|
||||||
|
|
||||||
// Validate 校验ApiUser聚合根的业务规则
|
// Validate 校验ApiUser聚合根的业务规则
|
||||||
func (u *ApiUser) Validate() error {
|
func (u *ApiUser) Validate() error {
|
||||||
if u.UserId == "" {
|
if u.UserId == "" {
|
||||||
@@ -144,9 +294,9 @@ func (u *ApiUser) Validate() error {
|
|||||||
if len(u.WhiteList) > 10 {
|
if len(u.WhiteList) > 10 {
|
||||||
return errors.New("白名单最多只能有10个")
|
return errors.New("白名单最多只能有10个")
|
||||||
}
|
}
|
||||||
for _, ip := range u.WhiteList {
|
for _, item := range u.WhiteList {
|
||||||
if net.ParseIP(ip) == nil {
|
if net.ParseIP(item.IPAddress) == nil {
|
||||||
return errors.New("白名单项必须为合法IP地址: " + ip)
|
return errors.New("白名单项必须为合法IP地址: " + item.IPAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -187,7 +337,26 @@ func (c *ApiUser) BeforeCreate(tx *gorm.DB) error {
|
|||||||
c.ID = uuid.New().String()
|
c.ID = uuid.New().String()
|
||||||
}
|
}
|
||||||
if c.WhiteList == nil {
|
if c.WhiteList == nil {
|
||||||
c.WhiteList = []string{}
|
c.WhiteList = WhiteList{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AfterFind GORM钩子:查询后处理数据,确保AddedAt不为零值
|
||||||
|
func (u *ApiUser) AfterFind(tx *gorm.DB) error {
|
||||||
|
// 如果 WhiteList 为空,初始化为空数组
|
||||||
|
if u.WhiteList == nil {
|
||||||
|
u.WhiteList = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有项的AddedAt不为零值(处理可能从旧数据迁移的情况)
|
||||||
|
now := time.Now()
|
||||||
|
for i := range u.WhiteList {
|
||||||
|
if u.WhiteList[i].AddedAt.IsZero() {
|
||||||
|
u.WhiteList[i].AddedAt = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
)
|
)
|
||||||
@@ -24,9 +25,26 @@ type ApiCallRepository interface {
|
|||||||
// 新增:统计用户API调用次数
|
// 新增:统计用户API调用次数
|
||||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||||
|
|
||||||
|
// 新增:根据用户ID和产品ID统计API调用次数
|
||||||
|
CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error)
|
||||||
|
|
||||||
// 新增:根据TransactionID查询
|
// 新增:根据TransactionID查询
|
||||||
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
||||||
|
|
||||||
|
// 统计相关方法
|
||||||
|
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||||
|
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
|
||||||
// 管理端:根据条件筛选所有API调用记录(包含产品名称)
|
// 管理端:根据条件筛选所有API调用记录(包含产品名称)
|
||||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
|
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
|
||||||
|
|
||||||
|
// 系统级别统计方法
|
||||||
|
GetSystemTotalCalls(ctx context.Context) (int64, error)
|
||||||
|
GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error)
|
||||||
|
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
|
||||||
|
// API受欢迎程度排行榜
|
||||||
|
GetApiPopularityRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,28 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"tyapi-server/internal/application/api/commands"
|
"tyapi-server/internal/application/api/commands"
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
"tyapi-server/internal/domains/api/services/processors/comb"
|
"tyapi-server/internal/domains/api/services/processors/comb"
|
||||||
|
"tyapi-server/internal/domains/api/services/processors/dwbg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/flxg"
|
"tyapi-server/internal/domains/api/services/processors/flxg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/ivyz"
|
"tyapi-server/internal/domains/api/services/processors/ivyz"
|
||||||
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
||||||
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
||||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||||
|
"tyapi-server/internal/domains/api/services/processors/test"
|
||||||
"tyapi-server/internal/domains/api/services/processors/yysy"
|
"tyapi-server/internal/domains/api/services/processors/yysy"
|
||||||
"tyapi-server/internal/domains/product/services"
|
"tyapi-server/internal/domains/product/services"
|
||||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||||
|
"tyapi-server/internal/infrastructure/external/muzi"
|
||||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||||
"tyapi-server/internal/infrastructure/external/westdex"
|
"tyapi-server/internal/infrastructure/external/westdex"
|
||||||
|
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||||
"tyapi-server/internal/infrastructure/external/yushan"
|
"tyapi-server/internal/infrastructure/external/yushan"
|
||||||
|
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +37,7 @@ var (
|
|||||||
type ApiRequestService struct {
|
type ApiRequestService struct {
|
||||||
// 可注入依赖,如第三方服务、模型等
|
// 可注入依赖,如第三方服务、模型等
|
||||||
westDexService *westdex.WestDexService
|
westDexService *westdex.WestDexService
|
||||||
|
muziService *muzi.MuziService
|
||||||
yushanService *yushan.YushanService
|
yushanService *yushan.YushanService
|
||||||
tianYanChaService *tianyancha.TianYanChaService
|
tianYanChaService *tianyancha.TianYanChaService
|
||||||
alicloudService *alicloud.AlicloudService
|
alicloudService *alicloud.AlicloudService
|
||||||
@@ -41,9 +48,12 @@ type ApiRequestService struct {
|
|||||||
|
|
||||||
func NewApiRequestService(
|
func NewApiRequestService(
|
||||||
westDexService *westdex.WestDexService,
|
westDexService *westdex.WestDexService,
|
||||||
|
muziService *muzi.MuziService,
|
||||||
yushanService *yushan.YushanService,
|
yushanService *yushan.YushanService,
|
||||||
tianYanChaService *tianyancha.TianYanChaService,
|
tianYanChaService *tianyancha.TianYanChaService,
|
||||||
alicloudService *alicloud.AlicloudService,
|
alicloudService *alicloud.AlicloudService,
|
||||||
|
zhichaService *zhicha.ZhichaService,
|
||||||
|
xingweiService *xingwei.XingweiService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
) *ApiRequestService {
|
) *ApiRequestService {
|
||||||
@@ -51,13 +61,14 @@ func NewApiRequestService(
|
|||||||
combService := comb.NewCombService(productManagementService)
|
combService := comb.NewCombService(productManagementService)
|
||||||
|
|
||||||
// 创建处理器依赖容器
|
// 创建处理器依赖容器
|
||||||
processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, alicloudService, validator, combService)
|
processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService)
|
||||||
|
|
||||||
// 统一注册所有处理器
|
// 统一注册所有处理器
|
||||||
registerAllProcessors(combService)
|
registerAllProcessors(combService)
|
||||||
|
|
||||||
return &ApiRequestService{
|
return &ApiRequestService{
|
||||||
westDexService: westDexService,
|
westDexService: westDexService,
|
||||||
|
muziService: muziService,
|
||||||
yushanService: yushanService,
|
yushanService: yushanService,
|
||||||
tianYanChaService: tianYanChaService,
|
tianYanChaService: tianYanChaService,
|
||||||
alicloudService: alicloudService,
|
alicloudService: alicloudService,
|
||||||
@@ -88,12 +99,40 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"FLXG5B2E": flxg.ProcessFLXG5B2ERequest,
|
"FLXG5B2E": flxg.ProcessFLXG5B2ERequest,
|
||||||
"FLXG0687": flxg.ProcessFLXG0687Request,
|
"FLXG0687": flxg.ProcessFLXG0687Request,
|
||||||
"FLXGBC21": flxg.ProcessFLXGBC21Request,
|
"FLXGBC21": flxg.ProcessFLXGBC21Request,
|
||||||
|
"FLXGDEA8": flxg.ProcessFLXGDEA8Request,
|
||||||
|
"FLXGDEA9": flxg.ProcessFLXGDEA9Request,
|
||||||
|
"FLXG5A3B": flxg.ProcessFLXG5A3BRequest,
|
||||||
|
"FLXG9C1D": flxg.ProcessFLXG9C1DRequest,
|
||||||
|
"FLXG2E8F": flxg.ProcessFLXG2E8FRequest,
|
||||||
|
"FLXG7E8F": flxg.ProcessFLXG7E8FRequest,
|
||||||
|
"FLXG3A9B": flxg.ProcessFLXG3A9BRequest,
|
||||||
|
"FLXGK5D2": flxg.ProcessFLXGK5D2Request,
|
||||||
// JRZQ系列处理器
|
// JRZQ系列处理器
|
||||||
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
|
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
|
||||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||||
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
|
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
|
||||||
"JRZQDCBE": jrzq.ProcessJRZQDCBERequest,
|
"JRZQDCBE": jrzq.ProcessJRZQDCBERequest,
|
||||||
|
"JRZQ09J8": jrzq.ProcessJRZQ09J8Request,
|
||||||
|
"JRZQ1D09": jrzq.ProcessJRZQ1D09Request,
|
||||||
|
"JRZQ3C7B": jrzq.ProcessJRZQ3C7BRequest,
|
||||||
|
"JRZQ8A2D": jrzq.ProcessJRZQ8A2DRequest,
|
||||||
|
"JRZQ5E9F": jrzq.ProcessJRZQ5E9FRequest,
|
||||||
|
"JRZQ4B6C": jrzq.ProcessJRZQ4B6CRequest,
|
||||||
|
"JRZQ7F1A": jrzq.ProcessJRZQ7F1ARequest,
|
||||||
|
"JRZQ9E2A": jrzq.ProcessJRZQ9E2ARequest,
|
||||||
|
"JRZQ6F2A": jrzq.ProcessJRZQ6F2ARequest,
|
||||||
|
"JRZQ8B3C": jrzq.ProcessJRZQ8B3CRequest,
|
||||||
|
"JRZQ9D4E": jrzq.ProcessJRZQ9D4ERequest,
|
||||||
|
"JRZQ0L85": jrzq.ProcessJRZQ0L85Request,
|
||||||
|
"JRZQ2F8A": jrzq.ProcessJRZQ2F8ARequest,
|
||||||
|
"JRZQ1E7B": jrzq.ProcessJRZQ1E7BRequest,
|
||||||
|
"JRZQ3C9R": jrzq.ProcessJRZQ3C9RRequest,
|
||||||
|
"JRZQ0B6Y": jrzq.ProcessJRZQ0B6YRequest,
|
||||||
|
"JRZQ9A1W": jrzq.ProcessJRZQ9A1WRequest,
|
||||||
|
"JRZQ8F7C": jrzq.ProcessJRZQ8F7CRequest,
|
||||||
|
"JRZQ1W4X": jrzq.ProcessJRZQ1W4XRequest,
|
||||||
|
"JRZQ3P01": jrzq.ProcessJRZQ3P01Request,
|
||||||
|
"JRZQ3AG6": jrzq.ProcessJRZQ3AG6Request,
|
||||||
|
|
||||||
// QYGL系列处理器
|
// QYGL系列处理器
|
||||||
"QYGL8261": qygl.ProcessQYGL8261Request,
|
"QYGL8261": qygl.ProcessQYGL8261Request,
|
||||||
@@ -103,7 +142,21 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"QYGL8271": qygl.ProcessQYGL8271Request,
|
"QYGL8271": qygl.ProcessQYGL8271Request,
|
||||||
"QYGLB4C0": qygl.ProcessQYGLB4C0Request,
|
"QYGLB4C0": qygl.ProcessQYGLB4C0Request,
|
||||||
"QYGL23T7": qygl.ProcessQYGL23T7Request, // 企业三要素验证
|
"QYGL23T7": qygl.ProcessQYGL23T7Request, // 企业三要素验证
|
||||||
|
"QYGL5A3C": qygl.ProcessQYGL5A3CRequest, // 对外投资历史
|
||||||
|
"QYGL8B4D": qygl.ProcessQYGL8B4DRequest, // 融资历史
|
||||||
|
"QYGL9E2F": qygl.ProcessQYGL9E2FRequest, // 行政处罚
|
||||||
|
"QYGL7C1A": qygl.ProcessQYGL7C1ARequest, // 经营异常
|
||||||
|
"QYGL3F8E": qygl.ProcessQYGL3F8ERequest, // 人企关系加强版
|
||||||
|
"QYGL7D9A": qygl.ProcessQYGL7D9ARequest, // 欠税公告
|
||||||
|
"QYGL4B2E": qygl.ProcessQYGL4B2ERequest, // 税收违法
|
||||||
"COMENT01": qygl.ProcessCOMENT01Request, // 企业风险报告
|
"COMENT01": qygl.ProcessCOMENT01Request, // 企业风险报告
|
||||||
|
"QYGL5F6A": qygl.ProcessQYGL5F6ARequest, // 企业相关查询
|
||||||
|
"QYGL2B5C": qygl.ProcessQYGL2B5CRequest, // 企业联系人实际经营地址
|
||||||
|
"QYGL6S1B": qygl.ProcessQYGL6S1BRequest, //董监高司法综合信息核验
|
||||||
|
"QYGL9T1Q": qygl.ProcessQYGL9T1QRequest, //全国企业借贷意向验证查询_V1
|
||||||
|
"QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询
|
||||||
|
"QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询
|
||||||
|
"QYGL5CMP": qygl.ProcessQYGL5CMPRequest, //企业五要素验证
|
||||||
|
|
||||||
// YYSY系列处理器
|
// YYSY系列处理器
|
||||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||||
@@ -113,6 +166,17 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"YYSY6F2E": yysy.ProcessYYSY6F2ERequest,
|
"YYSY6F2E": yysy.ProcessYYSY6F2ERequest,
|
||||||
"YYSYBE08": yysy.ProcessYYSYBE08Request,
|
"YYSYBE08": yysy.ProcessYYSYBE08Request,
|
||||||
"YYSYF7DB": yysy.ProcessYYSYF7DBRequest,
|
"YYSYF7DB": yysy.ProcessYYSYF7DBRequest,
|
||||||
|
"YYSY4F2E": yysy.ProcessYYSY4F2ERequest,
|
||||||
|
"YYSY8B1C": yysy.ProcessYYSY8B1CRequest,
|
||||||
|
"YYSY6D9A": yysy.ProcessYYSY6D9ARequest,
|
||||||
|
"YYSY3E7F": yysy.ProcessYYSY3E7FRequest,
|
||||||
|
"YYSY8F3A": yysy.ProcessYYSY8F3ARequest,
|
||||||
|
"YYSY9A1B": yysy.ProcessYYSY9A1BRequest,
|
||||||
|
"YYSY8C2D": yysy.ProcessYYSY8C2DRequest,
|
||||||
|
"YYSY7D3E": yysy.ProcessYYSY7D3ERequest,
|
||||||
|
"YYSY9E4A": yysy.ProcessYYSY9E4ARequest,
|
||||||
|
"YYSY9F1B": yysy.ProcessYYSY9F1BYequest,
|
||||||
|
"YYSY6F2B": yysy.ProcessYYSY6F2BRequest,
|
||||||
|
|
||||||
// IVYZ系列处理器
|
// IVYZ系列处理器
|
||||||
"IVYZ0B03": ivyz.ProcessIVYZ0B03Request,
|
"IVYZ0B03": ivyz.ProcessIVYZ0B03Request,
|
||||||
@@ -126,13 +190,46 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"IVYZ4E8B": ivyz.ProcessIVYZ4E8BRequest,
|
"IVYZ4E8B": ivyz.ProcessIVYZ4E8BRequest,
|
||||||
"IVYZ1C9D": ivyz.ProcessIVYZ1C9DRequest,
|
"IVYZ1C9D": ivyz.ProcessIVYZ1C9DRequest,
|
||||||
"IVYZGZ08": ivyz.ProcessIVYZGZ08Request,
|
"IVYZGZ08": ivyz.ProcessIVYZGZ08Request,
|
||||||
|
"IVYZ2A8B": ivyz.ProcessIVYZ2A8BRequest,
|
||||||
|
"IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest,
|
||||||
|
"IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest,
|
||||||
|
"IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest,
|
||||||
|
"IVYZ3P9M": ivyz.ProcessIVYZ3P9MRequest,
|
||||||
|
"IVYZ3A7F": ivyz.ProcessIVYZ3A7FRequest,
|
||||||
|
"IVYZ9D2E": ivyz.ProcessIVYZ9D2ERequest,
|
||||||
|
"IVYZ81NC": ivyz.ProcessIVYZ81NCRequest,
|
||||||
|
"IVYZ6G7H": ivyz.ProcessIVYZ6G7HRequest,
|
||||||
|
"IVYZ8I9J": ivyz.ProcessIVYZ8I9JRequest,
|
||||||
|
"IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest,
|
||||||
|
"IVYZ2C1P": ivyz.ProcessIVYZ2C1PRequest,
|
||||||
|
"IVYZP2Q6": ivyz.ProcessIVYZP2Q6Request,
|
||||||
|
"IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历)
|
||||||
|
"IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型
|
||||||
|
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
|
||||||
|
|
||||||
// COMB系列处理器
|
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||||
"COMB298Y": comb.ProcessCOMB298YRequest,
|
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||||
"COMB86PM": comb.ProcessCOMB86PMRequest,
|
"COMBHZY2": comb.ProcessCOMBHZY2Request, // 自定义处理:生成合规报告
|
||||||
|
"COMBWD01": comb.ProcessCOMBWD01Request, // 自定义处理:将返回结构从数组改为对象
|
||||||
|
|
||||||
// QCXG系列处理器
|
// QCXG系列处理器
|
||||||
"QCXG7A2B": qcxg.ProcessQCXG7A2BRequest,
|
"QCXG7A2B": qcxg.ProcessQCXG7A2BRequest,
|
||||||
|
"QCXG9P1C": qcxg.ProcessQCXG9P1CRequest,
|
||||||
|
"QCXG8A3D": qcxg.ProcessQCXG8A3DRequest,
|
||||||
|
"QCXG6B4E": qcxg.ProcessQCXG6B4ERequest,
|
||||||
|
|
||||||
|
// DWBG系列处理器 - 多维报告
|
||||||
|
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||||
|
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,
|
||||||
|
"DWBG7F3A": dwbg.ProcessDWBG7F3ARequest,
|
||||||
|
|
||||||
|
// FLXG系列处理器 - 风险管控 (包含原FXHY功能)
|
||||||
|
"FLXG8B4D": flxg.ProcessFLXG8B4DRequest,
|
||||||
|
|
||||||
|
// TEST系列处理器 - 测试用处理器
|
||||||
|
"TEST001": test.ProcessTestRequest,
|
||||||
|
"TEST002": test.ProcessTestErrorRequest,
|
||||||
|
"TEST003": test.ProcessTestTimeoutRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量注册到组合包服务
|
// 批量注册到组合包服务
|
||||||
@@ -149,10 +246,31 @@ var RequestProcessors map[string]processors.ProcessorFunc
|
|||||||
|
|
||||||
// PreprocessRequestApi 调用指定的请求处理函数
|
// PreprocessRequestApi 调用指定的请求处理函数
|
||||||
func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode string, params []byte, options *commands.ApiCallOptions, callContext *processors.CallContext) ([]byte, error) {
|
func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode string, params []byte, options *commands.ApiCallOptions, callContext *processors.CallContext) ([]byte, error) {
|
||||||
|
// 设置Options和CallContext到依赖容器
|
||||||
|
deps := a.processorDeps.WithOptions(options).WithCallContext(callContext)
|
||||||
|
|
||||||
|
// 1. 优先查找已注册的自定义处理器
|
||||||
if processor, exists := RequestProcessors[apiCode]; exists {
|
if processor, exists := RequestProcessors[apiCode]; exists {
|
||||||
// 设置Options和CallContext到依赖容器
|
|
||||||
deps := a.processorDeps.WithOptions(options).WithCallContext(callContext)
|
|
||||||
return processor(ctx, params, deps)
|
return processor(ctx, params, deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否为组合包(COMB开头),使用通用组合包处理器
|
||||||
|
if len(apiCode) >= 4 && apiCode[:4] == "COMB" {
|
||||||
|
return a.processGenericCombRequest(ctx, apiCode, params, deps)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("%s: 未找到处理器: %s", ErrSystem, apiCode)
|
return nil, fmt.Errorf("%s: 未找到处理器: %s", ErrSystem, apiCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processGenericCombRequest 通用组合包处理器
|
||||||
|
func (a *ApiRequestService) processGenericCombRequest(ctx context.Context, apiCode string, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
// 调用组合包服务处理请求
|
||||||
|
// 这里不需要验证参数,因为组合包的参数验证由各个子处理器负责
|
||||||
|
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, apiCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回组合结果,无任何自定义处理
|
||||||
|
return json.Marshal(combinedResult)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
"tyapi-server/internal/config"
|
||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
repo "tyapi-server/internal/domains/api/repositories"
|
repo "tyapi-server/internal/domains/api/repositories"
|
||||||
)
|
)
|
||||||
@@ -9,7 +11,7 @@ import (
|
|||||||
type ApiUserAggregateService interface {
|
type ApiUserAggregateService interface {
|
||||||
CreateApiUser(ctx context.Context, apiUserId string) error
|
CreateApiUser(ctx context.Context, apiUserId string) error
|
||||||
UpdateWhiteList(ctx context.Context, apiUserId string, whiteList []string) error
|
UpdateWhiteList(ctx context.Context, apiUserId string, whiteList []string) error
|
||||||
AddToWhiteList(ctx context.Context, apiUserId string, entry string) error
|
AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error
|
||||||
RemoveFromWhiteList(ctx context.Context, apiUserId string, entry string) error
|
RemoveFromWhiteList(ctx context.Context, apiUserId string, entry string) error
|
||||||
FreezeApiUser(ctx context.Context, apiUserId string) error
|
FreezeApiUser(ctx context.Context, apiUserId string) error
|
||||||
UnfreezeApiUser(ctx context.Context, apiUserId string) error
|
UnfreezeApiUser(ctx context.Context, apiUserId string) error
|
||||||
@@ -20,14 +22,15 @@ type ApiUserAggregateService interface {
|
|||||||
|
|
||||||
type ApiUserAggregateServiceImpl struct {
|
type ApiUserAggregateServiceImpl struct {
|
||||||
repo repo.ApiUserRepository
|
repo repo.ApiUserRepository
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiUserAggregateService(repo repo.ApiUserRepository) ApiUserAggregateService {
|
func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config) ApiUserAggregateService {
|
||||||
return &ApiUserAggregateServiceImpl{repo: repo}
|
return &ApiUserAggregateServiceImpl{repo: repo, cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
|
func (s *ApiUserAggregateServiceImpl) CreateApiUser(ctx context.Context, apiUserId string) error {
|
||||||
apiUser, err := entities.NewApiUser(apiUserId)
|
apiUser, err := entities.NewApiUser(apiUserId, s.cfg.Wallet.BalanceAlert.DefaultEnabled, s.cfg.Wallet.BalanceAlert.DefaultThreshold)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,16 +45,25 @@ func (s *ApiUserAggregateServiceImpl) UpdateWhiteList(ctx context.Context, apiUs
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apiUser.UpdateWhiteList(whiteList)
|
// 将字符串数组转换为WhiteListItem数组
|
||||||
|
items := make([]entities.WhiteListItem, 0, len(whiteList))
|
||||||
|
now := time.Now()
|
||||||
|
for _, ip := range whiteList {
|
||||||
|
items = append(items, entities.WhiteListItem{
|
||||||
|
IPAddress: ip,
|
||||||
|
AddedAt: now, // 批量更新时使用当前时间
|
||||||
|
})
|
||||||
|
}
|
||||||
|
apiUser.UpdateWhiteList(items) // UpdateWhiteList 会转换为 WhiteList 类型
|
||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string) error {
|
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error {
|
||||||
apiUser, err := s.repo.FindByUserId(ctx, apiUserId)
|
apiUser, err := s.repo.FindByUserId(ctx, apiUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = apiUser.AddToWhiteList(entry)
|
err = apiUser.AddToWhiteList(entry, remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -88,7 +100,6 @@ func (s *ApiUserAggregateServiceImpl) UnfreezeApiUser(ctx context.Context, apiUs
|
|||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *ApiUserAggregateServiceImpl) LoadApiUserByAccessId(ctx context.Context, accessId string) (*entities.ApiUser, error) {
|
func (s *ApiUserAggregateServiceImpl) LoadApiUserByAccessId(ctx context.Context, accessId string) (*entities.ApiUser, error) {
|
||||||
return s.repo.FindByAccessId(ctx, accessId)
|
return s.repo.FindByAccessId(ctx, accessId)
|
||||||
}
|
}
|
||||||
@@ -101,7 +112,7 @@ func (s *ApiUserAggregateServiceImpl) LoadApiUserByUserId(ctx context.Context, a
|
|||||||
|
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = entities.WhiteList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiUser, nil
|
return apiUser, nil
|
||||||
@@ -115,7 +126,7 @@ func (s *ApiUserAggregateServiceImpl) SaveApiUser(ctx context.Context, apiUser *
|
|||||||
if exists != nil {
|
if exists != nil {
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = []entities.WhiteListItem{}
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
635
internal/domains/api/services/form_config_service.go
Normal file
635
internal/domains/api/services/form_config_service.go
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"tyapi-server/internal/domains/api/dto"
|
||||||
|
product_services "tyapi-server/internal/domains/product/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormConfig 表单配置
|
||||||
|
type FormConfig struct {
|
||||||
|
ApiCode string `json:"api_code"`
|
||||||
|
Fields []FormField `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormConfigService 表单配置服务接口
|
||||||
|
type FormConfigService interface {
|
||||||
|
GetFormConfig(ctx context.Context, apiCode string) (*FormConfig, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormConfigServiceImpl 表单配置服务实现
|
||||||
|
type FormConfigServiceImpl struct {
|
||||||
|
productManagementService *product_services.ProductManagementService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFormConfigService 创建表单配置服务
|
||||||
|
func NewFormConfigService(productManagementService *product_services.ProductManagementService) FormConfigService {
|
||||||
|
return &FormConfigServiceImpl{
|
||||||
|
productManagementService: productManagementService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFormConfigServiceWithoutDependencies 创建表单配置服务(不注入依赖,用于测试)
|
||||||
|
func NewFormConfigServiceWithoutDependencies() FormConfigService {
|
||||||
|
return &FormConfigServiceImpl{
|
||||||
|
productManagementService: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFormConfig 获取指定API的表单配置
|
||||||
|
func (s *FormConfigServiceImpl) GetFormConfig(ctx context.Context, apiCode string) (*FormConfig, error) {
|
||||||
|
// 根据API代码获取对应的DTO结构体
|
||||||
|
dtoStruct, err := s.getDTOStruct(ctx, apiCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if dtoStruct == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过反射解析结构体字段
|
||||||
|
fields := s.parseDTOFields(dtoStruct)
|
||||||
|
|
||||||
|
config := &FormConfig{
|
||||||
|
ApiCode: apiCode,
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDTOStruct 根据API代码获取对应的DTO结构体
|
||||||
|
func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string) (interface{}, error) {
|
||||||
|
// 建立API代码到DTO结构体的映射
|
||||||
|
dtoMap := map[string]interface{}{
|
||||||
|
"IVYZ9363": &dto.IVYZ9363Req{},
|
||||||
|
"IVYZ385E": &dto.IVYZ385EReq{},
|
||||||
|
"IVYZ5733": &dto.IVYZ5733Req{},
|
||||||
|
"FLXG3D56": &dto.FLXG3D56Req{},
|
||||||
|
"FLXG75FE": &dto.FLXG75FEReq{},
|
||||||
|
"FLXG0V3B": &dto.FLXG0V3BReq{},
|
||||||
|
"FLXG0V4B": &dto.FLXG0V4BReq{},
|
||||||
|
"FLXG54F5": &dto.FLXG54F5Req{},
|
||||||
|
"FLXG162A": &dto.FLXG162AReq{},
|
||||||
|
"FLXG0687": &dto.FLXG0687Req{},
|
||||||
|
"FLXGBC21": &dto.FLXGBC21Req{},
|
||||||
|
"FLXG970F": &dto.FLXG970FReq{},
|
||||||
|
"FLXG5876": &dto.FLXG5876Req{},
|
||||||
|
"FLXG9687": &dto.FLXG9687Req{},
|
||||||
|
"FLXGC9D1": &dto.FLXGC9D1Req{},
|
||||||
|
"FLXGCA3D": &dto.FLXGCA3DReq{},
|
||||||
|
"FLXGDEC7": &dto.FLXGDEC7Req{},
|
||||||
|
"JRZQ0A03": &dto.JRZQ0A03Req{},
|
||||||
|
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
|
||||||
|
"JRZQ8203": &dto.JRZQ8203Req{},
|
||||||
|
"JRZQDCBE": &dto.JRZQDCBEReq{},
|
||||||
|
"QYGL2ACD": &dto.QYGL2ACDReq{},
|
||||||
|
"QYGL6F2D": &dto.QYGL6F2DReq{},
|
||||||
|
"QYGL45BD": &dto.QYGL45BDReq{},
|
||||||
|
"QYGL8261": &dto.QYGL8261Req{},
|
||||||
|
"QYGL8271": &dto.QYGL8271Req{},
|
||||||
|
"QYGLB4C0": &dto.QYGLB4C0Req{},
|
||||||
|
"QYGL23T7": &dto.QYGL23T7Req{},
|
||||||
|
"QYGL5A3C": &dto.QYGL5A3CReq{},
|
||||||
|
"QYGL8B4D": &dto.QYGL8B4DReq{},
|
||||||
|
"QYGL9E2F": &dto.QYGL9E2FReq{},
|
||||||
|
"QYGL7C1A": &dto.QYGL7C1AReq{},
|
||||||
|
"QYGL3F8E": &dto.QYGL3F8EReq{},
|
||||||
|
"YYSY4B37": &dto.YYSY4B37Req{},
|
||||||
|
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||||
|
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||||
|
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||||
|
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||||
|
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||||
|
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||||
|
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||||
|
"IVYZ9A2B": &dto.IVYZ9A2BReq{},
|
||||||
|
"IVYZ7F2A": &dto.IVYZ7F2AReq{},
|
||||||
|
"IVYZ4E8B": &dto.IVYZ4E8BReq{},
|
||||||
|
"IVYZ1C9D": &dto.IVYZ1C9DReq{},
|
||||||
|
"IVYZGZ08": &dto.IVYZGZ08Req{},
|
||||||
|
"FLXG8A3F": &dto.FLXG8A3FReq{},
|
||||||
|
"FLXG5B2E": &dto.FLXG5B2EReq{},
|
||||||
|
"COMB298Y": &dto.COMB298YReq{},
|
||||||
|
"COMB86PM": &dto.COMB86PMReq{},
|
||||||
|
"QCXG7A2B": &dto.QCXG7A2BReq{},
|
||||||
|
"COMENT01": &dto.COMENT01Req{},
|
||||||
|
"JRZQ09J8": &dto.JRZQ09J8Req{},
|
||||||
|
"FLXGDEA8": &dto.FLXGDEA8Req{},
|
||||||
|
"FLXGDEA9": &dto.FLXGDEA9Req{},
|
||||||
|
"JRZQ1D09": &dto.JRZQ1D09Req{},
|
||||||
|
"IVYZ2A8B": &dto.IVYZ2A8BReq{},
|
||||||
|
"IVYZ7C9D": &dto.IVYZ7C9DReq{},
|
||||||
|
"IVYZ5E3F": &dto.IVYZ5E3FReq{},
|
||||||
|
"YYSY4F2E": &dto.YYSY4F2EReq{},
|
||||||
|
"YYSY8B1C": &dto.YYSY8B1CReq{},
|
||||||
|
"YYSY6D9A": &dto.YYSY6D9AReq{},
|
||||||
|
"YYSY3E7F": &dto.YYSY3E7FReq{},
|
||||||
|
"FLXG5A3B": &dto.FLXG5A3BReq{},
|
||||||
|
"FLXG9C1D": &dto.FLXG9C1DReq{},
|
||||||
|
"FLXG2E8F": &dto.FLXG2E8FReq{},
|
||||||
|
"JRZQ3C7B": &dto.JRZQ3C7BReq{},
|
||||||
|
"JRZQ8A2D": &dto.JRZQ8A2DReq{},
|
||||||
|
"JRZQ5E9F": &dto.JRZQ5E9FReq{},
|
||||||
|
"JRZQ4B6C": &dto.JRZQ4B6CReq{},
|
||||||
|
"JRZQ7F1A": &dto.JRZQ7F1AReq{},
|
||||||
|
"DWBG6A2C": &dto.DWBG6A2CReq{},
|
||||||
|
"DWBG8B4D": &dto.DWBG8B4DReq{},
|
||||||
|
"FLXG8B4D": &dto.FLXG8B4DReq{},
|
||||||
|
"IVYZ81NC": &dto.IVYZ81NCReq{},
|
||||||
|
"IVYZ7F3A": &dto.IVYZ7F3AReq{},
|
||||||
|
"IVYZ3P9M": &dto.IVYZ3P9MReq{},
|
||||||
|
"IVYZ3A7F": &dto.IVYZ3A7FReq{},
|
||||||
|
"IVYZ9D2E": &dto.IVYZ9D2EReq{},
|
||||||
|
"IVYZ9K2L": &dto.IVYZ9K2LReq{},
|
||||||
|
"DWBG7F3A": &dto.DWBG7F3AReq{},
|
||||||
|
"YYSY8F3A": &dto.YYSY8F3AReq{},
|
||||||
|
"QCXG9P1C": &dto.QCXG9P1CReq{},
|
||||||
|
"JRZQ9E2A": &dto.JRZQ9E2AReq{},
|
||||||
|
"YYSY9A1B": &dto.YYSY9A1BReq{},
|
||||||
|
"YYSY8C2D": &dto.YYSY8C2DReq{},
|
||||||
|
"YYSY7D3E": &dto.YYSY7D3EReq{},
|
||||||
|
"YYSY9E4A": &dto.YYSY9E4AReq{},
|
||||||
|
"JRZQ6F2A": &dto.JRZQ6F2AReq{},
|
||||||
|
"JRZQ8B3C": &dto.JRZQ8B3CReq{},
|
||||||
|
"JRZQ9D4E": &dto.JRZQ9D4EReq{},
|
||||||
|
"FLXG7E8F": &dto.FLXG7E8FReq{},
|
||||||
|
"QYGL5F6A": &dto.QYGL5F6AReq{},
|
||||||
|
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||||
|
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||||
|
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||||
|
"COMBHZY2": &dto.COMBHZY2Req{}, // 自此无imp11.28
|
||||||
|
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||||
|
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||||
|
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||||
|
"JRZQ2F8A": &dto.JRZQ2F8AReq{},
|
||||||
|
"JRZQ1E7B": &dto.JRZQ1E7BReq{},
|
||||||
|
"JRZQ3C9R": &dto.JRZQ3C9RReq{},
|
||||||
|
"IVYZ2C1P": &dto.IVYZ2C1PReq{},
|
||||||
|
"YYSY9F1B": &dto.YYSY9F1BReq{},
|
||||||
|
"YYSY6F2B": &dto.YYSY6F2BReq{},
|
||||||
|
"QYGL6S1B": &dto.QYGL6S1BReq{},
|
||||||
|
"JRZQ0B6Y": &dto.JRZQ0B6YReq{},
|
||||||
|
"JRZQ9A1W": &dto.JRZQ9A1WReq{},
|
||||||
|
"JRZQ8F7C": &dto.JRZQ8F7CReq{}, //综合多头
|
||||||
|
"FLXGK5D2": &dto.FLXGK5D2Req{},
|
||||||
|
"FLXG3A9B": &dto.FLXG3A9BReq{},
|
||||||
|
"IVYZP2Q6": &dto.IVYZP2Q6Req{},
|
||||||
|
"JRZQ1W4X": &dto.JRZQ1W4XReq{}, //全景档案
|
||||||
|
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
||||||
|
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
||||||
|
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
||||||
|
"JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策
|
||||||
|
"JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积
|
||||||
|
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||||
|
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||||
|
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||||
|
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先返回已配置的DTO
|
||||||
|
if dto, exists := dtoMap[apiCode]; exists {
|
||||||
|
return dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为通用组合包(COMB开头且未单独配置)
|
||||||
|
if len(apiCode) >= 4 && apiCode[:4] == "COMB" {
|
||||||
|
// 动态从数据库获取组合包的子产品信息,并合并DTO
|
||||||
|
return s.mergeCombPackageDTOs(ctx, apiCode, dtoMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDTOFields 通过反射解析DTO结构体字段
|
||||||
|
func (s *FormConfigServiceImpl) parseDTOFields(dtoStruct interface{}) []FormField {
|
||||||
|
var fields []FormField
|
||||||
|
|
||||||
|
t := reflect.TypeOf(dtoStruct).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
// 获取JSON标签
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取验证标签
|
||||||
|
validateTag := field.Tag.Get("validate")
|
||||||
|
|
||||||
|
// 解析验证规则
|
||||||
|
required := strings.Contains(validateTag, "required")
|
||||||
|
validation := s.parseValidationRules(validateTag)
|
||||||
|
|
||||||
|
// 根据字段类型和验证规则生成前端字段类型
|
||||||
|
fieldType := s.getFieldType(field.Type, validation)
|
||||||
|
|
||||||
|
// 生成字段标签(将下划线转换为中文)
|
||||||
|
label := s.generateFieldLabel(jsonTag)
|
||||||
|
|
||||||
|
// 生成示例值
|
||||||
|
example := s.generateExampleValue(field.Type, jsonTag)
|
||||||
|
|
||||||
|
// 生成占位符
|
||||||
|
placeholder := s.generatePlaceholder(jsonTag, fieldType)
|
||||||
|
|
||||||
|
// 生成字段描述
|
||||||
|
description := s.generateDescription(jsonTag, validation)
|
||||||
|
|
||||||
|
formField := FormField{
|
||||||
|
Name: jsonTag,
|
||||||
|
Label: label,
|
||||||
|
Type: fieldType,
|
||||||
|
Required: required,
|
||||||
|
Validation: validation,
|
||||||
|
Description: description,
|
||||||
|
Example: example,
|
||||||
|
Placeholder: placeholder,
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, formField)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseValidationRules 解析验证规则
|
||||||
|
func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string {
|
||||||
|
if validateTag == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将验证规则转换为前端可理解的格式
|
||||||
|
rules := strings.Split(validateTag, ",")
|
||||||
|
var frontendRules []string
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
rule = strings.TrimSpace(rule)
|
||||||
|
switch {
|
||||||
|
case rule == "required":
|
||||||
|
frontendRules = append(frontendRules, "必填")
|
||||||
|
case strings.HasPrefix(rule, "min="):
|
||||||
|
min := strings.TrimPrefix(rule, "min=")
|
||||||
|
frontendRules = append(frontendRules, "最小长度"+min)
|
||||||
|
case strings.HasPrefix(rule, "max="):
|
||||||
|
max := strings.TrimPrefix(rule, "max=")
|
||||||
|
frontendRules = append(frontendRules, "最大长度"+max)
|
||||||
|
case rule == "validMobileNo":
|
||||||
|
frontendRules = append(frontendRules, "手机号格式")
|
||||||
|
case rule == "validIDCard":
|
||||||
|
frontendRules = append(frontendRules, "身份证格式")
|
||||||
|
case rule == "validName":
|
||||||
|
frontendRules = append(frontendRules, "姓名格式")
|
||||||
|
case rule == "validUSCI":
|
||||||
|
frontendRules = append(frontendRules, "统一社会信用代码格式")
|
||||||
|
case rule == "validEnterpriseName" || rule == "enterprise_name":
|
||||||
|
frontendRules = append(frontendRules, "企业名称格式")
|
||||||
|
case rule == "validBankCard":
|
||||||
|
frontendRules = append(frontendRules, "银行卡号格式")
|
||||||
|
case rule == "validDate":
|
||||||
|
frontendRules = append(frontendRules, "日期格式")
|
||||||
|
case rule == "validAuthDate":
|
||||||
|
frontendRules = append(frontendRules, "授权日期格式")
|
||||||
|
case rule == "validTimeRange":
|
||||||
|
frontendRules = append(frontendRules, "时间范围格式")
|
||||||
|
case rule == "validMobileType":
|
||||||
|
frontendRules = append(frontendRules, "手机类型")
|
||||||
|
case rule == "validUniqueID":
|
||||||
|
frontendRules = append(frontendRules, "唯一标识格式")
|
||||||
|
case rule == "validReturnURL":
|
||||||
|
frontendRules = append(frontendRules, "返回链接格式")
|
||||||
|
case rule == "validAuthorizationURL":
|
||||||
|
frontendRules = append(frontendRules, "授权链接格式")
|
||||||
|
case rule == "validBase64Image":
|
||||||
|
frontendRules = append(frontendRules, "Base64图片格式(JPG、BMP、PNG)")
|
||||||
|
case strings.HasPrefix(rule, "oneof="):
|
||||||
|
values := strings.TrimPrefix(rule, "oneof=")
|
||||||
|
frontendRules = append(frontendRules, "可选值: "+values)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(frontendRules, "、")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFieldType 根据字段类型和验证规则确定前端字段类型
|
||||||
|
func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation string) string {
|
||||||
|
switch fieldType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if strings.Contains(validation, "手机号") {
|
||||||
|
return "tel"
|
||||||
|
} else if strings.Contains(validation, "身份证") {
|
||||||
|
return "text"
|
||||||
|
} else if strings.Contains(validation, "姓名") {
|
||||||
|
return "text"
|
||||||
|
} else if strings.Contains(validation, "时间范围格式") {
|
||||||
|
return "text" // time_range是HH:MM-HH:MM格式,使用文本输入
|
||||||
|
} else if strings.Contains(validation, "授权日期格式") {
|
||||||
|
return "text" // auth_date是YYYYMMDD-YYYYMMDD格式,使用文本输入
|
||||||
|
} else if strings.Contains(validation, "日期") {
|
||||||
|
return "date"
|
||||||
|
} else if strings.Contains(validation, "链接") {
|
||||||
|
return "url"
|
||||||
|
} else if strings.Contains(validation, "可选值") {
|
||||||
|
return "select"
|
||||||
|
} else if strings.Contains(validation, "Base64图片") || strings.Contains(validation, "base64") {
|
||||||
|
return "textarea"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
case reflect.Int64:
|
||||||
|
return "number"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "checkbox"
|
||||||
|
default:
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFieldLabel 生成字段标签
|
||||||
|
func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||||
|
// 将下划线命名转换为中文标签
|
||||||
|
labelMap := map[string]string{
|
||||||
|
"mobile_no": "手机号码",
|
||||||
|
"id_card": "身份证号",
|
||||||
|
"name": "姓名",
|
||||||
|
"man_name": "男方姓名",
|
||||||
|
"woman_name": "女方姓名",
|
||||||
|
"man_id_card": "男方身份证",
|
||||||
|
"woman_id_card": "女方身份证",
|
||||||
|
"ent_name": "企业名称",
|
||||||
|
"legal_person": "法人姓名",
|
||||||
|
"ent_code": "企业代码",
|
||||||
|
"auth_date": "授权日期",
|
||||||
|
"time_range": "时间范围",
|
||||||
|
"authorized": "是否授权",
|
||||||
|
"authorization_url": "授权链接",
|
||||||
|
"unique_id": "唯一标识",
|
||||||
|
"return_url": "返回链接",
|
||||||
|
"mobile_type": "手机类型",
|
||||||
|
"start_date": "开始日期",
|
||||||
|
"years": "年数",
|
||||||
|
"bank_card": "银行卡号",
|
||||||
|
"user_type": "关系类型",
|
||||||
|
"vehicle_type": "车辆类型",
|
||||||
|
"page_num": "页码",
|
||||||
|
"page_size": "每页数量",
|
||||||
|
"use_scenario": "使用场景",
|
||||||
|
"auth_authorize_file_code": "授权文件编码",
|
||||||
|
"plate_no": "车牌号",
|
||||||
|
"plate_type": "号牌类型",
|
||||||
|
"vin_code": "车辆识别代号VIN码",
|
||||||
|
"return_type": "返回类型",
|
||||||
|
"photo_data": "人脸图片",
|
||||||
|
"owner_type": "企业主类型",
|
||||||
|
"type": "查询类型",
|
||||||
|
"query_reason_id": "查询原因ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
if label, exists := labelMap[jsonTag]; exists {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有预定义,尝试自动转换
|
||||||
|
return strings.ReplaceAll(jsonTag, "_", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateExampleValue 生成示例值
|
||||||
|
func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jsonTag string) string {
|
||||||
|
exampleMap := map[string]string{
|
||||||
|
"mobile_no": "13800138000",
|
||||||
|
"id_card": "110101199001011234",
|
||||||
|
"name": "张三",
|
||||||
|
"man_name": "张三",
|
||||||
|
"woman_name": "李四",
|
||||||
|
"ent_name": "示例企业有限公司",
|
||||||
|
"legal_person": "王五",
|
||||||
|
"ent_code": "91110000123456789X",
|
||||||
|
"auth_date": "20240101-20241231",
|
||||||
|
"time_range": "09:00-18:00",
|
||||||
|
"authorized": "1",
|
||||||
|
"years": "5",
|
||||||
|
"bank_card": "6222021234567890123",
|
||||||
|
"mobile_type": "移动",
|
||||||
|
"start_date": "2024-01-01",
|
||||||
|
"unique_id": "UNIQUE123456",
|
||||||
|
"return_url": "https://example.com/return",
|
||||||
|
"authorization_url": "https://example.com/auth20250101.pdf 注意:请不要使用示例链接,示例链接仅作为参考格式。必须为实际的被查询人授权具有法律效益的授权书文件链接,如访问不到或为不实授权书将追究责任。协议必须为http https",
|
||||||
|
"user_type": "1",
|
||||||
|
"vehicle_type": "0",
|
||||||
|
"page_num": "1",
|
||||||
|
"page_size": "10",
|
||||||
|
"use_scenario": "1",
|
||||||
|
"auth_authorize_file_code": "AUTH123456",
|
||||||
|
"plate_no": "京A12345",
|
||||||
|
"plate_type": "01",
|
||||||
|
"vin_code": "LSGBF53M8DS123456",
|
||||||
|
"return_type": "1",
|
||||||
|
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
|
"ownerType": "1",
|
||||||
|
"type": "per",
|
||||||
|
"query_reason_id": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
if example, exists := exampleMap[jsonTag]; exists {
|
||||||
|
return example
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据字段类型生成默认示例
|
||||||
|
switch fieldType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return "示例值"
|
||||||
|
case reflect.Int64:
|
||||||
|
return "123"
|
||||||
|
case reflect.Bool:
|
||||||
|
return "true"
|
||||||
|
default:
|
||||||
|
return "示例值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePlaceholder 生成占位符
|
||||||
|
func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType string) string {
|
||||||
|
placeholderMap := map[string]string{
|
||||||
|
"mobile_no": "请输入11位手机号码",
|
||||||
|
"id_card": "请输入18位身份证号码",
|
||||||
|
"name": "请输入真实姓名",
|
||||||
|
"man_name": "请输入男方真实姓名",
|
||||||
|
"woman_name": "请输入女方真实姓名",
|
||||||
|
"ent_name": "请输入企业全称",
|
||||||
|
"legal_person": "请输入法人真实姓名",
|
||||||
|
"ent_code": "请输入统一社会信用代码",
|
||||||
|
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||||
|
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||||
|
"authorized": "请选择是否授权",
|
||||||
|
"years": "请输入查询年数(0-100)",
|
||||||
|
"bank_card": "请输入银行卡号",
|
||||||
|
"mobile_type": "请选择手机类型",
|
||||||
|
"start_date": "请选择开始日期",
|
||||||
|
"unique_id": "请输入唯一标识",
|
||||||
|
"return_url": "请输入返回链接",
|
||||||
|
"authorization_url": "请输入授权链接",
|
||||||
|
"user_type": "请选择关系类型",
|
||||||
|
"vehicle_type": "请选择车辆类型",
|
||||||
|
"page_num": "请输入页码",
|
||||||
|
"page_size": "请输入每页数量(1-100)",
|
||||||
|
"use_scenario": "请选择使用场景",
|
||||||
|
"auth_authorize_file_code": "请输入授权文件编码",
|
||||||
|
"plate_no": "请输入车牌号",
|
||||||
|
"plate_type": "请选择号牌类型(01或02)",
|
||||||
|
"vin_code": "请输入17位车辆识别代号VIN码",
|
||||||
|
"return_type": "请选择返回类型",
|
||||||
|
"photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)",
|
||||||
|
"ownerType": "请选择企业主类型",
|
||||||
|
"type": "请选择查询类型",
|
||||||
|
"query_reason_id": "请选择查询原因ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据字段类型生成默认占位符
|
||||||
|
switch fieldType {
|
||||||
|
case "tel":
|
||||||
|
return "请输入电话号码"
|
||||||
|
case "date":
|
||||||
|
return "请选择日期"
|
||||||
|
case "url":
|
||||||
|
return "请输入链接地址"
|
||||||
|
case "number":
|
||||||
|
return "请输入数字"
|
||||||
|
default:
|
||||||
|
return "请输入" + s.generateFieldLabel(jsonTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDescription 生成字段描述
|
||||||
|
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
||||||
|
descMap := map[string]string{
|
||||||
|
"mobile_no": "请输入11位手机号码",
|
||||||
|
"id_card": "请输入18位身份证号码最后一位如是字母请大写",
|
||||||
|
"name": "请输入真实姓名",
|
||||||
|
"man_name": "请输入男方真实姓名",
|
||||||
|
"woman_name": "请输入女方真实姓名",
|
||||||
|
"ent_name": "请输入企业全称",
|
||||||
|
"legal_person": "请输入法人真实姓名",
|
||||||
|
"ent_code": "请输入统一社会信用代码",
|
||||||
|
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||||
|
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||||
|
"authorized": "请输入是否授权:0-未授权,1-已授权",
|
||||||
|
"years": "请输入查询年数(0-100)",
|
||||||
|
"bank_card": "请输入银行卡号",
|
||||||
|
"mobile_type": "请选择手机类型",
|
||||||
|
"start_date": "请选择开始日期",
|
||||||
|
"unique_id": "请输入唯一标识",
|
||||||
|
"return_url": "请输入返回链接",
|
||||||
|
"authorization_url": "请输入授权链接",
|
||||||
|
"user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)",
|
||||||
|
"vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)",
|
||||||
|
"page_num": "请输入页码,从1开始",
|
||||||
|
"page_size": "请输入每页数量,范围1-100",
|
||||||
|
"use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他",
|
||||||
|
"auth_authorize_file_code": "请输入授权文件编码",
|
||||||
|
"plate_no": "请输入车牌号",
|
||||||
|
"plate_type": "号牌类型:01-小型汽车;02-大型汽车(可选)",
|
||||||
|
"vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)",
|
||||||
|
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||||
|
"photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||||
|
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||||
|
"type": "查询类型:per-人员,ent-企业 ",
|
||||||
|
"query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调",
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc, exists := descMap[jsonTag]; exists {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
return "请输入" + s.generateFieldLabel(jsonTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeCombPackageDTOs 动态合并组合包的子产品DTO结构体
|
||||||
|
func (s *FormConfigServiceImpl) mergeCombPackageDTOs(ctx context.Context, apiCode string, dtoMap map[string]interface{}) (interface{}, error) {
|
||||||
|
// 如果productManagementService为nil(测试环境),返回空结构体
|
||||||
|
if s.productManagementService == nil {
|
||||||
|
return &struct{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 从数据库获取组合包产品信息
|
||||||
|
packageProduct, err := s.productManagementService.GetProductByCode(ctx, apiCode)
|
||||||
|
if err != nil {
|
||||||
|
// 如果获取失败,返回空结构体
|
||||||
|
return &struct{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否为组合包
|
||||||
|
if !packageProduct.IsPackage {
|
||||||
|
return &struct{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取组合包的所有子产品
|
||||||
|
packageItems, err := s.productManagementService.GetPackageItems(ctx, packageProduct.ID)
|
||||||
|
if err != nil || len(packageItems) == 0 {
|
||||||
|
return &struct{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 收集所有子产品的DTO字段并去重
|
||||||
|
// 使用map记录已存在的字段,key为json tag
|
||||||
|
fieldMap := make(map[string]reflect.StructField)
|
||||||
|
|
||||||
|
for _, item := range packageItems {
|
||||||
|
subProductCode := item.Product.Code
|
||||||
|
// 在dtoMap中查找子产品的DTO
|
||||||
|
if subDTO, exists := dtoMap[subProductCode]; exists {
|
||||||
|
// 解析DTO的字段
|
||||||
|
dtoType := reflect.TypeOf(subDTO).Elem()
|
||||||
|
for i := 0; i < dtoType.NumField(); i++ {
|
||||||
|
field := dtoType.Field(i)
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag != "" && jsonTag != "-" {
|
||||||
|
// 去除omitempty等选项
|
||||||
|
jsonTag = strings.Split(jsonTag, ",")[0]
|
||||||
|
// 如果字段不存在或已存在但新字段有required标记,则覆盖
|
||||||
|
if existingField, exists := fieldMap[jsonTag]; !exists {
|
||||||
|
fieldMap[jsonTag] = field
|
||||||
|
} else {
|
||||||
|
// 如果新字段有required且旧字段没有,则用新字段
|
||||||
|
newValidate := field.Tag.Get("validate")
|
||||||
|
oldValidate := existingField.Tag.Get("validate")
|
||||||
|
if strings.Contains(newValidate, "required") && !strings.Contains(oldValidate, "required") {
|
||||||
|
fieldMap[jsonTag] = field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 动态创建结构体
|
||||||
|
fields := make([]reflect.StructField, 0, len(fieldMap))
|
||||||
|
for _, field := range fieldMap {
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建结构体类型
|
||||||
|
structType := reflect.StructOf(fields)
|
||||||
|
|
||||||
|
// 创建并返回结构体实例
|
||||||
|
structValue := reflect.New(structType)
|
||||||
|
return structValue.Interface(), nil
|
||||||
|
}
|
||||||
134
internal/domains/api/services/form_config_service_test.go
Normal file
134
internal/domains/api/services/form_config_service_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormConfigService_GetFormConfig(t *testing.T) {
|
||||||
|
service := NewFormConfigServiceWithoutDependencies()
|
||||||
|
|
||||||
|
// 测试获取存在的API配置
|
||||||
|
config, err := service.GetFormConfig(context.Background(), "IVYZ9363")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取表单配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
t.Fatal("表单配置不应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ApiCode != "IVYZ9363" {
|
||||||
|
t.Errorf("期望API代码为 IVYZ9363,实际为 %s", config.ApiCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Fields) == 0 {
|
||||||
|
t.Fatal("字段列表不应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证字段信息
|
||||||
|
expectedFields := map[string]bool{
|
||||||
|
"man_name": false,
|
||||||
|
"man_id_card": false,
|
||||||
|
"woman_name": false,
|
||||||
|
"woman_id_card": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range config.Fields {
|
||||||
|
if _, exists := expectedFields[field.Name]; !exists {
|
||||||
|
t.Errorf("意外的字段: %s", field.Name)
|
||||||
|
}
|
||||||
|
expectedFields[field.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for fieldName, found := range expectedFields {
|
||||||
|
if !found {
|
||||||
|
t.Errorf("缺少字段: %s", fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试获取不存在的API配置
|
||||||
|
config, err = service.GetFormConfig(context.Background(), "NONEXISTENT")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取不存在的API配置不应返回错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
t.Fatal("不存在的API配置应返回nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormConfigService_FieldValidation(t *testing.T) {
|
||||||
|
service := NewFormConfigServiceWithoutDependencies()
|
||||||
|
|
||||||
|
config, err := service.GetFormConfig(context.Background(), "FLXG3D56")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取表单配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
t.Fatal("表单配置不应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证手机号字段
|
||||||
|
var mobileField *FormField
|
||||||
|
for _, field := range config.Fields {
|
||||||
|
if field.Name == "mobile_no" {
|
||||||
|
mobileField = &field
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mobileField == nil {
|
||||||
|
t.Fatal("应找到mobile_no字段")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mobileField.Required {
|
||||||
|
t.Error("mobile_no字段应为必填")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mobileField.Type != "tel" {
|
||||||
|
t.Errorf("mobile_no字段类型应为tel,实际为%s", mobileField.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(mobileField.Validation, "手机号格式") {
|
||||||
|
t.Errorf("mobile_no字段验证规则应包含'手机号格式',实际为: %s", mobileField.Validation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormConfigService_FieldLabels(t *testing.T) {
|
||||||
|
service := NewFormConfigServiceWithoutDependencies()
|
||||||
|
|
||||||
|
config, err := service.GetFormConfig(context.Background(), "IVYZ9363")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取表单配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证字段标签
|
||||||
|
expectedLabels := map[string]string{
|
||||||
|
"man_name": "男方姓名",
|
||||||
|
"man_id_card": "男方身份证",
|
||||||
|
"woman_name": "女方姓名",
|
||||||
|
"woman_id_card": "女方身份证",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range config.Fields {
|
||||||
|
if expectedLabel, exists := expectedLabels[field.Name]; exists {
|
||||||
|
if field.Label != expectedLabel {
|
||||||
|
t.Errorf("字段 %s 的标签应为 %s,实际为 %s", field.Name, expectedLabel, field.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || func() bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}())))
|
||||||
|
}
|
||||||
128
internal/domains/api/services/processors/README.md
Normal file
128
internal/domains/api/services/processors/README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 处理器错误处理解决方案
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
在使用 `errors.Join(processors.ErrInvalidParam, err)` 包装错误后,外层的 `errors.Is(err, processors.ErrInvalidParam)` 无法正确识别错误类型。
|
||||||
|
|
||||||
|
## 原因分析
|
||||||
|
|
||||||
|
`fmt.Errorf` 创建的包装错误虽然实现了 `Unwrap()` 接口,但没有实现 `Is()` 接口,因此 `errors.Is` 无法正确判断错误类型。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 🎯 **推荐方案:使用 `errors.Join`(Go 1.20+)**
|
||||||
|
|
||||||
|
这是最简洁、最标准的解决方案,Go 1.20+ 原生支持:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在处理器中创建错误
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
|
||||||
|
// 在应用服务层判断错误
|
||||||
|
if errors.Is(err, processors.ErrInvalidParam) {
|
||||||
|
// 现在可以正确识别了!
|
||||||
|
businessError = ErrInvalidParam
|
||||||
|
return ErrInvalidParam
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ **优势**
|
||||||
|
|
||||||
|
1. **极简代码**:一行代码解决问题
|
||||||
|
2. **标准库支持**:Go 1.20+ 原生功能
|
||||||
|
3. **完全兼容**:`errors.Is` 可以正确识别错误类型
|
||||||
|
4. **性能优秀**:标准库实现,性能最佳
|
||||||
|
5. **向后兼容**:现有的错误处理代码无需修改
|
||||||
|
|
||||||
|
### 📝 **使用方法**
|
||||||
|
|
||||||
|
#### 在处理器中(替换旧方式):
|
||||||
|
```go
|
||||||
|
// 旧方式 ❌
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
|
||||||
|
// 新方式 ✅
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在应用服务层(现在可以正确工作):
|
||||||
|
```go
|
||||||
|
if errors.Is(err, processors.ErrInvalidParam) {
|
||||||
|
// 现在可以正确识别了!
|
||||||
|
businessError = ErrInvalidParam
|
||||||
|
return ErrInvalidParam
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 其他方案对比
|
||||||
|
|
||||||
|
### 方案1:`errors.Join`(推荐 ⭐⭐⭐⭐⭐)
|
||||||
|
- **简洁度**:⭐⭐⭐⭐⭐
|
||||||
|
- **兼容性**:⭐⭐⭐⭐⭐
|
||||||
|
- **性能**:⭐⭐⭐⭐⭐
|
||||||
|
- **维护性**:⭐⭐⭐⭐⭐
|
||||||
|
|
||||||
|
### 方案2:自定义错误类型
|
||||||
|
- **简洁度**:⭐⭐⭐
|
||||||
|
- **兼容性**:⭐⭐⭐⭐⭐
|
||||||
|
- **性能**:⭐⭐⭐⭐
|
||||||
|
- **维护性**:⭐⭐⭐
|
||||||
|
|
||||||
|
### 方案3:继续使用 `fmt.Errorf`
|
||||||
|
- **简洁度**:⭐⭐⭐⭐
|
||||||
|
- **兼容性**:❌(无法识别错误类型)
|
||||||
|
- **性能**:⭐⭐⭐⭐
|
||||||
|
- **维护性**:❌
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 步骤1: 检查Go版本
|
||||||
|
确保项目使用 Go 1.20 或更高版本
|
||||||
|
|
||||||
|
### 步骤2: 更新错误创建
|
||||||
|
将所有处理器中的 `fmt.Errorf("%s: %w", processors.ErrXXX, err)` 替换为 `errors.Join(processors.ErrXXX, err)`
|
||||||
|
|
||||||
|
### 步骤3: 验证错误判断
|
||||||
|
确保应用服务层的 `errors.Is(err, processors.ErrXXX)` 能正确工作
|
||||||
|
|
||||||
|
### 步骤4: 测试验证
|
||||||
|
运行测试确保所有错误处理逻辑正常工作
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 处理器层
|
||||||
|
func ProcessRequest(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error) {
|
||||||
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
}
|
||||||
|
// ... 其他逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用服务层
|
||||||
|
if err := s.apiRequestService.PreprocessRequestApi(ctx, cmd.ApiName, requestParams, &cmd.Options, callContext); err != nil {
|
||||||
|
if errors.Is(err, processors.ErrInvalidParam) {
|
||||||
|
// 现在可以正确识别了!
|
||||||
|
businessError = ErrInvalidParam
|
||||||
|
return ErrInvalidParam
|
||||||
|
}
|
||||||
|
// ... 其他错误处理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **Go版本要求**:需要 Go 1.20 或更高版本
|
||||||
|
2. **错误消息格式**:`errors.Join` 使用换行符分隔多个错误
|
||||||
|
3. **完全兼容**:`errors.Is` 现在可以正确识别所有错误类型
|
||||||
|
4. **性能提升**:标准库实现,性能优于自定义解决方案
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
使用 `errors.Join` 是最简洁、最标准的解决方案:
|
||||||
|
- ✅ 一行代码解决问题
|
||||||
|
- ✅ 完全兼容 `errors.Is`
|
||||||
|
- ✅ Go 1.20+ 原生支持
|
||||||
|
- ✅ 性能优秀,维护简单
|
||||||
|
|
||||||
|
如果你的项目使用 Go 1.20+,强烈推荐使用这个方案!
|
||||||
74
internal/domains/api/services/processors/comb/README.md
Normal file
74
internal/domains/api/services/processors/comb/README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# 组合包处理器说明
|
||||||
|
|
||||||
|
## 🚀 动态组合包机制
|
||||||
|
|
||||||
|
从现在开始,组合包支持**动态处理机制**,大大简化了组合包的开发和维护工作。
|
||||||
|
|
||||||
|
## 📋 工作原理
|
||||||
|
|
||||||
|
### 1. 自动识别
|
||||||
|
- 所有以 `COMB` 开头的API编码会被自动识别为组合包
|
||||||
|
- 系统会自动调用通用组合包处理器处理请求
|
||||||
|
|
||||||
|
### 2. 处理流程
|
||||||
|
1. **优先级检查**:首先检查是否有注册的自定义处理器
|
||||||
|
2. **通用处理**:如果没有自定义处理器,且API编码以COMB开头,使用通用处理器
|
||||||
|
3. **数据库驱动**:根据数据库中的组合包配置自动调用相应的子产品处理器
|
||||||
|
|
||||||
|
## 🛠️ 使用方式
|
||||||
|
|
||||||
|
### 普通组合包(无自定义逻辑)
|
||||||
|
**只需要在数据库配置,无需编写任何代码!**
|
||||||
|
|
||||||
|
1. 在 `products` 表中创建组合包产品:
|
||||||
|
```sql
|
||||||
|
INSERT INTO products (code, name, is_package, ...)
|
||||||
|
VALUES ('COMB1234', '新组合包', true, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在 `product_package_items` 表中配置子产品:
|
||||||
|
```sql
|
||||||
|
INSERT INTO product_package_items (package_id, product_id, sort_order)
|
||||||
|
VALUES
|
||||||
|
('组合包产品ID', '子产品1ID', 1),
|
||||||
|
('组合包产品ID', '子产品2ID', 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **直接调用**:无需任何额外编码,API立即可用!
|
||||||
|
|
||||||
|
### 自定义组合包(有特殊逻辑)
|
||||||
|
如果需要对组合包结果进行后处理,才需要编写代码:
|
||||||
|
|
||||||
|
1. **创建处理器文件**:`combXXXX_processor.go`
|
||||||
|
2. **注册处理器**:在 `api_request_service.go` 中注册
|
||||||
|
3. **实现自定义逻辑**:在处理器中实现特殊业务逻辑
|
||||||
|
|
||||||
|
## 📁 现有组合包示例
|
||||||
|
|
||||||
|
### COMB86PM(自定义处理器)
|
||||||
|
```go
|
||||||
|
// 有自定义逻辑:重命名子产品ApiCode
|
||||||
|
for _, resp := range combinedResult.Responses {
|
||||||
|
if resp.ApiCode == "FLXGBC21" {
|
||||||
|
resp.ApiCode = "FLXG54F5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### COMB298Y(通用处理器)
|
||||||
|
- **无需编码**:已删除专门的处理器文件
|
||||||
|
- **自动处理**:通过数据库配置自动工作
|
||||||
|
|
||||||
|
## ✅ 优势
|
||||||
|
|
||||||
|
1. **零配置**:普通组合包只需数据库配置,无需编码
|
||||||
|
2. **灵活性**:特殊需求仍可通过自定义处理器实现
|
||||||
|
3. **维护性**:减少重复代码,统一处理逻辑
|
||||||
|
4. **扩展性**:新增组合包极其简单,配置即用
|
||||||
|
|
||||||
|
## 🔧 开发建议
|
||||||
|
|
||||||
|
1. **优先使用通用处理器**:除非有特殊业务逻辑,否则不要编写自定义处理器
|
||||||
|
2. **命名规范**:组合包编码必须以 `COMB` 开头
|
||||||
|
3. **数据库配置**:确保组合包在数据库中正确配置了 `is_package=true` 和子产品关系
|
||||||
|
4. **排序控制**:通过 `sort_order` 字段控制子产品在响应中的顺序
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package comb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"tyapi-server/internal/domains/api/dto"
|
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProcessCOMB298YRequest COMB298Y API处理方法
|
|
||||||
func ProcessCOMB298YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
|
||||||
var paramsDto dto.COMB298YReq
|
|
||||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用组合包服务处理请求
|
|
||||||
// Options会自动传递给所有子处理器
|
|
||||||
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB298Y")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return json.Marshal(combinedResult)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user