Compare commits

..

73 Commits

Author SHA1 Message Date
da37b4d7bc fix JRZQ8A2D 2025-10-29 22:09:00 +08:00
657d51ad57 fix 2025-10-29 22:05:53 +08:00
368dc2669c add jrzq0l85 2025-10-24 17:18:46 +08:00
79cd87bd18 add yysy9e4a 2025-10-22 16:40:14 +08:00
99d9405db0 fix 2025-10-21 15:23:03 +08:00
3d3ca98eb7 fix 2025-10-17 18:40:02 +08:00
96f22b4249 fix 2025-10-17 18:36:17 +08:00
e5a5e85e5d fix QCXG9P1C 2025-10-17 18:32:48 +08:00
adc9db7f78 new 2025-10-17 17:59:54 +08:00
b92dfd0d58 fix 2025-10-16 19:58:01 +08:00
b05d694755 fix 2025-10-16 18:46:44 +08:00
5a6e95906c new 2025-10-16 18:35:18 +08:00
a49d58365e fix 2025-10-14 20:50:32 +08:00
309a9a4c96 fix 2025-10-11 19:10:09 +08:00
532b92713b fix 2025-09-30 12:03:51 +08:00
1b931cb816 new 2025-09-20 23:29:49 +08:00
7b1b75e7a9 fix 2025-09-20 19:05:07 +08:00
2685df85c3 fix 2025-09-20 17:46:33 +08:00
353c57c98b fix ivyz7f3a 2025-09-17 19:02:50 +08:00
cfad2bce09 fix rank 2025-09-15 21:15:23 +08:00
6874f67c45 fix 2025-09-14 16:34:55 +08:00
a0fc9dc246 fix 2025-09-12 13:29:03 +08:00
c46c1e23a1 fix 2025-09-12 13:20:08 +08:00
e05ad9e223 new 2025-09-12 01:15:09 +08:00
c563b2266b fix dto 2025-09-11 16:18:26 +08:00
c1f127e9b1 fix 2025-09-03 13:51:52 +08:00
c579e53ad1 fix 2025-09-02 20:46:10 +08:00
d73659fed3 fix 2025-09-02 17:13:16 +08:00
c7c4ab7a19 add timed 2025-09-02 16:37:28 +08:00
2f3817c8f0 fix 2025-09-01 21:15:15 +08:00
16a8cd5506 fix 2025-09-01 21:02:19 +08:00
ebacec8e16 fix 2025-09-01 20:48:54 +08:00
5c5c2abfcd fix 2025-09-01 20:46:56 +08:00
5d5372e359 add article 2025-09-01 18:29:59 +08:00
34ff6ce916 fix: 修复validAuthDate验证器未定义错误 2025-08-30 01:34:54 +08:00
a2008e66e6 fix 2025-08-29 16:14:36 +08:00
ecc7495954 fix 2025-08-28 17:09:21 +08:00
f324f15397 fix 2025-08-28 01:05:46 +08:00
50a4fa86ce add cors 2025-08-28 00:50:30 +08:00
5051aea55c fix 2025-08-27 22:19:19 +08:00
4031277a91 fix 2025-08-26 21:40:02 +08:00
958f23487c fix 2025-08-26 16:52:31 +08:00
b1049cd984 fix processor 2025-08-26 16:47:24 +08:00
a91bde0c67 fix ivyz7c9d
fix external_logger
2025-08-26 16:03:46 +08:00
2a93d120f1 add new api 2025-08-26 14:43:27 +08:00
267ff92998 add JRZQ09J8、FLXGDEA8、FLXGDEA9、JRZQ1D09
add external_services log
2025-08-25 15:44:06 +08:00
365a2a8886 add Subscribe Discount 2025-08-23 16:30:34 +08:00
5dad8a3ccb add subscription gorm preload 2025-08-23 16:02:03 +08:00
4c9bb7cbf7 fix alipay return 2025-08-23 14:57:37 +08:00
9f8630784d change database config 2025-08-19 01:49:19 +08:00
ecfe7a6fd6 fix validator 2025-08-18 18:18:04 +08:00
59b3d76f57 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2025-08-18 18:12:37 +08:00
133e8e7e5a fix 2025-08-18 14:13:16 +08:00
067d7d1609 fix 2025-08-10 15:28:43 +08:00
ebc02b11e8 fix 2025-08-10 15:19:10 +08:00
bb291f6847 add limit 2025-08-10 14:40:02 +08:00
9e6248efb2 fix 2025-08-07 21:45:35 +08:00
c2685a17ba add coment01 2025-08-06 23:36:38 +08:00
2150e262a7 fix flxg5b2e flxg8a3f 2025-08-06 15:27:36 +08:00
9dc3c4bfd4 fix f;xg8a3f 2025-08-06 12:53:07 +08:00
ec1bba4898 fix certification 2025-08-05 17:20:49 +08:00
f1478e475f fix qygl8271 2025-08-05 15:42:46 +08:00
8ca672f1f4 fix flxg0v4b 2025-08-05 14:51:07 +08:00
3f4c3086f3 新增部分接口合同授权码 2025-08-04 22:02:09 +08:00
f482f0a6e8 接入阿里云二要素 2025-08-04 17:16:38 +08:00
bce55a3bb2 add qcxg7a2b 2025-08-03 23:30:30 +08:00
f991ad94d3 fix yushan 2025-08-03 19:18:53 +08:00
63fc9fa7b5 fix 2025-08-03 19:04:25 +08:00
6c5016912e fix 2025-08-03 18:46:53 +08:00
7b433e703a fix Category
add flxg5b2e
add flxg8a3f
add ivyz1c9d
add ivyz4e8b
add ivyz7f2a
2025-08-02 22:27:59 +08:00
14c3a23752 Adapt to old_id 2025-08-02 20:44:09 +08:00
719cd14269 add 合同类型 2025-08-02 19:58:14 +08:00
46915672cc fix 2025-08-02 05:38:35 +08:00
326 changed files with 82278 additions and 6763 deletions

1
.gitignore vendored
View File

@@ -25,6 +25,7 @@ Thumbs.db
# 临时文件
tmp/
temp/
console
# 依赖目录
vendor/

View File

@@ -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架构规范是一个优秀的架构实现。

64
Dockerfile.worker Normal file
View 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"]

View File

@@ -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的向后兼容性。
所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。

View File

@@ -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. **低优先级**: 外部服务集成(短信、邮件、文件存储)
## 注意事项
- 所有外部服务调用都应该有适当的错误处理和重试机制
- 事件发布失败不应该影响主业务流程
- 文件上传需要验证文件类型和大小
- 邮件发送需要支持模板和国际化

137
cmd/worker/main.go Normal file
View File

@@ -0,0 +1,137 @@
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"
)
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)
})
// 启动 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
}

View File

@@ -22,8 +22,8 @@ database:
name: "tyapi_dev"
sslmode: "disable"
timezone: "Asia/Shanghai"
max_open_conns: 25
max_idle_conns: 10
max_open_conns: 50
max_idle_conns: 20
conn_max_lifetime: 300s
auto_migrate: true
@@ -44,17 +44,73 @@ cache:
cleanup_interval: 600s
max_size: 1000
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger:
level: "info"
format: "console"
output: "file"
log_dir: "logs"
max_size: 100
max_backups: 3
max_age: 7
compress: true
use_color: true
use_daily: false
# 基础配置
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 # 是否启用性能日志
# 开发环境配置
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:
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
@@ -62,14 +118,14 @@ jwt:
refresh_expires_in: 168h
api:
domain: "apitest.tianyuanapi.com"
domain: "api.tianyuanapi.com"
sms:
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
endpoint_url: "dysmsapi.aliyuncs.com"
sign_name: "天远数据"
template_code: "SMS_474525324"
sign_name: "天远"
template_code: "SMS_302641455"
code_length: 6
expire_time: 5m
mock_enabled: false
@@ -104,7 +160,48 @@ ocr:
ratelimit:
requests: 5000
window: 60s
burst: 100
# 每日请求限制配置
daily_ratelimit:
max_requests_per_day: 200 # 每日最大请求次数
max_requests_per_ip: 10 # 每个IP每日最大请求次数
key_prefix: "daily_limit" # Redis键前缀
ttl: 24h # 键过期时间
max_concurrent: 5 # 最大并发请求数
# 安全配置
enable_ip_whitelist: false # 是否启用IP白名单
ip_whitelist: # IP白名单列表
- "192.168.1.*" # 内网IP段
- "10.0.0.*" # 内网IP段
- "127.0.0.1" # 本地回环
enable_ip_blacklist: true # 是否启用IP黑名单
ip_blacklist: # IP黑名单列表
- "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP
enable_user_agent: true # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人
- "crawler" # 爬虫
- "spider" # 蜘蛛
- "scraper" # 抓取器
- "curl" # curl工具
- "wget" # wget工具
- "python" # Python脚本
- "java" # Java脚本
- "go-http-client" # Go HTTP客户端
enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com" # 天元API控制台
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
enable_proxy_check: true # 是否检查代理
enable_geo_block: false # 是否启用地理位置阻止
blocked_countries: # 被阻止的国家/地区
- "XX" # 示例国家代码
monitoring:
metrics_enabled: true
@@ -130,14 +227,15 @@ development:
debug: true
enable_profiler: 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_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# 企业微信配置
wechat_work:
webhook_url: ""
secret: ""
# ===========================================
# 📝 e签宝服务配置
# ===========================================
@@ -178,6 +276,12 @@ wallet:
- recharge_amount: 10000.00 # 充值10000元
bonus_amount: 800.00 # 赠送800元
# 余额预警配置
balance_alert:
default_enabled: true # 默认启用余额预警
default_threshold: 200.00 # 默认预警阈值
alert_cooldown_hours: 24 # 预警冷却时间(小时)
# ===========================================
# 🌍 西部数据配置
# ===========================================
@@ -187,14 +291,66 @@ westdex:
secret_id: "449159"
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
# ===========================================
# 🌍 羽山配置
# ===========================================
yushan:
url: https://api.yushanshuju.com/credit-gw/service"
url: https://api.yushanshuju.com/credit-gw/service
api_key: "4c566c4a4b543164535455685655316c"
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
# ===========================================
# 💰 支付宝支付配置
# ===========================================
@@ -212,3 +368,79 @@ alipay:
tianyancha:
base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
# ===========================================
# ☁️ 阿里云配置
# ===========================================
alicloud:
host: "https://kzidcardv1.market.alicloudapi.com"
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
# ===========================================
# 🎯 行为数据配置
# ===========================================
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

View File

@@ -15,19 +15,6 @@ database:
name: "tyapi_dev"
# ===========================================
# 📝 日志配置
# ===========================================
logger:
level: info
format: json
output: "file"
log_dir: "logs"
max_size: 100
max_backups: 5
max_age: 30
compress: true
use_daily: true
# ===========================================
# 🔐 JWT配置
# ===========================================
jwt:
@@ -113,3 +100,46 @@ wallet:
tianyancha:
base_url: http://open.api.tianyancha.com/services
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"
# ===========================================
# 🚀 开发环境频率限制配置(放宽限制)
# ===========================================
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 # 开发环境禁用代理检查

View File

@@ -13,6 +13,15 @@ app:
server:
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"
password: ""
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配置
# ===========================================
@@ -98,7 +51,7 @@ jwt:
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
api:
domain: "apitest.tianyuanapi.com"
domain: "api.tianyuanapi.com"
# ===========================================
# 📁 存储服务配置 - 七牛云
# ===========================================
@@ -139,14 +92,6 @@ esign:
client_type: "ALL"
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:
@@ -173,3 +118,42 @@ wallet:
- recharge_amount: 10000.00 # 充值10000元
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 # 启用代理检查

View File

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

View File

@@ -44,6 +44,22 @@ services:
timeout: 3s
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:
image: jaegertracing/all-in-one:1.70.0

View File

@@ -68,7 +68,6 @@ services:
# 生产环境不暴露端口到主机
# ports:
# - "6379:6379"
# TYAPI 应用程序
tyapi-app:
build:
@@ -113,6 +112,78 @@ services:
memory: 256M
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:
postgres_data:
driver: local

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

View 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. **数据量**: 查询大量数据时建议使用分页和日期范围限制

File diff suppressed because it is too large Load Diff

9233
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

6209
docs/swagger.yaml Normal file

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

File diff suppressed because it is too large Load Diff

View 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 官方文档实现,确保最佳实践
使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能!

View 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. **易于维护**: 配置结构清晰,易于理解和修改
通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求!

View File

@@ -0,0 +1,200 @@
# 每日请求限制中间件使用指南
## 概述
每日请求限制中间件DailyRateLimitMiddleware是一个专门用于防护恶意请求的高级安全中间件。它不仅限制每个IP地址的每日请求次数还集成了多种安全防护措施确保API接口不被恶意攻击者滥用保护系统资源和成本。
## 核心特性
### 1. 基础限流功能
- **每日限制**每个IP地址每日最多请求10次
- **并发控制**每个IP同时最多5个并发请求
- **Redis存储**使用Redis确保分布式环境中的计数一致性
- **自动过期**计数器自动在24小时后过期
### 2. 高级安全防护
- **IP白名单/黑名单**精确控制IP访问权限
- **User-Agent检测**:阻止机器人、爬虫、脚本工具等恶意请求
- **Referer验证**:确保请求来源合法
- **代理检测**识别并处理代理IP
- **地理位置阻止**:阻止特定国家/地区的访问
### 3. 隐蔽性设计
- **隐藏限制信息**:客户端无法获知被限制的真实原因
- **通用错误响应**:返回"系统繁忙"等通用错误信息
- **内部监控头**:仅用于系统内部监控,客户端不可见
## 配置说明
### 基础配置
```yaml
daily_ratelimit:
max_requests_per_day: 10 # 每日最大请求次数
max_requests_per_ip: 10 # 每个IP每日最大请求次数
key_prefix: "daily_limit" # Redis键前缀
ttl: 24h # 键过期时间
max_concurrent: 5 # 最大并发请求数
```
### 安全配置
```yaml
daily_ratelimit:
# IP访问控制
enable_ip_whitelist: false # 是否启用IP白名单
ip_whitelist: # IP白名单列表
- "192.168.1.*" # 内网IP段
- "10.0.0.*" # 内网IP段
enable_ip_blacklist: true # 是否启用IP黑名单
ip_blacklist: # IP黑名单列表
- "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP
# User-Agent检测
enable_user_agent: true # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人
- "crawler" # 爬虫
- "curl" # curl工具
- "python" # Python脚本
# Referer验证
enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer
- "yourdomain.com" # 你的域名
# 代理检测
enable_proxy_check: true # 是否检查代理
```
## 防护恶意请求的措施
### 1. 机器人检测
- 自动识别并阻止常见的爬虫工具
- 阻止无User-Agent的请求
- 阻止自动化测试工具
### 2. IP地址控制
- 黑名单阻止已知的恶意IP
- 白名单仅允许受信任的IP访问
- 支持通配符匹配如192.168.1.*
### 3. 请求来源验证
- 验证Referer头部确保请求来源合法
- 阻止来自未知域名的请求
- 防止跨站请求伪造CSRF
### 4. 并发控制
- 限制单个IP的并发请求数
- 防止DDoS攻击
- 保护系统资源
### 5. 代理检测
- 识别各种代理头部
- 获取真实客户端IP
- 防止IP伪造攻击
## 使用示例
### 应用到特定路由
```go
// 在认证路由中应用
authGroup.POST("/enterprise-info", r.dailyRateLimit.Handle(), r.handler.SubmitEnterpriseInfo)
```
### 配置检查
```go
// 检查中间件状态
stats := dailyRateLimit.GetStats()
log.Printf("中间件配置: %+v", stats)
```
## 响应处理
### 正常响应
- 请求通过所有检查后正常处理
- 在隐藏的响应头中添加监控信息
### 被阻止的请求
- 返回通用错误信息:"访问被拒绝"或"系统繁忙"
- 不暴露具体的限制原因
- 记录详细的日志用于内部分析
### 隐藏的监控头
```http
X-System-Status: normal
X-Request-Count: 3
X-Reset-Time: 2024-01-02T00:00:00Z
```
## 监控和日志
### 日志记录
- 记录所有被阻止的请求
- 包含IP地址、User-Agent、Referer等信息
- 便于安全分析和威胁检测
### 统计信息
```go
stats := middleware.GetStats()
// 返回配置信息和安全特性状态
```
## 性能考虑
### Redis优化
- 使用Pipeline减少网络往返
- 设置合理的TTL避免内存泄漏
- 键名设计便于批量操作
### 内存使用
- 计数器自动过期
- 并发限制使用短期TTL
- 避免无限增长
## 安全最佳实践
### 1. 配置建议
- 启用User-Agent检测
- 启用Referer验证
- 启用代理检测
- 定期更新黑名单
### 2. 监控建议
- 监控被阻止的请求数量
- 分析攻击模式
- 及时调整安全策略
### 3. 部署建议
- 在生产环境中启用所有安全特性
- 根据业务需求调整限制参数
- 定期审查和更新配置
## 故障排除
### 常见问题
1. **误杀正常请求**
- 检查User-Agent白名单
- 调整Referer允许列表
- 检查IP白名单配置
2. **性能问题**
- 调整并发限制参数
- 检查Redis性能
- 优化键名设计
3. **配置不生效**
- 检查配置文件语法
- 确认中间件已正确注册
- 验证依赖注入
## 总结
每日请求限制中间件提供了一个强大而全面的解决方案来防护恶意请求。通过多层安全检查和隐蔽的限制机制有效防止API滥用保护系统资源同时为正常用户提供良好的使用体验。
该中间件特别适合需要控制成本的API接口通过精确的访问控制确保每次请求都来自合法用户避免恶意攻击造成的资源浪费。

View File

