Compare commits
56 Commits
11fe48809e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6147878dfe | |||
| be47a0f045 | |||
| 810696e0f0 | |||
| 17dbaf1ccb | |||
| 18f3d10518 | |||
| 0d4953c6d3 | |||
| 3f64600f02 | |||
| 2c89b8cb26 | |||
| 09d7a4f076 | |||
| 83d0fd6587 | |||
| 0fd28054f1 | |||
| ce858983ee | |||
| 9b2bffae15 | |||
| c68ece5bea | |||
| 398d2cee74 | |||
| b6c8d93af5 | |||
| b423aa6be8 | |||
| a47c306c87 | |||
| af88bcc8eb | |||
| 89367fb2ee | |||
| 05b6623e75 | |||
| bfedec249f | |||
| 9f669a9c94 | |||
| 0f5c4f4303 | |||
| d9c2d9f103 | |||
| 7e0d58b295 | |||
| a17ff2140e | |||
| 6a2241bc66 | |||
| e57bef6609 | |||
| 81639a81e6 | |||
| aaf17321ff | |||
| a8a4ff2d37 | |||
| 619deeb456 | |||
| f12c3fb8ad | |||
| 4ce8fe4023 | |||
| 7b45b43a0e | |||
| 752b90b048 | |||
| 68def7e08b | |||
| b0e8974d6c | |||
| b41d41ddf3 | |||
| b08a63fc99 | |||
| 1f06f21faf | |||
| 3f5a126bfa | |||
| 17ff48a642 | |||
| af629e96c2 | |||
| 63252fa30f | |||
| 1cf64e831c | |||
| 577c2bc581 | |||
| 6d73dad88e | |||
| 937c812ea5 | |||
| 63e2fba464 | |||
| 9c776b8bf3 | |||
| 500264e9e5 | |||
| b90935a7c3 | |||
| c404e797f3 | |||
| ce9052f85b |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -26,6 +26,7 @@ Thumbs.db
|
||||
tmp/
|
||||
temp/
|
||||
console
|
||||
worker
|
||||
|
||||
# 依赖目录
|
||||
vendor/
|
||||
@@ -34,6 +35,11 @@ vendor/
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# 字体文件(大文件,不进行版本控制)
|
||||
internal/shared/pdf/fonts/*.ttf
|
||||
internal/shared/pdf/fonts/*.ttc
|
||||
internal/shared/pdf/fonts/*.otf
|
||||
|
||||
# 其他
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
@@ -50,9 +50,11 @@ WORKDIR /app
|
||||
COPY --from=builder /app/tyapi-server .
|
||||
|
||||
# 复制配置文件
|
||||
COPY --chown=tyapi:tyapi config.yaml .
|
||||
COPY --chown=tyapi:tyapi configs/ ./configs/
|
||||
COPY config.yaml .
|
||||
COPY configs/ ./configs/
|
||||
|
||||
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||
COPY resources ./resources
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
Binary file not shown.
@@ -19,7 +19,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TaskTypeArticlePublish = "article:publish"
|
||||
TaskTypeArticlePublish = "article:publish"
|
||||
TaskTypeAnnouncementPublish = "announcement_publish"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -78,6 +79,9 @@ func main() {
|
||||
mux.HandleFunc(TaskTypeArticlePublish, func(ctx context.Context, t *asynq.Task) error {
|
||||
return handleArticlePublish(ctx, t, db, logger)
|
||||
})
|
||||
mux.HandleFunc(TaskTypeAnnouncementPublish, func(ctx context.Context, t *asynq.Task) error {
|
||||
return handleAnnouncementPublish(ctx, t, db, logger)
|
||||
})
|
||||
|
||||
// 启动 Worker
|
||||
go func() {
|
||||
@@ -135,3 +139,55 @@ func handleArticlePublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logge
|
||||
logger.Info("定时发布文章成功", zap.String("article_id", articleID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAnnouncementPublish 处理公告定时发布任务
|
||||
func handleAnnouncementPublish(ctx context.Context, t *asynq.Task, db *gorm.DB, logger *zap.Logger) error {
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||
logger.Error("解析任务载荷失败", zap.Error(err))
|
||||
return fmt.Errorf("解析任务载荷失败: %w", err)
|
||||
}
|
||||
|
||||
announcementID, ok := payload["announcement_id"].(string)
|
||||
if !ok {
|
||||
logger.Error("任务载荷中缺少公告ID")
|
||||
return fmt.Errorf("任务载荷中缺少公告ID")
|
||||
}
|
||||
|
||||
// 获取公告
|
||||
var announcement entities.Announcement
|
||||
if err := db.WithContext(ctx).First(&announcement, "id = ?", announcementID).Error; err != nil {
|
||||
logger.Error("获取公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||
return fmt.Errorf("获取公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否已取消定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
logger.Info("公告定时发布已取消,跳过执行",
|
||||
zap.String("announcement_id", announcementID),
|
||||
zap.String("status", string(announcement.Status)))
|
||||
return nil // 静默返回,不报错
|
||||
}
|
||||
|
||||
// 检查定时发布时间是否匹配
|
||||
if announcement.ScheduledAt == nil {
|
||||
logger.Info("公告没有定时发布时间,跳过执行",
|
||||
zap.String("announcement_id", announcementID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发布公告
|
||||
if err := announcement.Publish(); err != nil {
|
||||
logger.Error("发布公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := db.WithContext(ctx).Save(&announcement).Error; err != nil {
|
||||
logger.Error("保存公告失败", zap.String("announcement_id", announcementID), zap.Error(err))
|
||||
return fmt.Errorf("保存公告失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("定时发布公告成功", zap.String("announcement_id", announcementID))
|
||||
return nil
|
||||
}
|
||||
|
||||
24
config.yaml
24
config.yaml
@@ -243,7 +243,7 @@ esign:
|
||||
app_id: "7439073138"
|
||||
app_secret: "d76e27fdd169b391e09262a0959dac5c"
|
||||
server_url: "https://smlopenapi.esign.cn"
|
||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
||||
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||
contract:
|
||||
name: "天远数据API合作协议"
|
||||
expire_days: 7
|
||||
@@ -362,6 +362,28 @@ alipay:
|
||||
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
||||
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 微信支付配置
|
||||
# ===========================================
|
||||
Wxpay:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
mch_id: "1683589176"
|
||||
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||
notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback"
|
||||
refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback"
|
||||
|
||||
# 微信小程序配置
|
||||
WechatMini:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# 微信H5配置
|
||||
WechatH5:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
# ===========================================
|
||||
|
||||
@@ -43,7 +43,7 @@ esign:
|
||||
app_id: "7439073713"
|
||||
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
|
||||
server_url: "https://smlopenapi.esign.cn"
|
||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
||||
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||
contract:
|
||||
name: "天远数据API合作协议"
|
||||
expire_days: 7
|
||||
@@ -67,6 +67,8 @@ westdex:
|
||||
key: "121a1e41fc1690dd6b90afbcacd80cf4"
|
||||
secret_id: "449159"
|
||||
secret_second_id: "296804"
|
||||
yushan:
|
||||
url: https://api2.yushanshuju.com/credit-gw/service
|
||||
# ===========================================
|
||||
# 💰 支付宝支付配置
|
||||
# ===========================================
|
||||
@@ -79,6 +81,27 @@ alipay:
|
||||
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 微信支付配置
|
||||
# ===========================================
|
||||
Wxpay:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
mch_id: "1683589176"
|
||||
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||
notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback"
|
||||
refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback"
|
||||
|
||||
# 微信小程序配置
|
||||
WechatMini:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
|
||||
# 微信H5配置
|
||||
WechatH5:
|
||||
app_id: "wxa581992dc74d860e"
|
||||
# ===========================================
|
||||
# 💰 钱包配置
|
||||
# ===========================================
|
||||
wallet:
|
||||
@@ -112,34 +135,42 @@ development:
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||
|
||||
# ===========================================
|
||||
# 🚦 开发环境全局限流(放宽或近似关闭)
|
||||
# ===========================================
|
||||
ratelimit:
|
||||
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
|
||||
window: 1s # 时间窗口
|
||||
burst: 1000000 # 令牌桶突发容量
|
||||
|
||||
# ===========================================
|
||||
# 🚀 开发环境频率限制配置(放宽限制)
|
||||
# ===========================================
|
||||
daily_ratelimit:
|
||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||
max_concurrent: 50 # 开发环境最大并发请求数
|
||||
|
||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||
max_concurrent: 50 # 开发环境最大并发请求数
|
||||
|
||||
# 排除频率限制的路径
|
||||
exclude_paths:
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
# 排除频率限制的域名
|
||||
exclude_domains:
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
- "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 # 开发环境禁用代理检查
|
||||
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 # 开发环境禁用代理检查
|
||||
|
||||
@@ -75,7 +75,7 @@ esign:
|
||||
app_id: "5112008003"
|
||||
app_secret: "d487672273e7aa70c800804a1d9499b9"
|
||||
server_url: "https://openapi.esign.cn"
|
||||
template_id: "c82af4df2790430299c81321f309eef3"
|
||||
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||
contract:
|
||||
name: "天远数据API合作协议"
|
||||
expire_days: 7
|
||||
|
||||
210
docs/PDF缓存优化说明.md
Normal file
210
docs/PDF缓存优化说明.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# PDF接口文档下载缓存优化说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本次优化为PDF接口文档下载功能添加了本地文件缓存机制,显著提升了下载性能,减少了重复生成PDF的开销。
|
||||
|
||||
## 🔍 问题分析
|
||||
|
||||
### 原有问题
|
||||
|
||||
1. **性能问题**:
|
||||
- 每次请求都重新生成PDF,没有缓存机制
|
||||
- PDF生成涉及复杂的字体加载、页面构建、表格渲染等操作,耗时较长
|
||||
- 同一产品的PDF被多次下载时,会重复执行相同的生成过程
|
||||
|
||||
2. **资源浪费**:
|
||||
- CPU资源浪费在重复的PDF生成上
|
||||
- 数据库查询重复执行
|
||||
- 没有版本控制,即使产品文档没有变化,也会重新生成
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 1. PDF缓存管理器 (`PDFCacheManager`)
|
||||
|
||||
创建了专门的PDF缓存管理器,提供以下功能:
|
||||
|
||||
- **本地文件缓存**:将生成的PDF文件保存到本地文件系统
|
||||
- **版本控制**:基于产品ID和文档版本号生成缓存键,确保版本更新时自动失效
|
||||
- **自动过期**:支持TTL(Time To Live)机制,自动清理过期缓存
|
||||
- **大小限制**:支持最大缓存大小限制,防止磁盘空间耗尽
|
||||
- **定期清理**:后台任务每小时自动清理过期文件
|
||||
|
||||
### 2. 缓存键生成策略
|
||||
|
||||
```go
|
||||
// 基于产品ID和文档版本号生成唯一的缓存键
|
||||
cacheKey = MD5(productID + ":" + version)
|
||||
```
|
||||
|
||||
- 当产品文档版本更新时,自动生成新的缓存
|
||||
- 旧版本的缓存会在过期后自动清理
|
||||
|
||||
### 3. 缓存流程
|
||||
|
||||
```
|
||||
请求下载PDF
|
||||
↓
|
||||
检查缓存是否存在且有效
|
||||
↓
|
||||
├─ 缓存命中 → 直接返回缓存的PDF文件
|
||||
└─ 缓存未命中 → 生成PDF → 保存到缓存 → 返回PDF
|
||||
```
|
||||
|
||||
### 4. 集成到下载接口
|
||||
|
||||
修改了 `DownloadProductDocumentation` 方法:
|
||||
|
||||
- **缓存优先**:首先尝试从缓存获取PDF
|
||||
- **异步保存**:生成新PDF后异步保存到缓存,不阻塞响应
|
||||
- **缓存标识**:响应头中添加 `X-Cache: HIT/MISS` 标识,便于监控
|
||||
|
||||
## 🚀 性能提升
|
||||
|
||||
### 预期效果
|
||||
|
||||
1. **首次下载**:与之前相同,需要生成PDF(约1-3秒)
|
||||
2. **后续下载**:直接从缓存读取(< 100ms),性能提升 **10-30倍**
|
||||
3. **缓存命中率**:对于热门产品,缓存命中率可达 **80-90%**
|
||||
|
||||
### 响应时间对比
|
||||
|
||||
| 场景 | 优化前 | 优化后 | 提升 |
|
||||
|------|--------|--------|------|
|
||||
| 首次下载 | 1-3秒 | 1-3秒 | - |
|
||||
| 缓存命中 | 1-3秒 | < 100ms | **10-30倍** |
|
||||
| 版本更新后首次 | 1-3秒 | 1-3秒 | - |
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
可以通过环境变量自定义缓存配置:
|
||||
|
||||
```bash
|
||||
# 缓存目录(默认:系统临时目录下的tyapi_pdf_cache)
|
||||
export PDF_CACHE_DIR="/path/to/cache"
|
||||
|
||||
# 缓存过期时间(默认:24小时)
|
||||
export PDF_CACHE_TTL="24h"
|
||||
|
||||
# 最大缓存大小(默认:500MB)
|
||||
export PDF_CACHE_MAX_SIZE="524288000" # 字节
|
||||
```
|
||||
|
||||
### 默认配置
|
||||
|
||||
- **缓存目录**:系统临时目录下的 `tyapi_pdf_cache`
|
||||
- **TTL**:24小时
|
||||
- **最大缓存大小**:500MB
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
tyapi-server/
|
||||
├── internal/
|
||||
│ └── shared/
|
||||
│ └── pdf/
|
||||
│ ├── pdf_cache_manager.go # 新增:PDF缓存管理器
|
||||
│ ├── pdf_generator.go # 原有:PDF生成器
|
||||
│ └── ...
|
||||
├── internal/
|
||||
│ └── infrastructure/
|
||||
│ └── http/
|
||||
│ └── handlers/
|
||||
│ └── product_handler.go # 修改:集成缓存机制
|
||||
└── internal/
|
||||
└── container/
|
||||
└── container.go # 修改:初始化缓存管理器
|
||||
```
|
||||
|
||||
## 🔧 使用示例
|
||||
|
||||
### 基本使用
|
||||
|
||||
缓存机制已自动集成,无需额外代码:
|
||||
|
||||
```go
|
||||
// 用户请求下载PDF
|
||||
GET /api/v1/products/{id}/documentation/download
|
||||
|
||||
// 系统自动:
|
||||
// 1. 检查缓存
|
||||
// 2. 缓存命中 → 直接返回
|
||||
// 3. 缓存未命中 → 生成PDF → 保存缓存 → 返回
|
||||
```
|
||||
|
||||
### 手动管理缓存
|
||||
|
||||
如果需要手动管理缓存(如产品更新后清除缓存):
|
||||
|
||||
```go
|
||||
// 使特定产品的缓存失效
|
||||
cacheManager.InvalidateByProductID(productID)
|
||||
|
||||
// 使特定版本的缓存失效
|
||||
cacheManager.Invalidate(productID, version)
|
||||
|
||||
// 清空所有缓存
|
||||
cacheManager.Clear()
|
||||
|
||||
// 获取缓存统计信息
|
||||
stats, _ := cacheManager.GetCacheStats()
|
||||
```
|
||||
|
||||
## 📊 监控和日志
|
||||
|
||||
### 日志输出
|
||||
|
||||
系统会记录以下日志:
|
||||
|
||||
- **缓存命中**:`PDF缓存命中` - 包含产品ID、版本、文件大小
|
||||
- **缓存未命中**:`PDF缓存未命中,开始生成PDF`
|
||||
- **缓存保存**:`PDF已缓存` - 包含产品ID、缓存键、文件大小
|
||||
- **缓存清理**:`已清理过期缓存文件` - 包含清理数量和释放空间
|
||||
|
||||
### 响应头标识
|
||||
|
||||
响应头中添加了缓存标识:
|
||||
|
||||
- `X-Cache: HIT` - 缓存命中
|
||||
- `X-Cache: MISS` - 缓存未命中
|
||||
|
||||
## 🔒 安全考虑
|
||||
|
||||
1. **文件权限**:缓存文件权限设置为 `0644`,仅所有者可写
|
||||
2. **目录隔离**:缓存文件存储在独立目录,不影响其他文件
|
||||
3. **自动清理**:过期文件自动清理,防止磁盘空间耗尽
|
||||
|
||||
## 🐛 故障处理
|
||||
|
||||
### 缓存初始化失败
|
||||
|
||||
如果缓存管理器初始化失败,系统会:
|
||||
|
||||
- 记录警告日志
|
||||
- 继续正常运行(禁用缓存功能)
|
||||
- 所有请求都会重新生成PDF
|
||||
|
||||
### 缓存读取失败
|
||||
|
||||
如果缓存读取失败,系统会:
|
||||
|
||||
- 记录警告日志
|
||||
- 自动降级为重新生成PDF
|
||||
- 不影响用户体验
|
||||
|
||||
## 🔄 后续优化建议
|
||||
|
||||
1. **分布式缓存**:考虑使用Redis等分布式缓存,支持多实例部署
|
||||
2. **缓存预热**:在系统启动时预生成热门产品的PDF
|
||||
3. **压缩存储**:对PDF文件进行压缩存储,节省磁盘空间
|
||||
4. **缓存统计**:添加更详细的缓存统计和监控指标
|
||||
5. **智能清理**:基于LRU等算法,优先清理不常用的缓存
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
- **2024-12-XX**:初始版本,实现本地文件缓存机制
|
||||
- 添加PDF缓存管理器
|
||||
- 集成到下载接口
|
||||
- 支持版本控制和自动过期
|
||||
242
docs/Ubuntu服务器PDF字体配置指南.md
Normal file
242
docs/Ubuntu服务器PDF字体配置指南.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Ubuntu服务器PDF字体配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档说明如何在Ubuntu 24.04 LTS服务器上配置PDF生成功能所需的中文字体。
|
||||
|
||||
## 字体文件位置
|
||||
|
||||
确保字体文件存在于以下任一位置:
|
||||
|
||||
### 推荐路径(按优先级)
|
||||
|
||||
1. **工作目录相对路径**(最常用)
|
||||
```
|
||||
{工作目录}/internal/shared/pdf/fonts/
|
||||
```
|
||||
例如:如果工作目录是 `/www/tyapi-server`,则字体应在:
|
||||
```
|
||||
/www/tyapi-server/internal/shared/pdf/fonts/
|
||||
```
|
||||
|
||||
2. **可执行文件相对路径**
|
||||
```
|
||||
{可执行文件所在目录}/internal/shared/pdf/fonts/
|
||||
```
|
||||
|
||||
3. **环境变量指定路径**
|
||||
```bash
|
||||
export PDF_FONT_DIR=/path/to/fonts
|
||||
```
|
||||
|
||||
4. **硬编码路径**(后备方案)
|
||||
- `/www/tyapi-server/internal/shared/pdf/fonts` ✅(已配置)
|
||||
- `/app/internal/shared/pdf/fonts`(Docker)
|
||||
- `/usr/local/tyapi-server/internal/shared/pdf/fonts`
|
||||
- `/opt/tyapi-server/internal/shared/pdf/fonts`
|
||||
- `/home/ubuntu/tyapi-server/internal/shared/pdf/fonts`
|
||||
- `/root/tyapi-server/internal/shared/pdf/fonts`
|
||||
- `/var/www/tyapi-server/internal/shared/pdf/fonts`
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 方法1:直接复制字体文件(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 创建字体目录
|
||||
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
|
||||
|
||||
# 2. 复制字体文件(从本地或Git仓库)
|
||||
# 需要以下字体文件:
|
||||
# - simhei.ttf (黑体,必需)
|
||||
# - simkai.ttf (楷体,可选)
|
||||
# - simfang.ttf (仿宋,可选)
|
||||
# - YunFengFeiYunTi-2.ttf (水印字体,可选)
|
||||
|
||||
# 3. 设置权限
|
||||
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
|
||||
sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttc
|
||||
|
||||
# 4. 确保运行用户有读取权限
|
||||
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
|
||||
```
|
||||
|
||||
### 方法2:使用环境变量
|
||||
|
||||
```bash
|
||||
# 设置字体目录环境变量
|
||||
export PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts
|
||||
|
||||
# 或在 systemd 服务文件中添加
|
||||
# Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
|
||||
```
|
||||
|
||||
### 方法3:使用符号链接
|
||||
|
||||
如果字体文件在其他位置,可以创建符号链接:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts
|
||||
sudo ln -s /path/to/actual/fonts/*.ttf /www/tyapi-server/internal/shared/pdf/fonts/
|
||||
```
|
||||
|
||||
## 验证字体文件
|
||||
|
||||
### 1. 检查文件是否存在
|
||||
|
||||
```bash
|
||||
ls -lh /www/tyapi-server/internal/shared/pdf/fonts/
|
||||
```
|
||||
|
||||
应该看到:
|
||||
```
|
||||
-rw-r--r-- 1 user user 9.5M Dec 3 18:00 simhei.ttf
|
||||
-rw-r--r-- 1 user user 8.2M Dec 3 18:00 simkai.ttf
|
||||
-rw-r--r-- 1 user user 7.8M Dec 3 18:00 simfang.ttf
|
||||
```
|
||||
|
||||
### 2. 检查文件权限
|
||||
|
||||
```bash
|
||||
stat /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
|
||||
```
|
||||
|
||||
确保有读取权限(至少 `-r--r--r--`)。
|
||||
|
||||
### 3. 检查文件类型
|
||||
|
||||
```bash
|
||||
file /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf
|
||||
```
|
||||
|
||||
应该显示:`TrueType font data`
|
||||
|
||||
## 验证PDF生成功能
|
||||
|
||||
### 1. 查看日志
|
||||
|
||||
启动服务后,查看日志中是否有以下信息:
|
||||
|
||||
```json
|
||||
{"level":"INFO","msg":"找到字体文件","count":3,"paths":["/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf",...]}
|
||||
{"level":"INFO","msg":"成功加载中文字体","font_path":"/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf"}
|
||||
```
|
||||
|
||||
### 2. 测试PDF生成
|
||||
|
||||
调用PDF下载接口,检查:
|
||||
- PDF文件能正常生成
|
||||
- 中文文字正常显示(不是乱码或空白)
|
||||
- 没有字体相关的错误日志
|
||||
|
||||
### 3. 调试信息
|
||||
|
||||
如果字体未找到,查看日志中的调试信息:
|
||||
|
||||
```json
|
||||
{"level":"DEBUG","msg":"查找字体文件","total_paths":20,"paths":[...]}
|
||||
{"level":"DEBUG","msg":"字体文件不存在","font_path":"...","error":"..."}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题1:字体文件找不到
|
||||
|
||||
**症状**:日志显示 `"未找到中文字体文件"`
|
||||
|
||||
**解决方案**:
|
||||
1. 确认字体文件路径是否正确
|
||||
2. 检查文件权限:`chmod 644 *.ttf`
|
||||
3. 检查文件所有者:`chown user:user *.ttf`
|
||||
4. 查看日志中的 `"查找字体文件"` 调试信息,确认尝试的路径
|
||||
|
||||
### 问题2:字体文件无权限读取
|
||||
|
||||
**症状**:日志显示 `"字体文件无读取权限"`
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
sudo chmod 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf
|
||||
sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts
|
||||
```
|
||||
|
||||
### 问题3:中文显示为乱码
|
||||
|
||||
**症状**:PDF中中文显示为乱码或空白
|
||||
|
||||
**解决方案**:
|
||||
1. 确认字体文件已成功加载(查看日志)
|
||||
2. 确认字体文件是有效的TTF格式
|
||||
3. 检查字体文件是否损坏:`file *.ttf`
|
||||
|
||||
### 问题4:Docker容器中找不到字体
|
||||
|
||||
**症状**:在Docker容器中运行时找不到字体
|
||||
|
||||
**解决方案**:
|
||||
1. 确保Dockerfile中已复制字体文件:
|
||||
```dockerfile
|
||||
COPY --from=builder /app/internal/shared/pdf/fonts/ ./internal/shared/pdf/fonts/
|
||||
```
|
||||
2. 或使用volume挂载:
|
||||
```yaml
|
||||
volumes:
|
||||
- /www/tyapi-server/internal/shared/pdf/fonts:/app/internal/shared/pdf/fonts:ro
|
||||
```
|
||||
|
||||
## Systemd服务配置示例
|
||||
|
||||
如果使用systemd管理服务,可以在服务文件中设置环境变量:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=TYAPI Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/www/tyapi-server
|
||||
ExecStart=/www/tyapi-server/tyapi-server -env=production
|
||||
Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts"
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## 字体文件获取
|
||||
|
||||
如果本地没有字体文件,可以从以下来源获取:
|
||||
|
||||
1. **Windows系统字体**(如果服务器是Windows迁移过来的)
|
||||
- `C:\Windows\Fonts\simhei.ttf` → 复制到服务器
|
||||
|
||||
2. **Linux系统字体包**
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei
|
||||
# 然后从系统字体目录复制或创建符号链接
|
||||
```
|
||||
|
||||
3. **从项目仓库**
|
||||
- 确保字体文件已提交到Git仓库
|
||||
- 使用 `git pull` 拉取最新代码
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字体文件大小**:每个TTF文件约8-10MB,确保有足够磁盘空间
|
||||
2. **文件权限**:确保运行服务的用户有读取权限
|
||||
3. **路径一致性**:确保字体路径与代码中的查找路径一致
|
||||
4. **日志级别**:生产环境建议将字体查找日志设为DEBUG级别,避免日志过多
|
||||
|
||||
## 技术支持
|
||||
|
||||
如果遇到问题,请提供以下信息:
|
||||
1. 服务器操作系统版本:`lsb_release -a`
|
||||
2. 字体文件位置和权限:`ls -lh /www/tyapi-server/internal/shared/pdf/fonts/`
|
||||
3. 工作目录:`pwd`(服务运行时)
|
||||
4. 可执行文件位置:`which tyapi-server` 或 `readlink -f $(which tyapi-server)`
|
||||
5. 相关日志:包含 `"查找字体文件"` 和 `"字体文件"` 的日志条目
|
||||
|
||||
3
go.mod
3
go.mod
@@ -12,11 +12,13 @@ require (
|
||||
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/jung-kurt/gofpdf/v2 v2.17.3
|
||||
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/smartwalle/alipay/v3 v3.2.25
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
@@ -24,6 +26,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/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||
github.com/xuri/excelize/v2 v2.9.1
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -9,6 +9,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
||||
@@ -133,6 +135,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -206,6 +210,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
|
||||
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
||||
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||
@@ -260,6 +266,8 @@ 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/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||
|
||||
@@ -245,6 +245,8 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&articleEntities.Category{},
|
||||
&articleEntities.Tag{},
|
||||
&articleEntities.ScheduledTask{},
|
||||
// 公告
|
||||
&articleEntities.Announcement{},
|
||||
|
||||
// 统计域
|
||||
&statisticsEntities.StatisticsMetric{},
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/application/api/dto"
|
||||
@@ -37,8 +39,8 @@ type ApiApplicationService interface {
|
||||
GetUserApiKeys(ctx context.Context, userID string) (*dto.ApiKeysResponse, error)
|
||||
|
||||
// 用户白名单管理
|
||||
GetUserWhiteList(ctx context.Context, userID string) (*dto.WhiteListListResponse, error)
|
||||
AddWhiteListIP(ctx context.Context, userID string, ipAddress string) error
|
||||
GetUserWhiteList(ctx context.Context, userID string, remarkKeyword string) (*dto.WhiteListListResponse, error)
|
||||
AddWhiteListIP(ctx context.Context, userID string, ipAddress string, remark string) error
|
||||
DeleteWhiteListIP(ctx context.Context, userID string, ipAddress string) error
|
||||
|
||||
// 获取用户API调用记录
|
||||
@@ -466,7 +468,7 @@ func (s *ApiApplicationServiceImpl) GetUserApiKeys(ctx context.Context, userID s
|
||||
}
|
||||
|
||||
// GetUserWhiteList 获取用户白名单列表
|
||||
func (s *ApiApplicationServiceImpl) GetUserWhiteList(ctx context.Context, userID string) (*dto.WhiteListListResponse, error) {
|
||||
func (s *ApiApplicationServiceImpl) GetUserWhiteList(ctx context.Context, userID string, remarkKeyword string) (*dto.WhiteListListResponse, error) {
|
||||
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -474,28 +476,49 @@ func (s *ApiApplicationServiceImpl) GetUserWhiteList(ctx context.Context, userID
|
||||
|
||||
// 确保WhiteList不为nil
|
||||
if apiUser.WhiteList == nil {
|
||||
apiUser.WhiteList = []string{}
|
||||
apiUser.WhiteList = entities.WhiteList{}
|
||||
}
|
||||
|
||||
// 将白名单字符串数组转换为响应格式
|
||||
// 将白名单转换为响应格式
|
||||
var items []dto.WhiteListResponse
|
||||
for _, ip := range apiUser.WhiteList {
|
||||
for _, item := range apiUser.WhiteList {
|
||||
// 如果提供了备注关键词,进行模糊匹配过滤
|
||||
if remarkKeyword != "" {
|
||||
if !contains(item.Remark, remarkKeyword) {
|
||||
continue // 不匹配则跳过
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, dto.WhiteListResponse{
|
||||
ID: apiUser.ID, // 使用API用户ID作为标识
|
||||
UserID: apiUser.UserId,
|
||||
IPAddress: ip,
|
||||
CreatedAt: apiUser.CreatedAt, // 使用API用户创建时间
|
||||
IPAddress: item.IPAddress,
|
||||
Remark: item.Remark, // 备注
|
||||
CreatedAt: item.AddedAt, // 使用每个IP的实际添加时间
|
||||
})
|
||||
}
|
||||
|
||||
// 按添加时间降序排序(新的排在前面)
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].CreatedAt.After(items[j].CreatedAt)
|
||||
})
|
||||
|
||||
return &dto.WhiteListListResponse{
|
||||
Items: items,
|
||||
Total: len(items),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// contains 检查字符串是否包含子字符串(不区分大小写)
|
||||
func contains(s, substr string) bool {
|
||||
if substr == "" {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
|
||||
// AddWhiteListIP 添加白名单IP
|
||||
func (s *ApiApplicationServiceImpl) AddWhiteListIP(ctx context.Context, userID string, ipAddress string) error {
|
||||
func (s *ApiApplicationServiceImpl) AddWhiteListIP(ctx context.Context, userID string, ipAddress string, remark string) error {
|
||||
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -503,11 +526,11 @@ func (s *ApiApplicationServiceImpl) AddWhiteListIP(ctx context.Context, userID s
|
||||
|
||||
// 确保WhiteList不为nil
|
||||
if apiUser.WhiteList == nil {
|
||||
apiUser.WhiteList = []string{}
|
||||
apiUser.WhiteList = entities.WhiteList{}
|
||||
}
|
||||
|
||||
// 使用实体的领域方法添加IP到白名单
|
||||
err = apiUser.AddToWhiteList(ipAddress)
|
||||
// 使用实体的领域方法添加IP到白名单(会自动记录添加时间和备注)
|
||||
err = apiUser.AddToWhiteList(ipAddress, remark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -530,7 +553,7 @@ func (s *ApiApplicationServiceImpl) DeleteWhiteListIP(ctx context.Context, userI
|
||||
|
||||
// 确保WhiteList不为nil
|
||||
if apiUser.WhiteList == nil {
|
||||
apiUser.WhiteList = []string{}
|
||||
apiUser.WhiteList = entities.WhiteList{}
|
||||
}
|
||||
|
||||
// 使用实体的领域方法删除IP
|
||||
|
||||
@@ -26,11 +26,13 @@ type WhiteListResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Remark string `json:"remark"` // 备注
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type WhiteListRequest struct {
|
||||
IPAddress string `json:"ip_address" binding:"required,ip"`
|
||||
Remark string `json:"remark"` // 备注(可选)
|
||||
}
|
||||
|
||||
type WhiteListListResponse struct {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||
"tyapi-server/internal/application/article/dto/responses"
|
||||
)
|
||||
|
||||
// AnnouncementApplicationService 公告应用服务接口
|
||||
type AnnouncementApplicationService interface {
|
||||
// 公告管理
|
||||
CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error
|
||||
UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error
|
||||
DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error
|
||||
GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error)
|
||||
ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error)
|
||||
|
||||
// 公告状态管理
|
||||
PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error
|
||||
PublishAnnouncementByID(ctx context.Context, announcementID string) error // 通过ID发布公告 (用于定时任务)
|
||||
WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error
|
||||
ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error
|
||||
SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error
|
||||
UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error
|
||||
CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error
|
||||
|
||||
// 统计信息
|
||||
GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tyapi-server/internal/application/article/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||
"tyapi-server/internal/application/article/dto/responses"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories"
|
||||
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/domains/article/services"
|
||||
task_entities "tyapi-server/internal/infrastructure/task/entities"
|
||||
task_interfaces "tyapi-server/internal/infrastructure/task/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AnnouncementApplicationServiceImpl 公告应用服务实现
|
||||
type AnnouncementApplicationServiceImpl struct {
|
||||
announcementRepo repositories.AnnouncementRepository
|
||||
announcementService *services.AnnouncementService
|
||||
taskManager task_interfaces.TaskManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAnnouncementApplicationService 创建公告应用服务
|
||||
func NewAnnouncementApplicationService(
|
||||
announcementRepo repositories.AnnouncementRepository,
|
||||
announcementService *services.AnnouncementService,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) AnnouncementApplicationService {
|
||||
return &AnnouncementApplicationServiceImpl{
|
||||
announcementRepo: announcementRepo,
|
||||
announcementService: announcementService,
|
||||
taskManager: taskManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAnnouncement 创建公告
|
||||
func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error {
|
||||
// 1. 创建公告实体
|
||||
announcement := &entities.Announcement{
|
||||
Title: cmd.Title,
|
||||
Content: cmd.Content,
|
||||
Status: entities.AnnouncementStatusDraft,
|
||||
}
|
||||
|
||||
// 2. 调用领域服务验证
|
||||
if err := s.announcementService.ValidateAnnouncement(announcement); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 保存公告
|
||||
_, err := s.announcementRepo.Create(ctx, *announcement)
|
||||
if err != nil {
|
||||
s.logger.Error("创建公告失败", zap.Error(err))
|
||||
return fmt.Errorf("创建公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAnnouncement 更新公告
|
||||
func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error {
|
||||
// 1. 获取原公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以编辑
|
||||
if err := s.announcementService.CanEdit(&announcement); err != nil {
|
||||
return fmt.Errorf("公告状态不允许编辑: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
if cmd.Title != "" {
|
||||
announcement.Title = cmd.Title
|
||||
}
|
||||
if cmd.Content != "" {
|
||||
announcement.Content = cmd.Content
|
||||
}
|
||||
|
||||
// 4. 验证更新后的公告
|
||||
if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil {
|
||||
return fmt.Errorf("业务验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("更新公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("更新公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAnnouncement 删除公告
|
||||
func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error {
|
||||
// 1. 检查公告是否存在
|
||||
_, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 删除公告
|
||||
if err := s.announcementRepo.Delete(ctx, cmd.ID); err != nil {
|
||||
s.logger.Error("删除公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("删除公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("删除公告成功", zap.String("id", cmd.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnnouncementByID 获取公告详情
|
||||
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, query.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", query.ID), zap.Error(err))
|
||||
return nil, fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 转换为响应对象
|
||||
response := responses.FromAnnouncementEntity(&announcement)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ListAnnouncements 获取公告列表
|
||||
func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) {
|
||||
// 1. 构建仓储查询
|
||||
repoQuery := &repoQueries.ListAnnouncementQuery{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
Title: query.Title,
|
||||
OrderBy: query.OrderBy,
|
||||
OrderDir: query.OrderDir,
|
||||
}
|
||||
|
||||
// 2. 调用仓储
|
||||
announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告列表失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取公告列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
items := responses.FromAnnouncementEntityList(announcements)
|
||||
|
||||
response := &responses.AnnouncementListResponse{
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
Size: query.PageSize,
|
||||
Items: items,
|
||||
}
|
||||
|
||||
s.logger.Info("获取公告列表成功", zap.Int64("total", total))
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// PublishAnnouncement 发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以发布
|
||||
if err := s.announcementService.CanPublish(&announcement); err != nil {
|
||||
return fmt.Errorf("无法发布公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 发布公告
|
||||
if err := announcement.Publish(); err != nil {
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("发布公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublishAnnouncementByID 通过ID发布公告 (用于定时任务)
|
||||
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, announcementID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已取消定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
s.logger.Info("公告定时发布已取消,跳过执行",
|
||||
zap.String("id", announcementID),
|
||||
zap.String("status", string(announcement.Status)))
|
||||
return nil // 静默返回,不报错
|
||||
}
|
||||
|
||||
// 3. 检查定时发布时间是否匹配
|
||||
if announcement.ScheduledAt == nil {
|
||||
s.logger.Info("公告没有定时发布时间,跳过执行",
|
||||
zap.String("id", announcementID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 发布公告
|
||||
if err := announcement.Publish(); err != nil {
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("发布公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawAnnouncement 撤回公告
|
||||
func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以撤回
|
||||
if err := s.announcementService.CanWithdraw(&announcement); err != nil {
|
||||
return fmt.Errorf("无法撤回公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 撤回公告
|
||||
if err := announcement.Withdraw(); err != nil {
|
||||
return fmt.Errorf("撤回公告失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("撤回公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("撤回公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveAnnouncement 归档公告
|
||||
func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否可以归档
|
||||
if err := s.announcementService.CanArchive(&announcement); err != nil {
|
||||
return fmt.Errorf("无法归档公告: %w", err)
|
||||
}
|
||||
|
||||
// 3. 归档公告
|
||||
announcement.Status = entities.AnnouncementStatusArchived
|
||||
|
||||
// 4. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("归档公告失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("归档公告成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublishAnnouncement 定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 检查是否可以定时发布
|
||||
if err := s.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil {
|
||||
return fmt.Errorf("无法设置定时发布: %w", err)
|
||||
}
|
||||
|
||||
// 4. 取消旧任务(如果存在)
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 6. 创建并异步入队公告发布任务
|
||||
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 设置定时发布
|
||||
if err := announcement.SchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("设置定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishAnnouncement 更新定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error {
|
||||
// 1. 解析定时发布时间
|
||||
scheduledTime, err := cmd.GetScheduledTime()
|
||||
if err != nil {
|
||||
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
|
||||
return fmt.Errorf("定时发布时间格式错误: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 3. 检查是否已设置定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
return fmt.Errorf("公告未设置定时发布,无法修改时间")
|
||||
}
|
||||
|
||||
// 4. 取消旧任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 创建任务工厂
|
||||
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
|
||||
|
||||
// 6. 创建并异步入队新的公告发布任务
|
||||
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
|
||||
ctx,
|
||||
cmd.ID,
|
||||
scheduledTime,
|
||||
"system", // 暂时使用系统用户ID
|
||||
); err != nil {
|
||||
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
|
||||
return fmt.Errorf("创建定时发布任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 更新定时发布时间
|
||||
if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil {
|
||||
return fmt.Errorf("更新定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("修改定时发布时间失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelSchedulePublishAnnouncement 取消定时发布公告
|
||||
func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error {
|
||||
// 1. 获取公告
|
||||
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||
return fmt.Errorf("公告不存在: %w", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已设置定时发布
|
||||
if !announcement.IsScheduled() {
|
||||
return fmt.Errorf("公告未设置定时发布,无需取消")
|
||||
}
|
||||
|
||||
// 3. 取消任务
|
||||
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
|
||||
s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
|
||||
// 继续执行,即使取消任务失败也尝试取消定时发布状态
|
||||
}
|
||||
|
||||
// 4. 取消定时发布
|
||||
if err := announcement.CancelSchedulePublish(); err != nil {
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 保存更新
|
||||
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
|
||||
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
|
||||
return fmt.Errorf("取消定时发布失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnnouncementStats 获取公告统计信息
|
||||
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) {
|
||||
// 1. 统计总数
|
||||
total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("统计公告总数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 统计各状态数量
|
||||
published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished)
|
||||
if err != nil {
|
||||
s.logger.Error("统计已发布公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
|
||||
if err != nil {
|
||||
s.logger.Error("统计草稿公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived)
|
||||
if err != nil {
|
||||
s.logger.Error("统计归档公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 统计定时发布数量(需要查询有scheduled_at的草稿)
|
||||
scheduled, err := s.announcementRepo.FindScheduled(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("统计定时发布公告数失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||||
}
|
||||
|
||||
response := &responses.AnnouncementStatsResponse{
|
||||
TotalAnnouncements: total + published + archived,
|
||||
PublishedAnnouncements: published,
|
||||
DraftAnnouncements: draft,
|
||||
ArchivedAnnouncements: archived,
|
||||
ScheduledAnnouncements: int64(len(scheduled)),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateAnnouncementCommand 创建公告命令
|
||||
type CreateAnnouncementCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"公告标题"`
|
||||
Content string `json:"content" binding:"required" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// UpdateAnnouncementCommand 更新公告命令
|
||||
type UpdateAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// DeleteAnnouncementCommand 删除公告命令
|
||||
type DeleteAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// PublishAnnouncementCommand 发布公告命令
|
||||
type PublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// WithdrawAnnouncementCommand 撤回公告命令
|
||||
type WithdrawAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// ArchiveAnnouncementCommand 归档公告命令
|
||||
type ArchiveAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// SchedulePublishAnnouncementCommand 定时发布公告命令
|
||||
type SchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
|
||||
type UpdateSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
|
||||
type CancelSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListAnnouncementQuery 公告列表查询
|
||||
type ListAnnouncementQuery struct {
|
||||
Page int `form:"page" binding:"min=1" comment:"页码"`
|
||||
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
||||
Status entities.AnnouncementStatus `form:"status" comment:"公告状态"`
|
||||
Title string `form:"title" comment:"标题关键词"`
|
||||
OrderBy string `form:"order_by" comment:"排序字段"`
|
||||
OrderDir string `form:"order_dir" comment:"排序方向"`
|
||||
}
|
||||
|
||||
// GetAnnouncementQuery 获取公告详情查询
|
||||
type GetAnnouncementQuery struct {
|
||||
ID string `uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// AnnouncementInfoResponse 公告详情响应
|
||||
type AnnouncementInfoResponse struct {
|
||||
ID string `json:"id" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
Status string `json:"status" comment:"公告状态"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// AnnouncementListItemResponse 公告列表项响应
|
||||
type AnnouncementListItemResponse struct {
|
||||
ID string `json:"id" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
Status string `json:"status" comment:"公告状态"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// AnnouncementListResponse 公告列表响应
|
||||
type AnnouncementListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []AnnouncementListItemResponse `json:"items" comment:"公告列表"`
|
||||
}
|
||||
|
||||
// AnnouncementStatsResponse 公告统计响应
|
||||
type AnnouncementStatsResponse struct {
|
||||
TotalAnnouncements int64 `json:"total_announcements" comment:"公告总数"`
|
||||
PublishedAnnouncements int64 `json:"published_announcements" comment:"已发布公告数"`
|
||||
DraftAnnouncements int64 `json:"draft_announcements" comment:"草稿公告数"`
|
||||
ArchivedAnnouncements int64 `json:"archived_announcements" comment:"归档公告数"`
|
||||
ScheduledAnnouncements int64 `json:"scheduled_announcements" comment:"定时发布公告数"`
|
||||
}
|
||||
|
||||
// FromAnnouncementEntity 从公告实体转换为响应对象
|
||||
func FromAnnouncementEntity(announcement *entities.Announcement) *AnnouncementInfoResponse {
|
||||
if announcement == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &AnnouncementInfoResponse{
|
||||
ID: announcement.ID,
|
||||
Title: announcement.Title,
|
||||
Content: announcement.Content,
|
||||
Status: string(announcement.Status),
|
||||
ScheduledAt: announcement.ScheduledAt,
|
||||
CreatedAt: announcement.CreatedAt,
|
||||
UpdatedAt: announcement.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// FromAnnouncementEntityList 从公告实体列表转换为列表项响应
|
||||
func FromAnnouncementEntityList(announcements []*entities.Announcement) []AnnouncementListItemResponse {
|
||||
items := make([]AnnouncementListItemResponse, 0, len(announcements))
|
||||
for _, announcement := range announcements {
|
||||
items = append(items, AnnouncementListItemResponse{
|
||||
ID: announcement.ID,
|
||||
Title: announcement.Title,
|
||||
Content: announcement.Content,
|
||||
Status: string(announcement.Status),
|
||||
ScheduledAt: announcement.ScheduledAt,
|
||||
CreatedAt: announcement.CreatedAt,
|
||||
UpdatedAt: announcement.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
@@ -156,17 +156,15 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
}
|
||||
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
|
||||
}
|
||||
if cmd.UserID != "3fbd6917-bb13-40b3-bab0-de0d44c0afca" {
|
||||
err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("企业信息验证失败", zap.Error(err))
|
||||
record.MarkAsFailed(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())
|
||||
err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("企业信息验证失败", zap.Error(err))
|
||||
record.MarkAsFailed(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()
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
|
||||
@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
}
|
||||
|
||||
|
||||
// TransferRechargeCommand 对公转账充值命令
|
||||
type TransferRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
@@ -16,16 +15,24 @@ type TransferRechargeCommand struct {
|
||||
|
||||
// GiftRechargeCommand 赠送充值命令
|
||||
type GiftRechargeCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
UserID string `json:"user_id" binding:"required,uuid"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||
}
|
||||
|
||||
|
||||
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
||||
type CreateAlipayRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
||||
}
|
||||
|
||||
// CreateWechatRechargeCommand 创建微信充值订单命令
|
||||
type CreateWechatRechargeCommand struct {
|
||||
UserID string `json:"-"` // 用户ID(从token获取)
|
||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||
Subject string `json:"-"` // 订单标题
|
||||
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码,兼容传入native/wx_h5/h5
|
||||
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid(用于小程序/H5)
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
|
||||
// WalletResponse 钱包响应
|
||||
type WalletResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TransactionResponse 交易响应
|
||||
@@ -49,34 +49,36 @@ type WalletStatsResponse struct {
|
||||
|
||||
// RechargeRecordResponse 充值记录响应
|
||||
type RechargeRecordResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
OperatorID string `json:"operator_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||
WechatOrderID string `json:"wechat_order_id,omitempty"`
|
||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||
Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等
|
||||
Notes string `json:"notes,omitempty"`
|
||||
OperatorID string `json:"operator_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionResponse 钱包交易记录响应
|
||||
type WalletTransactionResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ApiCallID string `json:"api_call_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
User *UserSimpleResponse `json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WalletTransactionListResponse 钱包交易记录列表响应
|
||||
@@ -97,17 +99,17 @@ type RechargeRecordListResponse struct {
|
||||
|
||||
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
||||
type AlipayRechargeOrderResponse struct {
|
||||
PayURL string `json:"pay_url"` // 支付链接
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
PayURL string `json:"pay_url"` // 支付链接
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
}
|
||||
|
||||
// RechargeConfigResponse 充值配置响应
|
||||
type RechargeConfigResponse struct {
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// WechatOrderStatusResponse 微信订单状态响应
|
||||
type WechatOrderStatusResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
TransactionID *string `json:"transaction_id"` // 微信支付交易号
|
||||
Status string `json:"status"` // 订单状态
|
||||
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||
ErrorCode *string `json:"error_code"` // 错误码
|
||||
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package responses
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// WechatRechargeOrderResponse 微信充值下单响应
|
||||
type WechatRechargeOrderResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数)
|
||||
}
|
||||
@@ -17,13 +17,14 @@ type FinanceApplicationService interface {
|
||||
|
||||
// 充值管理
|
||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
||||
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
|
||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||
|
||||
// 交易记录
|
||||
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
|
||||
|
||||
|
||||
// 导出功能
|
||||
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
|
||||
@@ -33,12 +34,15 @@ type FinanceApplicationService interface {
|
||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||
|
||||
// 微信支付回调处理
|
||||
HandleWechatPayCallback(ctx context.Context, r *http.Request) error
|
||||
HandleWechatRefundCallback(ctx context.Context, r *http.Request) error
|
||||
GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error)
|
||||
|
||||
// 充值记录
|
||||
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||
|
||||
// 获取充值配置
|
||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ package finance
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"time"
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
"tyapi-server/internal/application/finance/dto/responses"
|
||||
@@ -16,19 +21,18 @@ import (
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FinanceApplicationServiceImpl 财务应用服务实现
|
||||
type FinanceApplicationServiceImpl struct {
|
||||
aliPayClient *payment.AliPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
walletService finance_services.WalletAggregateService
|
||||
rechargeRecordService finance_services.RechargeRecordService
|
||||
walletTransactionRepository finance_repositories.WalletTransactionRepository
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
exportManager *export.ExportManager
|
||||
@@ -39,10 +43,13 @@ type FinanceApplicationServiceImpl struct {
|
||||
// NewFinanceApplicationService 创建财务应用服务
|
||||
func NewFinanceApplicationService(
|
||||
aliPayClient *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
rechargeRecordService finance_services.RechargeRecordService,
|
||||
walletTransactionRepository finance_repositories.WalletTransactionRepository,
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -51,10 +58,13 @@ func NewFinanceApplicationService(
|
||||
) FinanceApplicationService {
|
||||
return &FinanceApplicationServiceImpl{
|
||||
aliPayClient: aliPayClient,
|
||||
wechatPayService: wechatPayService,
|
||||
walletService: walletService,
|
||||
rechargeRecordService: rechargeRecordService,
|
||||
walletTransactionRepository: walletTransactionRepository,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
exportManager: exportManager,
|
||||
@@ -100,8 +110,9 @@ func (s *FinanceApplicationServiceImpl) GetWallet(ctx context.Context, query *qu
|
||||
BalanceStatus: wallet.GetBalanceStatus(),
|
||||
IsArrears: wallet.IsArrears(),
|
||||
IsLowBalance: wallet.IsLowBalance(),
|
||||
CreatedAt: wallet.CreatedAt,
|
||||
UpdatedAt: wallet.UpdatedAt,
|
||||
|
||||
CreatedAt: wallet.CreatedAt,
|
||||
UpdatedAt: wallet.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -188,6 +199,168 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateWechatRechargeOrder 创建微信充值订单(完整流程编排)
|
||||
func (s *FinanceApplicationServiceImpl) CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error) {
|
||||
cmd.Subject = "天远数据API充值"
|
||||
amount, err := decimal.NewFromString(cmd.Amount)
|
||||
if err != nil {
|
||||
s.logger.Error("金额格式错误", zap.String("amount", cmd.Amount), zap.Error(err))
|
||||
return nil, fmt.Errorf("金额格式错误: %w", err)
|
||||
}
|
||||
|
||||
if amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("充值金额必须大于0")
|
||||
}
|
||||
|
||||
minAmount, err := decimal.NewFromString(s.config.Wallet.MinAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最低充值金额格式错误", zap.String("min_amount", s.config.Wallet.MinAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
maxAmount, err := decimal.NewFromString(s.config.Wallet.MaxAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最高充值金额格式错误", zap.String("max_amount", s.config.Wallet.MaxAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
|
||||
if amount.LessThan(minAmount) {
|
||||
return nil, fmt.Errorf("充值金额不能少于%s元", minAmount.String())
|
||||
}
|
||||
if amount.GreaterThan(maxAmount) {
|
||||
return nil, fmt.Errorf("单次充值金额不能超过%s元", maxAmount.String())
|
||||
}
|
||||
|
||||
platform := normalizeWechatPlatform(cmd.Platform)
|
||||
if platform != payment.PlatformWxNative && platform != payment.PlatformWxH5 {
|
||||
return nil, fmt.Errorf("不支持的支付平台: %s", cmd.Platform)
|
||||
}
|
||||
if s.wechatPayService == nil {
|
||||
return nil, fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
outTradeNo := s.wechatPayService.GenerateOutTradeNo()
|
||||
|
||||
s.logger.Info("开始创建微信充值订单",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("platform", cmd.Platform),
|
||||
zap.String("subject", cmd.Subject),
|
||||
)
|
||||
|
||||
var prepayData interface{}
|
||||
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 创建微信充值记录
|
||||
rechargeRecord := finance_entities.NewWechatRechargeRecord(cmd.UserID, amount, outTradeNo)
|
||||
createdRecord, createErr := s.rechargeRecordRepo.Create(txCtx, *rechargeRecord)
|
||||
if createErr != nil {
|
||||
s.logger.Error("创建微信充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(createErr),
|
||||
)
|
||||
return fmt.Errorf("创建微信充值记录失败: %w", createErr)
|
||||
}
|
||||
|
||||
s.logger.Info("创建微信充值记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
)
|
||||
|
||||
// 创建微信订单本地记录
|
||||
wechatOrder := finance_entities.NewWechatOrder(createdRecord.ID, outTradeNo, cmd.Subject, amount, platform)
|
||||
createdOrder, orderErr := s.wechatOrderRepo.Create(txCtx, *wechatOrder)
|
||||
if orderErr != nil {
|
||||
s.logger.Error("创建微信订单记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
zap.Error(orderErr),
|
||||
)
|
||||
return fmt.Errorf("创建微信订单记录失败: %w", orderErr)
|
||||
}
|
||||
|
||||
s.logger.Info("创建微信订单记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("order_id", createdOrder.ID),
|
||||
zap.String("recharge_id", createdRecord.ID),
|
||||
)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payCtx := context.WithValue(ctx, "platform", platform)
|
||||
payCtx = context.WithValue(payCtx, "user_id", cmd.UserID)
|
||||
|
||||
s.logger.Info("调用微信支付接口创建订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("platform", platform),
|
||||
)
|
||||
|
||||
prepayData, err = s.wechatPayService.CreateWechatOrder(payCtx, amount.InexactFloat64(), cmd.Subject, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("微信下单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
// 回写失败状态
|
||||
_ = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
order, getErr := s.wechatOrderRepo.GetByOutTradeNo(txCtx, outTradeNo)
|
||||
if getErr == nil && order != nil {
|
||||
order.MarkFailed("create_failed", err.Error())
|
||||
updateErr := s.wechatOrderRepo.Update(txCtx, *order)
|
||||
if updateErr != nil {
|
||||
s.logger.Error("回写微信订单失败状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(updateErr),
|
||||
)
|
||||
} else {
|
||||
s.logger.Info("回写微信订单失败状态成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil, fmt.Errorf("创建微信支付订单失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("微信充值订单创建成功",
|
||||
zap.String("user_id", cmd.UserID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("platform", cmd.Platform),
|
||||
)
|
||||
|
||||
return &responses.WechatRechargeOrderResponse{
|
||||
OutTradeNo: outTradeNo,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
Subject: cmd.Subject,
|
||||
PrepayData: prepayData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeWechatPlatform 将兼容写法(h5/mini)转换为系统内使用的wx_h5/wx_mini
|
||||
func normalizeWechatPlatform(p string) string {
|
||||
switch p {
|
||||
case "h5", payment.PlatformWxH5:
|
||||
return payment.PlatformWxNative
|
||||
case "native":
|
||||
return payment.PlatformWxNative
|
||||
default:
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// TransferRecharge 对公转账充值
|
||||
func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) {
|
||||
// 将字符串金额转换为 decimal.Decimal
|
||||
@@ -507,8 +680,8 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
}
|
||||
|
||||
// 准备导出数据
|
||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"}
|
||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20}
|
||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "微信订单号", "转账订单号", "备注", "充值时间"}
|
||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20, 20}
|
||||
|
||||
data := make([][]interface{}, len(allRecords))
|
||||
for i, record := range allRecords {
|
||||
@@ -523,6 +696,10 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
|
||||
alipayOrderID = *record.AlipayOrderID
|
||||
}
|
||||
wechatOrderID := ""
|
||||
if record.WechatOrderID != nil && *record.WechatOrderID != "" {
|
||||
wechatOrderID = *record.WechatOrderID
|
||||
}
|
||||
transferOrderID := ""
|
||||
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
|
||||
transferOrderID = *record.TransferOrderID
|
||||
@@ -543,6 +720,7 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
||||
translateRechargeType(record.RechargeType),
|
||||
translateRechargeStatus(record.Status),
|
||||
alipayOrderID,
|
||||
wechatOrderID,
|
||||
transferOrderID,
|
||||
notes,
|
||||
createdAt,
|
||||
@@ -566,6 +744,8 @@ func translateRechargeType(rechargeType finance_entities.RechargeType) string {
|
||||
switch rechargeType {
|
||||
case finance_entities.RechargeTypeAlipay:
|
||||
return "支付宝充值"
|
||||
case finance_entities.RechargeTypeWechat:
|
||||
return "微信充值"
|
||||
case finance_entities.RechargeTypeTransfer:
|
||||
return "对公转账"
|
||||
case finance_entities.RechargeTypeGift:
|
||||
@@ -890,15 +1070,27 @@ func (s *FinanceApplicationServiceImpl) GetAlipayOrderStatus(ctx context.Context
|
||||
|
||||
// GetUserRechargeRecords 获取用户充值记录
|
||||
func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
|
||||
// 查询用户充值记录
|
||||
records, err := s.rechargeRecordService.GetByUserID(ctx, userID)
|
||||
// 确保 filters 不为 nil
|
||||
if filters == nil {
|
||||
filters = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 添加 user_id 筛选条件,确保只能查询当前用户的记录
|
||||
filters["user_id"] = userID
|
||||
|
||||
// 查询用户充值记录(使用筛选和分页功能)
|
||||
records, err := s.rechargeRecordService.GetAll(ctx, filters, options)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
total := int64(len(records))
|
||||
// 获取总数(使用筛选条件)
|
||||
total, err := s.rechargeRecordService.Count(ctx, filters)
|
||||
if err != nil {
|
||||
s.logger.Error("统计用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应DTO
|
||||
var items []responses.RechargeRecordResponse
|
||||
@@ -914,9 +1106,20 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}
|
||||
|
||||
// 根据充值类型设置相应的订单号
|
||||
// 根据充值类型设置相应的订单号和平台信息
|
||||
if record.AlipayOrderID != nil {
|
||||
item.AlipayOrderID = *record.AlipayOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil {
|
||||
item.Platform = alipayOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.WechatOrderID != nil {
|
||||
item.WechatOrderID = *record.WechatOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil {
|
||||
item.Platform = wechatOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.TransferOrderID != nil {
|
||||
item.TransferOrderID = *record.TransferOrderID
|
||||
@@ -963,9 +1166,20 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}
|
||||
|
||||
// 根据充值类型设置相应的订单号
|
||||
// 根据充值类型设置相应的订单号和平台信息
|
||||
if record.AlipayOrderID != nil {
|
||||
item.AlipayOrderID = *record.AlipayOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, *record.AlipayOrderID); err == nil && alipayOrder != nil {
|
||||
item.Platform = alipayOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.WechatOrderID != nil {
|
||||
item.WechatOrderID = *record.WechatOrderID
|
||||
// 通过订单号获取平台信息
|
||||
if wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, *record.WechatOrderID); err == nil && wechatOrder != nil {
|
||||
item.Platform = wechatOrder.Platform
|
||||
}
|
||||
}
|
||||
if record.TransferOrderID != nil {
|
||||
item.TransferOrderID = *record.TransferOrderID
|
||||
@@ -1012,3 +1226,445 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (
|
||||
AlipayRechargeBonus: bonus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWechatOrderStatus 获取微信订单状态
|
||||
func (s *FinanceApplicationServiceImpl) GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error) {
|
||||
if outTradeNo == "" {
|
||||
return nil, fmt.Errorf("缺少商户订单号")
|
||||
}
|
||||
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return nil, fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return nil, fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 如果订单状态为pending,主动查询微信订单状态
|
||||
if wechatOrder.Status == finance_entities.WechatOrderStatusPending {
|
||||
s.logger.Info("订单状态为pending,主动查询微信订单状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
// 调用微信查询接口
|
||||
transaction, err := s.wechatPayService.QueryOrderStatus(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查询微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 查询失败不影响返回,继续使用数据库中的状态
|
||||
} else {
|
||||
// 解析微信返回的状态
|
||||
tradeState := ""
|
||||
transactionID := ""
|
||||
if transaction.TradeState != nil {
|
||||
tradeState = *transaction.TradeState
|
||||
}
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
|
||||
s.logger.Info("微信查询订单状态返回",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.String("transaction_id", transactionID),
|
||||
)
|
||||
|
||||
// 使用公共方法更新订单状态
|
||||
err = s.updateWechatOrderStatus(ctx, outTradeNo, tradeState, transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
// 重新获取更新后的订单信息
|
||||
updatedOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err == nil && updatedOrder != nil {
|
||||
wechatOrder = updatedOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否处理中
|
||||
isProcessing := wechatOrder.Status == finance_entities.WechatOrderStatusPending
|
||||
|
||||
// 判断是否可以重试(失败状态可以重试)
|
||||
canRetry := wechatOrder.Status == finance_entities.WechatOrderStatusFailed
|
||||
|
||||
// 转换为响应DTO
|
||||
response := &responses.WechatOrderStatusResponse{
|
||||
OutTradeNo: wechatOrder.OutTradeNo,
|
||||
TransactionID: wechatOrder.TradeNo,
|
||||
Status: string(wechatOrder.Status),
|
||||
Amount: wechatOrder.Amount,
|
||||
Subject: wechatOrder.Subject,
|
||||
Platform: wechatOrder.Platform,
|
||||
CreatedAt: wechatOrder.CreatedAt,
|
||||
UpdatedAt: wechatOrder.UpdatedAt,
|
||||
NotifyTime: wechatOrder.NotifyTime,
|
||||
ReturnTime: wechatOrder.ReturnTime,
|
||||
ErrorCode: &wechatOrder.ErrorCode,
|
||||
ErrorMessage: &wechatOrder.ErrorMessage,
|
||||
IsProcessing: isProcessing,
|
||||
CanRetry: canRetry,
|
||||
}
|
||||
|
||||
// 如果错误码为空,设置为nil
|
||||
if wechatOrder.ErrorCode == "" {
|
||||
response.ErrorCode = nil
|
||||
}
|
||||
if wechatOrder.ErrorMessage == "" {
|
||||
response.ErrorMessage = nil
|
||||
}
|
||||
|
||||
s.logger.Info("查询微信订单状态完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("status", string(wechatOrder.Status)),
|
||||
zap.Bool("is_processing", isProcessing),
|
||||
zap.Bool("can_retry", canRetry),
|
||||
)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// updateWechatOrderStatus 根据微信状态更新本地订单状态
|
||||
func (s *FinanceApplicationServiceImpl) updateWechatOrderStatus(ctx context.Context, outTradeNo string, tradeState string, transaction *payments.Transaction) error {
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
switch tradeState {
|
||||
case payment.TradeStateSuccess:
|
||||
// 支付成功,调用公共处理逻辑
|
||||
transactionID := ""
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
payAmount := decimal.Zero
|
||||
if transaction.Amount != nil && transaction.Amount.Total != nil {
|
||||
// 将分转换为元
|
||||
payAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100))
|
||||
}
|
||||
return s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, payAmount)
|
||||
case payment.TradeStateClosed:
|
||||
// 交易关闭
|
||||
s.logger.Info("微信订单交易关闭",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
wechatOrder.MarkClosed()
|
||||
err = s.wechatOrderRepo.Update(ctx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单关闭状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
s.logger.Info("微信订单关闭状态更新成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
case payment.TradeStateNotPay:
|
||||
// 未支付,保持pending状态
|
||||
s.logger.Info("微信订单未支付",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
default:
|
||||
// 其他状态,记录日志
|
||||
s.logger.Info("微信订单其他状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleWechatPayCallback 处理微信支付回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Context, r *http.Request) error {
|
||||
if s.wechatPayService == nil {
|
||||
s.logger.Error("微信支付服务未初始化")
|
||||
return fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
// 解析并验证微信支付回调通知
|
||||
transaction, err := s.wechatPayService.HandleWechatPayNotification(ctx, r)
|
||||
if err != nil {
|
||||
s.logger.Error("微信支付回调验证失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 提取回调数据
|
||||
outTradeNo := ""
|
||||
if transaction.OutTradeNo != nil {
|
||||
outTradeNo = *transaction.OutTradeNo
|
||||
}
|
||||
transactionID := ""
|
||||
if transaction.TransactionId != nil {
|
||||
transactionID = *transaction.TransactionId
|
||||
}
|
||||
tradeState := ""
|
||||
if transaction.TradeState != nil {
|
||||
tradeState = *transaction.TradeState
|
||||
}
|
||||
totalAmount := decimal.Zero
|
||||
if transaction.Amount != nil && transaction.Amount.Total != nil {
|
||||
// 将分转换为元
|
||||
totalAmount = decimal.NewFromInt(*transaction.Amount.Total).Div(decimal.NewFromInt(100))
|
||||
}
|
||||
|
||||
// 记录回调数据
|
||||
s.logger.Info("微信支付回调数据",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("trade_state", tradeState),
|
||||
zap.String("total_amount", totalAmount.String()),
|
||||
)
|
||||
|
||||
// 检查交易状态
|
||||
if tradeState != payment.TradeStateSuccess {
|
||||
s.logger.Warn("微信支付交易未成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_state", tradeState),
|
||||
)
|
||||
return nil // 不返回错误,因为这是正常的业务状态
|
||||
}
|
||||
|
||||
// 处理支付成功逻辑
|
||||
err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", totalAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processWechatPaymentSuccess 处理微信支付成功的公共逻辑
|
||||
func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.Context, outTradeNo, transactionID string, amount decimal.Decimal) error {
|
||||
// 查找微信订单
|
||||
wechatOrder, err := s.wechatOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找微信订单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找微信订单失败: %w", err)
|
||||
}
|
||||
|
||||
if wechatOrder == nil {
|
||||
s.logger.Error("微信订单不存在",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 查找对应的充值记录
|
||||
rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("查找充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", wechatOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志)
|
||||
if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess {
|
||||
s.logger.Info("微信支付订单已处理成功,跳过重复处理",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("order_id", wechatOrder.ID),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算充值赠送金额(复用支付宝的赠送逻辑)
|
||||
bonusAmount := decimal.Zero
|
||||
if len(s.config.Wallet.AliPayRechargeBonus) > 0 {
|
||||
for i := len(s.config.Wallet.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := s.config.Wallet.AliPayRechargeBonus[i]
|
||||
if amount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
bonusAmount = decimal.NewFromFloat(rule.BonusAmount)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录开始处理支付成功
|
||||
s.logger.Info("开始处理微信支付成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
)
|
||||
|
||||
// 在事务中处理支付成功逻辑
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 更新微信订单状态
|
||||
wechatOrder.MarkSuccess(transactionID, "", "", amount, amount)
|
||||
now := time.Now()
|
||||
wechatOrder.NotifyTime = &now
|
||||
err := s.wechatOrderRepo.Update(txCtx, *wechatOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新微信订单状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功
|
||||
rechargeRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, *rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := finance_entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
createdGift, err := s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
s.logger.Info("创建赠送充值记录成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("gift_recharge_id", createdGift.ID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
)
|
||||
}
|
||||
|
||||
// 充值到钱包(包含赠送金额)
|
||||
totalRechargeAmount := amount.Add(bonusAmount)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("充值到钱包失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("微信支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleWechatRefundCallback 处理微信退款回调
|
||||
func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.Context, r *http.Request) error {
|
||||
if s.wechatPayService == nil {
|
||||
s.logger.Error("微信支付服务未初始化")
|
||||
return fmt.Errorf("微信支付服务未初始化")
|
||||
}
|
||||
|
||||
// 解析并验证微信退款回调通知
|
||||
refund, err := s.wechatPayService.HandleRefundNotification(ctx, r)
|
||||
if err != nil {
|
||||
s.logger.Error("微信退款回调验证失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录回调数据
|
||||
s.logger.Info("微信退款回调数据",
|
||||
zap.String("out_trade_no", func() string {
|
||||
if refund.OutTradeNo != nil {
|
||||
return *refund.OutTradeNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("out_refund_no", func() string {
|
||||
if refund.OutRefundNo != nil {
|
||||
return *refund.OutRefundNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("refund_id", func() string {
|
||||
if refund.RefundId != nil {
|
||||
return *refund.RefundId
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.Any("status", func() interface{} {
|
||||
if refund.Status != nil {
|
||||
return *refund.Status
|
||||
}
|
||||
return nil
|
||||
}()),
|
||||
)
|
||||
|
||||
// 处理退款逻辑
|
||||
// 这里可以根据实际业务需求实现退款处理逻辑
|
||||
s.logger.Info("微信退款回调处理完成",
|
||||
zap.String("out_trade_no", func() string {
|
||||
if refund.OutTradeNo != nil {
|
||||
return *refund.OutTradeNo
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
zap.String("refund_id", func() string {
|
||||
if refund.RefundId != nil {
|
||||
return *refund.RefundId
|
||||
}
|
||||
return ""
|
||||
}()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
||||
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
// 5. 构建响应DTO
|
||||
return &dto.AvailableAmountResponse{
|
||||
AvailableAmount: availableAmount,
|
||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
|
||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
|
||||
TotalGifted: totalGifted,
|
||||
TotalInvoiced: totalInvoiced,
|
||||
PendingApplications: pendingAmount,
|
||||
@@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
||||
|
||||
// calculateAvailableAmount 计算可开票金额(私有方法)
|
||||
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
||||
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, err
|
||||
@@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
|
||||
fmt.Println("totalInvoiced", totalInvoiced)
|
||||
fmt.Println("pendingAmount", pendingAmount)
|
||||
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
||||
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
||||
fmt.Println("availableAmount", availableAmount)
|
||||
// 确保可开票金额不为负数
|
||||
@@ -452,16 +452,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us
|
||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
|
||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
|
||||
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
|
||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
|
||||
var totalGifted decimal.Decimal // 总赠送金额
|
||||
for _, record := range rechargeRecords {
|
||||
if record.IsSuccess() {
|
||||
if record.RechargeType == entities.RechargeTypeGift {
|
||||
// 赠送金额不计入可开票金额
|
||||
totalGifted = totalGifted.Add(record.Amount)
|
||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
|
||||
// 只有支付宝充值和对公转账计入可开票金额
|
||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
|
||||
// 支付宝充值、微信充值和对公转账计入可开票金额
|
||||
realRecharged = realRecharged.Add(record.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
@@ -28,6 +30,9 @@ type DocumentationApplicationServiceInterface interface {
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
|
||||
}
|
||||
|
||||
// DocumentationApplicationService 文档应用服务
|
||||
@@ -53,6 +58,7 @@ func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Contex
|
||||
ResponseFields: cmd.ResponseFields,
|
||||
ResponseExample: cmd.ResponseExample,
|
||||
ErrorCodes: cmd.ErrorCodes,
|
||||
PDFFilePath: cmd.PDFFilePath,
|
||||
}
|
||||
|
||||
// 调用领域服务创建文档
|
||||
@@ -88,6 +94,20 @@ func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Contex
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新PDF文件路径(如果提供)
|
||||
if cmd.PDFFilePath != "" {
|
||||
doc.PDFFilePath = cmd.PDFFilePath
|
||||
err = s.docService.UpdateDocumentationEntity(ctx, doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
|
||||
}
|
||||
// 重新获取更新后的文档以确保获取最新数据
|
||||
doc, err = s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
@@ -136,3 +156,93 @@ func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx cont
|
||||
|
||||
return docResponses, nil
|
||||
}
|
||||
|
||||
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
|
||||
// 通过产品ID获取文档
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取文档失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取文档时已经包含了产品信息(通过GetDocumentationWithProduct)
|
||||
// 如果没有产品信息,通过文档ID获取
|
||||
if doc.Product == nil && doc.ID != "" {
|
||||
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
|
||||
if err == nil && docWithProduct != nil {
|
||||
doc = docWithProduct
|
||||
}
|
||||
}
|
||||
|
||||
var markdown strings.Builder
|
||||
|
||||
// 添加文档标题
|
||||
productName := "产品"
|
||||
if doc.Product != nil {
|
||||
productName = doc.Product.Name
|
||||
}
|
||||
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
|
||||
|
||||
// 添加产品基本信息
|
||||
if doc.Product != nil {
|
||||
markdown.WriteString("## 产品信息\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
|
||||
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
|
||||
if doc.Product.Description != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
|
||||
}
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式
|
||||
markdown.WriteString("## 请求方式\n\n")
|
||||
if doc.RequestURL != "" {
|
||||
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
|
||||
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
|
||||
markdown.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加请求方式详细说明
|
||||
if doc.BasicInfo != "" {
|
||||
markdown.WriteString("### 请求方式说明\n\n")
|
||||
markdown.WriteString(doc.BasicInfo)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加请求参数
|
||||
if doc.RequestParams != "" {
|
||||
markdown.WriteString("## 请求参数\n\n")
|
||||
markdown.WriteString(doc.RequestParams)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加返回字段说明
|
||||
if doc.ResponseFields != "" {
|
||||
markdown.WriteString("## 返回字段说明\n\n")
|
||||
markdown.WriteString(doc.ResponseFields)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加响应示例
|
||||
if doc.ResponseExample != "" {
|
||||
markdown.WriteString("## 响应示例\n\n")
|
||||
markdown.WriteString(doc.ResponseExample)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加错误代码
|
||||
if doc.ErrorCodes != "" {
|
||||
markdown.WriteString("## 错误代码\n\n")
|
||||
markdown.WriteString(doc.ErrorCodes)
|
||||
markdown.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 添加文档版本信息
|
||||
markdown.WriteString("---\n\n")
|
||||
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
|
||||
if doc.UpdatedAt.Year() > 1900 {
|
||||
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
}
|
||||
|
||||
return markdown.String(), nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type CreateDocumentationCommand struct {
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateDocumentationCommand 更新文档命令
|
||||
@@ -21,4 +22,5 @@ type UpdateDocumentationCommand struct {
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type DocumentationResponse struct {
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
Version string `json:"version"`
|
||||
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -35,6 +36,7 @@ func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationR
|
||||
ResponseExample: doc.ResponseExample,
|
||||
ErrorCodes: doc.ErrorCodes,
|
||||
Version: doc.Version,
|
||||
PDFFilePath: doc.PDFFilePath,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
// ProductApplicationService 产品应用服务接口
|
||||
type ProductApplicationService interface {
|
||||
// 产品管理
|
||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error
|
||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error)
|
||||
|
||||
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
||||
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
||||
|
||||
@@ -46,6 +47,4 @@ 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
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func NewProductApplicationService(
|
||||
|
||||
// CreateProduct 创建产品
|
||||
// 业务流程<E6B581>?. 构建产品实体 2. 创建产品
|
||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error {
|
||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
|
||||
// 1. 构建产品实体
|
||||
product := &entities.Product{
|
||||
Name: cmd.Name,
|
||||
@@ -71,8 +71,13 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *
|
||||
}
|
||||
|
||||
// 2. 创建产品
|
||||
_, err := s.productManagementService.CreateProduct(ctx, product)
|
||||
return err
|
||||
createdProduct, err := s.productManagementService.CreateProduct(ctx, product)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 转换为响应对象
|
||||
return s.convertToProductAdminInfoResponse(createdProduct), nil
|
||||
}
|
||||
|
||||
// UpdateProduct 更新产品
|
||||
@@ -965,7 +970,7 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||
"JRZQ0A03": &dto.JRZQ0A03Req{},
|
||||
"JRZQ4AA8": &dto.JRZQ4AA8Req{},
|
||||
"JRZQ8203": &dto.JRZQ8203Req{},
|
||||
"JRZQDBCE": &dto.JRZQDCBEReq{},
|
||||
"JRZQDCBE": &dto.JRZQDCBEReq{},
|
||||
"QYGL2ACD": &dto.QYGL2ACDReq{},
|
||||
"QYGL6F2D": &dto.QYGL6F2DReq{},
|
||||
"QYGL45BD": &dto.QYGL45BDReq{},
|
||||
@@ -982,7 +987,7 @@ func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} {
|
||||
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||
"IVYZ0b03": &dto.IVYZ0b03Req{},
|
||||
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||
|
||||
@@ -20,6 +20,7 @@ type SubscriptionApplicationService interface {
|
||||
// 我的订阅(用户专用)
|
||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
||||
CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error
|
||||
|
||||
// 业务查询
|
||||
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||
product_service "tyapi-server/internal/domains/product/services"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
type SubscriptionApplicationServiceImpl struct {
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
userRepo user_repositories.UserRepository
|
||||
apiCallRepository domain_api_repo.ApiCallRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -28,11 +30,13 @@ type SubscriptionApplicationServiceImpl struct {
|
||||
func NewSubscriptionApplicationService(
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
userRepo user_repositories.UserRepository,
|
||||
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||
logger *zap.Logger,
|
||||
) SubscriptionApplicationService {
|
||||
return &SubscriptionApplicationServiceImpl{
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
userRepo: userRepo,
|
||||
apiCallRepository: apiCallRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -262,17 +266,30 @@ func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context
|
||||
}
|
||||
|
||||
// GetSubscriptionUsage 获取订阅使用情况
|
||||
// 业务流程:1. 获取订阅使用情况 2. 构建响应数据
|
||||
// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
|
||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||
// 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 根据用户ID和产品ID统计API调用次数
|
||||
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
|
||||
if err != nil {
|
||||
s.logger.Warn("统计API调用次数失败,使用订阅记录中的值",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("user_id", subscription.UserID),
|
||||
zap.String("product_id", subscription.ProductID),
|
||||
zap.Error(err))
|
||||
// 如果统计失败,使用订阅实体中的APIUsed字段作为备选
|
||||
apiCallCount = subscription.APIUsed
|
||||
}
|
||||
|
||||
return &responses.SubscriptionUsageResponse{
|
||||
ID: subscription.ID,
|
||||
ProductID: subscription.ProductID,
|
||||
APIUsed: subscription.APIUsed,
|
||||
APIUsed: apiCallCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -304,6 +321,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelMySubscription 取消我的订阅
|
||||
// 业务流程:1. 验证订阅是否属于当前用户 2. 取消订阅
|
||||
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
|
||||
// 1. 获取订阅信息
|
||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("订阅不存在")
|
||||
}
|
||||
|
||||
// 2. 验证订阅是否属于当前用户
|
||||
if subscription.UserID != userID {
|
||||
s.logger.Warn("用户尝试取消不属于自己的订阅",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.String("subscription_user_id", subscription.UserID))
|
||||
return fmt.Errorf("无权取消此订阅")
|
||||
}
|
||||
|
||||
// 3. 取消订阅(软删除)
|
||||
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
|
||||
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||
return fmt.Errorf("取消订阅失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("用户取消订阅成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("subscription_id", subscriptionID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||
// 查询用户信息
|
||||
|
||||
@@ -1301,8 +1301,10 @@ func (s *StatisticsApplicationServiceImpl) getUserApiCallsStats(ctx context.Cont
|
||||
}
|
||||
|
||||
// 获取今日调用次数
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayCalls, err := s.getApiCallsCountByDateRange(ctx, userID, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日API调用次数失败", zap.String("user_id", userID), zap.Error(err))
|
||||
@@ -1356,8 +1358,10 @@ func (s *StatisticsApplicationServiceImpl) getUserConsumptionStats(ctx context.C
|
||||
}
|
||||
|
||||
// 获取今日消费金额
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayAmount, err := s.getWalletTransactionsByDateRange(ctx, userID, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日消费金额失败", zap.String("user_id", userID), zap.Error(err))
|
||||
@@ -1411,8 +1415,10 @@ func (s *StatisticsApplicationServiceImpl) getUserRechargeStats(ctx context.Cont
|
||||
}
|
||||
|
||||
// 获取今日充值金额
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayAmount, err := s.getRechargeRecordsByDateRange(ctx, userID, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日充值金额失败", zap.String("user_id", userID), zap.Error(err))
|
||||
@@ -1682,13 +1688,13 @@ func (s *StatisticsApplicationServiceImpl) getCertificationStats(ctx context.Con
|
||||
successRate = float64(userStats.CertifiedUsers) / float64(userStats.TotalUsers)
|
||||
}
|
||||
|
||||
// 根据时间范围获取趋势数据
|
||||
// 根据时间范围获取认证趋势数据(基于is_certified字段)
|
||||
var trendData []map[string]interface{}
|
||||
if !startTime.IsZero() && !endTime.IsZero() {
|
||||
if period == "day" {
|
||||
trendData, err = s.userRepo.GetSystemDailyUserStats(ctx, startTime, endTime)
|
||||
trendData, err = s.userRepo.GetSystemDailyCertificationStats(ctx, startTime, endTime)
|
||||
} else if period == "month" {
|
||||
trendData, err = s.userRepo.GetSystemMonthlyUserStats(ctx, startTime, endTime)
|
||||
trendData, err = s.userRepo.GetSystemMonthlyCertificationStats(ctx, startTime, endTime)
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("获取认证趋势数据失败", zap.Error(err))
|
||||
@@ -1698,16 +1704,35 @@ func (s *StatisticsApplicationServiceImpl) getCertificationStats(ctx context.Con
|
||||
// 默认获取最近7天的数据
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, 0, -7)
|
||||
trendData, err = s.userRepo.GetSystemDailyUserStats(ctx, startDate, endDate)
|
||||
trendData, err = s.userRepo.GetSystemDailyCertificationStats(ctx, startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("获取认证每日趋势失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 获取今日认证用户数(基于is_certified字段,东八时区)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
|
||||
var certifiedToday int64
|
||||
todayCertStats, err := s.userRepo.GetSystemDailyCertificationStats(ctx, today, tomorrow)
|
||||
if err == nil && len(todayCertStats) > 0 {
|
||||
// 累加今日所有认证用户数
|
||||
for _, stat := range todayCertStats {
|
||||
if count, ok := stat["count"].(int64); ok {
|
||||
certifiedToday += count
|
||||
} else if count, ok := stat["count"].(int); ok {
|
||||
certifiedToday += int64(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_certified": userStats.CertifiedUsers,
|
||||
"certified_today": userStats.TodayRegistrations, // 今日注册的用户
|
||||
"certified_today": certifiedToday, // 今日认证的用户数(基于is_certified字段)
|
||||
"success_rate": successRate,
|
||||
"daily_trend": trendData,
|
||||
}
|
||||
@@ -1723,9 +1748,11 @@ func (s *StatisticsApplicationServiceImpl) getSystemApiCallStats(ctx context.Con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取今日API调用次数
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
// 获取今日API调用次数(东八时区)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayCalls, err := s.apiCallRepo.GetSystemCallsByDateRange(ctx, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日API调用次数失败", zap.Error(err))
|
||||
@@ -1780,8 +1807,11 @@ func (s *StatisticsApplicationServiceImpl) getSystemFinanceStats(ctx context.Con
|
||||
}
|
||||
|
||||
// 获取今日消费金额
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
|
||||
todayConsumption, err := s.walletTransactionRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
||||
@@ -2275,6 +2305,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetApiDomainStatistics(ctx conte
|
||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
// 如果是月统计,将开始日期调整为当月1号00:00:00
|
||||
if period == "month" {
|
||||
startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, startTime.Location())
|
||||
}
|
||||
}
|
||||
if endDate != "" {
|
||||
endTime, err = time.Parse("2006-01-02", endDate)
|
||||
@@ -2282,6 +2316,14 @@ func (s *StatisticsApplicationServiceImpl) AdminGetApiDomainStatistics(ctx conte
|
||||
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if period == "month" {
|
||||
// 如果是月统计,将结束日期调整为下个月1号00:00:00
|
||||
// 这样在查询时使用 created_at < endTime 可以包含整个月份的数据(到本月最后一天23:59:59.999)
|
||||
endTime = time.Date(endTime.Year(), endTime.Month()+1, 1, 0, 0, 0, 0, endTime.Location())
|
||||
} else {
|
||||
// 日统计:将结束日期设置为次日00:00:00,这样在查询时使用 created_at < endTime 可以包含当天的所有数据
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取API调用统计数据
|
||||
@@ -2318,6 +2360,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
// 如果是月统计,将开始日期调整为当月1号00:00:00
|
||||
if period == "month" {
|
||||
startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, startTime.Location())
|
||||
}
|
||||
}
|
||||
if endDate != "" {
|
||||
endTime, err = time.Parse("2006-01-02", endDate)
|
||||
@@ -2325,6 +2371,14 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
||||
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if period == "month" {
|
||||
// 如果是月统计,将结束日期调整为下个月1号00:00:00
|
||||
// 这样在查询时使用 created_at < endTime 可以包含整个月份的数据(到本月最后一天23:59:59.999)
|
||||
endTime = time.Date(endTime.Year(), endTime.Month()+1, 1, 0, 0, 0, 0, endTime.Location())
|
||||
} else {
|
||||
// 日统计:将结束日期设置为次日00:00:00,这样在查询时使用 created_at < endTime 可以包含当天的所有数据
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取消费统计数据
|
||||
@@ -2335,8 +2389,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
||||
}
|
||||
|
||||
// 获取今日消费金额
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayConsumption, err := s.walletTransactionRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
||||
@@ -2370,7 +2426,7 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
||||
defaultEndDate := time.Now()
|
||||
defaultStartDate := defaultEndDate.AddDate(0, 0, -7)
|
||||
consumptionTrend, err = s.walletTransactionRepo.GetSystemDailyStats(ctx, defaultStartDate, defaultEndDate)
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
s.logger.Error("获取消费每日趋势失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
@@ -2406,6 +2462,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetRechargeDomainStatistics(ctx
|
||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
// 如果是月统计,将开始日期调整为当月1号00:00:00
|
||||
if period == "month" {
|
||||
startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, startTime.Location())
|
||||
}
|
||||
}
|
||||
if endDate != "" {
|
||||
endTime, err = time.Parse("2006-01-02", endDate)
|
||||
@@ -2413,6 +2473,14 @@ func (s *StatisticsApplicationServiceImpl) AdminGetRechargeDomainStatistics(ctx
|
||||
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if period == "month" {
|
||||
// 如果是月统计,将结束日期调整为下个月1号00:00:00
|
||||
// 这样在查询时使用 created_at < endTime 可以包含整个月份的数据(到本月最后一天23:59:59.999)
|
||||
endTime = time.Date(endTime.Year(), endTime.Month()+1, 1, 0, 0, 0, 0, endTime.Location())
|
||||
} else {
|
||||
// 日统计:将结束日期设置为次日00:00:00,这样在查询时使用 created_at < endTime 可以包含当天的所有数据
|
||||
endTime = endTime.AddDate(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取充值统计数据
|
||||
@@ -2423,8 +2491,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetRechargeDomainStatistics(ctx
|
||||
}
|
||||
|
||||
// 获取今日充值金额
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||
now := time.Now().In(loc)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) // 当天0点
|
||||
tomorrow := today.AddDate(0, 0, 1) // 次日0点
|
||||
todayRecharge, err := s.rechargeRecordRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||
if err != nil {
|
||||
s.logger.Error("获取今日充值金额失败", zap.Error(err))
|
||||
@@ -2716,15 +2786,15 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
|
||||
}
|
||||
|
||||
enterprise := map[string]interface{}{
|
||||
"id": cert.ID,
|
||||
"user_id": cert.UserID,
|
||||
"username": user.Username,
|
||||
"enterprise_name": enterpriseInfo.CompanyName,
|
||||
"legal_person_name": enterpriseInfo.LegalPersonName,
|
||||
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
|
||||
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
|
||||
"enterprise_address": enterpriseInfo.EnterpriseAddress,
|
||||
"certified_at": cert.CompletedAt.Format(time.RFC3339),
|
||||
"id": cert.ID,
|
||||
"user_id": cert.UserID,
|
||||
"username": user.Username,
|
||||
"enterprise_name": enterpriseInfo.CompanyName,
|
||||
"legal_person_name": enterpriseInfo.LegalPersonName,
|
||||
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
|
||||
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
|
||||
"enterprise_address": enterpriseInfo.EnterpriseAddress,
|
||||
"certified_at": cert.CompletedAt.Format(time.RFC3339),
|
||||
}
|
||||
enterprises = append(enterprises, enterprise)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ type Config struct {
|
||||
Zhicha ZhichaConfig `mapstructure:"zhicha"`
|
||||
Muzi MuziConfig `mapstructure:"muzi"`
|
||||
AliPay AliPayConfig `mapstructure:"alipay"`
|
||||
Wxpay WxpayConfig `mapstructure:"wxpay"`
|
||||
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
|
||||
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
|
||||
Yushan YushanConfig `mapstructure:"yushan"`
|
||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||
@@ -429,6 +432,29 @@ type AliPayConfig struct {
|
||||
ReturnURL string `mapstructure:"return_url"`
|
||||
}
|
||||
|
||||
// WxpayConfig 微信支付配置
|
||||
type WxpayConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
MchID string `mapstructure:"mch_id"`
|
||||
MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"`
|
||||
MchApiv3Key string `mapstructure:"mch_apiv3_key"`
|
||||
MchPrivateKeyPath string `mapstructure:"mch_private_key_path"`
|
||||
MchPublicKeyID string `mapstructure:"mch_public_key_id"`
|
||||
MchPublicKeyPath string `mapstructure:"mch_public_key_path"`
|
||||
NotifyUrl string `mapstructure:"notify_url"`
|
||||
RefundNotifyUrl string `mapstructure:"refund_notify_url"`
|
||||
}
|
||||
|
||||
// WechatMiniConfig 微信小程序配置
|
||||
type WechatMiniConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
}
|
||||
|
||||
// WechatH5Config 微信H5配置
|
||||
type WechatH5Config struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
}
|
||||
|
||||
// YushanConfig 羽山配置
|
||||
type YushanConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
@@ -3,6 +3,8 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
@@ -66,6 +68,7 @@ import (
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
sharedOCR "tyapi-server/internal/shared/ocr"
|
||||
"tyapi-server/internal/shared/payment"
|
||||
"tyapi-server/internal/shared/pdf"
|
||||
"tyapi-server/internal/shared/resilience"
|
||||
"tyapi-server/internal/shared/saga"
|
||||
"tyapi-server/internal/shared/tracing"
|
||||
@@ -304,6 +307,16 @@ func NewContainer() *Container {
|
||||
}
|
||||
return payment.NewAliPayService(config)
|
||||
},
|
||||
// 微信支付服务
|
||||
func(cfg *config.Config, logger *zap.Logger) *payment.WechatPayService {
|
||||
// 根据配置选择初始化方式,默认使用平台证书方式
|
||||
initType := payment.InitTypePlatformCert
|
||||
// 如果配置了公钥ID,使用公钥方式
|
||||
if cfg.Wxpay.MchPublicKeyID != "" {
|
||||
initType = payment.InitTypeWxPayPubKey
|
||||
}
|
||||
return payment.NewWechatPayService(*cfg, initType, logger)
|
||||
},
|
||||
// 导出管理器
|
||||
func(logger *zap.Logger) *export.ExportManager {
|
||||
return export.NewExportManager(logger)
|
||||
@@ -509,6 +522,11 @@ func NewContainer() *Container {
|
||||
finance_repo.NewGormAlipayOrderRepository,
|
||||
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
||||
),
|
||||
// 微信订单仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormWechatOrderRepository,
|
||||
fx.As(new(domain_finance_repo.WechatOrderRepository)),
|
||||
),
|
||||
// 发票申请仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormInvoiceApplicationRepository,
|
||||
@@ -571,6 +589,11 @@ func NewContainer() *Container {
|
||||
article_repo.NewGormScheduledTaskRepository,
|
||||
fx.As(new(domain_article_repo.ScheduledTaskRepository)),
|
||||
),
|
||||
// 公告仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
article_repo.NewGormAnnouncementRepository,
|
||||
fx.As(new(domain_article_repo.AnnouncementRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// API域仓储层
|
||||
@@ -675,6 +698,8 @@ func NewContainer() *Container {
|
||||
certification_service.NewEnterpriseInfoSubmitRecordService,
|
||||
// 文章领域服务
|
||||
article_service.NewArticleService,
|
||||
// 公告领域服务
|
||||
article_service.NewAnnouncementService,
|
||||
// 统计领域服务
|
||||
statistics_service.NewStatisticsAggregateService,
|
||||
statistics_service.NewStatisticsCalculationService,
|
||||
@@ -775,6 +800,7 @@ func NewContainer() *Container {
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
articleApplicationService article.ArticleApplicationService,
|
||||
announcementApplicationService article.AnnouncementApplicationService,
|
||||
apiApplicationService api_app.ApiApplicationService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
subscriptionService *product_services.ProductSubscriptionService,
|
||||
@@ -785,6 +811,7 @@ func NewContainer() *Container {
|
||||
redisAddr,
|
||||
logger,
|
||||
articleApplicationService,
|
||||
announcementApplicationService,
|
||||
apiApplicationService,
|
||||
walletService,
|
||||
subscriptionService,
|
||||
@@ -843,10 +870,13 @@ func NewContainer() *Container {
|
||||
fx.Annotate(
|
||||
func(
|
||||
aliPayClient *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
walletService finance_services.WalletAggregateService,
|
||||
rechargeRecordService finance_services.RechargeRecordService,
|
||||
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -855,10 +885,13 @@ func NewContainer() *Container {
|
||||
) finance.FinanceApplicationService {
|
||||
return finance.NewFinanceApplicationService(
|
||||
aliPayClient,
|
||||
wechatPayService,
|
||||
walletService,
|
||||
rechargeRecordService,
|
||||
walletTransactionRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
logger,
|
||||
@@ -941,6 +974,23 @@ func NewContainer() *Container {
|
||||
},
|
||||
fx.As(new(article.ArticleApplicationService)),
|
||||
),
|
||||
// 公告应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
announcementRepo domain_article_repo.AnnouncementRepository,
|
||||
announcementService *article_service.AnnouncementService,
|
||||
taskManager task_interfaces.TaskManager,
|
||||
logger *zap.Logger,
|
||||
) article.AnnouncementApplicationService {
|
||||
return article.NewAnnouncementApplicationService(
|
||||
announcementRepo,
|
||||
announcementService,
|
||||
taskManager,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
fx.As(new(article.AnnouncementApplicationService)),
|
||||
),
|
||||
// 统计应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
func(
|
||||
@@ -980,6 +1030,62 @@ func NewContainer() *Container {
|
||||
),
|
||||
),
|
||||
|
||||
// PDF查找服务
|
||||
fx.Provide(
|
||||
func(logger *zap.Logger) (*pdf.PDFFinder, error) {
|
||||
docDir, err := pdf.GetDocumentationDir()
|
||||
if err != nil {
|
||||
logger.Warn("未找到接口文档文件夹,PDF自动查找功能将不可用", zap.Error(err))
|
||||
return nil, nil // 返回nil,handler中会检查
|
||||
}
|
||||
logger.Info("PDF查找服务已初始化", zap.String("documentation_dir", docDir))
|
||||
return pdf.NewPDFFinder(docDir, logger), nil
|
||||
},
|
||||
),
|
||||
// PDF生成器
|
||||
fx.Provide(
|
||||
func(logger *zap.Logger) *pdf.PDFGenerator {
|
||||
return pdf.NewPDFGenerator(logger)
|
||||
},
|
||||
),
|
||||
// PDF缓存管理器
|
||||
fx.Provide(
|
||||
func(logger *zap.Logger) (*pdf.PDFCacheManager, error) {
|
||||
// 使用默认配置:缓存目录在临时目录,TTL为24小时,最大缓存大小为500MB
|
||||
cacheDir := "" // 使用默认目录(临时目录下的tyapi_pdf_cache)
|
||||
ttl := 24 * time.Hour
|
||||
maxSize := int64(500 * 1024 * 1024) // 500MB
|
||||
|
||||
// 可以通过环境变量覆盖
|
||||
if envCacheDir := os.Getenv("PDF_CACHE_DIR"); envCacheDir != "" {
|
||||
cacheDir = envCacheDir
|
||||
}
|
||||
if envTTL := os.Getenv("PDF_CACHE_TTL"); envTTL != "" {
|
||||
if parsedTTL, err := time.ParseDuration(envTTL); err == nil {
|
||||
ttl = parsedTTL
|
||||
}
|
||||
}
|
||||
if envMaxSize := os.Getenv("PDF_CACHE_MAX_SIZE"); envMaxSize != "" {
|
||||
if parsedMaxSize, err := strconv.ParseInt(envMaxSize, 10, 64); err == nil {
|
||||
maxSize = parsedMaxSize
|
||||
}
|
||||
}
|
||||
|
||||
cacheManager, err := pdf.NewPDFCacheManager(logger, cacheDir, ttl, maxSize)
|
||||
if err != nil {
|
||||
logger.Warn("PDF缓存管理器初始化失败,将禁用缓存功能", zap.Error(err))
|
||||
return nil, nil // 返回nil,handler中会检查
|
||||
}
|
||||
|
||||
logger.Info("PDF缓存管理器已初始化",
|
||||
zap.String("cache_dir", cacheDir),
|
||||
zap.Duration("ttl", ttl),
|
||||
zap.Int64("max_size", maxSize),
|
||||
)
|
||||
|
||||
return cacheManager, nil
|
||||
},
|
||||
),
|
||||
// HTTP处理器
|
||||
fx.Provide(
|
||||
// 用户HTTP处理器
|
||||
@@ -1005,6 +1111,15 @@ func NewContainer() *Container {
|
||||
) *handlers.ArticleHandler {
|
||||
return handlers.NewArticleHandler(appService, responseBuilder, validator, logger)
|
||||
},
|
||||
// 公告HTTP处理器
|
||||
func(
|
||||
appService article.AnnouncementApplicationService,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *handlers.AnnouncementHandler {
|
||||
return handlers.NewAnnouncementHandler(appService, responseBuilder, validator, logger)
|
||||
},
|
||||
),
|
||||
|
||||
// 路由注册
|
||||
@@ -1021,6 +1136,8 @@ func NewContainer() *Container {
|
||||
routes.NewProductAdminRoutes,
|
||||
// 文章路由
|
||||
routes.NewArticleRoutes,
|
||||
// 公告路由
|
||||
routes.NewAnnouncementRoutes,
|
||||
// API路由
|
||||
routes.NewApiRoutes,
|
||||
// 统计路由
|
||||
@@ -1132,6 +1249,7 @@ func RegisterRoutes(
|
||||
productRoutes *routes.ProductRoutes,
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
announcementRoutes *routes.AnnouncementRoutes,
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
statisticsRoutes *routes.StatisticsRoutes,
|
||||
cfg *config.Config,
|
||||
@@ -1149,6 +1267,7 @@ func RegisterRoutes(
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
articleRoutes.Register(router)
|
||||
announcementRoutes.Register(router)
|
||||
statisticsRoutes.Register(router)
|
||||
|
||||
// 打印注册的路由信息
|
||||
|
||||
@@ -133,6 +133,13 @@ type QYGL23T7Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type QYGL5CMPReq struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
type YYSY4B37Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
@@ -151,7 +158,7 @@ type YYSY09CDReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
type IVYZ0b03Req struct {
|
||||
type IVYZ0B03Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
@@ -195,6 +202,18 @@ type IVYZGZ08Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type IVYZ2B2TReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
QueryReasonId int64 `json:"query_reason_id" validate:"required"`
|
||||
}
|
||||
|
||||
type IVYZ5A9OReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||
}
|
||||
|
||||
type FLXG8A3FReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
@@ -304,6 +323,17 @@ type IVYZ9K2LReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
type IVYZP2Q6Req struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type JRZQ1W4XReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type IVYZ9D2EReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
@@ -327,26 +357,26 @@ type DWBG7F3AReq struct {
|
||||
// 新增的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"`
|
||||
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type QYGL8B4DReq struct {
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int `json:"page_num" validate:"omitempty,min=1"`
|
||||
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type QYGL9E2FReq struct {
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int `json:"page_num" validate:"omitempty,min=1"`
|
||||
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type QYGL7C1AReq struct {
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
PageSize int `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int `json:"page_num" validate:"omitempty,min=1"`
|
||||
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
|
||||
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
type QYGL3F8EReq struct {
|
||||
@@ -362,11 +392,11 @@ type YYSY4F2EReq struct {
|
||||
|
||||
type YYSY9F1BReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Phone string `json:"phone" validate:"required,min=11,max=11,validMobileNo"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
type YYSY6F2BReq struct {
|
||||
Phone string `json:"phone" validate:"required,min=11,max=11,validMobileNo"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type YYSY8B1CReq struct {
|
||||
@@ -396,6 +426,31 @@ type FLXG9C1DReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// 法院被执行人限高版
|
||||
type FLXG3A9BReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// 法院被执行人高级版
|
||||
type FLXGK5D2Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// 综合多头
|
||||
|
||||
type JRZQ8F7CReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type FLXG2E8FReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
@@ -416,6 +471,20 @@ type JRZQ3C9RReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type JRZQ3P01Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// JRZQ3AG6Req JRZQ3AG6 轻松查公积API处理方法
|
||||
type JRZQ3AG6Req struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
|
||||
AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"`
|
||||
}
|
||||
type JRZQ8A2DReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
@@ -436,15 +505,15 @@ type JRZQ0B6YReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
BankCard string `json:"bank_card" validate:"omitempty,validBankCard"`
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
}
|
||||
|
||||
// 银行卡鉴权
|
||||
type JRZQ9A1WReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
BankCard string `json:"bank_card" validate:"omitempty,validBankCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
}
|
||||
|
||||
// 企业管理董监高司法综合信息核验
|
||||
@@ -520,6 +589,31 @@ type QYGL2B5CReq struct {
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// 全国企业借贷意向验证查询_V1
|
||||
type QYGL9T1QReq struct {
|
||||
OwnerType string `json:"owner_type" validate:"required,oneof=1 2 3 4 5"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
// 全国企业各类工商风险统计数量查询
|
||||
type QYGL5A9TReq struct {
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
}
|
||||
|
||||
// 失信被执行企业或个人查询
|
||||
type QYGL2S0WReq struct {
|
||||
Type string `json:"type" validate:"required,oneof=per ent"`
|
||||
Name string `json:"name" validate:"omitempty,min=1,validName"`
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
|
||||
type JRZQ2F8AReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
@@ -588,9 +682,7 @@ type FLXG7E8FReq struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
|
||||
type IVYZ6G7HReq struct {
|
||||
@@ -604,6 +696,11 @@ type IVYZ8I9JReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type IVYZ6M8PReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type YYSY9E4AReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package entities
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -18,14 +20,86 @@ const (
|
||||
ApiUserStatusFrozen = "frozen"
|
||||
)
|
||||
|
||||
// WhiteListItem 白名单项,包含IP地址、添加时间和备注
|
||||
type WhiteListItem struct {
|
||||
IPAddress string `json:"ip_address"` // IP地址
|
||||
AddedAt time.Time `json:"added_at"` // 添加时间
|
||||
Remark string `json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// WhiteList 白名单类型,支持向后兼容(旧的字符串数组格式)
|
||||
type WhiteList []WhiteListItem
|
||||
|
||||
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||
func (w WhiteList) Value() (driver.Value, error) {
|
||||
if w == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
data, err := json.Marshal(w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口,用于数据库读取(支持向后兼容)
|
||||
func (w *WhiteList) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*w = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
bytes = v
|
||||
case string:
|
||||
bytes = []byte(v)
|
||||
default:
|
||||
return errors.New("无法扫描 WhiteList 类型")
|
||||
}
|
||||
|
||||
if len(bytes) == 0 || string(bytes) == "[]" || string(bytes) == "null" {
|
||||
*w = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 首先尝试解析为新格式(结构体数组)
|
||||
var items []WhiteListItem
|
||||
if err := json.Unmarshal(bytes, &items); err == nil {
|
||||
// 成功解析为新格式
|
||||
*w = WhiteList(items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果失败,尝试解析为旧格式(字符串数组)
|
||||
var oldFormat []string
|
||||
if err := json.Unmarshal(bytes, &oldFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将旧格式转换为新格式
|
||||
now := time.Now()
|
||||
items = make([]WhiteListItem, 0, len(oldFormat))
|
||||
for _, ip := range oldFormat {
|
||||
items = append(items, WhiteListItem{
|
||||
IPAddress: ip,
|
||||
AddedAt: now, // 使用当前时间作为添加时间(因为旧数据没有时间信息)
|
||||
Remark: "", // 旧数据没有备注信息
|
||||
})
|
||||
}
|
||||
*w = WhiteList(items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApiUser API用户(聚合根)
|
||||
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 WhiteList `gorm:"type:json;default:'[]'" json:"white_list"` // 支持多个白名单,包含IP和添加时间,支持向后兼容
|
||||
|
||||
// 余额预警配置
|
||||
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
||||
@@ -41,7 +115,7 @@ type ApiUser struct {
|
||||
// IsWhiteListed 校验IP/域名是否在白名单
|
||||
func (u *ApiUser) IsWhiteListed(target string) bool {
|
||||
for _, w := range u.WhiteList {
|
||||
if w == target {
|
||||
if w.IPAddress == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -77,7 +151,7 @@ func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold f
|
||||
AccessId: accessId,
|
||||
SecretKey: secretKey,
|
||||
Status: ApiUserStatusNormal,
|
||||
WhiteList: []string{},
|
||||
WhiteList: WhiteList{},
|
||||
BalanceAlertEnabled: defaultAlertEnabled,
|
||||
BalanceAlertThreshold: defaultAlertThreshold,
|
||||
}, nil
|
||||
@@ -90,12 +164,12 @@ func (u *ApiUser) Freeze() {
|
||||
func (u *ApiUser) Unfreeze() {
|
||||
u.Status = ApiUserStatusNormal
|
||||
}
|
||||
func (u *ApiUser) UpdateWhiteList(list []string) {
|
||||
u.WhiteList = list
|
||||
func (u *ApiUser) UpdateWhiteList(list []WhiteListItem) {
|
||||
u.WhiteList = WhiteList(list)
|
||||
}
|
||||
|
||||
// AddToWhiteList 新增白名单项(防御性校验)
|
||||
func (u *ApiUser) AddToWhiteList(entry string) error {
|
||||
func (u *ApiUser) AddToWhiteList(entry string, remark string) error {
|
||||
if len(u.WhiteList) >= 10 {
|
||||
return errors.New("白名单最多只能有10个")
|
||||
}
|
||||
@@ -103,27 +177,31 @@ func (u *ApiUser) AddToWhiteList(entry string) error {
|
||||
return errors.New("非法IP")
|
||||
}
|
||||
for _, w := range u.WhiteList {
|
||||
if w == entry {
|
||||
if w.IPAddress == entry {
|
||||
return errors.New("白名单已存在")
|
||||
}
|
||||
}
|
||||
u.WhiteList = append(u.WhiteList, entry)
|
||||
u.WhiteList = append(u.WhiteList, WhiteListItem{
|
||||
IPAddress: entry,
|
||||
AddedAt: time.Now(),
|
||||
Remark: remark,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
||||
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
||||
if u.WhiteList == nil {
|
||||
u.WhiteList = []string{}
|
||||
u.WhiteList = WhiteList{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveFromWhiteList 删除白名单项
|
||||
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||
newList := make([]string, 0, len(u.WhiteList))
|
||||
newList := make([]WhiteListItem, 0, len(u.WhiteList))
|
||||
for _, w := range u.WhiteList {
|
||||
if w != entry {
|
||||
if w.IPAddress != entry {
|
||||
newList = append(newList, w)
|
||||
}
|
||||
}
|
||||
@@ -216,9 +294,9 @@ func (u *ApiUser) Validate() error {
|
||||
if len(u.WhiteList) > 10 {
|
||||
return errors.New("白名单最多只能有10个")
|
||||
}
|
||||
for _, ip := range u.WhiteList {
|
||||
if net.ParseIP(ip) == nil {
|
||||
return errors.New("白名单项必须为合法IP地址: " + ip)
|
||||
for _, item := range u.WhiteList {
|
||||
if net.ParseIP(item.IPAddress) == nil {
|
||||
return errors.New("白名单项必须为合法IP地址: " + item.IPAddress)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -259,7 +337,26 @@ func (c *ApiUser) BeforeCreate(tx *gorm.DB) error {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
if c.WhiteList == nil {
|
||||
c.WhiteList = []string{}
|
||||
c.WhiteList = WhiteList{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind GORM钩子:查询后处理数据,确保AddedAt不为零值
|
||||
func (u *ApiUser) AfterFind(tx *gorm.DB) error {
|
||||
// 如果 WhiteList 为空,初始化为空数组
|
||||
if u.WhiteList == nil {
|
||||
u.WhiteList = WhiteList{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 确保所有项的AddedAt不为零值(处理可能从旧数据迁移的情况)
|
||||
now := time.Now()
|
||||
for i := range u.WhiteList {
|
||||
if u.WhiteList[i].AddedAt.IsZero() {
|
||||
u.WhiteList[i].AddedAt = now
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ type ApiCallRepository interface {
|
||||
// 新增:统计用户API调用次数
|
||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||
|
||||
// 新增:根据用户ID和产品ID统计API调用次数
|
||||
CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error)
|
||||
|
||||
// 新增:根据TransactionID查询
|
||||
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"FLXG9C1D": flxg.ProcessFLXG9C1DRequest,
|
||||
"FLXG2E8F": flxg.ProcessFLXG2E8FRequest,
|
||||
"FLXG7E8F": flxg.ProcessFLXG7E8FRequest,
|
||||
|
||||
"FLXG3A9B": flxg.ProcessFLXG3A9BRequest,
|
||||
"FLXGK5D2": flxg.ProcessFLXGK5D2Request,
|
||||
// JRZQ系列处理器
|
||||
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
|
||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||
@@ -128,6 +129,11 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQ3C9R": jrzq.ProcessJRZQ3C9RRequest,
|
||||
"JRZQ0B6Y": jrzq.ProcessJRZQ0B6YRequest,
|
||||
"JRZQ9A1W": jrzq.ProcessJRZQ9A1WRequest,
|
||||
"JRZQ8F7C": jrzq.ProcessJRZQ8F7CRequest,
|
||||
"JRZQ1W4X": jrzq.ProcessJRZQ1W4XRequest,
|
||||
"JRZQ3P01": jrzq.ProcessJRZQ3P01Request,
|
||||
"JRZQ3AG6": jrzq.ProcessJRZQ3AG6Request,
|
||||
|
||||
// QYGL系列处理器
|
||||
"QYGL8261": qygl.ProcessQYGL8261Request,
|
||||
"QYGL2ACD": qygl.ProcessQYGL2ACDRequest,
|
||||
@@ -147,6 +153,10 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QYGL5F6A": qygl.ProcessQYGL5F6ARequest, // 企业相关查询
|
||||
"QYGL2B5C": qygl.ProcessQYGL2B5CRequest, // 企业联系人实际经营地址
|
||||
"QYGL6S1B": qygl.ProcessQYGL6S1BRequest, //董监高司法综合信息核验
|
||||
"QYGL9T1Q": qygl.ProcessQYGL9T1QRequest, //全国企业借贷意向验证查询_V1
|
||||
"QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询
|
||||
"QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询
|
||||
"QYGL5CMP": qygl.ProcessQYGL5CMPRequest, //企业五要素验证
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||
@@ -192,6 +202,10 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ8I9J": ivyz.ProcessIVYZ8I9JRequest,
|
||||
"IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest,
|
||||
"IVYZ2C1P": ivyz.ProcessIVYZ2C1PRequest,
|
||||
"IVYZP2Q6": ivyz.ProcessIVYZP2Q6Request,
|
||||
"IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历)
|
||||
"IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
|
||||
|
||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
repo "tyapi-server/internal/domains/api/repositories"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
type ApiUserAggregateService interface {
|
||||
CreateApiUser(ctx context.Context, apiUserId string) error
|
||||
UpdateWhiteList(ctx context.Context, apiUserId string, whiteList []string) error
|
||||
AddToWhiteList(ctx context.Context, apiUserId string, entry string) error
|
||||
AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error
|
||||
RemoveFromWhiteList(ctx context.Context, apiUserId string, entry string) error
|
||||
FreezeApiUser(ctx context.Context, apiUserId string) error
|
||||
UnfreezeApiUser(ctx context.Context, apiUserId string) error
|
||||
@@ -44,16 +45,25 @@ func (s *ApiUserAggregateServiceImpl) UpdateWhiteList(ctx context.Context, apiUs
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiUser.UpdateWhiteList(whiteList)
|
||||
// 将字符串数组转换为WhiteListItem数组
|
||||
items := make([]entities.WhiteListItem, 0, len(whiteList))
|
||||
now := time.Now()
|
||||
for _, ip := range whiteList {
|
||||
items = append(items, entities.WhiteListItem{
|
||||
IPAddress: ip,
|
||||
AddedAt: now, // 批量更新时使用当前时间
|
||||
})
|
||||
}
|
||||
apiUser.UpdateWhiteList(items) // UpdateWhiteList 会转换为 WhiteList 类型
|
||||
return s.repo.Update(ctx, apiUser)
|
||||
}
|
||||
|
||||
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string) error {
|
||||
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error {
|
||||
apiUser, err := s.repo.FindByUserId(ctx, apiUserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = apiUser.AddToWhiteList(entry)
|
||||
err = apiUser.AddToWhiteList(entry, remark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -90,7 +100,6 @@ func (s *ApiUserAggregateServiceImpl) UnfreezeApiUser(ctx context.Context, apiUs
|
||||
return s.repo.Update(ctx, apiUser)
|
||||
}
|
||||
|
||||
|
||||
func (s *ApiUserAggregateServiceImpl) LoadApiUserByAccessId(ctx context.Context, accessId string) (*entities.ApiUser, error) {
|
||||
return s.repo.FindByAccessId(ctx, accessId)
|
||||
}
|
||||
@@ -100,12 +109,12 @@ func (s *ApiUserAggregateServiceImpl) LoadApiUserByUserId(ctx context.Context, a
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
// 确保WhiteList不为nil
|
||||
if apiUser.WhiteList == nil {
|
||||
apiUser.WhiteList = []string{}
|
||||
apiUser.WhiteList = entities.WhiteList{}
|
||||
}
|
||||
|
||||
|
||||
return apiUser, nil
|
||||
}
|
||||
|
||||
@@ -117,10 +126,10 @@ func (s *ApiUserAggregateServiceImpl) SaveApiUser(ctx context.Context, apiUser *
|
||||
if exists != nil {
|
||||
// 确保WhiteList不为nil
|
||||
if apiUser.WhiteList == nil {
|
||||
apiUser.WhiteList = []string{}
|
||||
apiUser.WhiteList = []entities.WhiteListItem{}
|
||||
}
|
||||
return s.repo.Update(ctx, apiUser)
|
||||
} else {
|
||||
return s.repo.Create(ctx, apiUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"YYSY4B21": &dto.YYSY4B21Req{},
|
||||
"YYSY6F2E": &dto.YYSY6F2EReq{},
|
||||
"YYSY09CD": &dto.YYSY09CDReq{},
|
||||
"IVYZ0b03": &dto.IVYZ0b03Req{},
|
||||
"IVYZ0B03": &dto.IVYZ0B03Req{},
|
||||
"YYSYBE08": &dto.YYSYBE08Req{},
|
||||
"YYSYD50F": &dto.YYSYD50FReq{},
|
||||
"YYSYF7DB": &dto.YYSYF7DBReq{},
|
||||
@@ -172,7 +172,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZ6G7H": &dto.IVYZ6G7HReq{},
|
||||
"IVYZ8I9J": &dto.IVYZ8I9JReq{},
|
||||
"JRZQ0L85": &dto.JRZQ0L85Req{},
|
||||
"COMBHZY2": &dto.COMBHZY2Req{},
|
||||
"COMBHZY2": &dto.COMBHZY2Req{}, // 自此无imp11.28
|
||||
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||
@@ -185,6 +185,20 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"QYGL6S1B": &dto.QYGL6S1BReq{},
|
||||
"JRZQ0B6Y": &dto.JRZQ0B6YReq{},
|
||||
"JRZQ9A1W": &dto.JRZQ9A1WReq{},
|
||||
"JRZQ8F7C": &dto.JRZQ8F7CReq{}, //综合多头
|
||||
"FLXGK5D2": &dto.FLXGK5D2Req{},
|
||||
"FLXG3A9B": &dto.FLXG3A9BReq{},
|
||||
"IVYZP2Q6": &dto.IVYZP2Q6Req{},
|
||||
"JRZQ1W4X": &dto.JRZQ1W4XReq{}, //全景档案
|
||||
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
||||
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
||||
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
||||
"JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策
|
||||
"JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积
|
||||
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
@@ -308,6 +322,7 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string
|
||||
values := strings.TrimPrefix(rule, "oneof=")
|
||||
frontendRules = append(frontendRules, "可选值: "+values)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return strings.Join(frontendRules, "、")
|
||||
@@ -381,6 +396,9 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
"vin_code": "车辆识别代号VIN码",
|
||||
"return_type": "返回类型",
|
||||
"photo_data": "人脸图片",
|
||||
"owner_type": "企业主类型",
|
||||
"type": "查询类型",
|
||||
"query_reason_id": "查询原因ID",
|
||||
}
|
||||
|
||||
if label, exists := labelMap[jsonTag]; exists {
|
||||
@@ -423,6 +441,9 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
||||
"vin_code": "LSGBF53M8DS123456",
|
||||
"return_type": "1",
|
||||
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"ownerType": "1",
|
||||
"type": "per",
|
||||
"query_reason_id": "1",
|
||||
}
|
||||
|
||||
if example, exists := exampleMap[jsonTag]; exists {
|
||||
@@ -474,6 +495,9 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
"vin_code": "请输入17位车辆识别代号VIN码",
|
||||
"return_type": "请选择返回类型",
|
||||
"photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)",
|
||||
"ownerType": "请选择企业主类型",
|
||||
"type": "请选择查询类型",
|
||||
"query_reason_id": "请选择查询原因ID",
|
||||
}
|
||||
|
||||
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
||||
@@ -499,7 +523,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
||||
descMap := map[string]string{
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"id_card": "请输入18位身份证号码最后一位如是字母请大写",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
@@ -527,6 +551,9 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
|
||||
"vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)",
|
||||
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||
"photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||
"type": "查询类型:per-人员,ent-企业 ",
|
||||
"query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调",
|
||||
}
|
||||
|
||||
if desc, exists := descMap[jsonTag]; exists {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
)
|
||||
|
||||
// ProcessFLXG0687Request FLXG0687 API处理方法
|
||||
@@ -21,15 +21,14 @@ func ProcessFLXG0687Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"keyWord": paramsDto.IDCard,
|
||||
"type": 3,
|
||||
"keyWord": paramsDto.IDCard,
|
||||
"type": 3,
|
||||
}
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "RIS031", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
if errors.Is(err, yushan.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -24,7 +24,9 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// ProcessFLXG3A9BRequest FLXG3A9B API处理方法 - 法院被执行人限高版
|
||||
func ProcessFLXG3A9BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXG3A9BReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI045", 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
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
)
|
||||
|
||||
// ProcessFLXG5876Request FLXG5876 API处理方法
|
||||
// ProcessFLXG5876Request FLXG5876 易诉人识别API处理方法
|
||||
func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXG5876Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -42,4 +42,4 @@ func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
)
|
||||
|
||||
// ProcessFLXG970FRequest FLXG970F API处理方法
|
||||
// ProcessFLXG970FRequest FLXG970F 风险人员核验API处理方法
|
||||
func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXG970FReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
@@ -33,8 +33,8 @@ func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"cardNo": encryptedIDCard,
|
||||
"name": encryptedName,
|
||||
"cardNo": encryptedIDCard,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -48,4 +48,4 @@ func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
)
|
||||
|
||||
// ProcessFLXGBC21Request FLXGbc21 API处理方法
|
||||
@@ -21,14 +21,13 @@ func ProcessFLXGBC21Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "MOB032", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
if errors.Is(err, yushan.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -20,7 +20,9 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
@@ -33,7 +35,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"name": encryptedName,
|
||||
"id_card": encryptedIDCard,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// ProcessFLXGK5D2Request FLXGK5D2 API处理方法 - 法院被执行人高级版
|
||||
func ProcessFLXGK5D2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.FLXGK5D2Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI046", 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
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// ProcessIVYZ0B03Request IVYZ0B03 API处理方法
|
||||
func ProcessIVYZ0B03Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ0b03Req
|
||||
var paramsDto dto.IVYZ0B03Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
@@ -48,4 +48,4 @@ func ProcessIVYZ0B03Request(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
)
|
||||
|
||||
// ProcessIVYZ2B2TRequest IVYZ2B2T API处理方法 能力资质核验(学历)
|
||||
func ProcessIVYZ2B2TRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
|
||||
var paramsDto dto.IVYZ2B2TReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
encryptedQueryReasonId, err := deps.WestDexService.Encrypt(strconv.FormatInt(paramsDto.QueryReasonId, 10))
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"idCard": encryptedIDCard,
|
||||
"name": encryptedName,
|
||||
"queryReasonId": encryptedQueryReasonId,
|
||||
},
|
||||
}
|
||||
|
||||
respBytes, err := deps.WestDexService.CallAPI(ctx, "G11JX01", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
)
|
||||
|
||||
// ProcessIVYZ5A9ORequest IVYZ5A9O API处理方法 全国⾃然⼈⻛险评估评分模型
|
||||
func ProcessIVYZ5A9ORequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
|
||||
var paramsDto dto.IVYZ5A9OReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
encryptedAuthAuthorizeFileCode, err := deps.WestDexService.Encrypt(paramsDto.AuthAuthorizeFileCode)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"idcard": encryptedIDCard,
|
||||
"name": encryptedName,
|
||||
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
|
||||
},
|
||||
}
|
||||
|
||||
respBytes, err := deps.WestDexService.CallAPI(ctx, "G01SC01", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// ProcessIVYZ6M8PRequest IVYZ6M8P 职业资格证书API处理方法
|
||||
func ProcessIVYZ6M8PRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZ6M8PReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1147725836315455488"
|
||||
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
|
||||
}
|
||||
@@ -38,13 +38,27 @@ func ProcessIVYZ81NCRequest(ctx context.Context, params []byte, deps *processors
|
||||
},
|
||||
}
|
||||
|
||||
respBytes, err := deps.WestDexService.CallAPI(ctx, "G09XM02", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
const maxRetries = 5
|
||||
var respBytes []byte
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
var err error
|
||||
respBytes, err = deps.WestDexService.CallAPI(ctx, "G09XM02", reqData)
|
||||
if err == nil {
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
// 如果不是数据源异常,直接返回错误
|
||||
if !errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 如果是最后一次尝试,返回错误
|
||||
if attempt == maxRetries {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
|
||||
// 立即重试,不等待
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessIVYZP2Q6Request IVYZP2Q6 API处理方法 - 身份认证二要素
|
||||
func ProcessIVYZP2Q6Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZP2Q6Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 加密姓名
|
||||
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,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI011", 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
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
)
|
||||
|
||||
// ProcessJRZQ0B6YRequest JRZQ0B6Y 银行卡黑名单查询V1API处理方法
|
||||
@@ -20,39 +20,16 @@ func ProcessJRZQ0B6YRequest(ctx context.Context, params []byte, deps *processors
|
||||
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)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedBankCard, err := deps.WestDexService.Encrypt(paramsDto.BankCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"cardId": encryptedBankCard,
|
||||
"cardNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
},
|
||||
"name": paramsDto.Name,
|
||||
"cardld": paramsDto.BankCard,
|
||||
"cardNo": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "FIN019", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
if errors.Is(err, yushan.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessjrzqW4XRequest JRZQ1W4XAPI处理方法 - 全景档案
|
||||
func ProcessJRZQ1W4XRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ1W4XReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 加密姓名
|
||||
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,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI022", 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
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ3AG6Request JRZQ3AG6 轻松查公积API处理方法
|
||||
func ProcessJRZQ3AG6Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ3AG6Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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,
|
||||
"return_url": paramsDto.ReturnURL,
|
||||
"authorization_url": paramsDto.AuthorizationURL,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI108", 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
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ3P01Request JRZQ3P01 天远风控决策API处理方法
|
||||
func ProcessJRZQ3P01Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ3P01Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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, "ZCI109", 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
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessJRZQ8F7CRequest JRZQ8F7C API处理方法
|
||||
func ProcessJRZQ8F7CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ8F7CReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
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,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI047", 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
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
)
|
||||
|
||||
// ProcessJRZQ9A1WRequest JRZQ9A1W 银行卡鉴权V1API处理方法
|
||||
@@ -20,39 +20,16 @@ func ProcessJRZQ9A1WRequest(ctx context.Context, params []byte, deps *processors
|
||||
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)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedBankCard, err := deps.WestDexService.Encrypt(paramsDto.BankCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"cardId": encryptedBankCard,
|
||||
"cardNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
},
|
||||
"name": paramsDto.Name,
|
||||
"cardId": paramsDto.BankCard,
|
||||
"cardNo": paramsDto.IDCard,
|
||||
"phone": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "PCB145", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
if errors.Is(err, yushan.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -22,7 +22,7 @@ func ProcessQCXG6B4ERequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"vin": paramsDto.VINCode,
|
||||
"vin": paramsDto.VINCode,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
@@ -43,4 +43,3 @@ func ProcessQCXG6B4ERequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
)
|
||||
|
||||
// ProcessQCXG7A2BRequest QCXG7A2B API处理方法
|
||||
@@ -27,7 +27,7 @@ func ProcessQCXG7A2BRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "CAR061", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
if errors.Is(err, yushan.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ProcessQYGL23T7Request QYGL23T7 API处理方法 - 企业三要素验证
|
||||
// ProcessQYGL23T7Request QYGL23T7 API处理方法 - 企业四要素验证
|
||||
func ProcessQYGL23T7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGL23T7Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
|
||||
@@ -38,9 +38,9 @@ func ProcessQYGL2ACDRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"entname": encryptedEntName,
|
||||
"realname": encryptedLegalPerson,
|
||||
"idcard": encryptedEntCode,
|
||||
"name": encryptedEntName,
|
||||
"oper_name": encryptedLegalPerson,
|
||||
"keyword": encryptedEntCode,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -54,4 +54,4 @@ func ProcessQYGL2ACDRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@ func ProcessQYGL2B5CRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 两选一校验:EntName 和 EntCode 至少传一个
|
||||
var keyword string
|
||||
if paramsDto.EntName != "" && paramsDto.EntCode != "" {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("企业名称和企业统一信用代码只能传其中一个"))
|
||||
}
|
||||
if paramsDto.EntName == "" && paramsDto.EntCode == "" {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("必须提供企业名称或企业统一信用代码中的其中一个"))
|
||||
}
|
||||
@@ -39,7 +36,7 @@ func ProcessQYGL2B5CRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"keyword": keyword,
|
||||
"keyword": keyword,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
@@ -60,4 +57,3 @@ func ProcessQYGL2B5CRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// Processqygl2s0wRequest QYGL2S0W API处理方法 - 失信被执行企业个人查询
|
||||
func ProcessQYGL2S0WRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGL2S0WReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 验证逻辑
|
||||
var nameValue string
|
||||
if paramsDto.Type == "per" {
|
||||
// 个人查询:idCardNum 必填
|
||||
nameValue = paramsDto.Name
|
||||
if paramsDto.IDCard == "" {
|
||||
fmt.Print("个人身份证件号不能为空")
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当失信被执行人类型为个人时,身份证件号不能为空"))
|
||||
}
|
||||
} else if paramsDto.Type == "ent" {
|
||||
// 企业查询:name 和 entMark 两者必填其一
|
||||
nameValue = paramsDto.EntName
|
||||
if paramsDto.EntName == "" && paramsDto.EntCode == "" {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当查询为企业时,企业名称和企业标识统一代码注册号两者必填其一"))
|
||||
} // 确定使用哪个值作为 name
|
||||
if paramsDto.EntName != "" {
|
||||
nameValue = paramsDto.EntName
|
||||
} else {
|
||||
nameValue = paramsDto.EntCode
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("dto2s0w", paramsDto)
|
||||
// 构建请求数据(不传的参数也需要添加,值为空字符串)
|
||||
reqData := map[string]interface{}{
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"name": nameValue,
|
||||
"entMark": paramsDto.EntCode,
|
||||
"type": paramsDto.Type,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1079244717102657536"
|
||||
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
|
||||
}
|
||||
@@ -24,18 +24,18 @@ func ProcessQYGL4B2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 税收违法
|
||||
|
||||
@@ -24,18 +24,18 @@ func ProcessQYGL5A3CRequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 对外投资历史
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// Processqygl5a9tRequest QYGL5A9T API处理方法 - 全国企业各类工商风险统计数量查询
|
||||
func ProcessQYGL5A9TRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
|
||||
var paramsDto dto.QYGL5A9TReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 两选一校验:EntName 和 EntCode 至少传一个
|
||||
var keyword string
|
||||
if paramsDto.EntName == "" && paramsDto.EntCode == "" {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("必须提供企业名称或企业统一信用代码中的其中一个"))
|
||||
}
|
||||
|
||||
// 确定使用哪个值作为 keyword
|
||||
if paramsDto.EntName != "" {
|
||||
keyword = paramsDto.EntName
|
||||
} else {
|
||||
keyword = paramsDto.EntCode
|
||||
}
|
||||
|
||||
// 构建请求数据,
|
||||
reqData := map[string]interface{}{
|
||||
"nameCode": keyword,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1054665422426533888"
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ProcessQYGL5CMPRequest QYGL5CMP API处理方法 - 企业五要素验证
|
||||
func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGL5CMPReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"code": paramsDto.EntCode,
|
||||
"name": paramsDto.EntName,
|
||||
"legalPersonName": paramsDto.LegalPerson,
|
||||
}
|
||||
|
||||
// 调用天眼查API - 使用通用的CallAPI方法
|
||||
response, err := deps.TianYanChaService.CallAPI(ctx, "VerifyThreeElements", apiParams)
|
||||
if err != nil {
|
||||
return nil, convertTianYanChaError(err)
|
||||
}
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
// 天眼查API调用失败,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
}
|
||||
|
||||
// 解析天眼查响应数据
|
||||
if response.Data == nil {
|
||||
// 天眼查响应数据为空,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
}
|
||||
|
||||
// 将response.Data转换为JSON字符串,然后使用gjson解析
|
||||
dataBytes, err := json.Marshal(response.Data)
|
||||
if err != nil {
|
||||
// 数据序列化失败,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
}
|
||||
|
||||
// 使用gjson解析嵌套的data.result.data字段
|
||||
result := gjson.GetBytes(dataBytes, "result")
|
||||
if !result.Exists() {
|
||||
// 字段不存在,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
}
|
||||
|
||||
// 检查data.result.data是否等于1
|
||||
if result.Int() != 1 {
|
||||
// 不等于1,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
}
|
||||
|
||||
// 天眼查三要素验证通过,继续调用星维身份证三要素验证
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.LegalPerson,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1100244702166183936"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析星维API返回的数据
|
||||
var xingweiData map[string]interface{}
|
||||
if err := json.Unmarshal(respBytes, &xingweiData); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析星维API响应失败: %w", err))
|
||||
}
|
||||
|
||||
// 构建天眼查API返回的数据结构
|
||||
tianYanChaData := map[string]interface{}{
|
||||
"success": response.Success,
|
||||
"message": response.Message,
|
||||
"data": response.Data,
|
||||
}
|
||||
|
||||
// 解析status响应(将JSON字节解析为对象)
|
||||
statusBytes := createStatusResponsess(0) // 验证通过,status为0
|
||||
var statusData map[string]interface{}
|
||||
if err := json.Unmarshal(statusBytes, &statusData); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析status响应失败: %w", err))
|
||||
}
|
||||
|
||||
// 合并两个API的返回数据
|
||||
mergedData := map[string]interface{}{
|
||||
"Personal Information": xingweiData, // 星维API返回的数据
|
||||
"Enterprise Information": tianYanChaData, // 天眼查API返回的数据
|
||||
"status": statusData, // 解析后的status对象
|
||||
}
|
||||
|
||||
// 将合并后的数据序列化为JSON
|
||||
mergedBytes, err := json.Marshal(mergedData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("合并数据序列化失败: %w", err))
|
||||
}
|
||||
|
||||
return mergedBytes, nil
|
||||
|
||||
}
|
||||
|
||||
// createStatusResponsess 创建状态响应
|
||||
func createStatusResponsess(status int) []byte {
|
||||
response := map[string]interface{}{
|
||||
"status": status,
|
||||
}
|
||||
respBytes, _ := json.Marshal(response)
|
||||
return respBytes
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
@@ -20,24 +21,16 @@ func ProcessQYGL5F6ARequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
fmt.Println("paramsDto", paramsDto)
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
}
|
||||
|
||||
// 如果传了身份证,则添加到请求数据中
|
||||
if paramsDto.MobileNo != "" {
|
||||
reqData["phoneNumber"] = paramsDto.MobileNo
|
||||
}
|
||||
|
||||
// 如果传了企业统一信用代码,则添加到请求数据中
|
||||
if paramsDto.EntCode != "" {
|
||||
reqData["ucc"] = paramsDto.EntCode
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1101695397213958144"
|
||||
fmt.Println("reqData", reqData)
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
|
||||
@@ -42,4 +42,4 @@ func ProcessQYGL6F2DRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
)
|
||||
|
||||
// ProcessQYGL6S1BRequest QYGL6S1B API处理方法 - 董监高司法综合信息核验
|
||||
@@ -21,30 +22,28 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedPhone, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
encryptedIdCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"idCard": encryptedPhone,
|
||||
reqData := map[string]interface{}{
|
||||
"idCard": encryptedIdCard,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
// 调用天眼查API -
|
||||
response, err := deps.TianYanChaService.CallAPI(ctx, "ZCI043", apiParams)
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI043", reqData)
|
||||
if err != nil {
|
||||
return nil, convertTianYanChaError(err)
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
|
||||
}
|
||||
|
||||
// 返回天眼查响应数据
|
||||
respBytes, err := json.Marshal(response.Data)
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
@@ -16,6 +17,7 @@ func ProcessQYGL7C1ARequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
fmt.Println("paramsDto", paramsDto)
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
@@ -24,18 +26,18 @@ func ProcessQYGL7C1ARequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 经营异常
|
||||
|
||||
@@ -24,18 +24,18 @@ func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 欠税公告
|
||||
|
||||
@@ -42,8 +42,8 @@ func ProcessQYGL8271Request(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"org_name": encryptedEntName,
|
||||
"uscc": encryptedEntCode,
|
||||
"org_name": encryptedEntName,
|
||||
"uscc": encryptedEntCode,
|
||||
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
|
||||
"inquired_auth": fmt.Sprintf("authed:%s", paramsDto.AuthDate),
|
||||
},
|
||||
|
||||
@@ -24,18 +24,18 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 融资历史
|
||||
|
||||
@@ -24,18 +24,18 @@ func ProcessQYGL9E2FRequest(ctx context.Context, params []byte, deps *processors
|
||||
// 设置默认值
|
||||
pageSize := paramsDto.PageSize
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
pageSize = int64(20)
|
||||
}
|
||||
pageNum := paramsDto.PageNum
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
pageNum = int64(1)
|
||||
}
|
||||
|
||||
// 构建API调用参数
|
||||
apiParams := map[string]string{
|
||||
"keyword": paramsDto.EntCode,
|
||||
"pageSize": strconv.Itoa(pageSize),
|
||||
"pageNum": strconv.Itoa(pageNum),
|
||||
"pageSize": strconv.FormatInt(pageSize, 10),
|
||||
"pageNum": strconv.FormatInt(pageNum, 10),
|
||||
}
|
||||
|
||||
// 调用天眼查API - 行政处罚
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// Processqygl9t1qRequest QYGL9T1Q API处理方法 - 全国企业借贷意向验证查询_V1
|
||||
func ProcessQYGL9T1QRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
|
||||
var paramsDto dto.QYGL9T1QReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,直接传递姓名、身份证、手机号等
|
||||
reqData := map[string]interface{}{
|
||||
"ownerType": paramsDto.OwnerType,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
"searchKey": paramsDto.EntCode, // 企业统一信用代码和注册号两者必填其一
|
||||
"authAuthorizeFileCode": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1078965351139438592"
|
||||
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
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func ProcessQYGLB4C0Request(ctx context.Context, params []byte, deps *processors
|
||||
// 数据源错误
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}else if errors.Is(err, westdex.ErrNotFound) {
|
||||
} else if errors.Is(err, westdex.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
// 其他系统错误
|
||||
|
||||
@@ -21,13 +21,13 @@ func ProcessYYSY6F2BRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedPhone, err := deps.ZhichaService.Encrypt(paramsDto.Phone)
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"phone": encryptedPhone,
|
||||
"phone": encryptedMobileNo,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI041", reqData)
|
||||
|
||||
@@ -23,9 +23,9 @@ func ProcessYYSY8F3ARequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 构建请求数据,直接传递姓名、身份证、手机号
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
|
||||
@@ -27,14 +27,14 @@ func ProcessYYSY9F1BYequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedPhone, err := deps.ZhichaService.Encrypt(paramsDto.Phone)
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"phone": encryptedPhone,
|
||||
"phone": encryptedMobileNo,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
|
||||
137
internal/domains/article/entities/announcement.go
Normal file
137
internal/domains/article/entities/announcement.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AnnouncementStatus 公告状态枚举
|
||||
type AnnouncementStatus string
|
||||
|
||||
const (
|
||||
AnnouncementStatusDraft AnnouncementStatus = "draft" // 草稿
|
||||
AnnouncementStatusPublished AnnouncementStatus = "published" // 已发布
|
||||
AnnouncementStatusArchived AnnouncementStatus = "archived" // 已归档
|
||||
)
|
||||
|
||||
// Announcement 公告聚合根
|
||||
// 用于对系统公告进行管理,支持发布、撤回、定时发布等功能
|
||||
type Announcement struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"公告唯一标识"`
|
||||
Title string `gorm:"type:varchar(200);not null;index" json:"title" comment:"公告标题"`
|
||||
Content string `gorm:"type:text;not null" json:"content" comment:"公告内容"`
|
||||
Status AnnouncementStatus `gorm:"type:varchar(20);not null;default:'draft';index" json:"status" comment:"公告状态"`
|
||||
ScheduledAt *time.Time `gorm:"index" json:"scheduled_at" comment:"定时发布时间"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;index" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Announcement) TableName() string {
|
||||
return "announcements"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *Announcement) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (a *Announcement) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (a *Announcement) GetCreatedAt() time.Time {
|
||||
return a.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (a *Announcement) GetUpdatedAt() time.Time {
|
||||
return a.UpdatedAt
|
||||
}
|
||||
|
||||
// 验证公告信息
|
||||
func (a *Announcement) Validate() error {
|
||||
if a.Title == "" {
|
||||
return NewValidationError("公告标题不能为空")
|
||||
}
|
||||
if a.Content == "" {
|
||||
return NewValidationError("公告内容不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发布公告
|
||||
func (a *Announcement) Publish() error {
|
||||
if a.Status == AnnouncementStatusPublished {
|
||||
return NewValidationError("公告已经是发布状态")
|
||||
}
|
||||
a.Status = AnnouncementStatusPublished
|
||||
now := time.Now()
|
||||
a.CreatedAt = now
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 撤回公告
|
||||
func (a *Announcement) Withdraw() error {
|
||||
if a.Status == AnnouncementStatusDraft {
|
||||
return NewValidationError("公告已经是草稿状态")
|
||||
}
|
||||
a.Status = AnnouncementStatusDraft
|
||||
now := time.Now()
|
||||
a.CreatedAt = now
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 定时发布公告
|
||||
func (a *Announcement) SchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == AnnouncementStatusPublished {
|
||||
return NewValidationError("公告已经是发布状态")
|
||||
}
|
||||
a.Status = AnnouncementStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新定时发布时间
|
||||
func (a *Announcement) UpdateSchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == AnnouncementStatusPublished {
|
||||
return NewValidationError("公告已经是发布状态")
|
||||
}
|
||||
if scheduledTime.Before(time.Now()) {
|
||||
return NewValidationError("定时发布时间不能早于当前时间")
|
||||
}
|
||||
a.ScheduledAt = &scheduledTime
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelSchedulePublish 取消定时发布
|
||||
func (a *Announcement) CancelSchedulePublish() error {
|
||||
if a.Status == AnnouncementStatusPublished {
|
||||
return NewValidationError("公告已经是发布状态")
|
||||
}
|
||||
a.ScheduledAt = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScheduled 判断是否已设置定时发布
|
||||
func (a *Announcement) IsScheduled() bool {
|
||||
return a.ScheduledAt != nil && a.Status == AnnouncementStatusDraft
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取定时发布时间
|
||||
func (a *Announcement) GetScheduledTime() *time.Time {
|
||||
return a.ScheduledAt
|
||||
}
|
||||
24
internal/domains/article/repositories/announcement.go
Normal file
24
internal/domains/article/repositories/announcement.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// 存储公告的仓储接口
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// AnnouncementRepository 公告仓储接口
|
||||
type AnnouncementRepository interface {
|
||||
interfaces.Repository[entities.Announcement]
|
||||
|
||||
// 自定义查询方法
|
||||
FindByStatus(ctx context.Context, status entities.AnnouncementStatus) ([]*entities.Announcement, error)
|
||||
FindScheduled(ctx context.Context) ([]*entities.Announcement, error)
|
||||
ListAnnouncements(ctx context.Context, query *queries.ListAnnouncementQuery) ([]*entities.Announcement, int64, error)
|
||||
|
||||
// 统计方法
|
||||
CountByStatus(ctx context.Context, status entities.AnnouncementStatus) (int64, error)
|
||||
// 更新统计信息
|
||||
UpdateStatistics(ctx context.Context, announcementID string) error
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListAnnouncementQuery 公告列表查询
|
||||
type ListAnnouncementQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Status entities.AnnouncementStatus `json:"status"`
|
||||
Title string `json:"title"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"`
|
||||
}
|
||||
133
internal/domains/article/services/announcement_service.go
Normal file
133
internal/domains/article/services/announcement_service.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// AnnouncementService 公告领域服务
|
||||
// 处理公告相关的业务逻辑,包括验证、状态管理等
|
||||
type AnnouncementService struct{}
|
||||
|
||||
// NewAnnouncementService 创建公告领域服务
|
||||
func NewAnnouncementService() *AnnouncementService {
|
||||
return &AnnouncementService{}
|
||||
}
|
||||
|
||||
// ValidateAnnouncement 验证公告
|
||||
// 检查公告是否符合业务规则
|
||||
func (s *AnnouncementService) ValidateAnnouncement(announcement *entities.Announcement) error {
|
||||
// 1. 基础验证
|
||||
if err := announcement.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 业务规则验证
|
||||
// 标题不能包含敏感词
|
||||
if s.containsSensitiveWords(announcement.Title) {
|
||||
return entities.NewValidationError("公告标题包含敏感词")
|
||||
}
|
||||
|
||||
// 内容不能包含敏感词
|
||||
if s.containsSensitiveWords(announcement.Content) {
|
||||
return entities.NewValidationError("公告内容包含敏感词")
|
||||
}
|
||||
|
||||
// 标题长度验证
|
||||
if len(announcement.Title) > 200 {
|
||||
return entities.NewValidationError("公告标题不能超过200个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanPublish 检查是否可以发布
|
||||
func (s *AnnouncementService) CanPublish(announcement *entities.Announcement) error {
|
||||
if announcement.Status == entities.AnnouncementStatusPublished {
|
||||
return entities.NewValidationError("公告已经是发布状态")
|
||||
}
|
||||
|
||||
if announcement.Status == entities.AnnouncementStatusArchived {
|
||||
return entities.NewValidationError("已归档的公告不能发布")
|
||||
}
|
||||
|
||||
// 检查必填字段
|
||||
if announcement.Title == "" {
|
||||
return entities.NewValidationError("公告标题不能为空")
|
||||
}
|
||||
if announcement.Content == "" {
|
||||
return entities.NewValidationError("公告内容不能为空")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanEdit 检查是否可以编辑
|
||||
func (s *AnnouncementService) CanEdit(announcement *entities.Announcement) error {
|
||||
if announcement.Status == entities.AnnouncementStatusPublished {
|
||||
return entities.NewValidationError("已发布的公告不能编辑,请先撤回")
|
||||
}
|
||||
|
||||
if announcement.Status == entities.AnnouncementStatusArchived {
|
||||
return entities.NewValidationError("已归档的公告不能编辑")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanArchive 检查是否可以归档
|
||||
func (s *AnnouncementService) CanArchive(announcement *entities.Announcement) error {
|
||||
if announcement.Status != entities.AnnouncementStatusPublished {
|
||||
return entities.NewValidationError("只有已发布的公告才能归档")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanWithdraw 检查是否可以撤回
|
||||
func (s *AnnouncementService) CanWithdraw(announcement *entities.Announcement) error {
|
||||
if announcement.Status != entities.AnnouncementStatusPublished {
|
||||
return entities.NewValidationError("只有已发布的公告才能撤回")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanSchedulePublish 检查是否可以定时发布
|
||||
func (s *AnnouncementService) CanSchedulePublish(announcement *entities.Announcement, scheduledTime interface{}) error {
|
||||
if announcement.Status == entities.AnnouncementStatusPublished {
|
||||
return entities.NewValidationError("已发布的公告不能设置定时发布")
|
||||
}
|
||||
|
||||
if announcement.Status == entities.AnnouncementStatusArchived {
|
||||
return entities.NewValidationError("已归档的公告不能设置定时发布")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsSensitiveWords 检查是否包含敏感词
|
||||
func (s *AnnouncementService) containsSensitiveWords(text string) bool {
|
||||
// TODO: 实现敏感词检查逻辑
|
||||
// 这里可以集成敏感词库或调用外部服务
|
||||
sensitiveWords := []string{
|
||||
"敏感词1",
|
||||
"敏感词2",
|
||||
"敏感词3",
|
||||
}
|
||||
|
||||
for _, word := range sensitiveWords {
|
||||
if len(word) > 0 && len(text) > 0 {
|
||||
// 简单的字符串包含检查
|
||||
// 实际项目中应该使用更复杂的算法
|
||||
if len(text) >= len(word) {
|
||||
for i := 0; i <= len(text)-len(word); i++ {
|
||||
if text[i:i+len(word)] == word {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,140 +1,28 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AlipayOrderStatus 支付宝订单状态枚举
|
||||
type AlipayOrderStatus string
|
||||
// AlipayOrderStatus 支付宝订单状态枚举(别名)
|
||||
type AlipayOrderStatus = PayOrderStatus
|
||||
|
||||
const (
|
||||
AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭
|
||||
AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
)
|
||||
|
||||
// AlipayOrder 支付宝订单详情实体
|
||||
type AlipayOrder struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付宝订单唯一标识"`
|
||||
RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"`
|
||||
OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"支付宝交易号"`
|
||||
|
||||
// 订单信息
|
||||
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc"`
|
||||
Status AlipayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"`
|
||||
|
||||
// 支付宝返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家支付宝用户ID"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家支付宝用户ID"`
|
||||
PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
|
||||
|
||||
// 回调信息
|
||||
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
|
||||
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
|
||||
|
||||
// 错误信息
|
||||
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (AlipayOrder) TableName() string {
|
||||
return "alipay_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *AlipayOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待支付状态
|
||||
func (a *AlipayOrder) IsPending() bool {
|
||||
return a.Status == AlipayOrderStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为支付成功状态
|
||||
func (a *AlipayOrder) IsSuccess() bool {
|
||||
return a.Status == AlipayOrderStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (a *AlipayOrder) IsFailed() bool {
|
||||
return a.Status == AlipayOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (a *AlipayOrder) IsCancelled() bool {
|
||||
return a.Status == AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (a *AlipayOrder) IsClosed() bool {
|
||||
return a.Status == AlipayOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为支付成功
|
||||
func (a *AlipayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
a.Status = AlipayOrderStatusSuccess
|
||||
a.TradeNo = &tradeNo
|
||||
a.BuyerID = buyerID
|
||||
a.SellerID = sellerID
|
||||
a.PayAmount = payAmount
|
||||
a.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
a.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (a *AlipayOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
a.Status = AlipayOrderStatusFailed
|
||||
a.ErrorCode = errorCode
|
||||
a.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (a *AlipayOrder) MarkCancelled() {
|
||||
a.Status = AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (a *AlipayOrder) MarkClosed() {
|
||||
a.Status = AlipayOrderStatusClosed
|
||||
}
|
||||
// AlipayOrder 支付宝订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type AlipayOrder = PayOrder
|
||||
|
||||
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
||||
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
||||
return &AlipayOrder{
|
||||
ID: uuid.New().String(),
|
||||
RechargeID: rechargeID,
|
||||
OutTradeNo: outTradeNo,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
Status: AlipayOrderStatusPending,
|
||||
}
|
||||
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay")
|
||||
}
|
||||
|
||||
136
internal/domains/finance/entities/pay_order.go
Normal file
136
internal/domains/finance/entities/pay_order.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PayOrderStatus 支付订单状态枚举(通用)
|
||||
type PayOrderStatus string
|
||||
|
||||
const (
|
||||
PayOrderStatusPending PayOrderStatus = "pending" // 待支付
|
||||
PayOrderStatusSuccess PayOrderStatus = "success" // 支付成功
|
||||
PayOrderStatusFailed PayOrderStatus = "failed" // 支付失败
|
||||
PayOrderStatusCancelled PayOrderStatus = "cancelled" // 已取消
|
||||
PayOrderStatusClosed PayOrderStatus = "closed" // 已关闭
|
||||
)
|
||||
|
||||
// PayOrder 支付订单详情实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type PayOrder struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付订单唯一标识"`
|
||||
RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"`
|
||||
OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"第三方支付交易号"`
|
||||
|
||||
// 订单信息
|
||||
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc/wx_h5/wx_mini等"`
|
||||
PayChannel string `gorm:"type:varchar(20);not null;default:'alipay';index" json:"pay_channel" comment:"支付渠道:alipay/wechat"`
|
||||
Status PayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"`
|
||||
|
||||
// 支付渠道返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家ID(支付渠道方)"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家ID(支付渠道方)"`
|
||||
PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
|
||||
|
||||
// 回调信息
|
||||
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
|
||||
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
|
||||
|
||||
// 错误信息
|
||||
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (PayOrder) TableName() string {
|
||||
return "typay_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (p *PayOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待支付状态
|
||||
func (p *PayOrder) IsPending() bool {
|
||||
return p.Status == PayOrderStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为支付成功状态
|
||||
func (p *PayOrder) IsSuccess() bool {
|
||||
return p.Status == PayOrderStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (p *PayOrder) IsFailed() bool {
|
||||
return p.Status == PayOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (p *PayOrder) IsCancelled() bool {
|
||||
return p.Status == PayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (p *PayOrder) IsClosed() bool {
|
||||
return p.Status == PayOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为支付成功
|
||||
func (p *PayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
p.Status = PayOrderStatusSuccess
|
||||
p.TradeNo = &tradeNo
|
||||
p.BuyerID = buyerID
|
||||
p.SellerID = sellerID
|
||||
p.PayAmount = payAmount
|
||||
p.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
p.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (p *PayOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
p.Status = PayOrderStatusFailed
|
||||
p.ErrorCode = errorCode
|
||||
p.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (p *PayOrder) MarkCancelled() {
|
||||
p.Status = PayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (p *PayOrder) MarkClosed() {
|
||||
p.Status = PayOrderStatusClosed
|
||||
}
|
||||
|
||||
// NewPayOrder 通用工厂方法 - 创建支付订单(支持多支付渠道)
|
||||
func NewPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform, payChannel string) *PayOrder {
|
||||
return &PayOrder{
|
||||
ID: uuid.New().String(),
|
||||
RechargeID: rechargeID,
|
||||
OutTradeNo: outTradeNo,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
PayChannel: payChannel,
|
||||
Status: PayOrderStatusPending,
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ type RechargeType string
|
||||
|
||||
const (
|
||||
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
||||
RechargeTypeWechat RechargeType = "wechat" // 微信充值
|
||||
RechargeTypeTransfer RechargeType = "transfer" // 对公转账
|
||||
RechargeTypeGift RechargeType = "gift" // 赠送
|
||||
)
|
||||
@@ -42,6 +43,7 @@ type RechargeRecord struct {
|
||||
|
||||
// 订单号字段(根据充值类型使用不同字段)
|
||||
AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"`
|
||||
WechatOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"wechat_order_id,omitempty" comment:"微信订单号"`
|
||||
TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"`
|
||||
|
||||
// 通用字段
|
||||
@@ -104,14 +106,24 @@ func (r *RechargeRecord) MarkCancelled() {
|
||||
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
|
||||
func (r *RechargeRecord) ValidatePaymentMethod() error {
|
||||
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
|
||||
hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != ""
|
||||
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
||||
|
||||
if hasAlipay && hasTransfer {
|
||||
return errors.New("支付宝订单号和转账订单号不能同时存在")
|
||||
count := 0
|
||||
if hasAlipay {
|
||||
count++
|
||||
}
|
||||
|
||||
if !hasAlipay && !hasTransfer {
|
||||
return errors.New("必须提供支付宝订单号或转账订单号")
|
||||
if hasWechat {
|
||||
count++
|
||||
}
|
||||
if hasTransfer {
|
||||
count++
|
||||
}
|
||||
if count > 1 {
|
||||
return errors.New("支付宝、微信或转账订单号只能存在一个")
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("必须提供支付宝、微信或转账订单号")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,6 +136,10 @@ func (r *RechargeRecord) GetOrderID() string {
|
||||
if r.AlipayOrderID != nil {
|
||||
return *r.AlipayOrderID
|
||||
}
|
||||
case RechargeTypeWechat:
|
||||
if r.WechatOrderID != nil {
|
||||
return *r.WechatOrderID
|
||||
}
|
||||
case RechargeTypeTransfer:
|
||||
if r.TransferOrderID != nil {
|
||||
return *r.TransferOrderID
|
||||
@@ -137,6 +153,11 @@ func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
|
||||
r.AlipayOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetWechatOrderID 设置微信订单号
|
||||
func (r *RechargeRecord) SetWechatOrderID(orderID string) {
|
||||
r.WechatOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetTransferOrderID 设置转账订单号
|
||||
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||
r.TransferOrderID = &orderID
|
||||
@@ -153,6 +174,17 @@ func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderI
|
||||
}
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecord 工厂方法 - 创建微信充值记录
|
||||
func NewWechatRechargeRecord(userID string, amount decimal.Decimal, wechatOrderID string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeWechat,
|
||||
Status: RechargeStatusPending,
|
||||
WechatOrderID: &wechatOrderID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录
|
||||
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
|
||||
33
internal/domains/finance/entities/wechat_order.go
Normal file
33
internal/domains/finance/entities/wechat_order.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package entities
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// WechatOrderStatus 微信订单状态枚举(别名)
|
||||
type WechatOrderStatus = PayOrderStatus
|
||||
|
||||
const (
|
||||
WechatOrderStatusPending WechatOrderStatus = PayOrderStatusPending // 待支付
|
||||
WechatOrderStatusSuccess WechatOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||
WechatOrderStatusFailed WechatOrderStatus = PayOrderStatusFailed // 支付失败
|
||||
WechatOrderStatusCancelled WechatOrderStatus = PayOrderStatusCancelled // 已取消
|
||||
WechatOrderStatusClosed WechatOrderStatus = PayOrderStatusClosed // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
WechatOrderPlatformApp = "app" // 微信APP支付
|
||||
WechatOrderPlatformH5 = "h5" // 微信H5支付
|
||||
WechatOrderPlatformMini = "mini" // 微信小程序支付
|
||||
)
|
||||
|
||||
// WechatOrder 微信订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type WechatOrder = PayOrder
|
||||
|
||||
// NewWechatOrder 工厂方法 - 创建微信订单(统一表 typay_orders)
|
||||
func NewWechatOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder {
|
||||
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "wechat")
|
||||
}
|
||||
|
||||
// NewWechatPayOrder 工厂方法 - 创建微信支付订单(别名,保持向后兼容)
|
||||
func NewWechatPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder {
|
||||
return NewWechatOrder(rechargeID, outTradeNo, subject, amount, platform)
|
||||
}
|
||||
@@ -17,4 +17,4 @@ type AlipayOrderRepository interface {
|
||||
UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@ type RechargeRecordRepository interface {
|
||||
GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error)
|
||||
Update(ctx context.Context, record entities.RechargeRecord) error
|
||||
UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error
|
||||
|
||||
|
||||
// 管理员查询方法
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
|
||||
|
||||
// 统计相关方法
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, 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)
|
||||
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, 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)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user