@@ -0,0 +1,57 @@
# 每日限流中间件使用指南
## 概述
每日限流中间件实现了多层限流策略:
- 接口一天最大请求200次
- 一个IP一天最多10次
- 支持并发限制和安全防护
## 主要特性
1. **多层限流策略**
- 接口总请求限制200次/天
- IP请求限制10次/天/IP
- 并发请求限制5个/IP
2. **安全防护**
- IP白名单/黑名单
- User-Agent检查
- Referer验证
- 代理检测
## 使用方法
```go
// 配置限流参数
limitConfig := middleware.DailyRateLimitConfig{
MaxRequestsPerDay: 200, // 接口一天最大请求200次
MaxRequestsPerIP: 10, // 一个IP一天最多10次
KeyPrefix: "api_limit", // Redis键前缀
TTL: 24 * time.Hour, // 24小时过期
MaxConcurrent: 5, // 最大并发5个
}
// 创建中间件实例
rateLimitMiddleware := middleware.NewDailyRateLimitMiddleware(
config, redisClient, response, logger, limitConfig)
// 应用到路由
router.Use(rateLimitMiddleware.Handle())
```
## 限流逻辑
1. 检查IP访问权限
2. 验证User-Agent和Referer
3. 检查并发限制
4. 检查接口总请求次数200次/天)
5. 检查IP请求次数10次/天)
6. 更新计数器
## 监控信息
响应头包含隐藏的监控信息:
- `X-Total-Count`: 当前总请求次数
- `X-IP-Count`: 当前IP请求次数
- `X-Reset-Time`: 重置时间

View 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, &paramsDto); 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控制响应顺序
- **错误隔离**:每个子产品的错误独立处理
现在,创建一个新的组合包就像配置数据库一样简单!🎉

View 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使用模式和趋势
- 提高问题排查效率
现在您的西部数据服务已经具备了完整的日志记录能力!🚀

8
go.mod
View File

@@ -11,9 +11,11 @@ require (
github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/prometheus/client_golang v1.22.0
github.com/qiniu/go-sdk/v7 v7.25.4
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/smartwalle/alipay/v3 v3.2.25
github.com/spf13/viper v1.20.1
@@ -22,6 +24,7 @@ require (
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
github.com/tidwall/gjson v1.18.0
github.com/xuri/excelize/v2 v2.9.1
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
@@ -84,6 +87,8 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // 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/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect
@@ -95,8 +100,11 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // 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/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/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect

19
go.sum
View File

@@ -108,6 +108,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
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/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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -189,6 +191,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/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/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.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -241,6 +250,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/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
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/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@@ -249,6 +260,12 @@ 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/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/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=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
@@ -296,6 +313,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-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.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/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=

View File

@@ -22,8 +22,15 @@ import (
// 产品域实体
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"
"tyapi-server/internal/infrastructure/database"
taskEntities "tyapi-server/internal/infrastructure/task/entities"
)
// Application 应用程序结构
@@ -95,6 +102,7 @@ func (a *Application) Run() error {
// RunMigrations 运行数据库迁移
func (a *Application) RunMigrations() error {
return nil
a.logger.Info("Running database migrations...")
// 创建数据库连接
@@ -224,6 +232,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{},
// 产品域
&productEntities.Product{},
&productEntities.ProductPackageItem{},
@@ -231,9 +240,24 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&productEntities.Subscription{},
&productEntities.ProductDocumentation{},
&productEntities.ProductApiConfig{},
// 文章域
&articleEntities.Article{},
&articleEntities.Category{},
&articleEntities.Tag{},
&articleEntities.ScheduledTask{},
// 统计域
&statisticsEntities.StatisticsMetric{},
&statisticsEntities.StatisticsDashboard{},
&statisticsEntities.StatisticsReport{},
// api
&apiEntities.ApiUser{},
&apiEntities.ApiCall{},
// 任务域
&taskEntities.AsyncTask{},
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,63 @@ type ApiCallCommand struct {
}
type ApiCallOptions struct {
Json bool `json:"json,omitempty"` // 是否返回JSON格式
Json bool `json:"json,omitempty"` // 是否返回JSON格式
IsDebug bool `json:"is_debug,omitempty"` // 是否为调试调用
}
// EncryptCommand 加密命令
type EncryptCommand struct {
Data map[string]interface{} `json:"data" binding:"required"`
Data map[string]interface{} `json:"data" binding:"required"`
SecretKey string `json:"secret_key" binding:"required"`
}
// DecryptCommand 解密命令
type DecryptCommand struct {
EncryptedData string `json:"encrypted_data" binding:"required"`
SecretKey string `json:"secret_key" binding:"required"`
}
// SaveApiCallCommand 保存API调用命令
type SaveApiCallCommand struct {
ApiCallID string `json:"api_call_id"`
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
TransactionID string `json:"transaction_id"`
Status string `json:"status"`
Cost float64 `json:"cost"`
ErrorType string `json:"error_type"`
ErrorMsg string `json:"error_msg"`
ClientIP string `json:"client_ip"`
}
// ProcessDeductionCommand 处理扣款命令
type ProcessDeductionCommand struct {
UserID string `json:"user_id"`
Amount string `json:"amount"`
ApiCallID string `json:"api_call_id"`
TransactionID string `json:"transaction_id"`
ProductID string `json:"product_id"`
}
// UpdateUsageStatsCommand 更新使用统计命令
type UpdateUsageStatsCommand struct {
SubscriptionID string `json:"subscription_id"`
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
Increment int `json:"increment"`
}
// RecordApiLogCommand 记录API日志命令
type RecordApiLogCommand struct {
TransactionID string `json:"transaction_id"`
UserID string `json:"user_id"`
ApiName string `json:"api_name"`
ClientIP string `json:"client_ip"`
ResponseSize int64 `json:"response_size"`
}
// ProcessCompensationCommand 处理补偿命令
type ProcessCompensationCommand struct {
TransactionID string `json:"transaction_id"`
Type string `json:"type"`
}

View File

@@ -0,0 +1,104 @@
package dto
import (
api_entities "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
}

View File

@@ -40,24 +40,24 @@ type WhiteListListResponse struct {
// API调用记录相关DTO
type ApiCallRecordResponse struct {
ID string `json:"id"`
AccessId string `json:"access_id"`
UserId string `json:"user_id"`
ProductId *string `json:"product_id,omitempty"`
ProductName *string `json:"product_name,omitempty"`
TransactionId string `json:"transaction_id"`
ClientIp string `json:"client_ip"`
Status string `json:"status"`
StartAt string `json:"start_at"`
EndAt *string `json:"end_at,omitempty"`
Cost *string `json:"cost,omitempty"`
ErrorType *string `json:"error_type,omitempty"`
ErrorMsg *string `json:"error_msg,omitempty"`
TranslatedErrorMsg *string `json:"translated_error_msg,omitempty"`
CompanyName *string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID string `json:"id"`
AccessId string `json:"access_id"`
UserId string `json:"user_id"`
ProductId *string `json:"product_id,omitempty"`
ProductName *string `json:"product_name,omitempty"`
TransactionId string `json:"transaction_id"`
ClientIp string `json:"client_ip"`
Status string `json:"status"`
StartAt string `json:"start_at"`
EndAt *string `json:"end_at,omitempty"`
Cost *string `json:"cost,omitempty"`
ErrorType *string `json:"error_type,omitempty"`
ErrorMsg *string `json:"error_msg,omitempty"`
TranslatedErrorMsg *string `json:"translated_error_msg,omitempty"`
CompanyName *string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// UserSimpleResponse 用户简单信息响应
@@ -95,8 +95,6 @@ func NewErrorResponse(code int, message, transactionId string) *ApiCallResponse
Code: code,
Message: message,
TransactionId: transactionId,
Data: "",
Data: "",
}
}

View File

@@ -0,0 +1,19 @@
package dto
// FormField 表单字段配置
type FormField struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
Required bool `json:"required"`
Validation string `json:"validation"`
Description string `json:"description"`
Example string `json:"example"`
Placeholder string `json:"placeholder"`
}
// FormConfigResponse 表单配置响应
type FormConfigResponse struct {
ApiCode string `json:"api_code"`
Fields []FormField `json:"fields"`
}

View File

@@ -4,38 +4,46 @@ import "errors"
// API调用相关错误类型
var (
ErrQueryEmpty = errors.New("查询为空")
ErrSystem = errors.New("接口异常")
ErrDecryptFail = errors.New("解密失败")
ErrRequestParam = errors.New("请求参数结构不正确")
ErrInvalidParam = errors.New("参数校验不正确")
ErrInvalidIP = errors.New("未经授权的IP")
ErrMissingAccessId = errors.New("缺少Access-Id")
ErrInvalidAccessId = errors.New("未经授权的AccessId")
ErrFrozenAccount = errors.New("账户已冻结")
ErrArrears = errors.New("账户余额不足,无法请求")
ErrProductNotFound = errors.New("产品不存在")
ErrProductDisabled = errors.New("产品已停用")
ErrNotSubscribed = errors.New("未订阅此产品")
ErrBusiness = errors.New("业务失败")
ErrQueryEmpty = errors.New("查询为空")
ErrSystem = errors.New("接口异常")
ErrDecryptFail = errors.New("解密失败")
ErrRequestParam = errors.New("请求参数结构不正确")
ErrInvalidParam = errors.New("参数校验不正确")
ErrInvalidIP = errors.New("未经授权的IP")
ErrMissingAccessId = errors.New("缺少Access-Id")
ErrInvalidAccessId = errors.New("未经授权的AccessId")
ErrFrozenAccount = errors.New("账户已冻结")
ErrArrears = errors.New("账户余额不足,无法请求")
ErrInsufficientBalance = errors.New("钱包余额不足")
ErrProductNotFound = errors.New("产品不存在")
ErrProductDisabled = errors.New("产品已停用")
ErrNotSubscribed = errors.New("未订阅此产品")
ErrProductNotSubscribed = errors.New("未订阅此产品")
ErrSubscriptionExpired = errors.New("订阅已过期")
ErrSubscriptionSuspended = errors.New("订阅已暂停")
ErrBusiness = errors.New("业务失败")
)
// 错误码映射 - 严格按照用户要求
var ErrorCodeMap = map[error]int{
ErrQueryEmpty: 1000,
ErrSystem: 1001,
ErrDecryptFail: 1002,
ErrRequestParam: 1003,
ErrInvalidParam: 1003,
ErrInvalidIP: 1004,
ErrMissingAccessId: 1005,
ErrInvalidAccessId: 1006,
ErrFrozenAccount: 1007,
ErrArrears: 1007,
ErrProductNotFound: 1008,
ErrProductDisabled: 1008,
ErrNotSubscribed: 1008,
ErrBusiness: 2001,
ErrQueryEmpty: 1000,
ErrSystem: 1001,
ErrDecryptFail: 1002,
ErrRequestParam: 1003,
ErrInvalidParam: 1003,
ErrInvalidIP: 1004,
ErrMissingAccessId: 1005,
ErrInvalidAccessId: 1006,
ErrFrozenAccount: 1007,
ErrArrears: 1007,
ErrInsufficientBalance: 1007,
ErrProductNotFound: 1008,
ErrProductDisabled: 1008,
ErrNotSubscribed: 1008,
ErrProductNotSubscribed: 1008,
ErrSubscriptionExpired: 1008,
ErrSubscriptionSuspended: 1008,
ErrBusiness: 2001,
}
// GetErrorCode 获取错误对应的错误码
@@ -45,3 +53,9 @@ func GetErrorCode(err error) int {
}
return 1001 // 默认返回接口异常
}
// GetErrorMessage 获取错误对应的错误消息
func GetErrorMessage(err error) string {
// 直接返回预定义的错误消息
return err.Error()
}

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

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

View File

@@ -0,0 +1,47 @@
package commands
// CreateArticleCommand 创建文章命令
type CreateArticleCommand struct {
Title string `json:"title" binding:"required" comment:"文章标题"`
Content string `json:"content" binding:"required" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
}
// UpdateArticleCommand 更新文章命令
type UpdateArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Content string `json:"content" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
}
// DeleteArticleCommand 删除文章命令
type DeleteArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// PublishArticleCommand 发布文章命令
type PublishArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// ArchiveArticleCommand 归档文章命令
type ArchiveArticleCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
}
// SetFeaturedCommand 设置推荐状态命令
type SetFeaturedCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
}

View File

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

View File

@@ -0,0 +1,19 @@
package commands
// CreateCategoryCommand 创建分类命令
type CreateCategoryCommand struct {
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
}
// UpdateCategoryCommand 更新分类命令
type UpdateCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
}
// DeleteCategoryCommand 删除分类命令
type DeleteCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
}

View File

@@ -0,0 +1,36 @@
package commands
import (
"fmt"
"time"
)
// SchedulePublishCommand 定时发布文章命令
type SchedulePublishCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}

View File

@@ -0,0 +1,19 @@
package commands
// CreateTagCommand 创建标签命令
type CreateTagCommand struct {
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
}
// UpdateTagCommand 更新标签命令
type UpdateTagCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
}
// DeleteTagCommand 删除标签命令
type DeleteTagCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
}

View File

@@ -0,0 +1,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:"每页数量"`
}

View File

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

View File

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

View File

@@ -0,0 +1,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
}

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

View File

@@ -21,6 +21,9 @@ type CertificationApplicationService interface {
// 申请合同签署
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
// OCR营业执照识别
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
// ================ 查询用例 ================
// 获取认证详情

View File

@@ -22,6 +22,7 @@ import (
"tyapi-server/internal/infrastructure/external/storage"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/esign"
sharedOCR "tyapi-server/internal/shared/ocr"
"go.uber.org/zap"
)
@@ -40,6 +41,7 @@ type CertificationApplicationServiceImpl struct {
walletAggregateService finance_service.WalletAggregateService
apiUserAggregateService api_service.ApiUserAggregateService
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService
ocrService sharedOCR.OCRService
// 仓储依赖
queryRepository repositories.CertificationQueryRepository
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
@@ -62,6 +64,7 @@ func NewCertificationApplicationService(
walletAggregateService finance_service.WalletAggregateService,
apiUserAggregateService api_service.ApiUserAggregateService,
enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService,
ocrService sharedOCR.OCRService,
txManager *database.TransactionManager,
logger *zap.Logger,
) CertificationApplicationService {
@@ -78,6 +81,7 @@ func NewCertificationApplicationService(
walletAggregateService: walletAggregateService,
apiUserAggregateService: apiUserAggregateService,
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
ocrService: ocrService,
txManager: txManager,
logger: logger,
}
@@ -107,9 +111,9 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
// 验证验证码
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
record.MarkAsFailed(err.Error())
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("验证码错误或已过期")
}
@@ -119,17 +123,17 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID)
if err != nil {
record.MarkAsFailed(err.Error())
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
}
if exists {
record.MarkAsFailed(err.Error())
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确")
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确")
}
@@ -146,9 +150,9 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
if err != nil {
s.logger.Error("企业信息验证失败", zap.Error(err))
record.MarkAsFailed(err.Error())
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
}
@@ -156,16 +160,16 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
if err != nil {
s.logger.Error("企业信息验证失败", zap.Error(err))
record.MarkAsFailed(err.Error())
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("企业信息验证失败, %s", err.Error())
}
record.MarkAsVerified()
err = s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if err != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", err.Error())
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
var response *responses.CertificationResponse
@@ -987,3 +991,33 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
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
}

View File

@@ -3,7 +3,6 @@ package finance
import (
"context"
"net/http"
"tyapi-server/internal/application/finance/dto/commands"
"tyapi-server/internal/application/finance/dto/queries"
"tyapi-server/internal/application/finance/dto/responses"
@@ -12,29 +11,34 @@ import (
// FinanceApplicationService 财务应用服务接口
type FinanceApplicationService interface {
// 钱包管理
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
// 充值管理
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
// 交易记录
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 导出功能
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
// 支付宝回调处理
HandleAlipayCallback(ctx context.Context, r *http.Request) error
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
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)
// 获取用户充值记录
// 充值记录
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)
// 获取充值配置
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
}

View File

@@ -13,6 +13,7 @@ import (
finance_services "tyapi-server/internal/domains/finance/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/export"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/payment"
@@ -30,6 +31,7 @@ type FinanceApplicationServiceImpl struct {
alipayOrderRepo finance_repositories.AlipayOrderRepository
userRepo user_repositories.UserRepository
txManager *database.TransactionManager
exportManager *export.ExportManager
logger *zap.Logger
config *config.Config
}
@@ -45,6 +47,7 @@ func NewFinanceApplicationService(
txManager *database.TransactionManager,
logger *zap.Logger,
config *config.Config,
exportManager *export.ExportManager,
) FinanceApplicationService {
return &FinanceApplicationServiceImpl{
aliPayClient: aliPayClient,
@@ -54,6 +57,7 @@ func NewFinanceApplicationService(
alipayOrderRepo: alipayOrderRepo,
userRepo: userRepo,
txManager: txManager,
exportManager: exportManager,
logger: logger,
config: config,
}
@@ -344,6 +348,290 @@ func (s *FinanceApplicationServiceImpl) GetAdminWalletTransactions(ctx context.C
}, nil
}
// ExportAdminWalletTransactions 导出管理端钱包交易记录
func (s *FinanceApplicationServiceImpl) ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) {
const batchSize = 1000 // 每批处理1000条记录
var allTransactions []*finance_entities.WalletTransaction
var productNameMap map[string]string
// 分批获取数据
page := 1
for {
// 查询当前批次的数据
batchProductNameMap, transactions, _, err := s.walletTransactionRepository.ListWithFiltersAndProductName(ctx, filters, interfaces.ListOptions{
Page: page,
PageSize: batchSize,
Sort: "created_at",
Order: "desc",
})
if err != nil {
s.logger.Error("查询导出钱包交易记录失败", zap.Error(err))
return nil, err
}
// 合并产品名称映射
if productNameMap == nil {
productNameMap = batchProductNameMap
} else {
for k, v := range batchProductNameMap {
productNameMap[k] = v
}
}
// 添加到总数据中
allTransactions = append(allTransactions, transactions...)
// 如果当前批次数据少于批次大小,说明已经是最后一批
if len(transactions) < batchSize {
break
}
page++
}
// 检查是否有数据
if len(allTransactions) == 0 {
return nil, fmt.Errorf("没有找到符合条件的数据")
}
// 批量获取企业名称映射避免N+1查询问题
companyNameMap, err := s.batchGetCompanyNames(ctx, allTransactions)
if err != nil {
companyNameMap = make(map[string]string)
}
// 准备导出数据
headers := []string{"交易ID", "企业名称", "产品名称", "消费金额", "消费时间"}
columnWidths := []float64{20, 25, 20, 15, 20}
data := make([][]interface{}, len(allTransactions))
for i, transaction := range allTransactions {
companyName := companyNameMap[transaction.UserID]
if companyName == "" {
companyName = "未知企业"
}
productName := productNameMap[transaction.ProductID]
if productName == "" {
productName = "未知产品"
}
data[i] = []interface{}{
transaction.TransactionID,
companyName,
productName,
transaction.Amount.String(),
transaction.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
// 创建导出配置
config := &export.ExportConfig{
SheetName: "消费记录",
Headers: headers,
Data: data,
ColumnWidths: columnWidths,
}
// 使用导出管理器生成文件
return s.exportManager.Export(ctx, config, format)
}
// batchGetCompanyNames 批量获取企业名称映射
func (s *FinanceApplicationServiceImpl) batchGetCompanyNames(ctx context.Context, transactions []*finance_entities.WalletTransaction) (map[string]string, error) {
// 收集所有唯一的用户ID
userIDSet := make(map[string]bool)
for _, transaction := range transactions {
userIDSet[transaction.UserID] = true
}
// 转换为切片
userIDs := make([]string, 0, len(userIDSet))
for userID := range userIDSet {
userIDs = append(userIDs, userID)
}
// 批量查询用户信息
users, err := s.userRepo.BatchGetByIDsWithEnterpriseInfo(ctx, userIDs)
if err != nil {
return nil, err
}
// 构建企业名称映射
companyNameMap := make(map[string]string)
for _, user := range users {
companyName := "未知企业"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
companyNameMap[user.ID] = companyName
}
return companyNameMap, nil
}
// ExportAdminRechargeRecords 导出管理端充值记录
func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error) {
const batchSize = 1000 // 每批处理1000条记录
var allRecords []finance_entities.RechargeRecord
// 分批获取数据
page := 1
for {
// 查询当前批次的数据
records, err := s.rechargeRecordService.GetAll(ctx, filters, interfaces.ListOptions{
Page: page,
PageSize: batchSize,
Sort: "created_at",
Order: "desc",
})
if err != nil {
s.logger.Error("查询导出充值记录失败", zap.Error(err))
return nil, err
}
// 添加到总数据中
allRecords = append(allRecords, records...)
// 如果当前批次数据少于批次大小,说明已经是最后一批
if len(records) < batchSize {
break
}
page++
}
// 批量获取企业名称映射避免N+1查询问题
companyNameMap, err := s.batchGetCompanyNamesForRechargeRecords(ctx, convertToRechargeRecordPointers(allRecords))
if err != nil {
s.logger.Warn("批量获取企业名称失败,使用默认值", zap.Error(err))
companyNameMap = make(map[string]string)
}
// 准备导出数据
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"}
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20}
data := make([][]interface{}, len(allRecords))
for i, record := range allRecords {
// 从映射中获取企业名称
companyName := companyNameMap[record.UserID]
if companyName == "" {
companyName = "未知企业"
}
// 获取订单号
alipayOrderID := ""
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
alipayOrderID = *record.AlipayOrderID
}
transferOrderID := ""
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
transferOrderID = *record.TransferOrderID
}
// 获取备注
notes := ""
if record.Notes != "" {
notes = record.Notes
}
// 格式化时间
createdAt := record.CreatedAt.Format("2006-01-02 15:04:05")
data[i] = []interface{}{
companyName,
record.Amount.String(),
translateRechargeType(record.RechargeType),
translateRechargeStatus(record.Status),
alipayOrderID,
transferOrderID,
notes,
createdAt,
}
}
// 创建导出配置
config := &export.ExportConfig{
SheetName: "充值记录",
Headers: headers,
Data: data,
ColumnWidths: columnWidths,
}
// 使用导出管理器生成文件
return s.exportManager.Export(ctx, config, format)
}
// translateRechargeType 翻译充值类型为中文
func translateRechargeType(rechargeType finance_entities.RechargeType) string {
switch rechargeType {
case finance_entities.RechargeTypeAlipay:
return "支付宝充值"
case finance_entities.RechargeTypeTransfer:
return "对公转账"
case finance_entities.RechargeTypeGift:
return "赠送"
default:
return "未知类型"
}
}
// translateRechargeStatus 翻译充值状态为中文
func translateRechargeStatus(status finance_entities.RechargeStatus) string {
switch status {
case finance_entities.RechargeStatusPending:
return "待处理"
case finance_entities.RechargeStatusSuccess:
return "成功"
case finance_entities.RechargeStatusFailed:
return "失败"
case finance_entities.RechargeStatusCancelled:
return "已取消"
default:
return "未知状态"
}
}
// convertToRechargeRecordPointers 将RechargeRecord切片转换为指针切片
func convertToRechargeRecordPointers(records []finance_entities.RechargeRecord) []*finance_entities.RechargeRecord {
pointers := make([]*finance_entities.RechargeRecord, len(records))
for i := range records {
pointers[i] = &records[i]
}
return pointers
}
// batchGetCompanyNamesForRechargeRecords 批量获取企业名称映射(用于充值记录)
func (s *FinanceApplicationServiceImpl) batchGetCompanyNamesForRechargeRecords(ctx context.Context, records []*finance_entities.RechargeRecord) (map[string]string, error) {
// 收集所有唯一的用户ID
userIDSet := make(map[string]bool)
for _, record := range records {
userIDSet[record.UserID] = true
}
// 转换为切片
userIDs := make([]string, 0, len(userIDSet))
for userID := range userIDSet {
userIDs = append(userIDs, userID)
}
// 批量查询用户信息
users, err := s.userRepo.BatchGetByIDsWithEnterpriseInfo(ctx, userIDs)
if err != nil {
return nil, err
}
// 构建企业名称映射
companyNameMap := make(map[string]string)
for _, user := range users {
companyName := "未知企业"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
companyNameMap[user.ID] = companyName
}
return companyNameMap, nil
}
// HandleAlipayCallback 处理支付宝回调
func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context, r *http.Request) error {
@@ -402,7 +690,7 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
// 该服务内部会处理所有必要的检查、事务和更新操作
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
if err != nil {
s.logger.Error("处理支付宝支付成功失败",
s.logger.Error("处理支付宝支付成功失败",
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
@@ -665,14 +953,14 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
var items []responses.RechargeRecordResponse
for _, record := range records {
item := responses.RechargeRecordResponse{
ID: record.ID,
UserID: record.UserID,
Amount: record.Amount,
RechargeType: string(record.RechargeType),
Status: string(record.Status),
Notes: record.Notes,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
ID: record.ID,
UserID: record.UserID,
Amount: record.Amount,
RechargeType: string(record.RechargeType),
Status: string(record.Status),
Notes: record.Notes,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
}
// 根据充值类型设置相应的订单号
@@ -719,8 +1007,8 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (
})
}
return &responses.RechargeConfigResponse{
MinAmount: s.config.Wallet.MinAmount,
MaxAmount: s.config.Wallet.MaxAmount,
MinAmount: s.config.Wallet.MinAmount,
MaxAmount: s.config.Wallet.MaxAmount,
AlipayRechargeBonus: bonus,
}, nil
}

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

View File

@@ -10,4 +10,11 @@ type CreateSubscriptionCommand struct {
type UpdateSubscriptionPriceCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
}
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
type BatchUpdateSubscriptionPricesCommand struct {
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
Discount float64 `json:"discount" binding:"required,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
}

View File

@@ -15,6 +15,7 @@ type PackageItemResponse struct {
// ProductInfoResponse 产品详情响应
type ProductInfoResponse struct {
ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
@@ -59,6 +60,7 @@ type ProductSearchResponse struct {
// ProductSimpleResponse 产品简单信息响应
type ProductSimpleResponse struct {
ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`
@@ -79,6 +81,7 @@ type ProductStatsResponse struct {
// ProductAdminInfoResponse 管理员产品详情响应
type ProductAdminInfoResponse struct {
ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"`

View File

@@ -46,38 +46,6 @@ type ProductApplicationService interface {
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
DeleteProductApiConfig(ctx context.Context, configID string) error
}
// 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)
}

View File

@@ -436,9 +436,14 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Conte
// GetProductByIDForUser 根据ID获取产品用户端专用
// 业务流程1. 获取产品信息 2. 验证产品可见性 3. 构建用户响应数据
func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error) {
// 首先尝试通过新ID查找产品
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
if err != nil {
return nil, err
// 如果通过新ID找不到尝试通过旧ID查找
product, err = s.productManagementService.GetProductByOldIDWithCategory(ctx, query.ID)
if err != nil {
return nil, err
}
}
// 用户端只能查看可见的产品
@@ -452,7 +457,7 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
// 如果需要包含文档信息
if query.WithDocument != nil && *query.WithDocument {
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, query.ID)
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, product.ID)
if err == nil && doc != nil {
response.Documentation = doc
}
@@ -465,6 +470,7 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
response := &responses.ProductInfoResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
@@ -507,6 +513,7 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
response := &responses.ProductAdminInfoResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
@@ -577,3 +584,5 @@ func (s *ProductApplicationServiceImpl) UpdateProductApiConfig(ctx context.Conte
func (s *ProductApplicationServiceImpl) DeleteProductApiConfig(ctx context.Context, configID string) error {
return s.productApiConfigAppService.DeleteProductApiConfig(ctx, configID)
}

View File

@@ -0,0 +1,34 @@
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)
// 业务查询
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetProductSubscriptions(ctx context.Context, query *queries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error)
// 统计
GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error)
// 一键改价
BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error
}

View File

@@ -3,6 +3,7 @@ package product
import (
"context"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"tyapi-server/internal/application/product/dto/commands"
@@ -41,6 +42,54 @@ func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price)
}
// BatchUpdateSubscriptionPrices 一键改价
// 业务流程1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error {
subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{
UserID: cmd.UserID,
Page: 1,
PageSize: 1000,
})
if err != nil {
return err
}
// 根据范围筛选订阅
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)
}
}
}
// 批量更新价格
for _, sub := range targetSubscriptions {
if sub.Product != nil {
// 计算折扣后的价格
discountRatio := cmd.Discount / 10
newPrice := sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio))
// 四舍五入到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))
// 继续处理其他订阅,不中断整个流程
}
}
}
return nil
}
// CreateSubscription 创建订阅
// 业务流程1. 创建订阅
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
@@ -161,9 +210,9 @@ func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Co
}
return &responses.SubscriptionUsageResponse{
ID: subscription.ID,
ID: subscription.ID,
ProductID: subscription.ProductID,
APIUsed: subscription.APIUsed,
APIUsed: subscription.APIUsed,
}, nil
}
@@ -174,7 +223,7 @@ func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Co
if err != nil {
return nil, err
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
@@ -188,7 +237,7 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
if err != nil {
return nil, err
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
@@ -214,13 +263,18 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s
}
}
var productResponse *responses.ProductSimpleResponse
if subscription.Product != nil {
productResponse = s.convertToProductSimpleResponse(subscription.Product)
}
return &responses.SubscriptionInfoResponse{
ID: subscription.ID,
UserID: subscription.UserID,
ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(),
User: userInfo,
Product: s.convertToProductSimpleResponse(subscription.Product),
Product: productResponse,
APIUsed: subscription.APIUsed,
CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
@@ -229,19 +283,29 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s
// convertToProductSimpleResponse 转换为产品简单信息响应
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
var categoryResponse *responses.CategorySimpleResponse
if product.Category != nil {
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
}
return &responses.ProductSimpleResponse{
ID: product.ID,
OldID: product.OldID,
Name: product.Name,
Code: product.Code,
Description: product.Description,
Price: product.Price.InexactFloat64(),
Category: s.convertToCategorySimpleResponse(product.Category),
Category: categoryResponse,
IsPackage: product.IsPackage,
}
}
// convertToCategorySimpleResponse 转换为分类简单信息响应
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
if category == nil {
return nil
}
return &responses.CategorySimpleResponse{
ID: category.ID,
Name: category.Name,

View File

@@ -0,0 +1,412 @@
package statistics
import (
"fmt"
"time"
)
// ================ 命令对象 ================
// CreateMetricCommand 创建指标命令
type CreateMetricCommand struct {
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" validate:"required" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
Value float64 `json:"value" validate:"min=0" comment:"指标值"`
Metadata string `json:"metadata" comment:"额外维度信息"`
Date time.Time `json:"date" validate:"required" comment:"统计日期"`
}
// UpdateMetricCommand 更新指标命令
type UpdateMetricCommand struct {
ID string `json:"id" validate:"required" comment:"指标ID"`
Value float64 `json:"value" validate:"min=0" comment:"新指标值"`
}
// DeleteMetricCommand 删除指标命令
type DeleteMetricCommand struct {
ID string `json:"id" validate:"required" comment:"指标ID"`
}
// GenerateReportCommand 生成报告命令
type GenerateReportCommand struct {
ReportType string `json:"report_type" validate:"required" comment:"报告类型"`
Title string `json:"title" validate:"required" comment:"报告标题"`
Period string `json:"period" validate:"required" comment:"统计周期"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
GeneratedBy string `json:"generated_by" validate:"required" comment:"生成者ID"`
}
// CreateDashboardCommand 创建仪表板命令
type CreateDashboardCommand struct {
Name string `json:"name" validate:"required" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" validate:"required" comment:"创建者ID"`
}
// UpdateDashboardCommand 更新仪表板命令
type UpdateDashboardCommand struct {
ID string `json:"id" validate:"required" comment:"仪表板ID"`
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" validate:"min=30" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
}
// SetDefaultDashboardCommand 设置默认仪表板命令
type SetDefaultDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
UserRole string `json:"user_role" validate:"required" comment:"用户角色"`
UpdatedBy string `json:"updated_by" validate:"required" comment:"更新者ID"`
}
// ActivateDashboardCommand 激活仪表板命令
type ActivateDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
ActivatedBy string `json:"activated_by" validate:"required" comment:"激活者ID"`
}
// DeactivateDashboardCommand 停用仪表板命令
type DeactivateDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
DeactivatedBy string `json:"deactivated_by" validate:"required" comment:"停用者ID"`
}
// DeleteDashboardCommand 删除仪表板命令
type DeleteDashboardCommand struct {
DashboardID string `json:"dashboard_id" validate:"required" comment:"仪表板ID"`
DeletedBy string `json:"deleted_by" validate:"required" comment:"删除者ID"`
}
// ExportDataCommand 导出数据命令
type ExportDataCommand struct {
Format string `json:"format" validate:"required" comment:"导出格式"`
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
StartDate time.Time `json:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" validate:"required" comment:"结束日期"`
Dimension string `json:"dimension" comment:"统计维度"`
GroupBy string `json:"group_by" comment:"分组维度"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
Columns []string `json:"columns" comment:"导出列"`
IncludeCharts bool `json:"include_charts" comment:"是否包含图表"`
ExportedBy string `json:"exported_by" validate:"required" comment:"导出者ID"`
}
// TriggerAggregationCommand 触发数据聚合命令
type TriggerAggregationCommand struct {
MetricType string `json:"metric_type" validate:"required" comment:"指标类型"`
Period string `json:"period" validate:"required" comment:"聚合周期"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Force bool `json:"force" comment:"是否强制重新聚合"`
TriggeredBy string `json:"triggered_by" validate:"required" comment:"触发者ID"`
}
// Validate 验证触发聚合命令
func (c *TriggerAggregationCommand) Validate() error {
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.Period == "" {
return fmt.Errorf("聚合周期不能为空")
}
if c.TriggeredBy == "" {
return fmt.Errorf("触发者ID不能为空")
}
// 验证周期类型
validPeriods := []string{"hourly", "daily", "weekly", "monthly"}
isValidPeriod := false
for _, period := range validPeriods {
if c.Period == period {
isValidPeriod = true
break
}
}
if !isValidPeriod {
return fmt.Errorf("不支持的聚合周期: %s", c.Period)
}
return nil
}
// ================ 查询对象 ================
// GetMetricsQuery 获取指标查询
type GetMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// GetRealtimeMetricsQuery 获取实时指标查询
type GetRealtimeMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
TimeRange string `json:"time_range" form:"time_range" comment:"时间范围"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
}
// GetHistoricalMetricsQuery 获取历史指标查询
type GetHistoricalMetricsQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
Period string `json:"period" form:"period" comment:"统计周期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
AggregateBy string `json:"aggregate_by" form:"aggregate_by" comment:"聚合维度"`
GroupBy string `json:"group_by" form:"group_by" comment:"分组维度"`
}
// GetDashboardDataQuery 获取仪表板数据查询
type GetDashboardDataQuery struct {
UserRole string `json:"user_role" form:"user_role" validate:"required" comment:"用户角色"`
Period string `json:"period" form:"period" comment:"统计周期"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
MetricTypes []string `json:"metric_types" form:"metric_types" comment:"指标类型列表"`
Dimensions []string `json:"dimensions" form:"dimensions" comment:"统计维度列表"`
}
// GetReportsQuery 获取报告查询
type GetReportsQuery struct {
ReportType string `json:"report_type" form:"report_type" comment:"报告类型"`
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
Status string `json:"status" form:"status" comment:"报告状态"`
Period string `json:"period" form:"period" comment:"统计周期"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
GeneratedBy string `json:"generated_by" form:"generated_by" comment:"生成者ID"`
}
// GetDashboardsQuery 获取仪表板查询
type GetDashboardsQuery struct {
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
IsDefault *bool `json:"is_default" form:"is_default" comment:"是否默认"`
IsActive *bool `json:"is_active" form:"is_active" comment:"是否激活"`
AccessLevel string `json:"access_level" form:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" form:"created_by" comment:"创建者ID"`
Name string `json:"name" form:"name" comment:"仪表板名称"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// GetReportQuery 获取单个报告查询
type GetReportQuery struct {
ReportID string `json:"report_id" form:"report_id" validate:"required" comment:"报告ID"`
}
// GetDashboardQuery 获取单个仪表板查询
type GetDashboardQuery struct {
DashboardID string `json:"dashboard_id" form:"dashboard_id" validate:"required" comment:"仪表板ID"`
}
// GetMetricQuery 获取单个指标查询
type GetMetricQuery struct {
MetricID string `json:"metric_id" form:"metric_id" validate:"required" comment:"指标ID"`
}
// CalculateGrowthRateQuery 计算增长率查询
type CalculateGrowthRateQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
CurrentPeriod time.Time `json:"current_period" form:"current_period" validate:"required" comment:"当前周期"`
PreviousPeriod time.Time `json:"previous_period" form:"previous_period" validate:"required" comment:"上一周期"`
}
// CalculateTrendQuery 计算趋势查询
type CalculateTrendQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// CalculateCorrelationQuery 计算相关性查询
type CalculateCorrelationQuery struct {
MetricType1 string `json:"metric_type1" form:"metric_type1" validate:"required" comment:"指标类型1"`
MetricName1 string `json:"metric_name1" form:"metric_name1" validate:"required" comment:"指标名称1"`
MetricType2 string `json:"metric_type2" form:"metric_type2" validate:"required" comment:"指标类型2"`
MetricName2 string `json:"metric_name2" form:"metric_name2" validate:"required" comment:"指标名称2"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// CalculateMovingAverageQuery 计算移动平均查询
type CalculateMovingAverageQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
WindowSize int `json:"window_size" form:"window_size" validate:"min=1" comment:"窗口大小"`
}
// CalculateSeasonalityQuery 计算季节性查询
type CalculateSeasonalityQuery struct {
MetricType string `json:"metric_type" form:"metric_type" validate:"required" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" validate:"required" comment:"指标名称"`
StartDate time.Time `json:"start_date" form:"start_date" validate:"required" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" validate:"required" comment:"结束日期"`
}
// ================ 响应对象 ================
// CommandResponse 命令响应
type CommandResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// QueryResponse 查询响应
type QueryResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// ListResponse 列表响应
type ListResponse struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data ListDataDTO `json:"data" comment:"数据列表"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// ListDataDTO 列表数据DTO
type ListDataDTO struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []interface{} `json:"items" comment:"数据列表"`
}
// ================ 验证方法 ================
// Validate 验证创建指标命令
func (c *CreateMetricCommand) Validate() error {
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.MetricName == "" {
return fmt.Errorf("指标名称不能为空")
}
if c.Value < 0 {
return fmt.Errorf("指标值不能为负数")
}
if c.Date.IsZero() {
return fmt.Errorf("统计日期不能为空")
}
return nil
}
// Validate 验证更新指标命令
func (c *UpdateMetricCommand) Validate() error {
if c.ID == "" {
return fmt.Errorf("指标ID不能为空")
}
if c.Value < 0 {
return fmt.Errorf("指标值不能为负数")
}
return nil
}
// Validate 验证生成报告命令
func (c *GenerateReportCommand) Validate() error {
if c.ReportType == "" {
return fmt.Errorf("报告类型不能为空")
}
if c.Title == "" {
return fmt.Errorf("报告标题不能为空")
}
if c.Period == "" {
return fmt.Errorf("统计周期不能为空")
}
if c.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
if c.GeneratedBy == "" {
return fmt.Errorf("生成者ID不能为空")
}
return nil
}
// Validate 验证创建仪表板命令
func (c *CreateDashboardCommand) Validate() error {
if c.Name == "" {
return fmt.Errorf("仪表板名称不能为空")
}
if c.UserRole == "" {
return fmt.Errorf("用户角色不能为空")
}
if c.CreatedBy == "" {
return fmt.Errorf("创建者ID不能为空")
}
if c.RefreshInterval < 30 {
return fmt.Errorf("刷新间隔不能少于30秒")
}
return nil
}
// Validate 验证更新仪表板命令
func (c *UpdateDashboardCommand) Validate() error {
if c.ID == "" {
return fmt.Errorf("仪表板ID不能为空")
}
if c.UpdatedBy == "" {
return fmt.Errorf("更新者ID不能为空")
}
if c.RefreshInterval < 30 {
return fmt.Errorf("刷新间隔不能少于30秒")
}
return nil
}
// Validate 验证导出数据命令
func (c *ExportDataCommand) Validate() error {
if c.Format == "" {
return fmt.Errorf("导出格式不能为空")
}
if c.MetricType == "" {
return fmt.Errorf("指标类型不能为空")
}
if c.StartDate.IsZero() || c.EndDate.IsZero() {
return fmt.Errorf("开始日期和结束日期不能为空")
}
if c.StartDate.After(c.EndDate) {
return fmt.Errorf("开始日期不能晚于结束日期")
}
if c.ExportedBy == "" {
return fmt.Errorf("导出者ID不能为空")
}
return nil
}

View File

@@ -0,0 +1,258 @@
package statistics
import (
"time"
)
// StatisticsMetricDTO 统计指标DTO
type StatisticsMetricDTO struct {
ID string `json:"id" comment:"统计指标唯一标识"`
MetricType string `json:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
Value float64 `json:"value" comment:"指标值"`
Metadata string `json:"metadata" comment:"额外维度信息"`
Date time.Time `json:"date" comment:"统计日期"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// StatisticsReportDTO 统计报告DTO
type StatisticsReportDTO struct {
ID string `json:"id" comment:"报告唯一标识"`
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Content string `json:"content" comment:"报告内容"`
Period string `json:"period" comment:"统计周期"`
UserRole string `json:"user_role" comment:"用户角色"`
Status string `json:"status" comment:"报告状态"`
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
GeneratedAt *time.Time `json:"generated_at" comment:"生成时间"`
ExpiresAt *time.Time `json:"expires_at" comment:"过期时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// StatisticsDashboardDTO 统计仪表板DTO
type StatisticsDashboardDTO struct {
ID string `json:"id" comment:"仪表板唯一标识"`
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" comment:"用户角色"`
IsDefault bool `json:"is_default" comment:"是否为默认仪表板"`
IsActive bool `json:"is_active" comment:"是否激活"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
CreatedBy string `json:"created_by" comment:"创建者ID"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// DashboardDataDTO 仪表板数据DTO
type DashboardDataDTO struct {
// API调用统计
APICalls struct {
TotalCount int64 `json:"total_count" comment:"总调用次数"`
SuccessCount int64 `json:"success_count" comment:"成功调用次数"`
FailedCount int64 `json:"failed_count" comment:"失败调用次数"`
SuccessRate float64 `json:"success_rate" comment:"成功率"`
AvgResponseTime float64 `json:"avg_response_time" comment:"平均响应时间"`
} `json:"api_calls"`
// 用户统计
Users struct {
TotalCount int64 `json:"total_count" comment:"总用户数"`
CertifiedCount int64 `json:"certified_count" comment:"认证用户数"`
ActiveCount int64 `json:"active_count" comment:"活跃用户数"`
CertificationRate float64 `json:"certification_rate" comment:"认证完成率"`
RetentionRate float64 `json:"retention_rate" comment:"留存率"`
} `json:"users"`
// 财务统计
Finance struct {
TotalAmount float64 `json:"total_amount" comment:"总金额"`
RechargeAmount float64 `json:"recharge_amount" comment:"充值金额"`
DeductAmount float64 `json:"deduct_amount" comment:"扣款金额"`
NetAmount float64 `json:"net_amount" comment:"净金额"`
} `json:"finance"`
// 产品统计
Products struct {
TotalProducts int64 `json:"total_products" comment:"总产品数"`
ActiveProducts int64 `json:"active_products" comment:"活跃产品数"`
TotalSubscriptions int64 `json:"total_subscriptions" comment:"总订阅数"`
ActiveSubscriptions int64 `json:"active_subscriptions" comment:"活跃订阅数"`
} `json:"products"`
// 认证统计
Certification struct {
TotalCertifications int64 `json:"total_certifications" comment:"总认证数"`
CompletedCertifications int64 `json:"completed_certifications" comment:"完成认证数"`
PendingCertifications int64 `json:"pending_certifications" comment:"待处理认证数"`
FailedCertifications int64 `json:"failed_certifications" comment:"失败认证数"`
CompletionRate float64 `json:"completion_rate" comment:"完成率"`
} `json:"certification"`
// 时间信息
Period struct {
StartDate string `json:"start_date" comment:"开始日期"`
EndDate string `json:"end_date" comment:"结束日期"`
Period string `json:"period" comment:"统计周期"`
} `json:"period"`
// 元数据
Metadata struct {
GeneratedAt string `json:"generated_at" comment:"生成时间"`
UserRole string `json:"user_role" comment:"用户角色"`
DataVersion string `json:"data_version" comment:"数据版本"`
} `json:"metadata"`
}
// RealtimeMetricsDTO 实时指标DTO
type RealtimeMetricsDTO struct {
MetricType string `json:"metric_type" comment:"指标类型"`
Metrics map[string]float64 `json:"metrics" comment:"指标数据"`
Timestamp time.Time `json:"timestamp" comment:"时间戳"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// HistoricalMetricsDTO 历史指标DTO
type HistoricalMetricsDTO struct {
MetricType string `json:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" comment:"统计维度"`
DataPoints []DataPointDTO `json:"data_points" comment:"数据点"`
Summary MetricsSummaryDTO `json:"summary" comment:"汇总信息"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// DataPointDTO 数据点DTO
type DataPointDTO struct {
Date time.Time `json:"date" comment:"日期"`
Value float64 `json:"value" comment:"值"`
Label string `json:"label" comment:"标签"`
}
// MetricsSummaryDTO 指标汇总DTO
type MetricsSummaryDTO struct {
Total float64 `json:"total" comment:"总值"`
Average float64 `json:"average" comment:"平均值"`
Max float64 `json:"max" comment:"最大值"`
Min float64 `json:"min" comment:"最小值"`
Count int64 `json:"count" comment:"数据点数量"`
GrowthRate float64 `json:"growth_rate" comment:"增长率"`
Trend string `json:"trend" comment:"趋势"`
}
// ReportContentDTO 报告内容DTO
type ReportContentDTO struct {
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
Details map[string]interface{} `json:"details" comment:"详细信息"`
Charts []ChartDTO `json:"charts" comment:"图表数据"`
Tables []TableDTO `json:"tables" comment:"表格数据"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
}
// ChartDTO 图表DTO
type ChartDTO struct {
Type string `json:"type" comment:"图表类型"`
Title string `json:"title" comment:"图表标题"`
Data map[string]interface{} `json:"data" comment:"图表数据"`
Options map[string]interface{} `json:"options" comment:"图表选项"`
Description string `json:"description" comment:"图表描述"`
}
// TableDTO 表格DTO
type TableDTO struct {
Title string `json:"title" comment:"表格标题"`
Headers []string `json:"headers" comment:"表头"`
Rows [][]interface{} `json:"rows" comment:"表格行数据"`
Summary map[string]interface{} `json:"summary" comment:"汇总信息"`
Description string `json:"description" comment:"表格描述"`
}
// ExportDataDTO 导出数据DTO
type ExportDataDTO struct {
Format string `json:"format" comment:"导出格式"`
FileName string `json:"file_name" comment:"文件名"`
Data []map[string]interface{} `json:"data" comment:"导出数据"`
Headers []string `json:"headers" comment:"表头"`
Metadata map[string]interface{} `json:"metadata" comment:"元数据"`
DownloadURL string `json:"download_url" comment:"下载链接"`
}
// StatisticsQueryDTO 统计查询DTO
type StatisticsQueryDTO struct {
MetricType string `json:"metric_type" form:"metric_type" comment:"指标类型"`
MetricName string `json:"metric_name" form:"metric_name" comment:"指标名称"`
Dimension string `json:"dimension" form:"dimension" comment:"统计维度"`
StartDate time.Time `json:"start_date" form:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" form:"end_date" comment:"结束日期"`
Period string `json:"period" form:"period" comment:"统计周期"`
UserRole string `json:"user_role" form:"user_role" comment:"用户角色"`
Limit int `json:"limit" form:"limit" comment:"限制数量"`
Offset int `json:"offset" form:"offset" comment:"偏移量"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序顺序"`
}
// ReportGenerationDTO 报告生成DTO
type ReportGenerationDTO struct {
ReportType string `json:"report_type" comment:"报告类型"`
Title string `json:"title" comment:"报告标题"`
Period string `json:"period" comment:"统计周期"`
UserRole string `json:"user_role" comment:"用户角色"`
StartDate time.Time `json:"start_date" comment:"开始日期"`
EndDate time.Time `json:"end_date" comment:"结束日期"`
Filters map[string]interface{} `json:"filters" comment:"过滤条件"`
Format string `json:"format" comment:"输出格式"`
GeneratedBy string `json:"generated_by" comment:"生成者ID"`
}
// DashboardConfigDTO 仪表板配置DTO
type DashboardConfigDTO struct {
Name string `json:"name" comment:"仪表板名称"`
Description string `json:"description" comment:"仪表板描述"`
UserRole string `json:"user_role" comment:"用户角色"`
Layout string `json:"layout" comment:"布局配置"`
Widgets string `json:"widgets" comment:"组件配置"`
Settings string `json:"settings" comment:"设置配置"`
RefreshInterval int `json:"refresh_interval" comment:"刷新间隔(秒)"`
AccessLevel string `json:"access_level" comment:"访问级别"`
CreatedBy string `json:"created_by" comment:"创建者ID"`
}
// StatisticsResponseDTO 统计响应DTO
type StatisticsResponseDTO struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data interface{} `json:"data" comment:"响应数据"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}
// PaginationDTO 分页DTO
type PaginationDTO struct {
Page int `json:"page" comment:"当前页"`
PageSize int `json:"page_size" comment:"每页大小"`
Total int64 `json:"total" comment:"总数量"`
Pages int `json:"pages" comment:"总页数"`
HasNext bool `json:"has_next" comment:"是否有下一页"`
HasPrev bool `json:"has_prev" comment:"是否有上一页"`
}
// StatisticsListResponseDTO 统计列表响应DTO
type StatisticsListResponseDTO struct {
Success bool `json:"success" comment:"是否成功"`
Message string `json:"message" comment:"响应消息"`
Data []interface{} `json:"data" comment:"数据列表"`
Pagination PaginationDTO `json:"pagination" comment:"分页信息"`
Meta map[string]interface{} `json:"meta" comment:"元数据"`
Error string `json:"error,omitempty" comment:"错误信息"`
}

View File

@@ -0,0 +1,186 @@
package statistics
import (
"context"
"time"
)
// StatisticsApplicationService 统计应用服务接口
// 负责统计功能的业务逻辑编排和协调
type StatisticsApplicationService interface {
// ================ 指标管理 ================
// CreateMetric 创建统计指标
CreateMetric(ctx context.Context, cmd *CreateMetricCommand) (*CommandResponse, error)
// UpdateMetric 更新统计指标
UpdateMetric(ctx context.Context, cmd *UpdateMetricCommand) (*CommandResponse, error)
// DeleteMetric 删除统计指标
DeleteMetric(ctx context.Context, cmd *DeleteMetricCommand) (*CommandResponse, error)
// GetMetric 获取单个指标
GetMetric(ctx context.Context, query *GetMetricQuery) (*QueryResponse, error)
// GetMetrics 获取指标列表
GetMetrics(ctx context.Context, query *GetMetricsQuery) (*ListResponse, error)
// ================ 实时统计 ================
// GetRealtimeMetrics 获取实时指标
GetRealtimeMetrics(ctx context.Context, query *GetRealtimeMetricsQuery) (*QueryResponse, error)
// UpdateRealtimeMetric 更新实时指标
UpdateRealtimeMetric(ctx context.Context, metricType, metricName string, value float64) error
// ================ 历史统计 ================
// GetHistoricalMetrics 获取历史指标
GetHistoricalMetrics(ctx context.Context, query *GetHistoricalMetricsQuery) (*QueryResponse, error)
// AggregateMetrics 聚合指标
AggregateMetrics(ctx context.Context, metricType, dimension string, startDate, endDate time.Time) error
// ================ 仪表板管理 ================
// CreateDashboard 创建仪表板
CreateDashboard(ctx context.Context, cmd *CreateDashboardCommand) (*CommandResponse, error)
// UpdateDashboard 更新仪表板
UpdateDashboard(ctx context.Context, cmd *UpdateDashboardCommand) (*CommandResponse, error)
// DeleteDashboard 删除仪表板
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) (*CommandResponse, error)
// GetDashboard 获取单个仪表板
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*QueryResponse, error)
// GetDashboards 获取仪表板列表
GetDashboards(ctx context.Context, query *GetDashboardsQuery) (*ListResponse, error)
// SetDefaultDashboard 设置默认仪表板
SetDefaultDashboard(ctx context.Context, cmd *SetDefaultDashboardCommand) (*CommandResponse, error)
// ActivateDashboard 激活仪表板
ActivateDashboard(ctx context.Context, cmd *ActivateDashboardCommand) (*CommandResponse, error)
// DeactivateDashboard 停用仪表板
DeactivateDashboard(ctx context.Context, cmd *DeactivateDashboardCommand) (*CommandResponse, error)
// GetDashboardData 获取仪表板数据
GetDashboardData(ctx context.Context, query *GetDashboardDataQuery) (*QueryResponse, error)
// ================ 报告管理 ================
// GenerateReport 生成报告
GenerateReport(ctx context.Context, cmd *GenerateReportCommand) (*CommandResponse, error)
// GetReport 获取单个报告
GetReport(ctx context.Context, query *GetReportQuery) (*QueryResponse, error)
// GetReports 获取报告列表
GetReports(ctx context.Context, query *GetReportsQuery) (*ListResponse, error)
// DeleteReport 删除报告
DeleteReport(ctx context.Context, reportID string) (*CommandResponse, error)
// ================ 统计分析 ================
// CalculateGrowthRate 计算增长率
CalculateGrowthRate(ctx context.Context, query *CalculateGrowthRateQuery) (*QueryResponse, error)
// CalculateTrend 计算趋势
CalculateTrend(ctx context.Context, query *CalculateTrendQuery) (*QueryResponse, error)
// CalculateCorrelation 计算相关性
CalculateCorrelation(ctx context.Context, query *CalculateCorrelationQuery) (*QueryResponse, error)
// CalculateMovingAverage 计算移动平均
CalculateMovingAverage(ctx context.Context, query *CalculateMovingAverageQuery) (*QueryResponse, error)
// CalculateSeasonality 计算季节性
CalculateSeasonality(ctx context.Context, query *CalculateSeasonalityQuery) (*QueryResponse, error)
// ================ 数据导出 ================
// ExportData 导出数据
ExportData(ctx context.Context, cmd *ExportDataCommand) (*CommandResponse, error)
// ================ 定时任务 ================
// ProcessHourlyAggregation 处理小时级聚合
ProcessHourlyAggregation(ctx context.Context, date time.Time) error
// ProcessDailyAggregation 处理日级聚合
ProcessDailyAggregation(ctx context.Context, date time.Time) error
// ProcessWeeklyAggregation 处理周级聚合
ProcessWeeklyAggregation(ctx context.Context, date time.Time) error
// ProcessMonthlyAggregation 处理月级聚合
ProcessMonthlyAggregation(ctx context.Context, date time.Time) error
// CleanupExpiredData 清理过期数据
CleanupExpiredData(ctx context.Context) error
// ================ 管理员专用方法 ================
// AdminGetSystemStatistics 管理员获取系统统计
AdminGetSystemStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminTriggerAggregation 管理员触发数据聚合
AdminTriggerAggregation(ctx context.Context, cmd *TriggerAggregationCommand) (*CommandResponse, error)
// AdminGetUserStatistics 管理员获取单个用户统计
AdminGetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
// ================ 管理员独立域统计接口 ================
// AdminGetUserDomainStatistics 管理员获取用户域统计
AdminGetUserDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetApiDomainStatistics 管理员获取API域统计
AdminGetApiDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetConsumptionDomainStatistics 管理员获取消费域统计
AdminGetConsumptionDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// AdminGetRechargeDomainStatistics 管理员获取充值域统计
AdminGetRechargeDomainStatistics(ctx context.Context, period, startDate, endDate string) (*QueryResponse, error)
// ================ 公开和用户统计方法 ================
// GetPublicStatistics 获取公开统计信息
GetPublicStatistics(ctx context.Context) (*QueryResponse, error)
// GetUserStatistics 获取用户统计信息
GetUserStatistics(ctx context.Context, userID string) (*QueryResponse, error)
// ================ 独立统计接口 ================
// GetApiCallsStatistics 获取API调用统计
GetApiCallsStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetConsumptionStatistics 获取消费统计
GetConsumptionStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetRechargeStatistics 获取充值统计
GetRechargeStatistics(ctx context.Context, userID string, startDate, endDate time.Time, unit string) (*QueryResponse, error)
// GetLatestProducts 获取最新产品推荐
GetLatestProducts(ctx context.Context, limit int) (*QueryResponse, error)
// ================ 管理员排行榜接口 ================
// AdminGetUserCallRanking 获取用户调用排行榜
AdminGetUserCallRanking(ctx context.Context, rankingType, period string, limit int) (*QueryResponse, error)
// AdminGetRechargeRanking 获取充值排行榜
AdminGetRechargeRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
// AdminGetApiPopularityRanking 获取API受欢迎程度排行榜
AdminGetApiPopularityRanking(ctx context.Context, period string, limit int) (*QueryResponse, error)
// AdminGetTodayCertifiedEnterprises 获取今日认证企业列表
AdminGetTodayCertifiedEnterprises(ctx context.Context, limit int) (*QueryResponse, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,30 +6,34 @@ import (
// Config 应用程序总配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Cache CacheConfig `mapstructure:"cache"`
Logger LoggerConfig `mapstructure:"logger"`
JWT JWTConfig `mapstructure:"jwt"`
API APIConfig `mapstructure:"api"`
SMS SMSConfig `mapstructure:"sms"`
Email EmailConfig `mapstructure:"email"`
Storage StorageConfig `mapstructure:"storage"`
OCR OCRConfig `mapstructure:"ocr"`
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
Monitoring MonitoringConfig `mapstructure:"monitoring"`
Health HealthConfig `mapstructure:"health"`
Resilience ResilienceConfig `mapstructure:"resilience"`
Development DevelopmentConfig `mapstructure:"development"`
App AppConfig `mapstructure:"app"`
WechatWork WechatWorkConfig `mapstructure:"wechat_work"`
Esign EsignConfig `mapstructure:"esign"`
Wallet WalletConfig `mapstructure:"wallet"`
WestDex WestDexConfig `mapstructure:"westdex"`
AliPay AliPayConfig `mapstructure:"alipay"`
Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Cache CacheConfig `mapstructure:"cache"`
Logger LoggerConfig `mapstructure:"logger"`
JWT JWTConfig `mapstructure:"jwt"`
API APIConfig `mapstructure:"api"`
SMS SMSConfig `mapstructure:"sms"`
Email EmailConfig `mapstructure:"email"`
Storage StorageConfig `mapstructure:"storage"`
OCR OCRConfig `mapstructure:"ocr"`
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
DailyRateLimit DailyRateLimitConfig `mapstructure:"daily_ratelimit"`
Monitoring MonitoringConfig `mapstructure:"monitoring"`
Health HealthConfig `mapstructure:"health"`
Resilience ResilienceConfig `mapstructure:"resilience"`
Development DevelopmentConfig `mapstructure:"development"`
App AppConfig `mapstructure:"app"`
WechatWork WechatWorkConfig `mapstructure:"wechat_work"`
Esign EsignConfig `mapstructure:"esign"`
Wallet WalletConfig `mapstructure:"wallet"`
WestDex WestDexConfig `mapstructure:"westdex"`
Zhicha ZhichaConfig `mapstructure:"zhicha"`
AliPay AliPayConfig `mapstructure:"alipay"`
Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"`
Xingwei XingweiConfig `mapstructure:"xingwei"`
}
// ServerConfig HTTP服务器配置
@@ -117,6 +121,31 @@ type RateLimitConfig struct {
Burst int `mapstructure:"burst"`
}
// DailyRateLimitConfig 每日限流配置
type DailyRateLimitConfig struct {
MaxRequestsPerDay int `mapstructure:"max_requests_per_day"` // 每日最大请求次数
MaxRequestsPerIP int `mapstructure:"max_requests_per_ip"` // 每个IP每日最大请求次数
KeyPrefix string `mapstructure:"key_prefix"` // Redis键前缀
TTL time.Duration `mapstructure:"ttl"` // 键过期时间
// 新增安全配置
EnableIPWhitelist bool `mapstructure:"enable_ip_whitelist"` // 是否启用IP白名单
IPWhitelist []string `mapstructure:"ip_whitelist"` // IP白名单
EnableIPBlacklist bool `mapstructure:"enable_ip_blacklist"` // 是否启用IP黑名单
IPBlacklist []string `mapstructure:"ip_blacklist"` // IP黑名单
EnableUserAgent bool `mapstructure:"enable_user_agent"` // 是否检查User-Agent
BlockedUserAgents []string `mapstructure:"blocked_user_agents"` // 被阻止的User-Agent
EnableReferer bool `mapstructure:"enable_referer"` // 是否检查Referer
AllowedReferers []string `mapstructure:"allowed_referers"` // 允许的Referer
EnableGeoBlock bool `mapstructure:"enable_geo_block"` // 是否启用地理位置阻止
BlockedCountries []string `mapstructure:"blocked_countries"` // 被阻止的国家/地区
EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理
MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数
// 路径排除配置
ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径
// 域名排除配置
ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名
}
// MonitoringConfig 监控配置
type MonitoringConfig struct {
MetricsEnabled bool `mapstructure:"metrics_enabled"`
@@ -187,14 +216,14 @@ type SMSRateLimit struct {
// EmailConfig 邮件服务配置
type EmailConfig struct {
Host string `mapstructure:"host"` // SMTP服务器地址
Port int `mapstructure:"port"` // SMTP服务器端口
Username string `mapstructure:"username"` // 邮箱用户名
Password string `mapstructure:"password"` // 邮箱密码/授权码
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
Domain string `mapstructure:"domain"` // 控制台域名
Host string `mapstructure:"host"` // SMTP服务器地址
Port int `mapstructure:"port"` // SMTP服务器端口
Username string `mapstructure:"username"` // 邮箱用户名
Password string `mapstructure:"password"` // 邮箱密码/授权码
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
Domain string `mapstructure:"domain"` // 控制台域名
}
// GetDSN 获取数据库DSN连接字符串
@@ -290,6 +319,14 @@ type WalletConfig struct {
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
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 支付宝充值赠送规则
@@ -298,12 +335,60 @@ type AliPayRechargeBonusRule struct {
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
}
// WestDexConfig WestDex配置
// WestDexConfig 西部数据配置
type WestDexConfig struct {
URL string `mapstructure:"url"`
Key string `mapstructure:"key"`
SecretId string `mapstructure:"secret_id"`
SecretSecondId string `mapstructure:"secret_second_id"`
SecretID string `mapstructure:"secret_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"`
}
// AliPayConfig 支付宝配置
@@ -321,6 +406,26 @@ type YushanConfig struct {
URL string `mapstructure:"url"`
APIKey string `mapstructure:"api_key"`
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 天眼查配置
@@ -329,6 +434,38 @@ type TianYanChaConfig struct {
APIKey string `mapstructure:"api_key"`
}
type AlicloudConfig struct {
Host string `mapstructure:"host"`
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 域名配置
type DomainConfig struct {
API string `mapstructure:"api"` // API域名

View File

@@ -2,6 +2,7 @@ package container
import (
"context"
"fmt"
"time"
"go.uber.org/fx"
@@ -9,36 +10,52 @@ import (
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
"tyapi-server/internal/application/article"
"tyapi-server/internal/application/certification"
"tyapi-server/internal/application/finance"
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/statistics"
"tyapi-server/internal/application/user"
"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"
certification_service "tyapi-server/internal/domains/certification/services"
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
finance_service "tyapi-server/internal/domains/finance/services"
domain_product_repo "tyapi-server/internal/domains/product/repositories"
product_service "tyapi-server/internal/domains/product/services"
statistics_service "tyapi-server/internal/domains/statistics/services"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/cache"
"tyapi-server/internal/infrastructure/database"
article_repo "tyapi-server/internal/infrastructure/database/repositories/article"
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
infra_events "tyapi-server/internal/infrastructure/events"
"tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/email"
"tyapi-server/internal/infrastructure/external/ocr"
"tyapi-server/internal/infrastructure/external/sms"
"tyapi-server/internal/infrastructure/external/storage"
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/xingwei"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/infrastructure/http/handlers"
"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"
shared_database "tyapi-server/internal/shared/database"
"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/hooks"
sharedhttp "tyapi-server/internal/shared/http"
@@ -56,12 +73,18 @@ import (
domain_user_repo "tyapi-server/internal/domains/user/repositories"
user_repo "tyapi-server/internal/infrastructure/database/repositories/user"
hibiken_asynq "github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
api_app "tyapi-server/internal/application/api"
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"
statistics_repo "tyapi-server/internal/infrastructure/database/repositories/statistics"
)
// Container 应用容器
@@ -81,37 +104,44 @@ func NewContainer() *Container {
fx.Provide(
// 日志器 - 提供自定义Logger和*zap.Logger
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 {
// 使用按级别分文件的日志器
levelConfig := logger.LevelLoggerConfig{
BaseConfig: 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,
},
BaseConfig: logCfg,
EnableLevelSeparation: true,
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
}
return logger.NewLevelLogger(levelConfig)
} 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)
}
},
@@ -129,9 +159,8 @@ func NewContainer() *Container {
return infoLogger
}
}
// 如果类型转换失败,创建一个默认的zap logger
defaultLogger, _ := zap.NewProduction()
return defaultLogger
// 如果类型转换失败,使用全局日志器
return logger.GetGlobalLogger()
},
),
@@ -183,7 +212,7 @@ func NewContainer() *Container {
return 5 // 默认5个工作协程
},
fx.Annotate(
events.NewMemoryEventBus,
shared_events.NewMemoryEventBus,
fx.As(new(interfaces.EventBus)),
),
// 健康检查
@@ -274,6 +303,10 @@ func NewContainer() *Container {
}
return payment.NewAliPayService(config)
},
// 导出管理器
func(logger *zap.Logger) *export.ExportManager {
return export.NewExportManager(logger)
},
),
// 高级特性模块
@@ -303,19 +336,23 @@ func NewContainer() *Container {
sharedhttp.NewResponseBuilder,
validator.NewRequestValidator,
// WestDexService - 需要从配置中获取参数
func(cfg *config.Config) *westdex.WestDexService {
return westdex.NewWestDexService(
cfg.WestDex.URL,
cfg.WestDex.Key,
cfg.WestDex.SecretId,
cfg.WestDex.SecretSecondId,
)
func(cfg *config.Config) (*westdex.WestDexService, error) {
return westdex.NewWestDexServiceWithConfig(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 {
return yushan.NewYushanService(
cfg.Yushan.URL,
cfg.Yushan.APIKey,
cfg.Yushan.AcctID,
nil, // 暂时不传入logger使用无日志版本
)
},
// TianYanChaService - 天眼查服务
@@ -326,6 +363,13 @@ func NewContainer() *Container {
30*time.Second, // 默认超时时间
)
},
// AlicloudService - 阿里云服务
func(cfg *config.Config) *alicloud.AlicloudService {
return alicloud.NewAlicloudService(
cfg.Alicloud.Host,
cfg.Alicloud.AppCode,
)
},
sharedhttp.NewGinRouter,
),
@@ -336,6 +380,33 @@ func NewContainer() *Container {
middleware.NewResponseTimeMiddleware,
middleware.NewCORSMiddleware,
middleware.NewRateLimitMiddleware,
// 每日限流中间件
func(cfg *config.Config, redis *redis.Client, response interfaces.ResponseBuilder, logger *zap.Logger) *middleware.DailyRateLimitMiddleware {
limitConfig := middleware.DailyRateLimitConfig{
MaxRequestsPerDay: cfg.DailyRateLimit.MaxRequestsPerDay,
MaxRequestsPerIP: cfg.DailyRateLimit.MaxRequestsPerIP,
KeyPrefix: cfg.DailyRateLimit.KeyPrefix,
TTL: cfg.DailyRateLimit.TTL,
MaxConcurrent: cfg.DailyRateLimit.MaxConcurrent,
// 安全配置
EnableIPWhitelist: cfg.DailyRateLimit.EnableIPWhitelist,
IPWhitelist: cfg.DailyRateLimit.IPWhitelist,
EnableIPBlacklist: cfg.DailyRateLimit.EnableIPBlacklist,
IPBlacklist: cfg.DailyRateLimit.IPBlacklist,
EnableUserAgent: cfg.DailyRateLimit.EnableUserAgent,
BlockedUserAgents: cfg.DailyRateLimit.BlockedUserAgents,
EnableReferer: cfg.DailyRateLimit.EnableReferer,
AllowedReferers: cfg.DailyRateLimit.AllowedReferers,
EnableGeoBlock: cfg.DailyRateLimit.EnableGeoBlock,
BlockedCountries: cfg.DailyRateLimit.BlockedCountries,
EnableProxyCheck: cfg.DailyRateLimit.EnableProxyCheck,
// 排除路径配置
ExcludePaths: cfg.DailyRateLimit.ExcludePaths,
// 排除域名配置
ExcludeDomains: cfg.DailyRateLimit.ExcludeDomains,
}
return middleware.NewDailyRateLimitMiddleware(cfg, redis, response, logger, limitConfig)
},
NewRequestLoggerMiddlewareWrapper,
middleware.NewJWTAuthMiddleware,
middleware.NewOptionalAuthMiddleware,
@@ -473,6 +544,30 @@ func NewContainer() *Container {
),
),
// 仓储层 - 文章域
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)),
),
),
// API域仓储层
fx.Provide(
fx.Annotate(
@@ -485,6 +580,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.Annotate(
@@ -497,6 +608,25 @@ func NewContainer() *Container {
product_service.NewProductSubscriptionService,
product_service.NewProductApiConfigService,
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.NewRechargeRecordService,
// 发票领域服务
@@ -538,18 +668,124 @@ func NewContainer() *Container {
infra_events.NewInvoiceEventHandler,
certification_service.NewCertificationAggregateService,
certification_service.NewEnterpriseInfoSubmitRecordService,
// 文章领域服务
article_service.NewArticleService,
// 统计领域服务
statistics_service.NewStatisticsAggregateService,
statistics_service.NewStatisticsCalculationService,
statistics_service.NewStatisticsReportService,
),
// API域服务层
fx.Provide(
api_service.NewApiUserAggregateService,
api_service.NewApiCallAggregateService,
api_service.NewApiRequestService,
fx.Annotate(
api_services.NewApiUserAggregateService,
),
api_services.NewApiCallAggregateService,
api_services.NewApiRequestService,
api_services.NewFormConfigService,
),
// API域应用服务
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,
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,
apiApplicationService,
walletService,
subscriptionService,
asyncTaskRepo,
)
},
),
// 应用服务
@@ -561,12 +797,70 @@ func NewContainer() *Container {
),
// 认证应用服务 - 绑定到接口
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.Annotate(
finance.NewFinanceApplicationService,
func(
aliPayClient *payment.AliPayService,
walletService finance_services.WalletAggregateService,
rechargeRecordService finance_services.RechargeRecordService,
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
userRepo domain_user_repo.UserRepository,
txManager *shared_database.TransactionManager,
logger *zap.Logger,
config *config.Config,
exportManager *export.ExportManager,
) finance.FinanceApplicationService {
return finance.NewFinanceApplicationService(
aliPayClient,
walletService,
rechargeRecordService,
walletTransactionRepo,
alipayOrderRepo,
userRepo,
txManager,
logger,
config,
exportManager,
)
},
fx.As(new(finance.FinanceApplicationService)),
),
// 发票应用服务 - 绑定到接口
@@ -603,6 +897,66 @@ func NewContainer() *Container {
product.NewSubscriptionApplicationService,
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(
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)),
),
),
// HTTP处理器
@@ -619,6 +973,17 @@ func NewContainer() *Container {
handlers.NewProductAdminHandler,
// API Handler
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)
},
),
// 路由注册
@@ -633,8 +998,12 @@ func NewContainer() *Container {
routes.NewProductRoutes,
// 产品管理员路由
routes.NewProductAdminRoutes,
// 文章路由
routes.NewArticleRoutes,
// API路由
routes.NewApiRoutes,
// 统计路由
routes.NewStatisticsRoutes,
),
// 应用生命周期
@@ -669,15 +1038,34 @@ func (c *Container) Stop() error {
func RegisterLifecycleHooks(
lifecycle fx.Lifecycle,
logger *zap.Logger,
asynqWorker *asynq.AsynqWorker,
) {
lifecycle.Append(fx.Hook{
OnStart: func(context.Context) error {
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
},
OnStop: func(context.Context) error {
logger.Info("应用关闭中...")
// 停止AsynqWorker
asynqWorker.Stop()
asynqWorker.Shutdown()
logger.Info("AsynqWorker已停止")
return nil
},
})
@@ -693,6 +1081,7 @@ func RegisterMiddlewares(
responseTime *middleware.ResponseTimeMiddleware,
cors *middleware.CORSMiddleware,
rateLimit *middleware.RateLimitMiddleware,
dailyRateLimit *middleware.DailyRateLimitMiddleware,
requestLogger *middleware.RequestLoggerMiddleware,
traceIDMiddleware *middleware.TraceIDMiddleware,
errorTrackingMiddleware *middleware.ErrorTrackingMiddleware,
@@ -706,6 +1095,7 @@ func RegisterMiddlewares(
router.RegisterMiddleware(responseTime)
router.RegisterMiddleware(cors)
router.RegisterMiddleware(rateLimit)
router.RegisterMiddleware(dailyRateLimit)
router.RegisterMiddleware(requestLogger)
router.RegisterMiddleware(traceIDMiddleware)
router.RegisterMiddleware(errorTrackingMiddleware)
@@ -720,7 +1110,9 @@ func RegisterRoutes(
financeRoutes *routes.FinanceRoutes,
productRoutes *routes.ProductRoutes,
productAdminRoutes *routes.ProductAdminRoutes,
articleRoutes *routes.ArticleRoutes,
apiRoutes *routes.ApiRoutes,
statisticsRoutes *routes.StatisticsRoutes,
cfg *config.Config,
logger *zap.Logger,
) {
@@ -735,6 +1127,8 @@ func RegisterRoutes(
financeRoutes.Register(router)
productRoutes.Register(router)
productAdminRoutes.Register(router)
articleRoutes.Register(router)
statisticsRoutes.Register(router)
// 打印注册的路由信息
router.PrintRoutes()
@@ -782,6 +1176,7 @@ func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.
"panic": zapcore.PanicLevel,
}
// 只转换配置文件中存在的级别
for levelStr, config := range configs {
if level, exists := levelMap[levelStr]; exists {
result[level] = logger.LevelFileConfig{

View File

@@ -31,7 +31,7 @@ type FLXG162AReq struct {
type FLXG0687Req struct {
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"`
}
type FLXG970FReq struct {
@@ -68,6 +68,10 @@ type IVYZ5733Req struct {
Name string `json:"name" validate:"required,min=1,validName"`
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 {
ManName string `json:"man_name" validate:"required,min=1,validName"`
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
@@ -97,7 +101,7 @@ type JRZQDBCEReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
}
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"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
}
@@ -105,13 +109,13 @@ type QYGL6F2DReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
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"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
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 {
EntName string `json:"ent_name" validate:"required,min=1,validName"`
@@ -123,7 +127,7 @@ type QYGLB4C0Req 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"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
@@ -168,11 +172,45 @@ type IVYZ9A2BReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
}
type IVYZ7F2AReq struct {
ManName string `json:"man_name" validate:"required,min=1,validName"`
ManIDCard string `json:"man_id_card" validate:"required,validIDCard"`
WomanName string `json:"woman_name" validate:"required,min=1,validName"`
WomanIDCard string `json:"woman_id_card" validate:"required,validIDCard"`
}
type IVYZ4E8BReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type IVYZ1C9DReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Years int64 `json:"years" validate:"omitempty,min=0,max=100"`
}
type IVYZGZ08Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type FLXG8A3FReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type FLXG5B2EReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type COMB298YReq 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"`
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
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"`
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
TimeRange string `json:"time_range" validate:"omitempty,validTimeRange"` // 非必填字段
}
type COMB86PMReq struct {
@@ -181,3 +219,297 @@ type COMB86PMReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
}
type QCXG7A2BReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type COMENT01Req struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
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 IVYZ3A7FReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
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"`
}
// 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 int `json:"page_size" validate:"omitempty,min=1,max=100"`
PageNum int `json:"page_num" validate:"omitempty,min=1"`
}
type QYGL8B4DReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
PageNum int `json:"page_num" validate:"omitempty,min=1"`
}
type QYGL9E2FReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
PageNum int `json:"page_num" validate:"omitempty,min=1"`
}
type QYGL7C1AReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
PageNum int `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 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 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 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:"id_card" validate:"required,validIDCard"`
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}
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 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 {
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
}
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 YYSY9E4AReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}

View File

@@ -1,11 +1,7 @@
package entities
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"github.com/google/uuid"
@@ -56,7 +52,7 @@ type ApiCall struct {
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
UserId *string `gorm:"type:varchar(36);index" json:"user_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"`
RequestParams string `gorm:"type:text" json:"request_params"`
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
@@ -145,40 +141,9 @@ func (a *ApiCall) Validate() error {
return nil
}
// 全局计数器,用于确保TransactionID的唯一性
var (
transactionCounter int64
counterMutex sync.Mutex
)
// GenerateTransactionID 生成16位数的交易单号
// GenerateTransactionID 生成UUID格式的交易单号
func GenerateTransactionID() 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
return uuid.New().String()
}
// TableName 指定数据库表名

View File

@@ -20,12 +20,20 @@ const (
// ApiUser API用户聚合根
type ApiUser struct {
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id"`
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
WhiteList []string `gorm:"type:json;serializer:json;default:'[]'" json:"white_list"` // 支持多个白名单
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id"`
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
WhiteList []string `gorm:"type:json;serializer:json;default:'[]'" json:"white_list"` // 支持多个白名单
// 余额预警配置
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"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
@@ -51,7 +59,7 @@ func (u *ApiUser) IsFrozen() bool {
}
// NewApiUser 工厂方法
func NewApiUser(userId string) (*ApiUser, error) {
func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold float64) (*ApiUser, error) {
if userId == "" {
return nil, errors.New("用户ID不能为空")
}
@@ -64,12 +72,14 @@ func NewApiUser(userId string) (*ApiUser, error) {
return nil, err
}
return &ApiUser{
ID: uuid.New().String(),
UserId: userId,
AccessId: accessId,
SecretKey: secretKey,
Status: ApiUserStatusNormal,
WhiteList: []string{},
ID: uuid.New().String(),
UserId: userId,
AccessId: accessId,
SecretKey: secretKey,
Status: ApiUserStatusNormal,
WhiteList: []string{},
BalanceAlertEnabled: defaultAlertEnabled,
BalanceAlertThreshold: defaultAlertThreshold,
}, nil
}
@@ -124,6 +134,68 @@ func (u *ApiUser) RemoveFromWhiteList(entry string) error {
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聚合根的业务规则
func (u *ApiUser) Validate() error {
if u.UserId == "" {

View File

@@ -2,6 +2,7 @@ package repositories
import (
"context"
"time"
"tyapi-server/internal/domains/api/entities"
"tyapi-server/internal/shared/interfaces"
)
@@ -27,6 +28,20 @@ type ApiCallRepository interface {
// 新增根据TransactionID查询
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调用记录包含产品名称
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)
}

View File

@@ -2,20 +2,27 @@ package services
import (
"context"
"encoding/json"
"fmt"
"tyapi-server/internal/application/api/commands"
"tyapi-server/internal/domains/api/services/processors"
"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/ivyz"
"tyapi-server/internal/domains/api/services/processors/jrzq"
"tyapi-server/internal/domains/api/services/processors/qcxg"
"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/product/services"
"tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/xingwei"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/shared/interfaces"
)
@@ -31,6 +38,7 @@ type ApiRequestService struct {
westDexService *westdex.WestDexService
yushanService *yushan.YushanService
tianYanChaService *tianyancha.TianYanChaService
alicloudService *alicloud.AlicloudService
validator interfaces.RequestValidator
processorDeps *processors.ProcessorDependencies
combService *comb.CombService
@@ -40,6 +48,9 @@ func NewApiRequestService(
westDexService *westdex.WestDexService,
yushanService *yushan.YushanService,
tianYanChaService *tianyancha.TianYanChaService,
alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService,
xingweiService *xingwei.XingweiService,
validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService,
) *ApiRequestService {
@@ -47,7 +58,7 @@ func NewApiRequestService(
combService := comb.NewCombService(productManagementService)
// 创建处理器依赖容器
processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, validator, combService)
processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService)
// 统一注册所有处理器
registerAllProcessors(combService)
@@ -56,6 +67,7 @@ func NewApiRequestService(
westDexService: westDexService,
yushanService: yushanService,
tianYanChaService: tianYanChaService,
alicloudService: alicloudService,
validator: validator,
processorDeps: processorDeps,
combService: combService,
@@ -79,12 +91,34 @@ func registerAllProcessors(combService *comb.CombService) {
"FLXGC9D1": flxg.ProcessFLXGC9D1Request,
"FLXGCA3D": flxg.ProcessFLXGCA3DRequest,
"FLXGDEC7": flxg.ProcessFLXGDEC7Request,
"FLXG8A3F": flxg.ProcessFLXG8A3FRequest,
"FLXG5B2E": flxg.ProcessFLXG5B2ERequest,
"FLXG0687": flxg.ProcessFLXG0687Request,
"FLXGBC21": flxg.ProcessFLXGBC21Request,
"FLXGDEA8": flxg.ProcessFLXGDEA8Request,
"FLXGDEA9": flxg.ProcessFLXGDEA9Request,
"FLXG5A3B": flxg.ProcessFLXG5A3BRequest,
"FLXG9C1D": flxg.ProcessFLXG9C1DRequest,
"FLXG2E8F": flxg.ProcessFLXG2E8FRequest,
"FLXG7E8F": flxg.ProcessFLXG7E8FRequest,
// JRZQ系列处理器
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
"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,
// QYGL系列处理器
"QYGL8261": qygl.ProcessQYGL8261Request,
@@ -94,6 +128,15 @@ func registerAllProcessors(combService *comb.CombService) {
"QYGL8271": qygl.ProcessQYGL8271Request,
"QYGLB4C0": qygl.ProcessQYGLB4C0Request,
"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, // 企业风险报告
"QYGL5F6A": qygl.ProcessQYGL5F6ARequest, // 企业相关查询
// YYSY系列处理器
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
@@ -103,7 +146,16 @@ func registerAllProcessors(combService *comb.CombService) {
"YYSY6F2E": yysy.ProcessYYSY6F2ERequest,
"YYSYBE08": yysy.ProcessYYSYBE08Request,
"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,
// IVYZ系列处理器
"IVYZ0B03": ivyz.ProcessIVYZ0B03Request,
"IVYZ2125": ivyz.ProcessIVYZ2125Request,
@@ -112,10 +164,39 @@ func registerAllProcessors(combService *comb.CombService) {
"IVYZ9363": ivyz.ProcessIVYZ9363Request,
"IVYZ9A2B": ivyz.ProcessIVYZ9A2BRequest,
"IVYZADEE": ivyz.ProcessIVYZADEERequest,
"IVYZ7F2A": ivyz.ProcessIVYZ7F2ARequest,
"IVYZ4E8B": ivyz.ProcessIVYZ4E8BRequest,
"IVYZ1C9D": ivyz.ProcessIVYZ1C9DRequest,
"IVYZGZ08": ivyz.ProcessIVYZGZ08Request,
"IVYZ2A8B": ivyz.ProcessIVYZ2A8BRequest,
"IVYZ7C9D": ivyz.ProcessIVYZ7C9DRequest,
"IVYZ5E3F": ivyz.ProcessIVYZ5E3FRequest,
"IVYZ7F3A": ivyz.ProcessIVYZ7F3ARequest,
"IVYZ3A7F": ivyz.ProcessIVYZ3A7FRequest,
"IVYZ9D2E": ivyz.ProcessIVYZ9D2ERequest,
"IVYZ81NC": ivyz.ProcessIVYZ81NCRequest,
"IVYZ6G7H": ivyz.ProcessIVYZ6G7HRequest,
"IVYZ8I9J": ivyz.ProcessIVYZ8I9JRequest,
// COMB系列处理器 - 只注册有自定义逻辑的组合包
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑重命名ApiCode
// COMB系列处理器
"COMB298Y": comb.ProcessCOMB298YRequest,
"COMB86PM": comb.ProcessCOMB86PMRequest,
// QCXG系列处理器
"QCXG7A2B": qcxg.ProcessQCXG7A2BRequest,
"QCXG9P1C": qcxg.ProcessQCXG9P1CRequest,
// 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,
}
// 批量注册到组合包服务
@@ -131,11 +212,32 @@ func registerAllProcessors(combService *comb.CombService) {
var RequestProcessors map[string]processors.ProcessorFunc
// PreprocessRequestApi 调用指定的请求处理函数
func (a *ApiRequestService) PreprocessRequestApi(ctx context.Context, apiCode string, params []byte, options *commands.ApiCallOptions) ([]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 {
// 设置Options到依赖容器
deps := a.processorDeps.WithOptions(options)
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)
}
// 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)
}

View File

@@ -2,6 +2,7 @@ package services
import (
"context"
"tyapi-server/internal/config"
"tyapi-server/internal/domains/api/entities"
repo "tyapi-server/internal/domains/api/repositories"
)
@@ -20,14 +21,15 @@ type ApiUserAggregateService interface {
type ApiUserAggregateServiceImpl struct {
repo repo.ApiUserRepository
cfg *config.Config
}
func NewApiUserAggregateService(repo repo.ApiUserRepository) ApiUserAggregateService {
return &ApiUserAggregateServiceImpl{repo: repo}
func NewApiUserAggregateService(repo repo.ApiUserRepository, cfg *config.Config) ApiUserAggregateService {
return &ApiUserAggregateServiceImpl{repo: repo, cfg: cfg}
}
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 {
return err
}

View File

@@ -0,0 +1,453 @@
package services
import (
"reflect"
"strings"
"tyapi-server/internal/domains/api/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"`
}
// FormConfig 表单配置
type FormConfig struct {
ApiCode string `json:"api_code"`
Fields []FormField `json:"fields"`
}
// FormConfigService 表单配置服务接口
type FormConfigService interface {
GetFormConfig(apiCode string) (*FormConfig, error)
}
// FormConfigServiceImpl 表单配置服务实现
type FormConfigServiceImpl struct{}
// NewFormConfigService 创建表单配置服务
func NewFormConfigService() FormConfigService {
return &FormConfigServiceImpl{}
}
// GetFormConfig 获取指定API的表单配置
func (s *FormConfigServiceImpl) GetFormConfig(apiCode string) (*FormConfig, error) {
// 根据API代码获取对应的DTO结构体
dtoStruct := s.getDTOStruct(apiCode)
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(apiCode string) interface{} {
// 建立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{},
"JRZQDBCE": &dto.JRZQDBCEReq{},
"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{},
"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{},
}
// 优先返回已配置的DTO
if dto, exists := dtoMap[apiCode]; exists {
return dto
}
// 检查是否为通用组合包COMB开头且未单独配置
if len(apiCode) >= 4 && apiCode[:4] == "COMB" {
// 对于通用组合包,返回一个通用的空结构体,表示无需特定参数验证
// 因为组合包的参数验证由各个子处理器负责
return &struct{}{}
}
return 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 == "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 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 "date"
} else if strings.Contains(validation, "链接") {
return "url"
} else if strings.Contains(validation, "可选值") {
return "select"
}
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": "银行卡号",
}
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": "2024-01-01",
"time_range": "2024-01-01至2024-12-31",
"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/auth",
}
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": "请选择授权日期",
"time_range": "请输入查询时间范围",
"authorized": "请选择是否授权",
"years": "请输入查询年数0-100",
"bank_card": "请输入银行卡号",
"mobile_type": "请选择手机类型",
"start_date": "请选择开始日期",
"unique_id": "请输入唯一标识",
"return_url": "请输入返回链接",
"authorization_url": "请输入授权链接",
}
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": "请输入授权日期格式YYYY-MM-DD",
"time_range": "请输入查询时间范围",
"authorized": "请输入是否授权0-未授权1-已授权",
"years": "请输入查询年数0-100",
"bank_card": "请输入银行卡号",
"mobile_type": "请选择手机类型",
"start_date": "请选择开始日期",
"unique_id": "请输入唯一标识",
"return_url": "请输入返回链接",
"authorization_url": "请输入授权链接",
}
if desc, exists := descMap[jsonTag]; exists {
return desc
}
return "请输入" + s.generateFieldLabel(jsonTag)
}

View File

@@ -0,0 +1,133 @@
package services
import (
"testing"
)
func TestFormConfigService_GetFormConfig(t *testing.T) {
service := NewFormConfigService()
// 测试获取存在的API配置
config, err := service.GetFormConfig("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("NONEXISTENT")
if err != nil {
t.Fatalf("获取不存在的API配置不应返回错误: %v", err)
}
if config != nil {
t.Fatal("不存在的API配置应返回nil")
}
}
func TestFormConfigService_FieldValidation(t *testing.T) {
service := NewFormConfigService()
config, err := service.GetFormConfig("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 := NewFormConfigService()
config, err := service.GetFormConfig("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
}())))
}

View 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+,强烈推荐使用这个方案!

View 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` 字段控制子产品在响应中的顺序

View File

@@ -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, &paramsDto); 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)
}

View File

@@ -3,7 +3,7 @@ package comb
import (
"context"
"encoding/json"
"fmt"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -13,11 +13,11 @@ import (
func ProcessCOMB86PMRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.COMB86PMReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 调用组合包服务处理请求

View File

@@ -3,9 +3,12 @@ package processors
import (
"context"
"tyapi-server/internal/application/api/commands"
"tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/xingwei"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/shared/interfaces"
)
@@ -14,14 +17,23 @@ type CombServiceInterface interface {
ProcessCombRequest(ctx context.Context, params []byte, deps *ProcessorDependencies, packageCode string) (*CombinedResult, error)
}
// CallContext CallApi调用上下文包含调用相关的数据
type CallContext struct {
ContractCode string // 合同编号
}
// ProcessorDependencies 处理器依赖容器
type ProcessorDependencies struct {
WestDexService *westdex.WestDexService
YushanService *yushan.YushanService
TianYanChaService *tianyancha.TianYanChaService
AlicloudService *alicloud.AlicloudService
ZhichaService *zhicha.ZhichaService
XingweiService *xingwei.XingweiService
Validator interfaces.RequestValidator
CombService CombServiceInterface // Changed to interface to break import cycle
Options *commands.ApiCallOptions // 添加Options支持
CallContext *CallContext // 添加CallApi调用上下文
}
// NewProcessorDependencies 创建处理器依赖容器
@@ -29,6 +41,9 @@ func NewProcessorDependencies(
westDexService *westdex.WestDexService,
yushanService *yushan.YushanService,
tianYanChaService *tianyancha.TianYanChaService,
alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService,
xingweiService *xingwei.XingweiService,
validator interfaces.RequestValidator,
combService CombServiceInterface, // Changed to interface
) *ProcessorDependencies {
@@ -36,9 +51,13 @@ func NewProcessorDependencies(
WestDexService: westDexService,
YushanService: yushanService,
TianYanChaService: tianYanChaService,
AlicloudService: alicloudService,
ZhichaService: zhichaService,
XingweiService: xingweiService,
Validator: validator,
CombService: combService,
Options: nil, // 初始化为nil在调用时设置
CallContext: nil, // 初始化为nil在调用时设置
}
}
@@ -48,6 +67,12 @@ func (deps *ProcessorDependencies) WithOptions(options *commands.ApiCallOptions)
return deps
}
// WithCallContext 设置CallContext的便捷方法
func (deps *ProcessorDependencies) WithCallContext(callContext *CallContext) *ProcessorDependencies {
deps.CallContext = callContext
return deps
}
// ProcessorFunc 处理器函数类型定义
type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error)

View File

@@ -0,0 +1,67 @@
package dwbg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessDWBG6A2CRequest DWBG6A2C API处理方法 - 司南报告
func ProcessDWBG6A2CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.DWBG6A2CReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"accessoryUrl": paramsDto.AuthorizationURL,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI102", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 过滤响应数据,删除指定字段
if respMap, ok := respData.(map[string]interface{}); ok {
delete(respMap, "reportUrl")
delete(respMap, "multCourtInfo")
delete(respMap, "judiciaRiskInfos")
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,51 @@
package dwbg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// ProcessDWBG7F3ARequest DWBG7F3A API处理方法 - 行为数据查询
func ProcessDWBG7F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.DWBG7F3AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据使用xingwei服务的正确字段名
reqData := map[string]interface{}{
"name": paramsDto.Name,
"idCardNum": paramsDto.IDCard,
"phoneNumber": paramsDto.MobileNo,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1101695406546284544"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
// 查空情况,返回特定的查空错误
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
// 数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
// 系统错误
return nil, errors.Join(processors.ErrSystem, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,67 @@
package dwbg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessDWBG8B4DRequest DWBG8B4D API处理方法 - 谛听多维报告
func ProcessDWBG8B4DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.DWBG8B4DReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"accessoryUrl": paramsDto.AuthorizationURL,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI103", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 过滤响应数据,删除指定字段
if respMap, ok := respData.(map[string]interface{}); ok {
delete(respMap, "reportUrl")
delete(respMap, "multCourtInfo")
delete(respMap, "judiciaRiskInfos")
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,91 @@
package processors
import (
"errors"
"testing"
)
func TestErrorsJoin_Is(t *testing.T) {
// 创建一个参数验证错误
originalErr := errors.New("字段验证失败")
joinedErr := errors.Join(ErrInvalidParam, originalErr)
// 测试 errors.Is 是否能正确识别错误类型
if !errors.Is(joinedErr, ErrInvalidParam) {
t.Errorf("errors.Is(joinedErr, ErrInvalidParam) 应该返回 true")
}
if errors.Is(joinedErr, ErrSystem) {
t.Errorf("errors.Is(joinedErr, ErrSystem) 应该返回 false")
}
// 测试错误消息
expectedMsg := "参数校验不正确\n字段验证失败"
if joinedErr.Error() != expectedMsg {
t.Errorf("错误消息不匹配,期望: %s, 实际: %s", expectedMsg, joinedErr.Error())
}
}
func TestErrorsJoin_Unwrap(t *testing.T) {
originalErr := errors.New("原始错误")
joinedErr := errors.Join(ErrSystem, originalErr)
// 测试 Unwrap - errors.Join 的 Unwrap 行为
// errors.Join 的 Unwrap 可能返回 nil 或者第一个错误,这取决于实现
// 我们主要关心 errors.Is 是否能正确工作
if !errors.Is(joinedErr, ErrSystem) {
t.Errorf("errors.Is(joinedErr, ErrSystem) 应该返回 true")
}
}
func TestErrorsJoin_MultipleErrors(t *testing.T) {
err1 := errors.New("错误1")
err2 := errors.New("错误2")
joinedErr := errors.Join(ErrNotFound, err1, err2)
// 测试 errors.Is 识别多个错误类型
if !errors.Is(joinedErr, ErrNotFound) {
t.Errorf("errors.Is(joinedErr, ErrNotFound) 应该返回 true")
}
// 测试错误消息
expectedMsg := "查询为空\n错误1\n错误2"
if joinedErr.Error() != expectedMsg {
t.Errorf("错误消息不匹配,期望: %s, 实际: %s", expectedMsg, joinedErr.Error())
}
}
func TestErrorsJoin_RealWorldScenario(t *testing.T) {
// 模拟真实的处理器错误场景
validationErr := errors.New("手机号格式不正确")
processorErr := errors.Join(ErrInvalidParam, validationErr)
// 在应用服务层,现在应该可以正确识别错误类型
if !errors.Is(processorErr, ErrInvalidParam) {
t.Errorf("应用服务层应该能够识别 ErrInvalidParam")
}
// 错误消息应该包含两种信息
errorMsg := processorErr.Error()
if !contains(errorMsg, "参数校验不正确") {
t.Errorf("错误消息应该包含错误类型: %s", errorMsg)
}
if !contains(errorMsg, "手机号格式不正确") {
t.Errorf("错误消息应该包含原始错误: %s", errorMsg)
}
}
// 辅助函数:检查字符串是否包含子字符串
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 := 1; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}())))
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,11 +14,11 @@ import (
func ProcessFLXG0687Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG0687Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
@@ -28,12 +27,12 @@ func ProcessFLXG0687Request(ctx context.Context, params []byte, deps *processors
"type": 3,
}
respBytes, err := deps.YushanService.CallAPI("RIS031", reqData)
respBytes, err := deps.YushanService.CallAPI(ctx, "RIS031", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,21 +14,21 @@ import (
func ProcessFLXG0V3Bequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG0V3BReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -39,12 +38,12 @@ func ProcessFLXG0V3Bequest(ctx context.Context, params []byte, deps *processors.
},
}
respBytes, err := deps.WestDexService.CallAPI("G34BJ03", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G34BJ03", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -17,62 +18,449 @@ import (
func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG0V4BReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if deps.CallContext.ContractCode == "" {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, errors.New("合同编号不能为空"))
}
encryptedAuthAuthorizeFileCode, err := deps.WestDexService.Encrypt(deps.CallContext.ContractCode)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"data": map[string]interface{}{
"name": encryptedName,
"idcard": encryptedIDCard,
"inquired_auth": paramsDto.AuthDate,
"name": encryptedName,
"idcard": encryptedIDCard,
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
"inquired_auth": fmt.Sprintf("authed:%s", paramsDto.AuthDate),
},
}
respBytes, err := deps.WestDexService.CallAPI("G22SC01", reqData)
log.Println("reqData", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G22SC01", reqData)
if err != nil {
// 数据源错误
if errors.Is(err, westdex.ErrDatasource) {
// 如果有返回内容,优先解析返回内容
if respBytes != nil {
if deps.Options.Json {
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr == nil {
return parsed, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr == nil {
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G22SC0101.G22SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), errors.Join(processors.ErrDatasource, err)
}
// 解析失败,返回原始内容和系统错误
return respBytes, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
return parsed, errors.Join(processors.ErrDatasource, err)
}
return respBytes, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
// 解析失败,返回原始内容和系统错误
return respBytes, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
// 没有返回内容,直接返回数据源错误
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
}
// 其他系统错误
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
// 正常返回
if deps.Options.Json {
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
return parsed, nil
// 正常返回 - 不管有没有deps.Options.Json都进行ParseJsonResponse
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
return respBytes, nil
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G22SC0101.G22SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), nil
} else {
return nil, errors.Join(processors.ErrDatasource, err)
}
}
// Content 数据内容
type FLXG0V4BResponse struct {
Sxbzxr Sxbzxr `json:"sxbzxr"` // 失信被执行人
Entout Entout `json:"entout"` // 涉诉信息
Xgbzxr Xgbzxr `json:"xgbzxr"` // 限高被执行人
}
// Sxbzxr 失信被执行人
type Sxbzxr struct {
Msg string `json:"msg"` // 备注信息
Data SxbzxrData `json:"data"` // 数据结果
}
// SxbzxrData 失信被执行人数据
type SxbzxrData struct {
Sxbzxr []SxbzxrItem `json:"sxbzxr"` // 失信被执行人列表
}
// SxbzxrItem 失信被执行人项
type SxbzxrItem struct {
Yw string `json:"yw"` // 生效法律文书确定的义务
PjjeGj int `json:"pjje_gj"` // 判决金额_估计
Xwqx string `json:"xwqx"` // 失信被执行人行为具体情形
ID string `json:"id"` // 标识
Zxfy string `json:"zxfy"` // 执行法院
Ah string `json:"ah"` // 案号
Zxyjwh string `json:"zxyjwh"` // 执行依据文号
Lxqk string `json:"lxqk"` // 被执行人的履行情况
Zxyjdw string `json:"zxyjdw"` // 出执行依据单位
Fbrq string `json:"fbrq"` // 发布时间(日期)
Xb string `json:"xb"` // 性别
Larq string `json:"larq"` // 立案日期
Sf string `json:"sf"` // 省份
}
// Entout 涉诉信息
type Entout struct {
Msg string `json:"msg"` // 备注信息
Data EntoutData `json:"data"` // 数据结果
}
// EntoutData 涉诉信息数据
type EntoutData struct {
Administrative Administrative `json:"administrative"` // 行政案件
Implement Implement `json:"implement"` // 执行案件
Count Count `json:"count"` // 统计
Preservation Preservation `json:"preservation"` // 案件类型(非诉保全审查)
Crc int `json:"crc"` // 当事人变更码
Civil Civil `json:"civil"` // 民事案件
Criminal Criminal `json:"criminal"` // 刑事案件
CasesTree CasesTree `json:"cases_tree"` // 串联树
Bankrupt Bankrupt `json:"bankrupt"` // 强制清算与破产案件
}
// Administrative 行政案件
type Administrative struct {
Cases []AdministrativeCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// AdministrativeCase 行政案件项
type AdministrativeCase struct {
NjabdjeGjLevel int `json:"n_jabdje_gj_level"` // 结案标的金额估计等级
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
Njafs string `json:"n_jafs"` // 结案方式
Nssdw string `json:"n_ssdw"` // 诉讼地位
Djarq string `json:"d_jarq"` // 结案时间
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Nqsbdje int `json:"n_qsbdje"` // 起诉标的金额
Ncrc int `json:"n_crc"` // 案件变更码
Cssdy string `json:"c_ssdy"` // 所属地域
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
Njaay string `json:"n_jaay"` // 结案案由
Najlx string `json:"n_ajlx"` // 案件类型
CahYs string `json:"c_ah_ys"` // 原审案号
NlaayTree string `json:"n_laay_tree"` // 立案案由详细
NjabdjeLevel int `json:"n_jabdje_level"` // 结案标的金额等级
Nlaay string `json:"n_laay"` // 立案案由
Najbs string `json:"n_ajbs"` // 案件标识
Njbfy string `json:"n_jbfy"` // 经办法院
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
NjabdjeGj int `json:"n_jabdje_gj"` // 结案标的金额估计
NpjVictory string `json:"n_pj_victory"` // 胜诉估计
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
Nslcx string `json:"n_slcx"` // 审理程序
NqsbdjeLevel int `json:"n_qsbdje_level"` // 起诉标的金额等级
CID string `json:"c_id"` // 案件唯一ID
NssdwYs string `json:"n_ssdw_ys"` // 一审诉讼地位
Cslfsxx string `json:"c_slfsxx"` // 审理方式信息
Cah string `json:"c_ah"` // 案号
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
Dlarq string `json:"d_larq"` // 立案时间
NjaayTree string `json:"n_jaay_tree"` // 结案案由详细
CahHx string `json:"c_ah_hx"` // 后续案号
Njabdje int `json:"n_jabdje"` // 结案标的金额
}
// Implement 执行案件
type Implement struct {
Cases []ImplementCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// ImplementCase 执行案件项
type ImplementCase struct {
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
Cssdy string `json:"c_ssdy"` // 所属地域
NjabdjeGj int `json:"n_jabdje_gj"` // 结案标的金额估计
Ncrc int `json:"n_crc"` // 案件变更码
Nlaay string `json:"n_laay"` // 立案案由
Cah string `json:"c_ah"` // 案号
Nsqzxbdje int `json:"n_sqzxbdje"` // 申请执行标的金额
CahYs string `json:"c_ah_ys"` // 原审案号
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
Najbs string `json:"n_ajbs"` // 案件标识
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Njafs string `json:"n_jafs"` // 结案方式
Njaay string `json:"n_jaay"` // 结案案由
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
CID string `json:"c_id"` // 案件唯一ID
Njabdje int `json:"n_jabdje"` // 结案标的金额
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
Dlarq string `json:"d_larq"` // 立案时间
Najlx string `json:"n_ajlx"` // 案件类型
Nsjdwje int `json:"n_sjdwje"` // 实际到位金额
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
CahHx string `json:"c_ah_hx"` // 后续案号
Nwzxje int `json:"n_wzxje"` // 未执行金额
Djarq string `json:"d_jarq"` // 结案时间
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
Njbfy string `json:"n_jbfy"` // 经办法院
Nssdw string `json:"n_ssdw"` // 诉讼地位
}
// Preservation 案件类型(非诉保全审查)
type Preservation struct {
Cases []PreservationCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// PreservationCase 非诉保全审查案件项
type PreservationCase struct {
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
Nssdw string `json:"n_ssdw"` // 诉讼地位
Ncrc int `json:"n_crc"` // 案件变更码
Cssdy string `json:"c_ssdy"` // 所属地域
Dlarq string `json:"d_larq"` // 立案时间
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
CahYs string `json:"c_ah_ys"` // 原审案号
Nsqbqse int `json:"n_sqbqse"` // 申请保全数额
Djarq string `json:"d_jarq"` // 结案时间
Najbs string `json:"n_ajbs"` // 案件标识
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Njbfy string `json:"n_jbfy"` // 经办法院
Njafs string `json:"n_jafs"` // 结案方式
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
Najlx string `json:"n_ajlx"` // 案件类型
CID string `json:"c_id"` // 案件唯一ID
Cah string `json:"c_ah"` // 案号
NsqbqseLevel int `json:"n_sqbqse_level"` // 申请保全数额等级
CahHx string `json:"c_ah_hx"` // 后续案号
Csqbqbdw string `json:"c_sqbqbdw"` // 申请保全标的物
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
}
// Civil 民事案件
type Civil struct {
Cases []CivilCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// CivilCase 民事案件项
type CivilCase struct {
NjabdjeLevel int `json:"n_jabdje_level"` // 结案标的金额等级
Nslcx string `json:"n_slcx"` // 审理程序
NjabdjeGjLevel int `json:"n_jabdje_gj_level"` // 结案标的金额估计等级
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
Njafs string `json:"n_jafs"` // 结案方式
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Cslfsxx string `json:"c_slfsxx"` // 审理方式信息
Nlaay string `json:"n_laay"` // 立案案由
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
Nssdw string `json:"n_ssdw"` // 诉讼地位
NssdwYs string `json:"n_ssdw_ys"` // 一审诉讼地位
NlaayTag string `json:"n_laay_tag"` // 立案案由标签
NqsbdjeLevel int `json:"n_qsbdje_level"` // 起诉标的金额等级
Ncrc int `json:"n_crc"` // 案件变更码
CahHx string `json:"c_ah_hx"` // 后续案号
NqsbdjeGjLevel int `json:"n_qsbdje_gj_level"` // 起诉标的金额估计等级
Njbfy string `json:"n_jbfy"` // 经办法院
Cah string `json:"c_ah"` // 案号
Njabdje int `json:"n_jabdje"` // 结案标的金额
NjabdjeGj int `json:"n_jabdje_gj"` // 结案标的金额估计
NqsbdjeGj int `json:"n_qsbdje_gj"` // 起诉标的金额估计
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
Cssdy string `json:"c_ssdy"` // 所属地域
Dlarq string `json:"d_larq"` // 立案时间
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
NpjVictory string `json:"n_pj_victory"` // 胜诉估计
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
Djarq string `json:"d_jarq"` // 结案时间
Njaay string `json:"n_jaay"` // 结案案由
NlaayTree string `json:"n_laay_tree"` // 立案案由详细
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
CahYs string `json:"c_ah_ys"` // 原审案号
Nqsbdje int `json:"n_qsbdje"` // 起诉标的金额
NjaayTree string `json:"n_jaay_tree"` // 结案案由详细
Najlx string `json:"n_ajlx"` // 案件类型
CID string `json:"c_id"` // 案件唯一ID
Najbs string `json:"n_ajbs"` // 案件标识
}
// Criminal 刑事案件
type Criminal struct {
Cases []CriminalCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// CriminalCase 刑事案件项
type CriminalCase struct {
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
NpcpcjeLevel int `json:"n_pcpcje_level"` // 判处赔偿金额等级
Nbqqpcje int `json:"n_bqqpcje"` // 被请求赔偿金额
NpcpcjeGjLevel int `json:"n_pcpcje_gj_level"` // 判处赔偿金额估计等级
Dlarq string `json:"d_larq"` // 立案时间
Djarq string `json:"d_jarq"` // 结案时间
CahHx string `json:"c_ah_hx"` // 后续案号
Njafs string `json:"n_jafs"` // 结案方式
NjaayTag string `json:"n_jaay_tag"` // 结案案由标签
Njbfy string `json:"n_jbfy"` // 经办法院
NlaayTag string `json:"n_laay_tag"` // 立案案由标签
Ndzzm string `json:"n_dzzm"` // 定罪罪名
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
NlaayTree string `json:"n_laay_tree"` // 立案案由详细
NccxzxjeLevel int `json:"n_ccxzxje_level"` // 财产刑执行金额等级
Ncrc int `json:"n_crc"` // 案件变更码
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
NccxzxjeGjLevel int `json:"n_ccxzxje_gj_level"` // 财产刑执行金额估计等级
Nfzje int `json:"n_fzje"` // 犯罪金额
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
Cah string `json:"c_ah"` // 案号
Cssdy string `json:"c_ssdy"` // 所属地域
Npcpcje int `json:"n_pcpcje"` // 判处赔偿金额
CahYs string `json:"c_ah_ys"` // 原审案号
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Cslfsxx string `json:"c_slfsxx"` // 审理方式信息
NpcpcjeGj int `json:"n_pcpcje_gj"` // 判处赔偿金额估计
Najbs string `json:"n_ajbs"` // 案件标识
Nlaay string `json:"n_laay"` // 立案案由
Njaay string `json:"n_jaay"` // 结案案由
Nssdw string `json:"n_ssdw"` // 诉讼地位
NdzzmTree string `json:"n_dzzm_tree"` // 定罪罪名树
NjaayTree string `json:"n_jaay_tree"` // 结案案由详细
Npcjg string `json:"n_pcjg"` // 判处结果
CID string `json:"c_id"` // 案件唯一ID
NssdwYs string `json:"n_ssdw_ys"` // 一审诉讼地位
Nccxzxje int `json:"n_ccxzxje"` // 财产刑执行金额
NfzjeLevel int `json:"n_fzje_level"` // 犯罪金额等级
Nslcx string `json:"n_slcx"` // 审理程序
Najlx string `json:"n_ajlx"` // 案件类型
NbqqpcjeLevel int `json:"n_bqqpcje_level"` // 被请求赔偿金额等级
NccxzxjeGj int `json:"n_ccxzxje_gj"` // 财产刑执行金额估计
}
// CasesTree 串联树
type CasesTree struct {
Administrative []CasesTreeItem `json:"administrative"` // 行政案件
Criminal []CasesTreeItem `json:"criminal"` // 刑事案件
Civil []CasesTreeItem `json:"civil"` // 民事案件
}
// CasesTreeItem 串联树项
type CasesTreeItem struct {
Cah string `json:"c_ah"` // 案号
CaseType int `json:"case_type"` // 案件类型
Najbs string `json:"n_ajbs"` // 案件标识
StageType int `json:"stage_type"` // 审理阶段类型
Next *CasesTreeItem `json:"next"` // 下一个案件
}
// Bankrupt 强制清算与破产案件
type Bankrupt struct {
Cases []BankruptCase `json:"cases"` // 案件
Count Count `json:"count"` // 统计
}
// BankruptCase 强制清算与破产案件项
type BankruptCase struct {
Cdsrxx []Dsrxx `json:"c_dsrxx"` // 当事人
CgkwsID string `json:"c_gkws_id"` // 公开文书ID
Najbs string `json:"n_ajbs"` // 案件标识
NjbfyCj string `json:"n_jbfy_cj"` // 法院所属层级
CgkwsDsr string `json:"c_gkws_dsr"` // 当事人
CID string `json:"c_id"` // 案件唯一ID
Dlarq string `json:"d_larq"` // 立案时间
Djarq string `json:"d_jarq"` // 结案时间
Najlx string `json:"n_ajlx"` // 案件类型
CgkwsGlah string `json:"c_gkws_glah"` // 相关案件号
Njbfy string `json:"n_jbfy"` // 经办法院
Najjzjd string `json:"n_ajjzjd"` // 案件进展阶段
CgkwsPjjg string `json:"c_gkws_pjjg"` // 判决结果
Cssdy string `json:"c_ssdy"` // 所属地域
Ncrc int `json:"n_crc"` // 案件变更码
Nssdw string `json:"n_ssdw"` // 诉讼地位
Njafs string `json:"n_jafs"` // 结案方式
Cah string `json:"c_ah"` // 案号
}
// Dsrxx 当事人
type Dsrxx struct {
Nssdw string `json:"n_ssdw"` // 诉讼地位
CMc string `json:"c_mc"` // 名称
Ndsrlx string `json:"n_dsrlx"` // 当事人类型
}
// Count 统计
type Count struct {
MoneyYuangao int `json:"money_yuangao"` // 原告金额
AreaStat string `json:"area_stat"` // 涉案地点分布
CountJieBeigao int `json:"count_jie_beigao"` // 被告已结案总数
CountTotal int `json:"count_total"` // 案件总数
MoneyWeiYuangao int `json:"money_wei_yuangao"` // 原告未结案金额
CountWeiTotal int `json:"count_wei_total"` // 未结案总数
MoneyWeiBeigao int `json:"money_wei_beigao"` // 被告未结案金额
CountOther int `json:"count_other"` // 第三人总数
MoneyBeigao int `json:"money_beigao"` // 被告金额
CountYuangao int `json:"count_yuangao"` // 原告总数
MoneyJieOther int `json:"money_jie_other"` // 第三人已结案金额
MoneyTotal int `json:"money_total"` // 涉案总金额
MoneyWeiTotal int `json:"money_wei_total"` // 未结案金额
CountWeiYuangao int `json:"count_wei_yuangao"` // 原告未结案总数
AyStat string `json:"ay_stat"` // 涉案案由分布
CountBeigao int `json:"count_beigao"` // 被告总数
MoneyJieYuangao int `json:"money_jie_yuangao"` // 原告已结金额
JafsStat string `json:"jafs_stat"` // 结案方式分布
MoneyJieBeigao int `json:"money_jie_beigao"` // 被告已结案金额
CountWeiBeigao int `json:"count_wei_beigao"` // 被告未结案总数
CountJieOther int `json:"count_jie_other"` // 第三人已结案总数
CountJieTotal int `json:"count_jie_total"` // 已结案总数
CountWeiOther int `json:"count_wei_other"` // 第三人未结案总数
MoneyOther int `json:"money_other"` // 第三人金额
CountJieYuangao int `json:"count_jie_yuangao"` // 原告已结案总数
MoneyJieTotal int `json:"money_jie_total"` // 已结案金额
MoneyWeiOther int `json:"money_wei_other"` // 第三人未结案金额
MoneyWeiPercent float64 `json:"money_wei_percent"` // 未结案金额百分比
LarqStat string `json:"larq_stat"` // 涉案时间分布
}
// Xgbzxr 限高被执行人
type Xgbzxr struct {
Msg string `json:"msg"` // 备注信息
Data XgbzxrData `json:"data"` // 数据结果
}
// XgbzxrData 限高被执行人数据
type XgbzxrData struct {
Xgbzxr []XgbzxrItem `json:"xgbzxr"` // 限高被执行人列表
}
// XgbzxrItem 限高被执行人项
type XgbzxrItem struct {
Ah string `json:"ah"` // 案号
ID string `json:"id"` // 标识
Zxfy string `json:"zxfy"` // 执行法院
Fbrq string `json:"fbrq"` // 发布时间
}
// ParseWestResponse 解析西部返回的响应数据获取data字段后解析

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,26 +14,26 @@ import (
func ProcessFLXG162ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG162AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -45,12 +44,12 @@ func ProcessFLXG162ARequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G32BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G32BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -0,0 +1,60 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXG2E8FRequest FLXG2E8F API处理方法 - 司法核验报告
func ProcessFLXG2E8FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG2E8FReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"accessoryUrl": paramsDto.AuthorizationURL,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI101", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,48 +14,53 @@ import (
func ProcessFLXG3D56Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG3D56Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedTimeRange, err := deps.WestDexService.Encrypt(paramsDto.TimeRange)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"data": map[string]interface{}{
"name": encryptedName,
"id": encryptedIDCard,
"cell": encryptedMobileNo,
"time_range": encryptedTimeRange,
"name": encryptedName,
"id": encryptedIDCard,
"cell": encryptedMobileNo,
},
}
respBytes, err := deps.WestDexService.CallAPI("G26BJ05", reqData)
// 只有当 TimeRange 不为空时才加密和传参
if paramsDto.TimeRange != "" {
encryptedTimeRange, err := deps.WestDexService.Encrypt(paramsDto.TimeRange)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData["data"].(map[string]interface{})["time_range"] = encryptedTimeRange
}
respBytes, err := deps.WestDexService.CallAPI(ctx, "G26BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,16 +14,16 @@ import (
func ProcessFLXG54F5Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG54F5Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -33,12 +32,12 @@ func ProcessFLXG54F5Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G03HZ01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx,"G03HZ01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,16 +14,16 @@ import (
func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG5876Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -33,12 +32,12 @@ func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G03XM02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G03XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -0,0 +1,56 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXG5A3BRequest FLXG5A3B API处理方法 - 个人司法涉诉
func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG5A3BReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI006", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,88 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex"
"github.com/tidwall/gjson"
)
// ProcessFLXG5B2ERequest FLXG5B2E API处理方法
func ProcessFLXG5B2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG5B2EReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if deps.CallContext.ContractCode == "" {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, errors.New("合同编号不能为空"))
}
encryptedAuthAuthorizeFileCode, err := deps.WestDexService.Encrypt(deps.CallContext.ContractCode)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"data": map[string]interface{}{
"name": encryptedName,
"idcard": encryptedIDCard,
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
},
}
respBytes, err := deps.WestDexService.CallAPI(ctx, "G36SC01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
// 如果有返回内容,优先解析返回内容
if respBytes != nil {
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr == nil {
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G36SC0101.G36SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), errors.Join(processors.ErrDatasource, err)
}
return parsed, errors.Join(processors.ErrDatasource, err)
}
// 解析失败,返回原始内容和系统错误
return respBytes, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
// 没有返回内容,直接返回数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 正常返回 - 不管有没有deps.Options.Json都进行ParseJsonResponse
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G36SC0101.G36SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), nil
} else {
return nil, errors.Join(processors.ErrDatasource, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,11 +14,11 @@ import (
func ProcessFLXG75FERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG75FEReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
reqData := map[string]interface{}{
@@ -28,12 +27,12 @@ func ProcessFLXG75FERequest(ctx context.Context, params []byte, deps *processors
"mobile": paramsDto.MobileNo,
}
respBytes, err := deps.WestDexService.CallAPI("FLXG75FE", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx,"FLXG75FE", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -0,0 +1,47 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// ProcessFLXG7E8FRequest FLXG7E8F API处理方法 - 个人司法数据查询
func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG7E8FReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
reqData := map[string]interface{}{
"name": paramsDto.Name,
"idCardNum": paramsDto.IDCard,
"phoneNumber": paramsDto.MobileNo,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1101695378264092672"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
return nil, errors.Join(processors.ErrSystem, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,87 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex"
"github.com/tidwall/gjson"
)
// ProcessFLXG8A3FRequest FLXG8A3F API处理方法
func ProcessFLXG8A3FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG8A3FReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if deps.CallContext.ContractCode == "" {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, errors.New("合同编号不能为空"))
}
encryptedAuthAuthorizeFileCode, err := deps.WestDexService.Encrypt(deps.CallContext.ContractCode)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"data": map[string]interface{}{
"name": encryptedName,
"idcard": encryptedIDCard,
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
},
}
respBytes, err := deps.WestDexService.CallAPI(ctx, "G37SC01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
// 如果有返回内容,优先解析返回内容
if respBytes != nil {
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr == nil {
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G37SC0101.G37SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), errors.Join(processors.ErrDatasource, err)
}
return parsed, errors.Join(processors.ErrDatasource, err)
}
// 解析失败,返回原始内容和系统错误
return respBytes, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
// 没有返回内容,直接返回数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 正常返回 - 不管有没有deps.Options.Json都进行ParseJsonResponse
parsed, parseErr := ParseJsonResponse(respBytes)
if parseErr != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, parseErr)
}
// 通过gjson获取指定路径的数据
contentResult := gjson.GetBytes(parsed, "G37SC0101.G37SC0102.content")
if contentResult.Exists() {
return []byte(contentResult.Raw), nil
} else {
return nil, errors.Join(processors.ErrDatasource, err)
}
}

View File

@@ -0,0 +1,104 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXG8B4DRequest FLXG8B4D API处理方法 - 涉赌涉诈风险评估
func ProcessFLXG8B4DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG8B4DReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 三选一校验MobileNo、IDCard、BankCard 必须且只能有一个
var fieldCount int
var selectedField string
var selectedValue string
if paramsDto.MobileNo != "" {
fieldCount++
selectedField = "mobile_no"
selectedValue = paramsDto.MobileNo
}
if paramsDto.IDCard != "" {
fieldCount++
selectedField = "id_card"
selectedValue = paramsDto.IDCard
}
if paramsDto.BankCard != "" {
fieldCount++
selectedField = "bank_card"
selectedValue = paramsDto.BankCard
}
if fieldCount == 0 {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("必须提供手机号、身份证号或银行卡号中的其中一个"))
}
if fieldCount > 1 {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("只能提供手机号、身份证号或银行卡号中的一个,不能同时提供多个"))
}
// 只对选中的字段进行加密
var encryptedValue string
var err error
switch selectedField {
case "mobile_no":
encryptedValue, err = deps.ZhichaService.Encrypt(selectedValue)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
case "id_card":
encryptedValue, err = deps.ZhichaService.Encrypt(selectedValue)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
case "bank_card":
encryptedValue, err = deps.ZhichaService.Encrypt(selectedValue)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 构建请求数据,根据选中的字段类型设置对应的参数
reqData := map[string]interface{}{
"authorized": paramsDto.Authorized,
}
switch selectedField {
case "mobile_no":
reqData["phone"] = encryptedValue
case "id_card":
reqData["idCard"] = encryptedValue
case "bank_card":
reqData["name"] = encryptedValue
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI027", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,26 +14,26 @@ import (
func ProcessFLXG9687Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG9687Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -45,12 +44,12 @@ func ProcessFLXG9687Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G31BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G31BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,21 +14,21 @@ import (
func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG970FReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -39,12 +38,12 @@ func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("WEST00028", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "WEST00028", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -0,0 +1,56 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXG9C1DRequest FLXG9C1D API处理方法 - 法院信息详情高级版
func ProcessFLXG9C1DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG9C1DReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI007", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -4,22 +4,21 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex"
)
// ProcessFLXGbc21Request FLXGbc21 API处理方法
func ProcessFLXGbc21Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG21Req
// ProcessFLXGBC21Request FLXGbc21 API处理方法
func ProcessFLXGBC21Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXGBC21Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
@@ -27,12 +26,12 @@ func ProcessFLXGbc21Request(ctx context.Context, params []byte, deps *processors
"mobile": paramsDto.MobileNo,
}
respBytes, err := deps.YushanService.CallAPI("MOB032", reqData)
respBytes, err := deps.YushanService.CallAPI(ctx, "MOB032", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
@@ -15,26 +14,26 @@ import (
func ProcessFLXGC9D1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXGC9D1Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
return nil, errors.Join(processors.ErrInvalidParam, err)
}
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{
@@ -45,12 +44,12 @@ func ProcessFLXGC9D1Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G30BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G30BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
return nil, errors.Join(processors.ErrSystem, err)
}
}

Some files were not shown because too many files have changed in this diff Show More