Compare commits
52 Commits
63e2fba464
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 451d869361 | |||
| 08ea153cac | |||
| 6147878dfe | |||
| be47a0f045 | |||
| 810696e0f0 | |||
| 17dbaf1ccb | |||
| 18f3d10518 | |||
| 0d4953c6d3 | |||
| 3f64600f02 | |||
| 2c89b8cb26 | |||
| 09d7a4f076 | |||
| 83d0fd6587 | |||
| 0fd28054f1 | |||
| ce858983ee | |||
| 9b2bffae15 | |||
| c68ece5bea | |||
| 398d2cee74 | |||
| b6c8d93af5 | |||
| b423aa6be8 | |||
| a47c306c87 | |||
| af88bcc8eb | |||
| 89367fb2ee | |||
| 05b6623e75 | |||
| bfedec249f | |||
| 9f669a9c94 | |||
| 0f5c4f4303 | |||
| d9c2d9f103 | |||
| 7e0d58b295 | |||
| a17ff2140e | |||
| 6a2241bc66 | |||
| e57bef6609 | |||
| 81639a81e6 | |||
| aaf17321ff | |||
| a8a4ff2d37 | |||
| 619deeb456 | |||
| f12c3fb8ad | |||
| 4ce8fe4023 | |||
| 7b45b43a0e | |||
| 752b90b048 | |||
| 68def7e08b | |||
| b0e8974d6c | |||
| b41d41ddf3 | |||
| b08a63fc99 | |||
| 1f06f21faf | |||
| 3f5a126bfa | |||
| 17ff48a642 | |||
| af629e96c2 | |||
| 63252fa30f | |||
| 1cf64e831c | |||
| 577c2bc581 | |||
| 6d73dad88e | |||
| 937c812ea5 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -26,6 +26,7 @@ Thumbs.db
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
console
|
console
|
||||||
|
worker
|
||||||
|
|
||||||
# 依赖目录
|
# 依赖目录
|
||||||
vendor/
|
vendor/
|
||||||
@@ -34,6 +35,11 @@ vendor/
|
|||||||
coverage.out
|
coverage.out
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
|
# 字体文件(大文件,不进行版本控制)
|
||||||
|
internal/shared/pdf/fonts/*.ttf
|
||||||
|
internal/shared/pdf/fonts/*.ttc
|
||||||
|
internal/shared/pdf/fonts/*.otf
|
||||||
|
|
||||||
# 其他
|
# 其他
|
||||||
*.exe
|
*.exe
|
||||||
*.dll
|
*.dll
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ WORKDIR /app
|
|||||||
COPY --from=builder /app/tyapi-server .
|
COPY --from=builder /app/tyapi-server .
|
||||||
|
|
||||||
# 复制配置文件
|
# 复制配置文件
|
||||||
COPY --chown=tyapi:tyapi config.yaml .
|
COPY config.yaml .
|
||||||
COPY --chown=tyapi:tyapi configs/ ./configs/
|
COPY configs/ ./configs/
|
||||||
|
|
||||||
|
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||||
|
COPY resources ./resources
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TaskTypeArticlePublish = "article:publish"
|
TaskTypeArticlePublish = "article:publish"
|
||||||
|
TaskTypeAnnouncementPublish = "announcement_publish"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -78,6 +79,9 @@ func main() {
|
|||||||
mux.HandleFunc(TaskTypeArticlePublish, func(ctx context.Context, t *asynq.Task) error {
|
mux.HandleFunc(TaskTypeArticlePublish, func(ctx context.Context, t *asynq.Task) error {
|
||||||
return handleArticlePublish(ctx, t, db, logger)
|
return handleArticlePublish(ctx, t, db, logger)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc(TaskTypeAnnouncementPublish, func(ctx context.Context, t *asynq.Task) error {
|
||||||
|
return handleAnnouncementPublish(ctx, t, db, logger)
|
||||||
|
})
|
||||||
|
|
||||||
// 启动 Worker
|
// 启动 Worker
|
||||||
go func() {
|
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))
|
logger.Info("定时发布文章成功", zap.String("article_id", articleID))
|
||||||
return nil
|
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_id: "7439073138"
|
||||||
app_secret: "d76e27fdd169b391e09262a0959dac5c"
|
app_secret: "d76e27fdd169b391e09262a0959dac5c"
|
||||||
server_url: "https://smlopenapi.esign.cn"
|
server_url: "https://smlopenapi.esign.cn"
|
||||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
@@ -362,6 +362,28 @@ alipay:
|
|||||||
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
notify_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/callback"
|
||||||
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
return_url: "https://console.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 💰 微信支付配置
|
||||||
|
# ===========================================
|
||||||
|
Wxpay:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
mch_id: "1683589176"
|
||||||
|
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||||
|
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||||
|
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||||
|
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||||
|
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||||
|
notify_url: "https://console.tianyuanapi.com/api/v1/pay/wechat/callback"
|
||||||
|
refund_notify_url: "https://console.tianyuanapi.com/api/v1/wechat/refund_callback"
|
||||||
|
|
||||||
|
# 微信小程序配置
|
||||||
|
WechatMini:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
|
# 微信H5配置
|
||||||
|
WechatH5:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🔍 天眼查配置
|
# 🔍 天眼查配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ esign:
|
|||||||
app_id: "7439073713"
|
app_id: "7439073713"
|
||||||
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
|
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
|
||||||
server_url: "https://smlopenapi.esign.cn"
|
server_url: "https://smlopenapi.esign.cn"
|
||||||
template_id: "1fd7ed9c6d134d1db7b5af9582633d76"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
@@ -81,6 +81,27 @@ alipay:
|
|||||||
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
# 💰 微信支付配置
|
||||||
|
# ===========================================
|
||||||
|
Wxpay:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
mch_id: "1683589176"
|
||||||
|
mch_certificate_serial_number: "1F4E8B3C39C60035D4CC154F276D03D9CC2C603D"
|
||||||
|
mch_apiv3_key: "TY8X9nP2qR5tY7uW3zA6bC4dE1flgGJ0"
|
||||||
|
mch_private_key_path: "resources/etc/wxetc_cert/apiclient_key.pem"
|
||||||
|
mch_public_key_id: "PUB_KEY_ID_0116835891762025062600211574000800"
|
||||||
|
mch_public_key_path: "resources/etc/wxetc_cert/pub_key.pem"
|
||||||
|
notify_url: "https://bx89915628g.vicp.fun/api/v1/pay/wechat/callback"
|
||||||
|
refund_notify_url: "https://bx89915628g.vicp.fun/api/v1/wechat/refund_callback"
|
||||||
|
|
||||||
|
# 微信小程序配置
|
||||||
|
WechatMini:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
|
||||||
|
# 微信H5配置
|
||||||
|
WechatH5:
|
||||||
|
app_id: "wxa581992dc74d860e"
|
||||||
|
# ===========================================
|
||||||
# 💰 钱包配置
|
# 💰 钱包配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
wallet:
|
wallet:
|
||||||
@@ -114,34 +135,42 @@ development:
|
|||||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 🚦 开发环境全局限流(放宽或近似关闭)
|
||||||
|
# ===========================================
|
||||||
|
ratelimit:
|
||||||
|
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
|
||||||
|
window: 1s # 时间窗口
|
||||||
|
burst: 1000000 # 令牌桶突发容量
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🚀 开发环境频率限制配置(放宽限制)
|
# 🚀 开发环境频率限制配置(放宽限制)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
daily_ratelimit:
|
daily_ratelimit:
|
||||||
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
|
||||||
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
|
||||||
max_concurrent: 50 # 开发环境最大并发请求数
|
max_concurrent: 50 # 开发环境最大并发请求数
|
||||||
|
|
||||||
# 排除频率限制的路径
|
# 排除频率限制的路径
|
||||||
exclude_paths:
|
exclude_paths:
|
||||||
- "/health" # 健康检查接口
|
- "/health" # 健康检查接口
|
||||||
- "/metrics" # 监控指标接口
|
- "/metrics" # 监控指标接口
|
||||||
|
|
||||||
# 排除频率限制的域名
|
# 排除频率限制的域名
|
||||||
exclude_domains:
|
exclude_domains:
|
||||||
- "api.*" # API二级域名不受频率限制
|
- "api.*" # API二级域名不受频率限制
|
||||||
- "*.api.*" # 支持多级API域名
|
- "*.api.*" # 支持多级API域名
|
||||||
|
|
||||||
# 开发环境安全配置(放宽限制)
|
# 开发环境安全配置(放宽限制)
|
||||||
enable_ip_whitelist: true # 启用IP白名单
|
enable_ip_whitelist: true # 启用IP白名单
|
||||||
ip_whitelist: # 开发环境IP白名单
|
ip_whitelist: # 开发环境IP白名单
|
||||||
- "127.0.0.1" # 本地回环
|
- "127.0.0.1" # 本地回环
|
||||||
- "localhost" # 本地主机
|
- "localhost" # 本地主机
|
||||||
- "192.168.*" # 内网IP段
|
- "192.168.*" # 内网IP段
|
||||||
- "10.*" # 内网IP段
|
- "10.*" # 内网IP段
|
||||||
- "172.16.*" # 内网IP段
|
- "172.16.*" # 内网IP段
|
||||||
|
|
||||||
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
enable_ip_blacklist: false # 开发环境禁用IP黑名单
|
||||||
enable_user_agent: false # 开发环境禁用User-Agent检查
|
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||||
enable_referer: false # 开发环境禁用Referer检查
|
enable_referer: false # 开发环境禁用Referer检查
|
||||||
enable_proxy_check: false # 开发环境禁用代理检查
|
enable_proxy_check: false # 开发环境禁用代理检查
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ esign:
|
|||||||
app_id: "5112008003"
|
app_id: "5112008003"
|
||||||
app_secret: "d487672273e7aa70c800804a1d9499b9"
|
app_secret: "d487672273e7aa70c800804a1d9499b9"
|
||||||
server_url: "https://openapi.esign.cn"
|
server_url: "https://openapi.esign.cn"
|
||||||
template_id: "c82af4df2790430299c81321f309eef3"
|
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
|
||||||
contract:
|
contract:
|
||||||
name: "天远数据API合作协议"
|
name: "天远数据API合作协议"
|
||||||
expire_days: 7
|
expire_days: 7
|
||||||
|
|||||||
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/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.25.1
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/qiniu/go-sdk/v7 v7.25.4
|
github.com/qiniu/go-sdk/v7 v7.25.4
|
||||||
github.com/redis/go-redis/v9 v9.11.0
|
github.com/redis/go-redis/v9 v9.11.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25
|
github.com/smartwalle/alipay/v3 v3.2.25
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -24,6 +26,7 @@ require (
|
|||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||||
github.com/xuri/excelize/v2 v2.9.1
|
github.com/xuri/excelize/v2 v2.9.1
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.37.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||||
|
|||||||
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/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||||
|
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
||||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
||||||
@@ -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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||||
|
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
@@ -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/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
|
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
|
||||||
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
|
||||||
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
|
||||||
@@ -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/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
|
||||||
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
|
||||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||||
|
|||||||
@@ -245,6 +245,8 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
|||||||
&articleEntities.Category{},
|
&articleEntities.Category{},
|
||||||
&articleEntities.Tag{},
|
&articleEntities.Tag{},
|
||||||
&articleEntities.ScheduledTask{},
|
&articleEntities.ScheduledTask{},
|
||||||
|
// 公告
|
||||||
|
&articleEntities.Announcement{},
|
||||||
|
|
||||||
// 统计域
|
// 统计域
|
||||||
&statisticsEntities.StatisticsMetric{},
|
&statisticsEntities.StatisticsMetric{},
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tyapi-server/internal/application/api/commands"
|
"tyapi-server/internal/application/api/commands"
|
||||||
"tyapi-server/internal/application/api/dto"
|
"tyapi-server/internal/application/api/dto"
|
||||||
@@ -37,8 +39,8 @@ type ApiApplicationService interface {
|
|||||||
GetUserApiKeys(ctx context.Context, userID string) (*dto.ApiKeysResponse, error)
|
GetUserApiKeys(ctx context.Context, userID string) (*dto.ApiKeysResponse, error)
|
||||||
|
|
||||||
// 用户白名单管理
|
// 用户白名单管理
|
||||||
GetUserWhiteList(ctx context.Context, userID string) (*dto.WhiteListListResponse, error)
|
GetUserWhiteList(ctx context.Context, userID string, remarkKeyword string) (*dto.WhiteListListResponse, error)
|
||||||
AddWhiteListIP(ctx context.Context, userID string, ipAddress string) error
|
AddWhiteListIP(ctx context.Context, userID string, ipAddress string, remark string) error
|
||||||
DeleteWhiteListIP(ctx context.Context, userID string, ipAddress string) error
|
DeleteWhiteListIP(ctx context.Context, userID string, ipAddress string) error
|
||||||
|
|
||||||
// 获取用户API调用记录
|
// 获取用户API调用记录
|
||||||
@@ -466,7 +468,7 @@ func (s *ApiApplicationServiceImpl) GetUserApiKeys(ctx context.Context, userID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUserWhiteList 获取用户白名单列表
|
// 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)
|
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -474,28 +476,49 @@ func (s *ApiApplicationServiceImpl) GetUserWhiteList(ctx context.Context, userID
|
|||||||
|
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = entities.WhiteList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将白名单字符串数组转换为响应格式
|
// 将白名单转换为响应格式
|
||||||
var items []dto.WhiteListResponse
|
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{
|
items = append(items, dto.WhiteListResponse{
|
||||||
ID: apiUser.ID, // 使用API用户ID作为标识
|
ID: apiUser.ID, // 使用API用户ID作为标识
|
||||||
UserID: apiUser.UserId,
|
UserID: apiUser.UserId,
|
||||||
IPAddress: ip,
|
IPAddress: item.IPAddress,
|
||||||
CreatedAt: apiUser.CreatedAt, // 使用API用户创建时间
|
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{
|
return &dto.WhiteListListResponse{
|
||||||
Items: items,
|
Items: items,
|
||||||
Total: len(items),
|
Total: len(items),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contains 检查字符串是否包含子字符串(不区分大小写)
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
if substr == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||||
|
}
|
||||||
|
|
||||||
// AddWhiteListIP 添加白名单IP
|
// 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)
|
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -503,11 +526,11 @@ func (s *ApiApplicationServiceImpl) AddWhiteListIP(ctx context.Context, userID s
|
|||||||
|
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = entities.WhiteList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用实体的领域方法添加IP到白名单
|
// 使用实体的领域方法添加IP到白名单(会自动记录添加时间和备注)
|
||||||
err = apiUser.AddToWhiteList(ipAddress)
|
err = apiUser.AddToWhiteList(ipAddress, remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -530,7 +553,7 @@ func (s *ApiApplicationServiceImpl) DeleteWhiteListIP(ctx context.Context, userI
|
|||||||
|
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = entities.WhiteList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用实体的领域方法删除IP
|
// 使用实体的领域方法删除IP
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ type WhiteListResponse struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IPAddress string `json:"ip_address"`
|
IPAddress string `json:"ip_address"`
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhiteListRequest struct {
|
type WhiteListRequest struct {
|
||||||
IPAddress string `json:"ip_address" binding:"required,ip"`
|
IPAddress string `json:"ip_address" binding:"required,ip"`
|
||||||
|
Remark string `json:"remark"` // 备注(可选)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhiteListListResponse struct {
|
type WhiteListListResponse struct {
|
||||||
|
|||||||
@@ -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())
|
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
|
||||||
}
|
}
|
||||||
if cmd.UserID != "3fbd6917-bb13-40b3-bab0-de0d44c0afca" {
|
err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo)
|
||||||
err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo)
|
if err != nil {
|
||||||
if err != nil {
|
s.logger.Error("企业信息验证失败", zap.Error(err))
|
||||||
s.logger.Error("企业信息验证失败", zap.Error(err))
|
record.MarkAsFailed(err.Error())
|
||||||
record.MarkAsFailed(err.Error())
|
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
if saveErr != nil {
|
||||||
if saveErr != nil {
|
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||||
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("企业信息验证失败, %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("企业信息验证失败, %s", err.Error())
|
||||||
}
|
}
|
||||||
record.MarkAsVerified()
|
record.MarkAsVerified()
|
||||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ type CreateWalletCommand struct {
|
|||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TransferRechargeCommand 对公转账充值命令
|
// TransferRechargeCommand 对公转账充值命令
|
||||||
type TransferRechargeCommand struct {
|
type TransferRechargeCommand struct {
|
||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
@@ -16,16 +15,24 @@ type TransferRechargeCommand struct {
|
|||||||
|
|
||||||
// GiftRechargeCommand 赠送充值命令
|
// GiftRechargeCommand 赠送充值命令
|
||||||
type GiftRechargeCommand struct {
|
type GiftRechargeCommand struct {
|
||||||
UserID string `json:"user_id" binding:"required,uuid"`
|
UserID string `json:"user_id" binding:"required,uuid"`
|
||||||
Amount string `json:"amount" binding:"required"`
|
Amount string `json:"amount" binding:"required"`
|
||||||
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
|
||||||
type CreateAlipayRechargeCommand struct {
|
type CreateAlipayRechargeCommand struct {
|
||||||
UserID string `json:"-"` // 用户ID(从token获取)
|
UserID string `json:"-"` // 用户ID(从token获取)
|
||||||
Amount string `json:"amount" binding:"required"` // 充值金额
|
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||||
Subject string `json:"-"` // 订单标题
|
Subject string `json:"-"` // 订单标题
|
||||||
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台:app/h5/pc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateWechatRechargeCommand 创建微信充值订单命令
|
||||||
|
type CreateWechatRechargeCommand struct {
|
||||||
|
UserID string `json:"-"` // 用户ID(从token获取)
|
||||||
|
Amount string `json:"amount" binding:"required"` // 充值金额
|
||||||
|
Subject string `json:"-"` // 订单标题
|
||||||
|
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码,兼容传入native/wx_h5/h5
|
||||||
|
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid(用于小程序/H5)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import (
|
|||||||
|
|
||||||
// WalletResponse 钱包响应
|
// WalletResponse 钱包响应
|
||||||
type WalletResponse struct {
|
type WalletResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Balance decimal.Decimal `json:"balance"`
|
Balance decimal.Decimal `json:"balance"`
|
||||||
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
BalanceStatus string `json:"balance_status"` // normal, low, arrears
|
||||||
IsArrears bool `json:"is_arrears"` // 是否欠费
|
IsArrears bool `json:"is_arrears"` // 是否欠费
|
||||||
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionResponse 交易响应
|
// TransactionResponse 交易响应
|
||||||
@@ -49,34 +49,36 @@ type WalletStatsResponse struct {
|
|||||||
|
|
||||||
// RechargeRecordResponse 充值记录响应
|
// RechargeRecordResponse 充值记录响应
|
||||||
type RechargeRecordResponse struct {
|
type RechargeRecordResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Amount decimal.Decimal `json:"amount"`
|
Amount decimal.Decimal `json:"amount"`
|
||||||
RechargeType string `json:"recharge_type"`
|
RechargeType string `json:"recharge_type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
AlipayOrderID string `json:"alipay_order_id,omitempty"`
|
||||||
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
WechatOrderID string `json:"wechat_order_id,omitempty"`
|
||||||
Notes string `json:"notes,omitempty"`
|
TransferOrderID string `json:"transfer_order_id,omitempty"`
|
||||||
OperatorID string `json:"operator_id,omitempty"`
|
Platform string `json:"platform,omitempty"` // 支付平台:pc/wx_native等
|
||||||
CompanyName string `json:"company_name,omitempty"`
|
Notes string `json:"notes,omitempty"`
|
||||||
User *UserSimpleResponse `json:"user,omitempty"`
|
OperatorID string `json:"operator_id,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
User *UserSimpleResponse `json:"user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletTransactionResponse 钱包交易记录响应
|
// WalletTransactionResponse 钱包交易记录响应
|
||||||
type WalletTransactionResponse struct {
|
type WalletTransactionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ApiCallID string `json:"api_call_id"`
|
ApiCallID string `json:"api_call_id"`
|
||||||
TransactionID string `json:"transaction_id"`
|
TransactionID string `json:"transaction_id"`
|
||||||
ProductID string `json:"product_id"`
|
ProductID string `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
Amount decimal.Decimal `json:"amount"`
|
Amount decimal.Decimal `json:"amount"`
|
||||||
CompanyName string `json:"company_name,omitempty"`
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
User *UserSimpleResponse `json:"user,omitempty"`
|
User *UserSimpleResponse `json:"user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletTransactionListResponse 钱包交易记录列表响应
|
// WalletTransactionListResponse 钱包交易记录列表响应
|
||||||
@@ -97,17 +99,17 @@ type RechargeRecordListResponse struct {
|
|||||||
|
|
||||||
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
// AlipayRechargeOrderResponse 支付宝充值订单响应
|
||||||
type AlipayRechargeOrderResponse struct {
|
type AlipayRechargeOrderResponse struct {
|
||||||
PayURL string `json:"pay_url"` // 支付链接
|
PayURL string `json:"pay_url"` // 支付链接
|
||||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||||
Platform string `json:"platform"` // 支付平台
|
Platform string `json:"platform"` // 支付平台
|
||||||
Subject string `json:"subject"` // 订单标题
|
Subject string `json:"subject"` // 订单标题
|
||||||
}
|
}
|
||||||
|
|
||||||
// RechargeConfigResponse 充值配置响应
|
// RechargeConfigResponse 充值配置响应
|
||||||
type RechargeConfigResponse struct {
|
type RechargeConfigResponse struct {
|
||||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WechatOrderStatusResponse 微信订单状态响应
|
||||||
|
type WechatOrderStatusResponse struct {
|
||||||
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
|
TransactionID *string `json:"transaction_id"` // 微信支付交易号
|
||||||
|
Status string `json:"status"` // 订单状态
|
||||||
|
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||||
|
Subject string `json:"subject"` // 订单标题
|
||||||
|
Platform string `json:"platform"` // 支付平台
|
||||||
|
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||||
|
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||||
|
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||||
|
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||||
|
ErrorCode *string `json:"error_code"` // 错误码
|
||||||
|
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||||
|
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||||
|
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import "github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
// WechatRechargeOrderResponse 微信充值下单响应
|
||||||
|
type WechatRechargeOrderResponse struct {
|
||||||
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||||
|
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||||
|
Platform string `json:"platform"` // 支付平台
|
||||||
|
Subject string `json:"subject"` // 订单标题
|
||||||
|
PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ type FinanceApplicationService interface {
|
|||||||
|
|
||||||
// 充值管理
|
// 充值管理
|
||||||
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
|
||||||
|
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
|
||||||
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||||
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
|
||||||
|
|
||||||
@@ -33,12 +34,15 @@ type FinanceApplicationService interface {
|
|||||||
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
|
||||||
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
|
||||||
|
|
||||||
|
// 微信支付回调处理
|
||||||
|
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)
|
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||||
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
|
||||||
|
|
||||||
// 获取充值配置
|
// 获取充值配置
|
||||||
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ package finance
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/smartwalle/alipay/v3"
|
||||||
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||||
|
"go.uber.org/zap"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
"tyapi-server/internal/application/finance/dto/commands"
|
"tyapi-server/internal/application/finance/dto/commands"
|
||||||
"tyapi-server/internal/application/finance/dto/queries"
|
"tyapi-server/internal/application/finance/dto/queries"
|
||||||
"tyapi-server/internal/application/finance/dto/responses"
|
"tyapi-server/internal/application/finance/dto/responses"
|
||||||
@@ -16,19 +21,18 @@ import (
|
|||||||
"tyapi-server/internal/shared/export"
|
"tyapi-server/internal/shared/export"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
"tyapi-server/internal/shared/payment"
|
"tyapi-server/internal/shared/payment"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"github.com/smartwalle/alipay/v3"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FinanceApplicationServiceImpl 财务应用服务实现
|
// FinanceApplicationServiceImpl 财务应用服务实现
|
||||||
type FinanceApplicationServiceImpl struct {
|
type FinanceApplicationServiceImpl struct {
|
||||||
aliPayClient *payment.AliPayService
|
aliPayClient *payment.AliPayService
|
||||||
|
wechatPayService *payment.WechatPayService
|
||||||
walletService finance_services.WalletAggregateService
|
walletService finance_services.WalletAggregateService
|
||||||
rechargeRecordService finance_services.RechargeRecordService
|
rechargeRecordService finance_services.RechargeRecordService
|
||||||
walletTransactionRepository finance_repositories.WalletTransactionRepository
|
walletTransactionRepository finance_repositories.WalletTransactionRepository
|
||||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||||
|
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||||
|
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||||
userRepo user_repositories.UserRepository
|
userRepo user_repositories.UserRepository
|
||||||
txManager *database.TransactionManager
|
txManager *database.TransactionManager
|
||||||
exportManager *export.ExportManager
|
exportManager *export.ExportManager
|
||||||
@@ -39,10 +43,13 @@ type FinanceApplicationServiceImpl struct {
|
|||||||
// NewFinanceApplicationService 创建财务应用服务
|
// NewFinanceApplicationService 创建财务应用服务
|
||||||
func NewFinanceApplicationService(
|
func NewFinanceApplicationService(
|
||||||
aliPayClient *payment.AliPayService,
|
aliPayClient *payment.AliPayService,
|
||||||
|
wechatPayService *payment.WechatPayService,
|
||||||
walletService finance_services.WalletAggregateService,
|
walletService finance_services.WalletAggregateService,
|
||||||
rechargeRecordService finance_services.RechargeRecordService,
|
rechargeRecordService finance_services.RechargeRecordService,
|
||||||
walletTransactionRepository finance_repositories.WalletTransactionRepository,
|
walletTransactionRepository finance_repositories.WalletTransactionRepository,
|
||||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||||
|
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||||
|
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||||
userRepo user_repositories.UserRepository,
|
userRepo user_repositories.UserRepository,
|
||||||
txManager *database.TransactionManager,
|
txManager *database.TransactionManager,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
@@ -51,10 +58,13 @@ func NewFinanceApplicationService(
|
|||||||
) FinanceApplicationService {
|
) FinanceApplicationService {
|
||||||
return &FinanceApplicationServiceImpl{
|
return &FinanceApplicationServiceImpl{
|
||||||
aliPayClient: aliPayClient,
|
aliPayClient: aliPayClient,
|
||||||
|
wechatPayService: wechatPayService,
|
||||||
walletService: walletService,
|
walletService: walletService,
|
||||||
rechargeRecordService: rechargeRecordService,
|
rechargeRecordService: rechargeRecordService,
|
||||||
walletTransactionRepository: walletTransactionRepository,
|
walletTransactionRepository: walletTransactionRepository,
|
||||||
alipayOrderRepo: alipayOrderRepo,
|
alipayOrderRepo: alipayOrderRepo,
|
||||||
|
wechatOrderRepo: wechatOrderRepo,
|
||||||
|
rechargeRecordRepo: rechargeRecordRepo,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
txManager: txManager,
|
txManager: txManager,
|
||||||
exportManager: exportManager,
|
exportManager: exportManager,
|
||||||
@@ -100,8 +110,9 @@ func (s *FinanceApplicationServiceImpl) GetWallet(ctx context.Context, query *qu
|
|||||||
BalanceStatus: wallet.GetBalanceStatus(),
|
BalanceStatus: wallet.GetBalanceStatus(),
|
||||||
IsArrears: wallet.IsArrears(),
|
IsArrears: wallet.IsArrears(),
|
||||||
IsLowBalance: wallet.IsLowBalance(),
|
IsLowBalance: wallet.IsLowBalance(),
|
||||||
CreatedAt: wallet.CreatedAt,
|
|
||||||
UpdatedAt: wallet.UpdatedAt,
|
CreatedAt: wallet.CreatedAt,
|
||||||
|
UpdatedAt: wallet.UpdatedAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +199,168 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co
|
|||||||
}, nil
|
}, 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 对公转账充值
|
// TransferRecharge 对公转账充值
|
||||||
func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) {
|
func (s *FinanceApplicationServiceImpl) TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error) {
|
||||||
// 将字符串金额转换为 decimal.Decimal
|
// 将字符串金额转换为 decimal.Decimal
|
||||||
@@ -507,8 +680,8 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 准备导出数据
|
// 准备导出数据
|
||||||
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "转账订单号", "备注", "充值时间"}
|
headers := []string{"企业名称", "充值金额", "充值类型", "状态", "支付宝订单号", "微信订单号", "转账订单号", "备注", "充值时间"}
|
||||||
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20}
|
columnWidths := []float64{25, 15, 15, 10, 20, 20, 20, 20, 20}
|
||||||
|
|
||||||
data := make([][]interface{}, len(allRecords))
|
data := make([][]interface{}, len(allRecords))
|
||||||
for i, record := range allRecords {
|
for i, record := range allRecords {
|
||||||
@@ -523,6 +696,10 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
|||||||
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
|
if record.AlipayOrderID != nil && *record.AlipayOrderID != "" {
|
||||||
alipayOrderID = *record.AlipayOrderID
|
alipayOrderID = *record.AlipayOrderID
|
||||||
}
|
}
|
||||||
|
wechatOrderID := ""
|
||||||
|
if record.WechatOrderID != nil && *record.WechatOrderID != "" {
|
||||||
|
wechatOrderID = *record.WechatOrderID
|
||||||
|
}
|
||||||
transferOrderID := ""
|
transferOrderID := ""
|
||||||
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
|
if record.TransferOrderID != nil && *record.TransferOrderID != "" {
|
||||||
transferOrderID = *record.TransferOrderID
|
transferOrderID = *record.TransferOrderID
|
||||||
@@ -543,6 +720,7 @@ func (s *FinanceApplicationServiceImpl) ExportAdminRechargeRecords(ctx context.C
|
|||||||
translateRechargeType(record.RechargeType),
|
translateRechargeType(record.RechargeType),
|
||||||
translateRechargeStatus(record.Status),
|
translateRechargeStatus(record.Status),
|
||||||
alipayOrderID,
|
alipayOrderID,
|
||||||
|
wechatOrderID,
|
||||||
transferOrderID,
|
transferOrderID,
|
||||||
notes,
|
notes,
|
||||||
createdAt,
|
createdAt,
|
||||||
@@ -566,6 +744,8 @@ func translateRechargeType(rechargeType finance_entities.RechargeType) string {
|
|||||||
switch rechargeType {
|
switch rechargeType {
|
||||||
case finance_entities.RechargeTypeAlipay:
|
case finance_entities.RechargeTypeAlipay:
|
||||||
return "支付宝充值"
|
return "支付宝充值"
|
||||||
|
case finance_entities.RechargeTypeWechat:
|
||||||
|
return "微信充值"
|
||||||
case finance_entities.RechargeTypeTransfer:
|
case finance_entities.RechargeTypeTransfer:
|
||||||
return "对公转账"
|
return "对公转账"
|
||||||
case finance_entities.RechargeTypeGift:
|
case finance_entities.RechargeTypeGift:
|
||||||
@@ -890,15 +1070,27 @@ func (s *FinanceApplicationServiceImpl) GetAlipayOrderStatus(ctx context.Context
|
|||||||
|
|
||||||
// GetUserRechargeRecords 获取用户充值记录
|
// GetUserRechargeRecords 获取用户充值记录
|
||||||
func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
|
func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
|
||||||
// 查询用户充值记录
|
// 确保 filters 不为 nil
|
||||||
records, err := s.rechargeRecordService.GetByUserID(ctx, userID)
|
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 {
|
if err != nil {
|
||||||
s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
s.logger.Error("查询用户充值记录失败", zap.Error(err), zap.String("userID", userID))
|
||||||
return nil, err
|
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
|
// 转换为响应DTO
|
||||||
var items []responses.RechargeRecordResponse
|
var items []responses.RechargeRecordResponse
|
||||||
@@ -914,9 +1106,20 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte
|
|||||||
UpdatedAt: record.UpdatedAt,
|
UpdatedAt: record.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据充值类型设置相应的订单号
|
// 根据充值类型设置相应的订单号和平台信息
|
||||||
if record.AlipayOrderID != nil {
|
if record.AlipayOrderID != nil {
|
||||||
item.AlipayOrderID = *record.AlipayOrderID
|
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 {
|
if record.TransferOrderID != nil {
|
||||||
item.TransferOrderID = *record.TransferOrderID
|
item.TransferOrderID = *record.TransferOrderID
|
||||||
@@ -963,9 +1166,20 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
|
|||||||
UpdatedAt: record.UpdatedAt,
|
UpdatedAt: record.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据充值类型设置相应的订单号
|
// 根据充值类型设置相应的订单号和平台信息
|
||||||
if record.AlipayOrderID != nil {
|
if record.AlipayOrderID != nil {
|
||||||
item.AlipayOrderID = *record.AlipayOrderID
|
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 {
|
if record.TransferOrderID != nil {
|
||||||
item.TransferOrderID = *record.TransferOrderID
|
item.TransferOrderID = *record.TransferOrderID
|
||||||
@@ -1012,3 +1226,445 @@ func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (
|
|||||||
AlipayRechargeBonus: bonus,
|
AlipayRechargeBonus: bonus,
|
||||||
}, nil
|
}, 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -408,7 +408,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
|||||||
// 5. 构建响应DTO
|
// 5. 构建响应DTO
|
||||||
return &dto.AvailableAmountResponse{
|
return &dto.AvailableAmountResponse{
|
||||||
AvailableAmount: availableAmount,
|
AvailableAmount: availableAmount,
|
||||||
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
|
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
|
||||||
TotalGifted: totalGifted,
|
TotalGifted: totalGifted,
|
||||||
TotalInvoiced: totalInvoiced,
|
TotalInvoiced: totalInvoiced,
|
||||||
PendingApplications: pendingAmount,
|
PendingApplications: pendingAmount,
|
||||||
@@ -417,7 +417,7 @@ func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context,
|
|||||||
|
|
||||||
// calculateAvailableAmount 计算可开票金额(私有方法)
|
// calculateAvailableAmount 计算可开票金额(私有方法)
|
||||||
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||||
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
|
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
|
||||||
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return decimal.Zero, err
|
return decimal.Zero, err
|
||||||
@@ -433,7 +433,7 @@ func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Con
|
|||||||
fmt.Println("totalInvoiced", totalInvoiced)
|
fmt.Println("totalInvoiced", totalInvoiced)
|
||||||
fmt.Println("pendingAmount", pendingAmount)
|
fmt.Println("pendingAmount", pendingAmount)
|
||||||
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
|
||||||
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
|
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
|
||||||
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
|
||||||
fmt.Println("availableAmount", availableAmount)
|
fmt.Println("availableAmount", availableAmount)
|
||||||
// 确保可开票金额不为负数
|
// 确保可开票金额不为负数
|
||||||
@@ -452,16 +452,16 @@ func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, us
|
|||||||
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
|
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
|
||||||
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
|
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
|
||||||
var totalGifted decimal.Decimal // 总赠送金额
|
var totalGifted decimal.Decimal // 总赠送金额
|
||||||
for _, record := range rechargeRecords {
|
for _, record := range rechargeRecords {
|
||||||
if record.IsSuccess() {
|
if record.IsSuccess() {
|
||||||
if record.RechargeType == entities.RechargeTypeGift {
|
if record.RechargeType == entities.RechargeTypeGift {
|
||||||
// 赠送金额不计入可开票金额
|
// 赠送金额不计入可开票金额
|
||||||
totalGifted = totalGifted.Add(record.Amount)
|
totalGifted = totalGifted.Add(record.Amount)
|
||||||
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
|
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
|
||||||
// 只有支付宝充值和对公转账计入可开票金额
|
// 支付宝充值、微信充值和对公转账计入可开票金额
|
||||||
realRecharged = realRecharged.Add(record.Amount)
|
realRecharged = realRecharged.Add(record.Amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package product
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
@@ -28,6 +30,9 @@ type DocumentationApplicationServiceInterface interface {
|
|||||||
|
|
||||||
// GetDocumentationsByProductIDs 批量获取文档
|
// GetDocumentationsByProductIDs 批量获取文档
|
||||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||||
|
|
||||||
|
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||||
|
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentationApplicationService 文档应用服务
|
// DocumentationApplicationService 文档应用服务
|
||||||
@@ -53,6 +58,7 @@ func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Contex
|
|||||||
ResponseFields: cmd.ResponseFields,
|
ResponseFields: cmd.ResponseFields,
|
||||||
ResponseExample: cmd.ResponseExample,
|
ResponseExample: cmd.ResponseExample,
|
||||||
ErrorCodes: cmd.ErrorCodes,
|
ErrorCodes: cmd.ErrorCodes,
|
||||||
|
PDFFilePath: cmd.PDFFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用领域服务创建文档
|
// 调用领域服务创建文档
|
||||||
@@ -88,6 +94,20 @@ func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Contex
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新PDF文件路径(如果提供)
|
||||||
|
if cmd.PDFFilePath != "" {
|
||||||
|
doc.PDFFilePath = cmd.PDFFilePath
|
||||||
|
err = s.docService.UpdateDocumentationEntity(ctx, doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
|
||||||
|
}
|
||||||
|
// 重新获取更新后的文档以确保获取最新数据
|
||||||
|
doc, err = s.docService.GetDocumentation(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 返回响应
|
// 返回响应
|
||||||
resp := responses.NewDocumentationResponse(doc)
|
resp := responses.NewDocumentationResponse(doc)
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
@@ -136,3 +156,93 @@ func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx cont
|
|||||||
|
|
||||||
return docResponses, nil
|
return docResponses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateFullDocumentation 生成完整的接口文档(Markdown格式)
|
||||||
|
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
|
||||||
|
// 通过产品ID获取文档
|
||||||
|
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取文档失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档时已经包含了产品信息(通过GetDocumentationWithProduct)
|
||||||
|
// 如果没有产品信息,通过文档ID获取
|
||||||
|
if doc.Product == nil && doc.ID != "" {
|
||||||
|
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
|
||||||
|
if err == nil && docWithProduct != nil {
|
||||||
|
doc = docWithProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var markdown strings.Builder
|
||||||
|
|
||||||
|
// 添加文档标题
|
||||||
|
productName := "产品"
|
||||||
|
if doc.Product != nil {
|
||||||
|
productName = doc.Product.Name
|
||||||
|
}
|
||||||
|
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
|
||||||
|
|
||||||
|
// 添加产品基本信息
|
||||||
|
if doc.Product != nil {
|
||||||
|
markdown.WriteString("## 产品信息\n\n")
|
||||||
|
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
|
||||||
|
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
|
||||||
|
if doc.Product.Description != "" {
|
||||||
|
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
|
||||||
|
}
|
||||||
|
markdown.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求方式
|
||||||
|
markdown.WriteString("## 请求方式\n\n")
|
||||||
|
if doc.RequestURL != "" {
|
||||||
|
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
|
||||||
|
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
|
||||||
|
markdown.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求方式详细说明
|
||||||
|
if doc.BasicInfo != "" {
|
||||||
|
markdown.WriteString("### 请求方式说明\n\n")
|
||||||
|
markdown.WriteString(doc.BasicInfo)
|
||||||
|
markdown.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求参数
|
||||||
|
if doc.RequestParams != "" {
|
||||||
|
markdown.WriteString("## 请求参数\n\n")
|
||||||
|
markdown.WriteString(doc.RequestParams)
|
||||||
|
markdown.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加返回字段说明
|
||||||
|
if doc.ResponseFields != "" {
|
||||||
|
markdown.WriteString("## 返回字段说明\n\n")
|
||||||
|
markdown.WriteString(doc.ResponseFields)
|
||||||
|
markdown.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加响应示例
|
||||||
|
if doc.ResponseExample != "" {
|
||||||
|
markdown.WriteString("## 响应示例\n\n")
|
||||||
|
markdown.WriteString(doc.ResponseExample)
|
||||||
|
markdown.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加错误代码
|
||||||
|
if doc.ErrorCodes != "" {
|
||||||
|
markdown.WriteString("## 错误代码\n\n")
|
||||||
|
markdown.WriteString(doc.ErrorCodes)
|
||||||
|
markdown.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文档版本信息
|
||||||
|
markdown.WriteString("---\n\n")
|
||||||
|
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
|
||||||
|
if doc.UpdatedAt.Year() > 1900 {
|
||||||
|
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown.String(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type CreateDocumentationCommand struct {
|
|||||||
ResponseFields string `json:"response_fields"`
|
ResponseFields string `json:"response_fields"`
|
||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDocumentationCommand 更新文档命令
|
// UpdateDocumentationCommand 更新文档命令
|
||||||
@@ -21,4 +22,5 @@ type UpdateDocumentationCommand struct {
|
|||||||
ResponseFields string `json:"response_fields"`
|
ResponseFields string `json:"response_fields"`
|
||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type DocumentationResponse struct {
|
|||||||
ResponseExample string `json:"response_example"`
|
ResponseExample string `json:"response_example"`
|
||||||
ErrorCodes string `json:"error_codes"`
|
ErrorCodes string `json:"error_codes"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
PDFFilePath string `json:"pdf_file_path,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationR
|
|||||||
ResponseExample: doc.ResponseExample,
|
ResponseExample: doc.ResponseExample,
|
||||||
ErrorCodes: doc.ErrorCodes,
|
ErrorCodes: doc.ErrorCodes,
|
||||||
Version: doc.Version,
|
Version: doc.Version,
|
||||||
|
PDFFilePath: doc.PDFFilePath,
|
||||||
CreatedAt: doc.CreatedAt,
|
CreatedAt: doc.CreatedAt,
|
||||||
UpdatedAt: doc.UpdatedAt,
|
UpdatedAt: doc.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
// ProductApplicationService 产品应用服务接口
|
// ProductApplicationService 产品应用服务接口
|
||||||
type ProductApplicationService interface {
|
type ProductApplicationService interface {
|
||||||
// 产品管理
|
// 产品管理
|
||||||
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error
|
CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error)
|
||||||
|
|
||||||
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error
|
||||||
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error
|
||||||
|
|
||||||
@@ -46,6 +47,4 @@ type ProductApplicationService interface {
|
|||||||
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error
|
||||||
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error
|
||||||
DeleteProductApiConfig(ctx context.Context, configID string) error
|
DeleteProductApiConfig(ctx context.Context, configID string) error
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func NewProductApplicationService(
|
|||||||
|
|
||||||
// CreateProduct 创建产品
|
// CreateProduct 创建产品
|
||||||
// 业务流程<E6B581>?. 构建产品实体 2. 创建产品
|
// 业务流程<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. 构建产品实体
|
// 1. 构建产品实体
|
||||||
product := &entities.Product{
|
product := &entities.Product{
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
@@ -71,8 +71,13 @@ func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 创建产品
|
// 2. 创建产品
|
||||||
_, err := s.productManagementService.CreateProduct(ctx, product)
|
createdProduct, err := s.productManagementService.CreateProduct(ctx, product)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为响应对象
|
||||||
|
return s.convertToProductAdminInfoResponse(createdProduct), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProduct 更新产品
|
// UpdateProduct 更新产品
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type SubscriptionApplicationService interface {
|
|||||||
// 我的订阅(用户专用)
|
// 我的订阅(用户专用)
|
||||||
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
ListMySubscriptions(ctx context.Context, userID string, query *queries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error)
|
||||||
GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error)
|
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)
|
GetUserSubscriptions(ctx context.Context, query *queries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
appQueries "tyapi-server/internal/application/product/dto/queries"
|
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
|
domain_api_repo "tyapi-server/internal/domains/api/repositories"
|
||||||
"tyapi-server/internal/domains/product/entities"
|
"tyapi-server/internal/domains/product/entities"
|
||||||
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||||||
product_service "tyapi-server/internal/domains/product/services"
|
product_service "tyapi-server/internal/domains/product/services"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
type SubscriptionApplicationServiceImpl struct {
|
type SubscriptionApplicationServiceImpl struct {
|
||||||
productSubscriptionService *product_service.ProductSubscriptionService
|
productSubscriptionService *product_service.ProductSubscriptionService
|
||||||
userRepo user_repositories.UserRepository
|
userRepo user_repositories.UserRepository
|
||||||
|
apiCallRepository domain_api_repo.ApiCallRepository
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,11 +30,13 @@ type SubscriptionApplicationServiceImpl struct {
|
|||||||
func NewSubscriptionApplicationService(
|
func NewSubscriptionApplicationService(
|
||||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||||
userRepo user_repositories.UserRepository,
|
userRepo user_repositories.UserRepository,
|
||||||
|
apiCallRepository domain_api_repo.ApiCallRepository,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) SubscriptionApplicationService {
|
) SubscriptionApplicationService {
|
||||||
return &SubscriptionApplicationServiceImpl{
|
return &SubscriptionApplicationServiceImpl{
|
||||||
productSubscriptionService: productSubscriptionService,
|
productSubscriptionService: productSubscriptionService,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
|
apiCallRepository: apiCallRepository,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,17 +266,30 @@ func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSubscriptionUsage 获取订阅使用情况
|
// GetSubscriptionUsage 获取订阅使用情况
|
||||||
// 业务流程:1. 获取订阅使用情况 2. 构建响应数据
|
// 业务流程:1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
|
||||||
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
|
||||||
|
// 获取订阅信息
|
||||||
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据用户ID和产品ID统计API调用次数
|
||||||
|
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("统计API调用次数失败,使用订阅记录中的值",
|
||||||
|
zap.String("subscription_id", subscriptionID),
|
||||||
|
zap.String("user_id", subscription.UserID),
|
||||||
|
zap.String("product_id", subscription.ProductID),
|
||||||
|
zap.Error(err))
|
||||||
|
// 如果统计失败,使用订阅实体中的APIUsed字段作为备选
|
||||||
|
apiCallCount = subscription.APIUsed
|
||||||
|
}
|
||||||
|
|
||||||
return &responses.SubscriptionUsageResponse{
|
return &responses.SubscriptionUsageResponse{
|
||||||
ID: subscription.ID,
|
ID: subscription.ID,
|
||||||
ProductID: subscription.ProductID,
|
ProductID: subscription.ProductID,
|
||||||
APIUsed: subscription.APIUsed,
|
APIUsed: apiCallCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +321,38 @@ func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelMySubscription 取消我的订阅
|
||||||
|
// 业务流程:1. 验证订阅是否属于当前用户 2. 取消订阅
|
||||||
|
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
|
||||||
|
// 1. 获取订阅信息
|
||||||
|
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||||
|
return fmt.Errorf("订阅不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证订阅是否属于当前用户
|
||||||
|
if subscription.UserID != userID {
|
||||||
|
s.logger.Warn("用户尝试取消不属于自己的订阅",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("subscription_id", subscriptionID),
|
||||||
|
zap.String("subscription_user_id", subscription.UserID))
|
||||||
|
return fmt.Errorf("无权取消此订阅")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 取消订阅(软删除)
|
||||||
|
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
|
||||||
|
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
|
||||||
|
return fmt.Errorf("取消订阅失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("用户取消订阅成功",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("subscription_id", subscriptionID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
// convertToSubscriptionInfoResponse 转换为订阅信息响应
|
||||||
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
|
|||||||
@@ -1301,8 +1301,10 @@ func (s *StatisticsApplicationServiceImpl) getUserApiCallsStats(ctx context.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取今日调用次数
|
// 获取今日调用次数
|
||||||
today := time.Now().Truncate(24 * time.Hour)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayCalls, err := s.getApiCallsCountByDateRange(ctx, userID, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日API调用次数失败", zap.String("user_id", userID), zap.Error(err))
|
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)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayAmount, err := s.getWalletTransactionsByDateRange(ctx, userID, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日消费金额失败", zap.String("user_id", userID), zap.Error(err))
|
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)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayAmount, err := s.getRechargeRecordsByDateRange(ctx, userID, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日充值金额失败", zap.String("user_id", userID), zap.Error(err))
|
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)
|
successRate = float64(userStats.CertifiedUsers) / float64(userStats.TotalUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据时间范围获取趋势数据
|
// 根据时间范围获取认证趋势数据(基于is_certified字段)
|
||||||
var trendData []map[string]interface{}
|
var trendData []map[string]interface{}
|
||||||
if !startTime.IsZero() && !endTime.IsZero() {
|
if !startTime.IsZero() && !endTime.IsZero() {
|
||||||
if period == "day" {
|
if period == "day" {
|
||||||
trendData, err = s.userRepo.GetSystemDailyUserStats(ctx, startTime, endTime)
|
trendData, err = s.userRepo.GetSystemDailyCertificationStats(ctx, startTime, endTime)
|
||||||
} else if period == "month" {
|
} else if period == "month" {
|
||||||
trendData, err = s.userRepo.GetSystemMonthlyUserStats(ctx, startTime, endTime)
|
trendData, err = s.userRepo.GetSystemMonthlyCertificationStats(ctx, startTime, endTime)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取认证趋势数据失败", zap.Error(err))
|
s.logger.Error("获取认证趋势数据失败", zap.Error(err))
|
||||||
@@ -1698,16 +1704,35 @@ func (s *StatisticsApplicationServiceImpl) getCertificationStats(ctx context.Con
|
|||||||
// 默认获取最近7天的数据
|
// 默认获取最近7天的数据
|
||||||
endDate := time.Now()
|
endDate := time.Now()
|
||||||
startDate := endDate.AddDate(0, 0, -7)
|
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 {
|
if err != nil {
|
||||||
s.logger.Error("获取认证每日趋势失败", zap.Error(err))
|
s.logger.Error("获取认证每日趋势失败", zap.Error(err))
|
||||||
return nil, 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{}{
|
stats := map[string]interface{}{
|
||||||
"total_certified": userStats.CertifiedUsers,
|
"total_certified": userStats.CertifiedUsers,
|
||||||
"certified_today": userStats.TodayRegistrations, // 今日注册的用户
|
"certified_today": certifiedToday, // 今日认证的用户数(基于is_certified字段)
|
||||||
"success_rate": successRate,
|
"success_rate": successRate,
|
||||||
"daily_trend": trendData,
|
"daily_trend": trendData,
|
||||||
}
|
}
|
||||||
@@ -1723,9 +1748,11 @@ func (s *StatisticsApplicationServiceImpl) getSystemApiCallStats(ctx context.Con
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取今日API调用次数
|
// 获取今日API调用次数(东八时区)
|
||||||
today := time.Now().Truncate(24 * time.Hour)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayCalls, err := s.apiCallRepo.GetSystemCallsByDateRange(ctx, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日API调用次数失败", zap.Error(err))
|
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)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayConsumption, err := s.walletTransactionRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
||||||
@@ -2275,6 +2305,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetApiDomainStatistics(ctx conte
|
|||||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||||
return nil, 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 != "" {
|
if endDate != "" {
|
||||||
endTime, err = time.Parse("2006-01-02", 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))
|
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||||
return nil, 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调用统计数据
|
// 获取API调用统计数据
|
||||||
@@ -2318,6 +2360,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
|||||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||||
return nil, 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 != "" {
|
if endDate != "" {
|
||||||
endTime, err = time.Parse("2006-01-02", endDate)
|
endTime, err = time.Parse("2006-01-02", endDate)
|
||||||
@@ -2325,6 +2371,14 @@ func (s *StatisticsApplicationServiceImpl) AdminGetConsumptionDomainStatistics(c
|
|||||||
s.logger.Error("解析结束日期失败", zap.Error(err))
|
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||||
return nil, 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)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayConsumption, err := s.walletTransactionRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
s.logger.Error("获取今日消费金额失败", zap.Error(err))
|
||||||
@@ -2406,6 +2462,10 @@ func (s *StatisticsApplicationServiceImpl) AdminGetRechargeDomainStatistics(ctx
|
|||||||
s.logger.Error("解析开始日期失败", zap.Error(err))
|
s.logger.Error("解析开始日期失败", zap.Error(err))
|
||||||
return nil, 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 != "" {
|
if endDate != "" {
|
||||||
endTime, err = time.Parse("2006-01-02", endDate)
|
endTime, err = time.Parse("2006-01-02", endDate)
|
||||||
@@ -2413,6 +2473,14 @@ func (s *StatisticsApplicationServiceImpl) AdminGetRechargeDomainStatistics(ctx
|
|||||||
s.logger.Error("解析结束日期失败", zap.Error(err))
|
s.logger.Error("解析结束日期失败", zap.Error(err))
|
||||||
return nil, 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)
|
loc, _ := time.LoadLocation("Asia/Shanghai") // 东八时区
|
||||||
tomorrow := today.Add(24 * time.Hour)
|
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)
|
todayRecharge, err := s.rechargeRecordRepo.GetSystemAmountByDateRange(ctx, today, tomorrow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("获取今日充值金额失败", zap.Error(err))
|
s.logger.Error("获取今日充值金额失败", zap.Error(err))
|
||||||
@@ -2716,15 +2786,15 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
|
|||||||
}
|
}
|
||||||
|
|
||||||
enterprise := map[string]interface{}{
|
enterprise := map[string]interface{}{
|
||||||
"id": cert.ID,
|
"id": cert.ID,
|
||||||
"user_id": cert.UserID,
|
"user_id": cert.UserID,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"enterprise_name": enterpriseInfo.CompanyName,
|
"enterprise_name": enterpriseInfo.CompanyName,
|
||||||
"legal_person_name": enterpriseInfo.LegalPersonName,
|
"legal_person_name": enterpriseInfo.LegalPersonName,
|
||||||
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
|
"legal_person_phone": enterpriseInfo.LegalPersonPhone,
|
||||||
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
|
"unified_social_code": enterpriseInfo.UnifiedSocialCode,
|
||||||
"enterprise_address": enterpriseInfo.EnterpriseAddress,
|
"enterprise_address": enterpriseInfo.EnterpriseAddress,
|
||||||
"certified_at": cert.CompletedAt.Format(time.RFC3339),
|
"certified_at": cert.CompletedAt.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
enterprises = append(enterprises, enterprise)
|
enterprises = append(enterprises, enterprise)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ type Config struct {
|
|||||||
Zhicha ZhichaConfig `mapstructure:"zhicha"`
|
Zhicha ZhichaConfig `mapstructure:"zhicha"`
|
||||||
Muzi MuziConfig `mapstructure:"muzi"`
|
Muzi MuziConfig `mapstructure:"muzi"`
|
||||||
AliPay AliPayConfig `mapstructure:"alipay"`
|
AliPay AliPayConfig `mapstructure:"alipay"`
|
||||||
|
Wxpay WxpayConfig `mapstructure:"wxpay"`
|
||||||
|
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
|
||||||
|
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
|
||||||
Yushan YushanConfig `mapstructure:"yushan"`
|
Yushan YushanConfig `mapstructure:"yushan"`
|
||||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||||
@@ -429,6 +432,29 @@ type AliPayConfig struct {
|
|||||||
ReturnURL string `mapstructure:"return_url"`
|
ReturnURL string `mapstructure:"return_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WxpayConfig 微信支付配置
|
||||||
|
type WxpayConfig struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
MchID string `mapstructure:"mch_id"`
|
||||||
|
MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"`
|
||||||
|
MchApiv3Key string `mapstructure:"mch_apiv3_key"`
|
||||||
|
MchPrivateKeyPath string `mapstructure:"mch_private_key_path"`
|
||||||
|
MchPublicKeyID string `mapstructure:"mch_public_key_id"`
|
||||||
|
MchPublicKeyPath string `mapstructure:"mch_public_key_path"`
|
||||||
|
NotifyUrl string `mapstructure:"notify_url"`
|
||||||
|
RefundNotifyUrl string `mapstructure:"refund_notify_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WechatMiniConfig 微信小程序配置
|
||||||
|
type WechatMiniConfig struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WechatH5Config 微信H5配置
|
||||||
|
type WechatH5Config struct {
|
||||||
|
AppID string `mapstructure:"app_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// YushanConfig 羽山配置
|
// YushanConfig 羽山配置
|
||||||
type YushanConfig struct {
|
type YushanConfig struct {
|
||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -66,6 +68,7 @@ import (
|
|||||||
"tyapi-server/internal/shared/middleware"
|
"tyapi-server/internal/shared/middleware"
|
||||||
sharedOCR "tyapi-server/internal/shared/ocr"
|
sharedOCR "tyapi-server/internal/shared/ocr"
|
||||||
"tyapi-server/internal/shared/payment"
|
"tyapi-server/internal/shared/payment"
|
||||||
|
"tyapi-server/internal/shared/pdf"
|
||||||
"tyapi-server/internal/shared/resilience"
|
"tyapi-server/internal/shared/resilience"
|
||||||
"tyapi-server/internal/shared/saga"
|
"tyapi-server/internal/shared/saga"
|
||||||
"tyapi-server/internal/shared/tracing"
|
"tyapi-server/internal/shared/tracing"
|
||||||
@@ -304,6 +307,16 @@ func NewContainer() *Container {
|
|||||||
}
|
}
|
||||||
return payment.NewAliPayService(config)
|
return payment.NewAliPayService(config)
|
||||||
},
|
},
|
||||||
|
// 微信支付服务
|
||||||
|
func(cfg *config.Config, logger *zap.Logger) *payment.WechatPayService {
|
||||||
|
// 根据配置选择初始化方式,默认使用平台证书方式
|
||||||
|
initType := payment.InitTypePlatformCert
|
||||||
|
// 如果配置了公钥ID,使用公钥方式
|
||||||
|
if cfg.Wxpay.MchPublicKeyID != "" {
|
||||||
|
initType = payment.InitTypeWxPayPubKey
|
||||||
|
}
|
||||||
|
return payment.NewWechatPayService(*cfg, initType, logger)
|
||||||
|
},
|
||||||
// 导出管理器
|
// 导出管理器
|
||||||
func(logger *zap.Logger) *export.ExportManager {
|
func(logger *zap.Logger) *export.ExportManager {
|
||||||
return export.NewExportManager(logger)
|
return export.NewExportManager(logger)
|
||||||
@@ -509,6 +522,11 @@ func NewContainer() *Container {
|
|||||||
finance_repo.NewGormAlipayOrderRepository,
|
finance_repo.NewGormAlipayOrderRepository,
|
||||||
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
|
||||||
),
|
),
|
||||||
|
// 微信订单仓储
|
||||||
|
fx.Annotate(
|
||||||
|
finance_repo.NewGormWechatOrderRepository,
|
||||||
|
fx.As(new(domain_finance_repo.WechatOrderRepository)),
|
||||||
|
),
|
||||||
// 发票申请仓储
|
// 发票申请仓储
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
finance_repo.NewGormInvoiceApplicationRepository,
|
finance_repo.NewGormInvoiceApplicationRepository,
|
||||||
@@ -571,6 +589,11 @@ func NewContainer() *Container {
|
|||||||
article_repo.NewGormScheduledTaskRepository,
|
article_repo.NewGormScheduledTaskRepository,
|
||||||
fx.As(new(domain_article_repo.ScheduledTaskRepository)),
|
fx.As(new(domain_article_repo.ScheduledTaskRepository)),
|
||||||
),
|
),
|
||||||
|
// 公告仓储 - 同时注册具体类型和接口类型
|
||||||
|
fx.Annotate(
|
||||||
|
article_repo.NewGormAnnouncementRepository,
|
||||||
|
fx.As(new(domain_article_repo.AnnouncementRepository)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// API域仓储层
|
// API域仓储层
|
||||||
@@ -675,6 +698,8 @@ func NewContainer() *Container {
|
|||||||
certification_service.NewEnterpriseInfoSubmitRecordService,
|
certification_service.NewEnterpriseInfoSubmitRecordService,
|
||||||
// 文章领域服务
|
// 文章领域服务
|
||||||
article_service.NewArticleService,
|
article_service.NewArticleService,
|
||||||
|
// 公告领域服务
|
||||||
|
article_service.NewAnnouncementService,
|
||||||
// 统计领域服务
|
// 统计领域服务
|
||||||
statistics_service.NewStatisticsAggregateService,
|
statistics_service.NewStatisticsAggregateService,
|
||||||
statistics_service.NewStatisticsCalculationService,
|
statistics_service.NewStatisticsCalculationService,
|
||||||
@@ -775,6 +800,7 @@ func NewContainer() *Container {
|
|||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
articleApplicationService article.ArticleApplicationService,
|
articleApplicationService article.ArticleApplicationService,
|
||||||
|
announcementApplicationService article.AnnouncementApplicationService,
|
||||||
apiApplicationService api_app.ApiApplicationService,
|
apiApplicationService api_app.ApiApplicationService,
|
||||||
walletService finance_services.WalletAggregateService,
|
walletService finance_services.WalletAggregateService,
|
||||||
subscriptionService *product_services.ProductSubscriptionService,
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
@@ -785,6 +811,7 @@ func NewContainer() *Container {
|
|||||||
redisAddr,
|
redisAddr,
|
||||||
logger,
|
logger,
|
||||||
articleApplicationService,
|
articleApplicationService,
|
||||||
|
announcementApplicationService,
|
||||||
apiApplicationService,
|
apiApplicationService,
|
||||||
walletService,
|
walletService,
|
||||||
subscriptionService,
|
subscriptionService,
|
||||||
@@ -843,10 +870,13 @@ func NewContainer() *Container {
|
|||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
func(
|
func(
|
||||||
aliPayClient *payment.AliPayService,
|
aliPayClient *payment.AliPayService,
|
||||||
|
wechatPayService *payment.WechatPayService,
|
||||||
walletService finance_services.WalletAggregateService,
|
walletService finance_services.WalletAggregateService,
|
||||||
rechargeRecordService finance_services.RechargeRecordService,
|
rechargeRecordService finance_services.RechargeRecordService,
|
||||||
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
walletTransactionRepo domain_finance_repo.WalletTransactionRepository,
|
||||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||||
|
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||||
|
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||||
userRepo domain_user_repo.UserRepository,
|
userRepo domain_user_repo.UserRepository,
|
||||||
txManager *shared_database.TransactionManager,
|
txManager *shared_database.TransactionManager,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
@@ -855,10 +885,13 @@ func NewContainer() *Container {
|
|||||||
) finance.FinanceApplicationService {
|
) finance.FinanceApplicationService {
|
||||||
return finance.NewFinanceApplicationService(
|
return finance.NewFinanceApplicationService(
|
||||||
aliPayClient,
|
aliPayClient,
|
||||||
|
wechatPayService,
|
||||||
walletService,
|
walletService,
|
||||||
rechargeRecordService,
|
rechargeRecordService,
|
||||||
walletTransactionRepo,
|
walletTransactionRepo,
|
||||||
alipayOrderRepo,
|
alipayOrderRepo,
|
||||||
|
wechatOrderRepo,
|
||||||
|
rechargeRecordRepo,
|
||||||
userRepo,
|
userRepo,
|
||||||
txManager,
|
txManager,
|
||||||
logger,
|
logger,
|
||||||
@@ -941,6 +974,23 @@ func NewContainer() *Container {
|
|||||||
},
|
},
|
||||||
fx.As(new(article.ArticleApplicationService)),
|
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(
|
fx.Annotate(
|
||||||
func(
|
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处理器
|
// HTTP处理器
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
// 用户HTTP处理器
|
// 用户HTTP处理器
|
||||||
@@ -1005,6 +1111,15 @@ func NewContainer() *Container {
|
|||||||
) *handlers.ArticleHandler {
|
) *handlers.ArticleHandler {
|
||||||
return handlers.NewArticleHandler(appService, responseBuilder, validator, logger)
|
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.NewProductAdminRoutes,
|
||||||
// 文章路由
|
// 文章路由
|
||||||
routes.NewArticleRoutes,
|
routes.NewArticleRoutes,
|
||||||
|
// 公告路由
|
||||||
|
routes.NewAnnouncementRoutes,
|
||||||
// API路由
|
// API路由
|
||||||
routes.NewApiRoutes,
|
routes.NewApiRoutes,
|
||||||
// 统计路由
|
// 统计路由
|
||||||
@@ -1132,6 +1249,7 @@ func RegisterRoutes(
|
|||||||
productRoutes *routes.ProductRoutes,
|
productRoutes *routes.ProductRoutes,
|
||||||
productAdminRoutes *routes.ProductAdminRoutes,
|
productAdminRoutes *routes.ProductAdminRoutes,
|
||||||
articleRoutes *routes.ArticleRoutes,
|
articleRoutes *routes.ArticleRoutes,
|
||||||
|
announcementRoutes *routes.AnnouncementRoutes,
|
||||||
apiRoutes *routes.ApiRoutes,
|
apiRoutes *routes.ApiRoutes,
|
||||||
statisticsRoutes *routes.StatisticsRoutes,
|
statisticsRoutes *routes.StatisticsRoutes,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
@@ -1149,6 +1267,7 @@ func RegisterRoutes(
|
|||||||
productRoutes.Register(router)
|
productRoutes.Register(router)
|
||||||
productAdminRoutes.Register(router)
|
productAdminRoutes.Register(router)
|
||||||
articleRoutes.Register(router)
|
articleRoutes.Register(router)
|
||||||
|
announcementRoutes.Register(router)
|
||||||
statisticsRoutes.Register(router)
|
statisticsRoutes.Register(router)
|
||||||
|
|
||||||
// 打印注册的路由信息
|
// 打印注册的路由信息
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ type QYGL23T7Req struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QYGL5CMPReq struct {
|
||||||
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
|
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||||
|
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
}
|
||||||
type YYSY4B37Req struct {
|
type YYSY4B37Req struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
}
|
}
|
||||||
@@ -195,6 +202,18 @@ type IVYZGZ08Req struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IVYZ2B2TReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
QueryReasonId int64 `json:"query_reason_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IVYZ5A9OReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type FLXG8A3FReq struct {
|
type FLXG8A3FReq struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
@@ -452,6 +471,20 @@ type JRZQ3C9RReq struct {
|
|||||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
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 {
|
type JRZQ8A2DReq struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
@@ -663,6 +696,11 @@ type IVYZ8I9JReq struct {
|
|||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IVYZ6M8PReq struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
}
|
||||||
|
|
||||||
type YYSY9E4AReq struct {
|
type YYSY9E4AReq struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package entities
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -18,14 +20,86 @@ const (
|
|||||||
ApiUserStatusFrozen = "frozen"
|
ApiUserStatusFrozen = "frozen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WhiteListItem 白名单项,包含IP地址、添加时间和备注
|
||||||
|
type WhiteListItem struct {
|
||||||
|
IPAddress string `json:"ip_address"` // IP地址
|
||||||
|
AddedAt time.Time `json:"added_at"` // 添加时间
|
||||||
|
Remark string `json:"remark"` // 备注
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhiteList 白名单类型,支持向后兼容(旧的字符串数组格式)
|
||||||
|
type WhiteList []WhiteListItem
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||||
|
func (w WhiteList) Value() (driver.Value, error) {
|
||||||
|
if w == nil {
|
||||||
|
return "[]", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口,用于数据库读取(支持向后兼容)
|
||||||
|
func (w *WhiteList) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*w = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes []byte
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
bytes = v
|
||||||
|
case string:
|
||||||
|
bytes = []byte(v)
|
||||||
|
default:
|
||||||
|
return errors.New("无法扫描 WhiteList 类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bytes) == 0 || string(bytes) == "[]" || string(bytes) == "null" {
|
||||||
|
*w = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先尝试解析为新格式(结构体数组)
|
||||||
|
var items []WhiteListItem
|
||||||
|
if err := json.Unmarshal(bytes, &items); err == nil {
|
||||||
|
// 成功解析为新格式
|
||||||
|
*w = WhiteList(items)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果失败,尝试解析为旧格式(字符串数组)
|
||||||
|
var oldFormat []string
|
||||||
|
if err := json.Unmarshal(bytes, &oldFormat); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将旧格式转换为新格式
|
||||||
|
now := time.Now()
|
||||||
|
items = make([]WhiteListItem, 0, len(oldFormat))
|
||||||
|
for _, ip := range oldFormat {
|
||||||
|
items = append(items, WhiteListItem{
|
||||||
|
IPAddress: ip,
|
||||||
|
AddedAt: now, // 使用当前时间作为添加时间(因为旧数据没有时间信息)
|
||||||
|
Remark: "", // 旧数据没有备注信息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*w = WhiteList(items)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ApiUser API用户(聚合根)
|
// ApiUser API用户(聚合根)
|
||||||
type ApiUser struct {
|
type ApiUser struct {
|
||||||
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
ID string `gorm:"primaryKey;type:varchar(64)" json:"id"`
|
||||||
UserId string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_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"`
|
AccessId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"access_id"`
|
||||||
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
SecretKey string `gorm:"type:varchar(128);not null" json:"secret_key"`
|
||||||
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
Status string `gorm:"type:varchar(20);not null;default:'normal'" json:"status"`
|
||||||
WhiteList []string `gorm:"type:json;serializer:json;default:'[]'" json:"white_list"` // 支持多个白名单
|
WhiteList WhiteList `gorm:"type:json;default:'[]'" json:"white_list"` // 支持多个白名单,包含IP和添加时间,支持向后兼容
|
||||||
|
|
||||||
// 余额预警配置
|
// 余额预警配置
|
||||||
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
BalanceAlertEnabled bool `gorm:"default:true" json:"balance_alert_enabled" comment:"是否启用余额预警"`
|
||||||
@@ -41,7 +115,7 @@ type ApiUser struct {
|
|||||||
// IsWhiteListed 校验IP/域名是否在白名单
|
// IsWhiteListed 校验IP/域名是否在白名单
|
||||||
func (u *ApiUser) IsWhiteListed(target string) bool {
|
func (u *ApiUser) IsWhiteListed(target string) bool {
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w == target {
|
if w.IPAddress == target {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +151,7 @@ func NewApiUser(userId string, defaultAlertEnabled bool, defaultAlertThreshold f
|
|||||||
AccessId: accessId,
|
AccessId: accessId,
|
||||||
SecretKey: secretKey,
|
SecretKey: secretKey,
|
||||||
Status: ApiUserStatusNormal,
|
Status: ApiUserStatusNormal,
|
||||||
WhiteList: []string{},
|
WhiteList: WhiteList{},
|
||||||
BalanceAlertEnabled: defaultAlertEnabled,
|
BalanceAlertEnabled: defaultAlertEnabled,
|
||||||
BalanceAlertThreshold: defaultAlertThreshold,
|
BalanceAlertThreshold: defaultAlertThreshold,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -90,12 +164,12 @@ func (u *ApiUser) Freeze() {
|
|||||||
func (u *ApiUser) Unfreeze() {
|
func (u *ApiUser) Unfreeze() {
|
||||||
u.Status = ApiUserStatusNormal
|
u.Status = ApiUserStatusNormal
|
||||||
}
|
}
|
||||||
func (u *ApiUser) UpdateWhiteList(list []string) {
|
func (u *ApiUser) UpdateWhiteList(list []WhiteListItem) {
|
||||||
u.WhiteList = list
|
u.WhiteList = WhiteList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddToWhiteList 新增白名单项(防御性校验)
|
// AddToWhiteList 新增白名单项(防御性校验)
|
||||||
func (u *ApiUser) AddToWhiteList(entry string) error {
|
func (u *ApiUser) AddToWhiteList(entry string, remark string) error {
|
||||||
if len(u.WhiteList) >= 10 {
|
if len(u.WhiteList) >= 10 {
|
||||||
return errors.New("白名单最多只能有10个")
|
return errors.New("白名单最多只能有10个")
|
||||||
}
|
}
|
||||||
@@ -103,27 +177,31 @@ func (u *ApiUser) AddToWhiteList(entry string) error {
|
|||||||
return errors.New("非法IP")
|
return errors.New("非法IP")
|
||||||
}
|
}
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w == entry {
|
if w.IPAddress == entry {
|
||||||
return errors.New("白名单已存在")
|
return errors.New("白名单已存在")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
u.WhiteList = append(u.WhiteList, entry)
|
u.WhiteList = append(u.WhiteList, WhiteListItem{
|
||||||
|
IPAddress: entry,
|
||||||
|
AddedAt: time.Now(),
|
||||||
|
Remark: remark,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
// BeforeUpdate GORM钩子:更新前确保WhiteList不为nil
|
||||||
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
func (u *ApiUser) BeforeUpdate(tx *gorm.DB) error {
|
||||||
if u.WhiteList == nil {
|
if u.WhiteList == nil {
|
||||||
u.WhiteList = []string{}
|
u.WhiteList = WhiteList{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFromWhiteList 删除白名单项
|
// RemoveFromWhiteList 删除白名单项
|
||||||
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
func (u *ApiUser) RemoveFromWhiteList(entry string) error {
|
||||||
newList := make([]string, 0, len(u.WhiteList))
|
newList := make([]WhiteListItem, 0, len(u.WhiteList))
|
||||||
for _, w := range u.WhiteList {
|
for _, w := range u.WhiteList {
|
||||||
if w != entry {
|
if w.IPAddress != entry {
|
||||||
newList = append(newList, w)
|
newList = append(newList, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,9 +294,9 @@ func (u *ApiUser) Validate() error {
|
|||||||
if len(u.WhiteList) > 10 {
|
if len(u.WhiteList) > 10 {
|
||||||
return errors.New("白名单最多只能有10个")
|
return errors.New("白名单最多只能有10个")
|
||||||
}
|
}
|
||||||
for _, ip := range u.WhiteList {
|
for _, item := range u.WhiteList {
|
||||||
if net.ParseIP(ip) == nil {
|
if net.ParseIP(item.IPAddress) == nil {
|
||||||
return errors.New("白名单项必须为合法IP地址: " + ip)
|
return errors.New("白名单项必须为合法IP地址: " + item.IPAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -259,7 +337,26 @@ func (c *ApiUser) BeforeCreate(tx *gorm.DB) error {
|
|||||||
c.ID = uuid.New().String()
|
c.ID = uuid.New().String()
|
||||||
}
|
}
|
||||||
if c.WhiteList == nil {
|
if c.WhiteList == nil {
|
||||||
c.WhiteList = []string{}
|
c.WhiteList = WhiteList{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AfterFind GORM钩子:查询后处理数据,确保AddedAt不为零值
|
||||||
|
func (u *ApiUser) AfterFind(tx *gorm.DB) error {
|
||||||
|
// 如果 WhiteList 为空,初始化为空数组
|
||||||
|
if u.WhiteList == nil {
|
||||||
|
u.WhiteList = WhiteList{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有项的AddedAt不为零值(处理可能从旧数据迁移的情况)
|
||||||
|
now := time.Now()
|
||||||
|
for i := range u.WhiteList {
|
||||||
|
if u.WhiteList[i].AddedAt.IsZero() {
|
||||||
|
u.WhiteList[i].AddedAt = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ type ApiCallRepository interface {
|
|||||||
// 新增:统计用户API调用次数
|
// 新增:统计用户API调用次数
|
||||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||||
|
|
||||||
|
// 新增:根据用户ID和产品ID统计API调用次数
|
||||||
|
CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error)
|
||||||
|
|
||||||
// 新增:根据TransactionID查询
|
// 新增:根据TransactionID查询
|
||||||
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"JRZQ0B6Y": jrzq.ProcessJRZQ0B6YRequest,
|
"JRZQ0B6Y": jrzq.ProcessJRZQ0B6YRequest,
|
||||||
"JRZQ9A1W": jrzq.ProcessJRZQ9A1WRequest,
|
"JRZQ9A1W": jrzq.ProcessJRZQ9A1WRequest,
|
||||||
"JRZQ8F7C": jrzq.ProcessJRZQ8F7CRequest,
|
"JRZQ8F7C": jrzq.ProcessJRZQ8F7CRequest,
|
||||||
|
"JRZQ1W4X": jrzq.ProcessJRZQ1W4XRequest,
|
||||||
|
"JRZQ3P01": jrzq.ProcessJRZQ3P01Request,
|
||||||
|
"JRZQ3AG6": jrzq.ProcessJRZQ3AG6Request,
|
||||||
|
|
||||||
// QYGL系列处理器
|
// QYGL系列处理器
|
||||||
"QYGL8261": qygl.ProcessQYGL8261Request,
|
"QYGL8261": qygl.ProcessQYGL8261Request,
|
||||||
@@ -153,6 +156,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"QYGL9T1Q": qygl.ProcessQYGL9T1QRequest, //全国企业借贷意向验证查询_V1
|
"QYGL9T1Q": qygl.ProcessQYGL9T1QRequest, //全国企业借贷意向验证查询_V1
|
||||||
"QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询
|
"QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询
|
||||||
"QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询
|
"QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询
|
||||||
|
"QYGL5CMP": qygl.ProcessQYGL5CMPRequest, //企业五要素验证
|
||||||
|
|
||||||
// YYSY系列处理器
|
// YYSY系列处理器
|
||||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||||
@@ -199,7 +203,9 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest,
|
"IVYZ9K2L": ivyz.ProcessIVYZ9K2LRequest,
|
||||||
"IVYZ2C1P": ivyz.ProcessIVYZ2C1PRequest,
|
"IVYZ2C1P": ivyz.ProcessIVYZ2C1PRequest,
|
||||||
"IVYZP2Q6": ivyz.ProcessIVYZP2Q6Request,
|
"IVYZP2Q6": ivyz.ProcessIVYZP2Q6Request,
|
||||||
"JRZQ1W4X": jrzq.ProcessJRZQ1W4XRequest,
|
"IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历)
|
||||||
|
"IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型
|
||||||
|
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
|
||||||
|
|
||||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
"tyapi-server/internal/config"
|
"tyapi-server/internal/config"
|
||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
repo "tyapi-server/internal/domains/api/repositories"
|
repo "tyapi-server/internal/domains/api/repositories"
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
type ApiUserAggregateService interface {
|
type ApiUserAggregateService interface {
|
||||||
CreateApiUser(ctx context.Context, apiUserId string) error
|
CreateApiUser(ctx context.Context, apiUserId string) error
|
||||||
UpdateWhiteList(ctx context.Context, apiUserId string, whiteList []string) error
|
UpdateWhiteList(ctx context.Context, apiUserId string, whiteList []string) error
|
||||||
AddToWhiteList(ctx context.Context, apiUserId string, entry string) error
|
AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error
|
||||||
RemoveFromWhiteList(ctx context.Context, apiUserId string, entry string) error
|
RemoveFromWhiteList(ctx context.Context, apiUserId string, entry string) error
|
||||||
FreezeApiUser(ctx context.Context, apiUserId string) error
|
FreezeApiUser(ctx context.Context, apiUserId string) error
|
||||||
UnfreezeApiUser(ctx context.Context, apiUserId string) error
|
UnfreezeApiUser(ctx context.Context, apiUserId string) error
|
||||||
@@ -44,16 +45,25 @@ func (s *ApiUserAggregateServiceImpl) UpdateWhiteList(ctx context.Context, apiUs
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apiUser.UpdateWhiteList(whiteList)
|
// 将字符串数组转换为WhiteListItem数组
|
||||||
|
items := make([]entities.WhiteListItem, 0, len(whiteList))
|
||||||
|
now := time.Now()
|
||||||
|
for _, ip := range whiteList {
|
||||||
|
items = append(items, entities.WhiteListItem{
|
||||||
|
IPAddress: ip,
|
||||||
|
AddedAt: now, // 批量更新时使用当前时间
|
||||||
|
})
|
||||||
|
}
|
||||||
|
apiUser.UpdateWhiteList(items) // UpdateWhiteList 会转换为 WhiteList 类型
|
||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string) error {
|
func (s *ApiUserAggregateServiceImpl) AddToWhiteList(ctx context.Context, apiUserId string, entry string, remark string) error {
|
||||||
apiUser, err := s.repo.FindByUserId(ctx, apiUserId)
|
apiUser, err := s.repo.FindByUserId(ctx, apiUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = apiUser.AddToWhiteList(entry)
|
err = apiUser.AddToWhiteList(entry, remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -90,7 +100,6 @@ func (s *ApiUserAggregateServiceImpl) UnfreezeApiUser(ctx context.Context, apiUs
|
|||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *ApiUserAggregateServiceImpl) LoadApiUserByAccessId(ctx context.Context, accessId string) (*entities.ApiUser, error) {
|
func (s *ApiUserAggregateServiceImpl) LoadApiUserByAccessId(ctx context.Context, accessId string) (*entities.ApiUser, error) {
|
||||||
return s.repo.FindByAccessId(ctx, accessId)
|
return s.repo.FindByAccessId(ctx, accessId)
|
||||||
}
|
}
|
||||||
@@ -103,7 +112,7 @@ func (s *ApiUserAggregateServiceImpl) LoadApiUserByUserId(ctx context.Context, a
|
|||||||
|
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = entities.WhiteList{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiUser, nil
|
return apiUser, nil
|
||||||
@@ -117,7 +126,7 @@ func (s *ApiUserAggregateServiceImpl) SaveApiUser(ctx context.Context, apiUser *
|
|||||||
if exists != nil {
|
if exists != nil {
|
||||||
// 确保WhiteList不为nil
|
// 确保WhiteList不为nil
|
||||||
if apiUser.WhiteList == nil {
|
if apiUser.WhiteList == nil {
|
||||||
apiUser.WhiteList = []string{}
|
apiUser.WhiteList = []entities.WhiteListItem{}
|
||||||
}
|
}
|
||||||
return s.repo.Update(ctx, apiUser)
|
return s.repo.Update(ctx, apiUser)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -193,7 +193,12 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
|||||||
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
"QYGL2S0W": &dto.QYGL2S0WReq{}, //失信被执行企业个人查询
|
||||||
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
"QYGL9T1Q": &dto.QYGL9T1QReq{}, //全国企业借贷意向验证查询_V1
|
||||||
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
"QYGL5A9T": &dto.QYGL5A9TReq{}, //全国企业各类工商风险统计数量查询
|
||||||
|
"JRZQ3P01": &dto.JRZQ3P01Req{}, //天远风控决策
|
||||||
|
"JRZQ3AG6": &dto.JRZQ3AG6Req{}, //轻松查公积
|
||||||
|
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||||
|
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||||
|
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||||
|
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先返回已配置的DTO
|
// 优先返回已配置的DTO
|
||||||
@@ -317,6 +322,7 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string
|
|||||||
values := strings.TrimPrefix(rule, "oneof=")
|
values := strings.TrimPrefix(rule, "oneof=")
|
||||||
frontendRules = append(frontendRules, "可选值: "+values)
|
frontendRules = append(frontendRules, "可选值: "+values)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(frontendRules, "、")
|
return strings.Join(frontendRules, "、")
|
||||||
@@ -392,6 +398,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
|||||||
"photo_data": "人脸图片",
|
"photo_data": "人脸图片",
|
||||||
"owner_type": "企业主类型",
|
"owner_type": "企业主类型",
|
||||||
"type": "查询类型",
|
"type": "查询类型",
|
||||||
|
"query_reason_id": "查询原因ID",
|
||||||
}
|
}
|
||||||
|
|
||||||
if label, exists := labelMap[jsonTag]; exists {
|
if label, exists := labelMap[jsonTag]; exists {
|
||||||
@@ -436,6 +443,7 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
|||||||
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
"photo_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
"ownerType": "1",
|
"ownerType": "1",
|
||||||
"type": "per",
|
"type": "per",
|
||||||
|
"query_reason_id": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
if example, exists := exampleMap[jsonTag]; exists {
|
if example, exists := exampleMap[jsonTag]; exists {
|
||||||
@@ -489,6 +497,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
|||||||
"photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)",
|
"photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)",
|
||||||
"ownerType": "请选择企业主类型",
|
"ownerType": "请选择企业主类型",
|
||||||
"type": "请选择查询类型",
|
"type": "请选择查询类型",
|
||||||
|
"query_reason_id": "请选择查询原因ID",
|
||||||
}
|
}
|
||||||
|
|
||||||
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
||||||
@@ -514,7 +523,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
|||||||
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
||||||
descMap := map[string]string{
|
descMap := map[string]string{
|
||||||
"mobile_no": "请输入11位手机号码",
|
"mobile_no": "请输入11位手机号码",
|
||||||
"id_card": "请输入18位身份证号码",
|
"id_card": "请输入18位身份证号码最后一位如是字母请大写",
|
||||||
"name": "请输入真实姓名",
|
"name": "请输入真实姓名",
|
||||||
"man_name": "请输入男方真实姓名",
|
"man_name": "请输入男方真实姓名",
|
||||||
"woman_name": "请输入女方真实姓名",
|
"woman_name": "请输入女方真实姓名",
|
||||||
@@ -543,7 +552,8 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
|
|||||||
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||||
"photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
"photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||||
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||||
"type": "查询类型:per-人员,ent-企业 1",
|
"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 {
|
if desc, exists := descMap[jsonTag]; exists {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
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)
|
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(processors.ErrSystem, err)
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"tyapi-server/internal/infrastructure/external/westdex"
|
"tyapi-server/internal/infrastructure/external/westdex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessFLXG5876Request FLXG5876 API处理方法
|
// ProcessFLXG5876Request FLXG5876 易诉人识别API处理方法
|
||||||
func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
var paramsDto dto.FLXG5876Req
|
var paramsDto dto.FLXG5876Req
|
||||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
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)
|
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(processors.ErrSystem, err)
|
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 {
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
}
|
}
|
||||||
|
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||||
|
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||||
|
}
|
||||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||||
reqData := map[string]interface{}{
|
reqData := map[string]interface{}{
|
||||||
"name": paramsDto.Name,
|
"name": paramsDto.Name,
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
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)
|
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(processors.ErrSystem, err)
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(processors.ErrSystem, err)
|
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)
|
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(processors.ErrSystem, err)
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
|||||||
@@ -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)
|
const maxRetries = 5
|
||||||
if err != nil {
|
var respBytes []byte
|
||||||
if errors.Is(err, westdex.ErrDatasource) {
|
|
||||||
return nil, errors.Join(processors.ErrDatasource, err)
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||||
} else {
|
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)
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是最后一次尝试,返回错误
|
||||||
|
if attempt == maxRetries {
|
||||||
|
return nil, errors.Join(processors.ErrDatasource, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即重试,不等待
|
||||||
}
|
}
|
||||||
|
|
||||||
return respBytes, nil
|
return respBytes, nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"tyapi-server/internal/domains/api/dto"
|
"tyapi-server/internal/domains/api/dto"
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
@@ -20,6 +21,10 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors
|
|||||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
}
|
}
|
||||||
|
// 新增:身份证一致性校验
|
||||||
|
if strings.EqualFold(strings.TrimSpace(paramsDto.ManIDCard), strings.TrimSpace(paramsDto.WomanIDCard)) {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, errors.New("请正确填写身份信息"))
|
||||||
|
}
|
||||||
|
|
||||||
encryptedManName, err := deps.WestDexService.Encrypt(paramsDto.ManName)
|
encryptedManName, err := deps.WestDexService.Encrypt(paramsDto.ManName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,7 +49,7 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors
|
|||||||
reqData := map[string]interface{}{
|
reqData := map[string]interface{}{
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
"name_man": encryptedManName,
|
"name_man": encryptedManName,
|
||||||
"idcard_man": encryptedManIDCard,
|
"idcard_man": encryptedManIDCard,
|
||||||
"name_woman": encryptedWomanName,
|
"name_woman": encryptedWomanName,
|
||||||
"idcard_woman": encryptedWomanIDCard,
|
"idcard_woman": encryptedWomanIDCard,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"tyapi-server/internal/domains/api/dto"
|
"tyapi-server/internal/domains/api/dto"
|
||||||
"tyapi-server/internal/domains/api/services/processors"
|
"tyapi-server/internal/domains/api/services/processors"
|
||||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessQYGL23T7Request QYGL23T7 API处理方法 - 企业三要素验证
|
// ProcessQYGL23T7Request QYGL23T7 API处理方法 - 企业四要素验证
|
||||||
func ProcessQYGL23T7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
func ProcessQYGL23T7Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
var paramsDto dto.QYGL23T7Req
|
var paramsDto dto.QYGL23T7Req
|
||||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
|||||||
@@ -26,31 +26,25 @@ func ProcessQYGL2S0WRequest(ctx context.Context, params []byte, deps *processors
|
|||||||
var nameValue string
|
var nameValue string
|
||||||
if paramsDto.Type == "per" {
|
if paramsDto.Type == "per" {
|
||||||
// 个人查询:idCardNum 必填
|
// 个人查询:idCardNum 必填
|
||||||
fmt.Print("个人")
|
|
||||||
nameValue = paramsDto.Name
|
nameValue = paramsDto.Name
|
||||||
fmt.Println("nameValue", nameValue)
|
|
||||||
if paramsDto.IDCard == "" {
|
if paramsDto.IDCard == "" {
|
||||||
fmt.Print("个人身份证件号不能为空")
|
fmt.Print("个人身份证件号不能为空")
|
||||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当失信被执行人类型为个人时,身份证件号不能为空"))
|
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当失信被执行人类型为个人时,身份证件号不能为空"))
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if paramsDto.Type == "ent" {
|
} else if paramsDto.Type == "ent" {
|
||||||
// 企业查询:name 和 entMark 两者必填其一
|
// 企业查询:name 和 entMark 两者必填其一
|
||||||
|
nameValue = paramsDto.EntName
|
||||||
if paramsDto.EntName == "" && paramsDto.EntCode == "" {
|
if paramsDto.EntName == "" && paramsDto.EntCode == "" {
|
||||||
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当查询为企业时,企业名称和企业标识统一代码注册号两者必填其一"))
|
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, errors.New("当查询为企业时,企业名称和企业标识统一代码注册号两者必填其一"))
|
||||||
|
} // 确定使用哪个值作为 name
|
||||||
|
if paramsDto.EntName != "" {
|
||||||
|
nameValue = paramsDto.EntName
|
||||||
|
} else {
|
||||||
|
nameValue = paramsDto.EntCode
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定使用哪个值作为 name
|
|
||||||
if paramsDto.Type == "ent" && paramsDto.EntName != "" {
|
|
||||||
nameValue = paramsDto.EntName
|
|
||||||
} else {
|
|
||||||
nameValue = paramsDto.EntCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("dto2s0w", paramsDto)
|
fmt.Println("dto2s0w", paramsDto)
|
||||||
|
|
||||||
// 构建请求数据(不传的参数也需要添加,值为空字符串)
|
// 构建请求数据(不传的参数也需要添加,值为空字符串)
|
||||||
reqData := map[string]interface{}{
|
reqData := map[string]interface{}{
|
||||||
"idCardNum": paramsDto.IDCard,
|
"idCardNum": paramsDto.IDCard,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -42,8 +42,8 @@ func ProcessQYGL8271Request(ctx context.Context, params []byte, deps *processors
|
|||||||
}
|
}
|
||||||
reqData := map[string]interface{}{
|
reqData := map[string]interface{}{
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
"org_name": encryptedEntName,
|
"org_name": encryptedEntName,
|
||||||
"uscc": encryptedEntCode,
|
"uscc": encryptedEntCode,
|
||||||
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
|
"auth_authorizeFileCode": encryptedAuthAuthorizeFileCode,
|
||||||
"inquired_auth": fmt.Sprintf("authed:%s", paramsDto.AuthDate),
|
"inquired_auth": fmt.Sprintf("authed:%s", paramsDto.AuthDate),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func ProcessQYGLB4C0Request(ctx context.Context, params []byte, deps *processors
|
|||||||
// 数据源错误
|
// 数据源错误
|
||||||
if errors.Is(err, westdex.ErrDatasource) {
|
if errors.Is(err, westdex.ErrDatasource) {
|
||||||
return nil, errors.Join(processors.ErrDatasource, err)
|
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)
|
return nil, errors.Join(processors.ErrNotFound, err)
|
||||||
}
|
}
|
||||||
// 其他系统错误
|
// 其他系统错误
|
||||||
|
|||||||
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
|
package entities
|
||||||
|
|
||||||
import (
|
import "github.com/shopspring/decimal"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
// AlipayOrderStatus 支付宝订单状态枚举(别名)
|
||||||
"github.com/shopspring/decimal"
|
type AlipayOrderStatus = PayOrderStatus
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AlipayOrderStatus 支付宝订单状态枚举
|
|
||||||
type AlipayOrderStatus string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付
|
AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付
|
||||||
AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功
|
AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||||
AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败
|
AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败
|
||||||
AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消
|
AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消
|
||||||
AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭
|
AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlipayOrder 支付宝订单详情实体
|
// AlipayOrder 支付宝订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||||
type AlipayOrder struct {
|
type AlipayOrder = PayOrder
|
||||||
// 基础标识
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
||||||
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
||||||
return &AlipayOrder{
|
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay")
|
||||||
ID: uuid.New().String(),
|
|
||||||
RechargeID: rechargeID,
|
|
||||||
OutTradeNo: outTradeNo,
|
|
||||||
Subject: subject,
|
|
||||||
Amount: amount,
|
|
||||||
Platform: platform,
|
|
||||||
Status: AlipayOrderStatusPending,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 (
|
const (
|
||||||
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
||||||
|
RechargeTypeWechat RechargeType = "wechat" // 微信充值
|
||||||
RechargeTypeTransfer RechargeType = "transfer" // 对公转账
|
RechargeTypeTransfer RechargeType = "transfer" // 对公转账
|
||||||
RechargeTypeGift RechargeType = "gift" // 赠送
|
RechargeTypeGift RechargeType = "gift" // 赠送
|
||||||
)
|
)
|
||||||
@@ -42,6 +43,7 @@ type RechargeRecord struct {
|
|||||||
|
|
||||||
// 订单号字段(根据充值类型使用不同字段)
|
// 订单号字段(根据充值类型使用不同字段)
|
||||||
AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"`
|
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:"转账订单号"`
|
TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"`
|
||||||
|
|
||||||
// 通用字段
|
// 通用字段
|
||||||
@@ -104,14 +106,24 @@ func (r *RechargeRecord) MarkCancelled() {
|
|||||||
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
|
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
|
||||||
func (r *RechargeRecord) ValidatePaymentMethod() error {
|
func (r *RechargeRecord) ValidatePaymentMethod() error {
|
||||||
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
|
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
|
||||||
|
hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != ""
|
||||||
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
||||||
|
|
||||||
if hasAlipay && hasTransfer {
|
count := 0
|
||||||
return errors.New("支付宝订单号和转账订单号不能同时存在")
|
if hasAlipay {
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
|
if hasWechat {
|
||||||
if !hasAlipay && !hasTransfer {
|
count++
|
||||||
return errors.New("必须提供支付宝订单号或转账订单号")
|
}
|
||||||
|
if hasTransfer {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count > 1 {
|
||||||
|
return errors.New("支付宝、微信或转账订单号只能存在一个")
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return errors.New("必须提供支付宝、微信或转账订单号")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -124,6 +136,10 @@ func (r *RechargeRecord) GetOrderID() string {
|
|||||||
if r.AlipayOrderID != nil {
|
if r.AlipayOrderID != nil {
|
||||||
return *r.AlipayOrderID
|
return *r.AlipayOrderID
|
||||||
}
|
}
|
||||||
|
case RechargeTypeWechat:
|
||||||
|
if r.WechatOrderID != nil {
|
||||||
|
return *r.WechatOrderID
|
||||||
|
}
|
||||||
case RechargeTypeTransfer:
|
case RechargeTypeTransfer:
|
||||||
if r.TransferOrderID != nil {
|
if r.TransferOrderID != nil {
|
||||||
return *r.TransferOrderID
|
return *r.TransferOrderID
|
||||||
@@ -137,6 +153,11 @@ func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
|
|||||||
r.AlipayOrderID = &orderID
|
r.AlipayOrderID = &orderID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWechatOrderID 设置微信订单号
|
||||||
|
func (r *RechargeRecord) SetWechatOrderID(orderID string) {
|
||||||
|
r.WechatOrderID = &orderID
|
||||||
|
}
|
||||||
|
|
||||||
// SetTransferOrderID 设置转账订单号
|
// SetTransferOrderID 设置转账订单号
|
||||||
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||||
r.TransferOrderID = &orderID
|
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 工厂方法 - 创建对公转账充值记录
|
// NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录
|
||||||
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
|
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
|
||||||
return &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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"tyapi-server/internal/domains/finance/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WechatOrderRepository 微信订单仓储接口
|
||||||
|
type WechatOrderRepository interface {
|
||||||
|
Create(ctx context.Context, order entities.WechatOrder) (entities.WechatOrder, error)
|
||||||
|
GetByID(ctx context.Context, id string) (entities.WechatOrder, error)
|
||||||
|
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.WechatOrder, error)
|
||||||
|
GetByRechargeID(ctx context.Context, rechargeID string) (*entities.WechatOrder, error)
|
||||||
|
GetByUserID(ctx context.Context, userID string) ([]entities.WechatOrder, error)
|
||||||
|
Update(ctx context.Context, order entities.WechatOrder) error
|
||||||
|
UpdateStatus(ctx context.Context, id string, status entities.WechatOrderStatus) error
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
Exists(ctx context.Context, id string) (bool, error)
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ type Product struct {
|
|||||||
Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"`
|
Price decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"产品价格"`
|
||||||
CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"`
|
CostPrice decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"成本价"`
|
||||||
Remark string `gorm:"type:text" comment:"备注"`
|
Remark string `gorm:"type:text" comment:"备注"`
|
||||||
IsEnabled bool `gorm:"default:true" comment:"是否启用"`
|
IsEnabled bool `gorm:"default:false" comment:"是否启用"`
|
||||||
IsVisible bool `gorm:"default:true" comment:"是否展示"`
|
IsVisible bool `gorm:"default:false" comment:"是否展示"`
|
||||||
IsPackage bool `gorm:"default:false" comment:"是否组合包"`
|
IsPackage bool `gorm:"default:false" comment:"是否组合包"`
|
||||||
// 组合包相关关联
|
// 组合包相关关联
|
||||||
PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"`
|
PackageItems []*ProductPackageItem `gorm:"foreignKey:PackageID" comment:"组合包项目列表"`
|
||||||
@@ -62,7 +62,6 @@ func (p *Product) CanBeSubscribed() bool {
|
|||||||
return p.IsValid()
|
return p.IsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// UpdateSEO 更新SEO信息
|
// UpdateSEO 更新SEO信息
|
||||||
func (p *Product) UpdateSEO(title, description, keywords string) {
|
func (p *Product) UpdateSEO(title, description, keywords string) {
|
||||||
p.SEOTitle = title
|
p.SEOTitle = title
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type ProductDocumentation struct {
|
|||||||
ResponseExample string `gorm:"type:text" comment:"响应示例"`
|
ResponseExample string `gorm:"type:text" comment:"响应示例"`
|
||||||
ErrorCodes string `gorm:"type:text" comment:"错误代码"`
|
ErrorCodes string `gorm:"type:text" comment:"错误代码"`
|
||||||
Version string `gorm:"type:varchar(20);default:'1.0'" comment:"文档版本"`
|
Version string `gorm:"type:varchar(20);default:'1.0'" comment:"文档版本"`
|
||||||
|
PDFFilePath string `gorm:"type:varchar(500)" comment:"PDF文档文件路径或URL"`
|
||||||
|
|
||||||
// 关联关系
|
// 关联关系
|
||||||
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
||||||
@@ -44,7 +45,6 @@ func (pd *ProductDocumentation) IsValid() bool {
|
|||||||
return pd.DeletedAt.Time.IsZero()
|
return pd.DeletedAt.Time.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// UpdateContent 更新文档内容
|
// UpdateContent 更新文档内容
|
||||||
func (pd *ProductDocumentation) UpdateContent(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) {
|
func (pd *ProductDocumentation) UpdateContent(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) {
|
||||||
pd.RequestURL = requestURL
|
pd.RequestURL = requestURL
|
||||||
@@ -58,13 +58,42 @@ func (pd *ProductDocumentation) UpdateContent(requestURL, requestMethod, basicIn
|
|||||||
|
|
||||||
// IncrementVersion 增加版本号
|
// IncrementVersion 增加版本号
|
||||||
func (pd *ProductDocumentation) IncrementVersion() {
|
func (pd *ProductDocumentation) IncrementVersion() {
|
||||||
// 简单的版本号递增逻辑,实际项目中可能需要更复杂的版本管理
|
|
||||||
if pd.Version == "" {
|
if pd.Version == "" {
|
||||||
pd.Version = "1.0"
|
pd.Version = "1.0"
|
||||||
} else {
|
return
|
||||||
// 这里可以实现更复杂的版本号递增逻辑
|
|
||||||
pd.Version = pd.Version + ".1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析版本号 major.minor
|
||||||
|
parts := strings.Split(pd.Version, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
// 如果格式不正确,重置为 1.0
|
||||||
|
pd.Version = "1.0"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 major 和 minor
|
||||||
|
var major, minor int
|
||||||
|
_, err := fmt.Sscanf(parts[0], "%d", &major)
|
||||||
|
if err != nil {
|
||||||
|
pd.Version = "1.0"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = fmt.Sscanf(parts[1], "%d", &minor)
|
||||||
|
if err != nil {
|
||||||
|
pd.Version = "1.0"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递增 minor
|
||||||
|
minor++
|
||||||
|
// 如果 minor 达到 10,则 major +1,minor 重置为 0
|
||||||
|
if minor >= 10 {
|
||||||
|
major++
|
||||||
|
minor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新版本号
|
||||||
|
pd.Version = fmt.Sprintf("%d.%d", major, minor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate 验证文档完整性
|
// Validate 验证文档完整性
|
||||||
@@ -137,7 +166,6 @@ func (pd *ProductDocumentation) UpdateDocumentation(requestURL, requestMethod, b
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GetDocumentationSummary 获取文档摘要
|
// GetDocumentationSummary 获取文档摘要
|
||||||
func (pd *ProductDocumentation) GetDocumentationSummary() map[string]interface{} {
|
func (pd *ProductDocumentation) GetDocumentationSummary() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
|
|||||||
@@ -114,3 +114,15 @@ func (s *ProductDocumentationService) GetDocumentationWithProduct(ctx context.Co
|
|||||||
func (s *ProductDocumentationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]*entities.ProductDocumentation, error) {
|
func (s *ProductDocumentationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]*entities.ProductDocumentation, error) {
|
||||||
return s.docRepo.FindByProductIDs(ctx, productIDs)
|
return s.docRepo.FindByProductIDs(ctx, productIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDocumentationEntity 更新文档实体(用于更新PDFFilePath等字段)
|
||||||
|
func (s *ProductDocumentationService) UpdateDocumentationEntity(ctx context.Context, doc *entities.ProductDocumentation) error {
|
||||||
|
// 验证文档是否存在
|
||||||
|
_, err := s.docRepo.FindByID(ctx, doc.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("文档不存在: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新
|
||||||
|
return s.docRepo.Update(ctx, doc)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
// ProductManagementService 产品管理领域服务
|
// ProductManagementService 产品管理领域服务
|
||||||
// 负责产品的基本管理操作,包括创建、查询、更新等
|
// 负责产品的基本管理操作,包括创建、查询、更新等
|
||||||
type ProductManagementService struct {
|
type ProductManagementService struct {
|
||||||
productRepo repositories.ProductRepository
|
productRepo repositories.ProductRepository
|
||||||
categoryRepo repositories.ProductCategoryRepository
|
categoryRepo repositories.ProductCategoryRepository
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProductManagementService 创建产品管理领域服务
|
// NewProductManagementService 创建产品管理领域服务
|
||||||
@@ -79,6 +79,7 @@ func (s *ProductManagementService) GetProductByCode(ctx context.Context, product
|
|||||||
}
|
}
|
||||||
return product, nil
|
return product, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProductWithCategory 获取产品及其分类信息
|
// GetProductWithCategory 获取产品及其分类信息
|
||||||
func (s *ProductManagementService) GetProductWithCategory(ctx context.Context, productID string) (*entities.Product, error) {
|
func (s *ProductManagementService) GetProductWithCategory(ctx context.Context, productID string) (*entities.Product, error) {
|
||||||
product, err := s.productRepo.GetByID(ctx, productID)
|
product, err := s.productRepo.GetByID(ctx, productID)
|
||||||
@@ -326,9 +327,9 @@ func (s *ProductManagementService) ValidateProductCode(code string, excludeID st
|
|||||||
func (s *ProductManagementService) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.Product, int64, error) {
|
func (s *ProductManagementService) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.Product, int64, error) {
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
query := &queries.ListProductsQuery{
|
query := &queries.ListProductsQuery{
|
||||||
Page: options.Page,
|
Page: options.Page,
|
||||||
PageSize: options.PageSize,
|
PageSize: options.PageSize,
|
||||||
SortBy: options.Sort,
|
SortBy: options.Sort,
|
||||||
SortOrder: options.Order,
|
SortOrder: options.Order,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +371,9 @@ func (s *ProductManagementService) ListProducts(ctx context.Context, filters map
|
|||||||
func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.Product, map[string]bool, int64, error) {
|
func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.Product, map[string]bool, int64, error) {
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
query := &queries.ListProductsQuery{
|
query := &queries.ListProductsQuery{
|
||||||
Page: options.Page,
|
Page: options.Page,
|
||||||
PageSize: options.PageSize,
|
PageSize: options.PageSize,
|
||||||
SortBy: options.Sort,
|
SortBy: options.Sort,
|
||||||
SortOrder: options.Order,
|
SortOrder: options.Order,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type UserStats struct {
|
|||||||
// UserRepository 用户仓储接口
|
// UserRepository 用户仓储接口
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
interfaces.Repository[entities.User]
|
interfaces.Repository[entities.User]
|
||||||
|
|
||||||
// 基础查询 - 直接使用实体
|
// 基础查询 - 直接使用实体
|
||||||
GetByPhone(ctx context.Context, phone string) (*entities.User, error)
|
GetByPhone(ctx context.Context, phone string) (*entities.User, error)
|
||||||
GetByUsername(ctx context.Context, username string) (*entities.User, error)
|
GetByUsername(ctx context.Context, username string) (*entities.User, error)
|
||||||
@@ -54,6 +53,8 @@ type UserRepository interface {
|
|||||||
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
|
GetSystemUserStatsByDateRange(ctx context.Context, startDate, endDate time.Time) (*UserStats, error)
|
||||||
GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
GetSystemDailyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||||
|
|
||||||
// 排行榜查询方法
|
// 排行榜查询方法
|
||||||
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func NewConnection(config Config) (*DB, error) {
|
|||||||
NowFunc: func() time.Time {
|
NowFunc: func() time.Time {
|
||||||
return time.Now().In(time.FixedZone("CST", 8*3600)) // 强制使用北京时间
|
return time.Now().In(time.FixedZone("CST", 8*3600)) // 强制使用北京时间
|
||||||
},
|
},
|
||||||
PrepareStmt: true,
|
PrepareStmt: true,
|
||||||
DisableAutomaticPing: false,
|
DisableAutomaticPing: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
"tyapi-server/internal/domains/api/repositories"
|
"tyapi-server/internal/domains/api/repositories"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ApiCallsTable = "api_calls"
|
ApiCallsTable = "api_calls"
|
||||||
ApiCallCacheTTL = 10 * time.Minute
|
ApiCallCacheTTL = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -229,6 +230,11 @@ func (r *GormApiCallRepository) CountByUserId(ctx context.Context, userId string
|
|||||||
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ?", userId)
|
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ?", userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountByUserIdAndProductId 按用户ID和产品ID统计API调用次数
|
||||||
|
func (r *GormApiCallRepository) CountByUserIdAndProductId(ctx context.Context, userId string, productId string) (int64, error) {
|
||||||
|
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND product_id = ?", userId, productId)
|
||||||
|
}
|
||||||
|
|
||||||
// CountByUserIdAndDateRange 按用户ID和日期范围统计API调用次数
|
// CountByUserIdAndDateRange 按用户ID和日期范围统计API调用次数
|
||||||
func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error) {
|
func (r *GormApiCallRepository) CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error) {
|
||||||
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate)
|
return r.CountWhere(ctx, &entities.ApiCall{}, "user_id = ? AND created_at >= ? AND created_at < ?", userId, startDate, endDate)
|
||||||
@@ -304,8 +310,29 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
|
|||||||
|
|
||||||
// 应用筛选条件
|
// 应用筛选条件
|
||||||
if filters != nil {
|
if filters != nil {
|
||||||
// 用户ID筛选
|
// 用户ID筛选(支持单个user_id和多个user_ids)
|
||||||
if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
// 如果同时存在,优先使用user_ids(批量查询)
|
||||||
|
if userIds, ok := filters["user_ids"].(string); ok && userIds != "" {
|
||||||
|
// 解析逗号分隔的用户ID列表
|
||||||
|
userIdsList := strings.Split(userIds, ",")
|
||||||
|
// 去除空白字符
|
||||||
|
var cleanUserIds []string
|
||||||
|
for _, id := range userIdsList {
|
||||||
|
id = strings.TrimSpace(id)
|
||||||
|
if id != "" {
|
||||||
|
cleanUserIds = append(cleanUserIds, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cleanUserIds) > 0 {
|
||||||
|
placeholders := strings.Repeat("?,", len(cleanUserIds))
|
||||||
|
placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号
|
||||||
|
whereCondition += " AND ac.user_id IN (" + placeholders + ")"
|
||||||
|
for _, id := range cleanUserIds {
|
||||||
|
whereArgs = append(whereArgs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
||||||
|
// 单个用户ID筛选
|
||||||
whereCondition += " AND ac.user_id = ?"
|
whereCondition += " AND ac.user_id = ?"
|
||||||
whereArgs = append(whereArgs, userId)
|
whereArgs = append(whereArgs, userId)
|
||||||
}
|
}
|
||||||
@@ -332,6 +359,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
|
|||||||
whereArgs = append(whereArgs, "%"+productName+"%")
|
whereArgs = append(whereArgs, "%"+productName+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName, ok := filters["company_name"].(string); ok && companyName != "" {
|
||||||
|
whereCondition += " AND ei.company_name LIKE ?"
|
||||||
|
whereArgs = append(whereArgs, "%"+companyName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
// 状态筛选
|
// 状态筛选
|
||||||
if status, ok := filters["status"].(string); ok && status != "" {
|
if status, ok := filters["status"].(string); ok && status != "" {
|
||||||
whereCondition += " AND ac.status = ?"
|
whereCondition += " AND ac.status = ?"
|
||||||
@@ -340,9 +373,12 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建JOIN查询
|
// 构建JOIN查询
|
||||||
|
// 需要JOIN product表获取产品名称,JOIN users和enterprise_infos表获取企业名称
|
||||||
query := r.GetDB(ctx).Table("api_calls ac").
|
query := r.GetDB(ctx).Table("api_calls ac").
|
||||||
Select("ac.*, p.name as product_name").
|
Select("ac.*, p.name as product_name").
|
||||||
Joins("LEFT JOIN product p ON ac.product_id = p.id").
|
Joins("LEFT JOIN product p ON ac.product_id = p.id").
|
||||||
|
Joins("LEFT JOIN users u ON ac.user_id = u.id").
|
||||||
|
Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id").
|
||||||
Where(whereCondition, whereArgs...)
|
Where(whereCondition, whereArgs...)
|
||||||
|
|
||||||
// 获取总数
|
// 获取总数
|
||||||
@@ -395,10 +431,11 @@ func (r *GormApiCallRepository) GetSystemTotalCalls(ctx context.Context) (int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemCallsByDateRange 获取系统指定时间范围内的API调用次数
|
// GetSystemCallsByDateRange 获取系统指定时间范围内的API调用次数
|
||||||
|
// endDate 应该是结束日期当天的次日00:00:00(日统计)或下个月1号00:00:00(月统计),使用 < 而不是 <=
|
||||||
func (r *GormApiCallRepository) GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error) {
|
func (r *GormApiCallRepository) GetSystemCallsByDateRange(ctx context.Context, startDate, endDate time.Time) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := r.GetDB(ctx).Model(&entities.ApiCall{}).
|
err := r.GetDB(ctx).Model(&entities.ApiCall{}).
|
||||||
Where("created_at >= ? AND created_at <= ?", startDate, endDate).
|
Where("created_at >= ? AND created_at < ?", startDate, endDate).
|
||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
@@ -436,7 +473,7 @@ func (r *GormApiCallRepository) GetSystemMonthlyStats(ctx context.Context, start
|
|||||||
COUNT(*) as calls
|
COUNT(*) as calls
|
||||||
FROM api_calls
|
FROM api_calls
|
||||||
WHERE created_at >= $1
|
WHERE created_at >= $1
|
||||||
AND created_at <= $2
|
AND created_at < $2
|
||||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||||
ORDER BY month ASC
|
ORDER BY month ASC
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"tyapi-server/internal/domains/article/entities"
|
||||||
|
"tyapi-server/internal/domains/article/repositories"
|
||||||
|
repoQueries "tyapi-server/internal/domains/article/repositories/queries"
|
||||||
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GormAnnouncementRepository GORM公告仓储实现
|
||||||
|
type GormAnnouncementRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译时检查接口实现
|
||||||
|
var _ repositories.AnnouncementRepository = (*GormAnnouncementRepository)(nil)
|
||||||
|
|
||||||
|
// NewGormAnnouncementRepository 创建GORM公告仓储
|
||||||
|
func NewGormAnnouncementRepository(db *gorm.DB, logger *zap.Logger) *GormAnnouncementRepository {
|
||||||
|
return &GormAnnouncementRepository{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建公告
|
||||||
|
func (r *GormAnnouncementRepository) Create(ctx context.Context, entity entities.Announcement) (entities.Announcement, error) {
|
||||||
|
r.logger.Info("创建公告", zap.String("id", entity.ID), zap.String("title", entity.Title))
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Create(&entity).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("创建公告失败", zap.Error(err))
|
||||||
|
return entity, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据ID获取公告
|
||||||
|
func (r *GormAnnouncementRepository) GetByID(ctx context.Context, id string) (entities.Announcement, error) {
|
||||||
|
var entity entities.Announcement
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&entity).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return entity, fmt.Errorf("公告不存在")
|
||||||
|
}
|
||||||
|
r.logger.Error("获取公告失败", zap.String("id", id), zap.Error(err))
|
||||||
|
return entity, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新公告
|
||||||
|
func (r *GormAnnouncementRepository) Update(ctx context.Context, entity entities.Announcement) error {
|
||||||
|
r.logger.Info("更新公告", zap.String("id", entity.ID))
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Save(&entity).Error
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("更新公告失败", zap.String("id", entity.ID), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除公告
|
||||||
|
func (r *GormAnnouncementRepository) Delete(ctx context.Context, id string) error {
|
||||||
|
r.logger.Info("删除公告", zap.String("id", id))
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("删除公告失败", zap.String("id", id), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByStatus 根据状态查找公告
|
||||||
|
func (r *GormAnnouncementRepository) FindByStatus(ctx context.Context, status entities.AnnouncementStatus) ([]*entities.Announcement, error) {
|
||||||
|
var announcements []entities.Announcement
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("status = ?", status).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&announcements).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("根据状态查找公告失败", zap.String("status", string(status)), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为指针切片
|
||||||
|
result := make([]*entities.Announcement, len(announcements))
|
||||||
|
for i := range announcements {
|
||||||
|
result[i] = &announcements[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindScheduled 查找定时发布的公告
|
||||||
|
func (r *GormAnnouncementRepository) FindScheduled(ctx context.Context) ([]*entities.Announcement, error) {
|
||||||
|
var announcements []entities.Announcement
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("status = ? AND scheduled_at IS NOT NULL AND scheduled_at <= ?", entities.AnnouncementStatusDraft, now).
|
||||||
|
Order("scheduled_at ASC").
|
||||||
|
Find(&announcements).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("查找定时发布公告失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为指针切片
|
||||||
|
result := make([]*entities.Announcement, len(announcements))
|
||||||
|
for i := range announcements {
|
||||||
|
result[i] = &announcements[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAnnouncements 获取公告列表
|
||||||
|
func (r *GormAnnouncementRepository) ListAnnouncements(ctx context.Context, query *repoQueries.ListAnnouncementQuery) ([]*entities.Announcement, int64, error) {
|
||||||
|
var announcements []entities.Announcement
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
|
||||||
|
|
||||||
|
// 应用筛选条件
|
||||||
|
if query.Status != "" {
|
||||||
|
dbQuery = dbQuery.Where("status = ?", query.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Title != "" {
|
||||||
|
dbQuery = dbQuery.Where("title ILIKE ?", "%"+query.Title+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
if err := dbQuery.Count(&total).Error; err != nil {
|
||||||
|
r.logger.Error("获取公告列表总数失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用排序
|
||||||
|
if query.OrderBy != "" {
|
||||||
|
orderDir := "DESC"
|
||||||
|
if query.OrderDir != "" {
|
||||||
|
orderDir = strings.ToUpper(query.OrderDir)
|
||||||
|
}
|
||||||
|
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", query.OrderBy, orderDir))
|
||||||
|
} else {
|
||||||
|
dbQuery = dbQuery.Order("created_at DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用分页
|
||||||
|
if query.Page > 0 && query.PageSize > 0 {
|
||||||
|
offset := (query.Page - 1) * query.PageSize
|
||||||
|
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
if err := dbQuery.Find(&announcements).Error; err != nil {
|
||||||
|
r.logger.Error("获取公告列表失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为指针切片
|
||||||
|
result := make([]*entities.Announcement, len(announcements))
|
||||||
|
for i := range announcements {
|
||||||
|
result[i] = &announcements[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountByStatus 根据状态统计公告数量
|
||||||
|
func (r *GormAnnouncementRepository) CountByStatus(ctx context.Context, status entities.AnnouncementStatus) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Model(&entities.Announcement{}).
|
||||||
|
Where("status = ?", status).
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("统计公告数量失败", zap.String("status", string(status)), zap.Error(err))
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatistics 更新统计信息
|
||||||
|
// 注意:公告实体目前没有统计字段,此方法预留扩展
|
||||||
|
func (r *GormAnnouncementRepository) UpdateStatistics(ctx context.Context, announcementID string) error {
|
||||||
|
r.logger.Info("更新公告统计信息", zap.String("announcement_id", announcementID))
|
||||||
|
// TODO: 如果将来需要统计字段(如阅读量等),可以在这里实现
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ 实现 BaseRepository 接口的其他方法 ================
|
||||||
|
|
||||||
|
// Count 统计数量
|
||||||
|
func (r *GormAnnouncementRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||||
|
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
|
||||||
|
|
||||||
|
// 应用筛选条件
|
||||||
|
if options.Filters != nil {
|
||||||
|
for key, value := range options.Filters {
|
||||||
|
dbQuery = dbQuery.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Search != "" {
|
||||||
|
search := "%" + options.Search + "%"
|
||||||
|
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err := dbQuery.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists 检查是否存在
|
||||||
|
func (r *GormAnnouncementRepository) Exists(ctx context.Context, id string) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).Model(&entities.Announcement{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftDelete 软删除
|
||||||
|
func (r *GormAnnouncementRepository) SoftDelete(ctx context.Context, id string) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore 恢复软删除
|
||||||
|
func (r *GormAnnouncementRepository) Restore(ctx context.Context, id string) error {
|
||||||
|
return r.db.WithContext(ctx).Unscoped().Model(&entities.Announcement{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("deleted_at", nil).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBatch 批量创建
|
||||||
|
func (r *GormAnnouncementRepository) CreateBatch(ctx context.Context, entities []entities.Announcement) error {
|
||||||
|
return r.db.WithContext(ctx).Create(&entities).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIDs 根据ID列表获取
|
||||||
|
func (r *GormAnnouncementRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Announcement, error) {
|
||||||
|
var announcements []entities.Announcement
|
||||||
|
err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&announcements).Error
|
||||||
|
return announcements, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBatch 批量更新
|
||||||
|
func (r *GormAnnouncementRepository) UpdateBatch(ctx context.Context, entities []entities.Announcement) error {
|
||||||
|
return r.db.WithContext(ctx).Save(&entities).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBatch 批量删除
|
||||||
|
func (r *GormAnnouncementRepository) DeleteBatch(ctx context.Context, ids []string) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Announcement{}, "id IN ?", ids).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 列表查询
|
||||||
|
func (r *GormAnnouncementRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Announcement, error) {
|
||||||
|
var announcements []entities.Announcement
|
||||||
|
|
||||||
|
dbQuery := r.db.WithContext(ctx).Model(&entities.Announcement{})
|
||||||
|
|
||||||
|
// 应用筛选条件
|
||||||
|
if options.Filters != nil {
|
||||||
|
for key, value := range options.Filters {
|
||||||
|
dbQuery = dbQuery.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Search != "" {
|
||||||
|
search := "%" + options.Search + "%"
|
||||||
|
dbQuery = dbQuery.Where("title LIKE ? OR content LIKE ?", search, search)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用排序
|
||||||
|
if options.Sort != "" {
|
||||||
|
order := "DESC"
|
||||||
|
if options.Order != "" {
|
||||||
|
order = strings.ToUpper(options.Order)
|
||||||
|
}
|
||||||
|
dbQuery = dbQuery.Order(fmt.Sprintf("%s %s", options.Sort, order))
|
||||||
|
} else {
|
||||||
|
dbQuery = dbQuery.Order("created_at DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用分页
|
||||||
|
if options.Page > 0 && options.PageSize > 0 {
|
||||||
|
offset := (options.Page - 1) * options.PageSize
|
||||||
|
dbQuery = dbQuery.Offset(offset).Limit(options.PageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载关联数据
|
||||||
|
if len(options.Include) > 0 {
|
||||||
|
for _, include := range options.Include {
|
||||||
|
dbQuery = dbQuery.Preload(include)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dbQuery.Find(&announcements).Error
|
||||||
|
return announcements, err
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlipayOrdersTable = "alipay_orders"
|
AlipayOrdersTable = "typay_orders"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GormAlipayOrderRepository struct {
|
type GormAlipayOrderRepository struct {
|
||||||
@@ -72,9 +72,9 @@ func (r *GormAlipayOrderRepository) GetByRechargeID(ctx context.Context, recharg
|
|||||||
func (r *GormAlipayOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error) {
|
func (r *GormAlipayOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error) {
|
||||||
var orders []entities.AlipayOrder
|
var orders []entities.AlipayOrder
|
||||||
err := r.GetDB(ctx).
|
err := r.GetDB(ctx).
|
||||||
Joins("JOIN recharge_records ON alipay_orders.recharge_id = recharge_records.id").
|
Joins("JOIN recharge_records ON typay_orders.recharge_id = recharge_records.id").
|
||||||
Where("recharge_records.user_id = ?", userID).
|
Where("recharge_records.user_id = ?", userID).
|
||||||
Order("alipay_orders.created_at DESC").
|
Order("typay_orders.created_at DESC").
|
||||||
Find(&orders).Error
|
Find(&orders).Error
|
||||||
return orders, err
|
return orders, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repositories
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tyapi-server/internal/domains/finance/entities"
|
"tyapi-server/internal/domains/finance/entities"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"tyapi-server/internal/shared/database"
|
"tyapi-server/internal/shared/database"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -87,15 +89,86 @@ func (r *GormRechargeRecordRepository) UpdateStatus(ctx context.Context, id stri
|
|||||||
|
|
||||||
func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
func (r *GormRechargeRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
query := r.GetDB(ctx).Model(&entities.RechargeRecord{})
|
|
||||||
|
// 检查是否有 company_name 筛选,如果有则需要 JOIN 表
|
||||||
|
hasCompanyNameFilter := false
|
||||||
|
if options.Filters != nil {
|
||||||
|
if companyName, ok := options.Filters["company_name"].(string); ok && companyName != "" {
|
||||||
|
hasCompanyNameFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query *gorm.DB
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
// 使用 JOIN 查询以支持企业名称筛选
|
||||||
|
query = r.GetDB(ctx).Table("recharge_records rr").
|
||||||
|
Joins("LEFT JOIN users u ON rr.user_id = u.id").
|
||||||
|
Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id")
|
||||||
|
} else {
|
||||||
|
// 普通查询
|
||||||
|
query = r.GetDB(ctx).Model(&entities.RechargeRecord{})
|
||||||
|
}
|
||||||
|
|
||||||
if options.Filters != nil {
|
if options.Filters != nil {
|
||||||
for key, value := range options.Filters {
|
for key, value := range options.Filters {
|
||||||
query = query.Where(key+" = ?", value)
|
// 特殊处理时间范围过滤器
|
||||||
|
if key == "start_time" {
|
||||||
|
if startTime, ok := value.(time.Time); ok {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.created_at >= ?", startTime)
|
||||||
|
} else {
|
||||||
|
query = query.Where("created_at >= ?", startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "end_time" {
|
||||||
|
if endTime, ok := value.(time.Time); ok {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.created_at <= ?", endTime)
|
||||||
|
} else {
|
||||||
|
query = query.Where("created_at <= ?", endTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "company_name" {
|
||||||
|
// 处理企业名称筛选
|
||||||
|
if companyName, ok := value.(string); ok && companyName != "" {
|
||||||
|
query = query.Where("ei.company_name LIKE ?", "%"+companyName+"%")
|
||||||
|
}
|
||||||
|
} else if key == "min_amount" {
|
||||||
|
// 处理最小金额,支持string、int、int64类型
|
||||||
|
if amount, err := r.parseAmount(value); err == nil {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.amount >= ?", amount)
|
||||||
|
} else {
|
||||||
|
query = query.Where("amount >= ?", amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "max_amount" {
|
||||||
|
// 处理最大金额,支持string、int、int64类型
|
||||||
|
if amount, err := r.parseAmount(value); err == nil {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.amount <= ?", amount)
|
||||||
|
} else {
|
||||||
|
query = query.Where("amount <= ?", amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他过滤器使用等值查询
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr."+key+" = ?", value)
|
||||||
|
} else {
|
||||||
|
query = query.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.Search != "" {
|
if options.Search != "" {
|
||||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
|
if hasCompanyNameFilter {
|
||||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?",
|
||||||
|
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||||
|
} else {
|
||||||
|
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?",
|
||||||
|
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return count, query.Count(&count).Error
|
return count, query.Count(&count).Error
|
||||||
}
|
}
|
||||||
@@ -108,24 +181,98 @@ func (r *GormRechargeRecordRepository) Exists(ctx context.Context, id string) (b
|
|||||||
|
|
||||||
func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) {
|
func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error) {
|
||||||
var records []entities.RechargeRecord
|
var records []entities.RechargeRecord
|
||||||
query := r.GetDB(ctx).Model(&entities.RechargeRecord{})
|
|
||||||
|
// 检查是否有 company_name 筛选,如果有则需要 JOIN 表
|
||||||
|
hasCompanyNameFilter := false
|
||||||
|
if options.Filters != nil {
|
||||||
|
if companyName, ok := options.Filters["company_name"].(string); ok && companyName != "" {
|
||||||
|
hasCompanyNameFilter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query *gorm.DB
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
// 使用 JOIN 查询以支持企业名称筛选
|
||||||
|
query = r.GetDB(ctx).Table("recharge_records rr").
|
||||||
|
Select("rr.*").
|
||||||
|
Joins("LEFT JOIN users u ON rr.user_id = u.id").
|
||||||
|
Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id")
|
||||||
|
} else {
|
||||||
|
// 普通查询
|
||||||
|
query = r.GetDB(ctx).Model(&entities.RechargeRecord{})
|
||||||
|
}
|
||||||
|
|
||||||
if options.Filters != nil {
|
if options.Filters != nil {
|
||||||
for key, value := range options.Filters {
|
for key, value := range options.Filters {
|
||||||
// 特殊处理 user_ids 过滤器
|
// 特殊处理 user_ids 过滤器
|
||||||
if key == "user_ids" {
|
if key == "user_ids" {
|
||||||
if userIds, ok := value.(string); ok && userIds != "" {
|
if userIds, ok := value.(string); ok && userIds != "" {
|
||||||
query = query.Where("user_id IN ?", strings.Split(userIds, ","))
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.user_id IN ?", strings.Split(userIds, ","))
|
||||||
|
} else {
|
||||||
|
query = query.Where("user_id IN ?", strings.Split(userIds, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "company_name" {
|
||||||
|
// 处理企业名称筛选
|
||||||
|
if companyName, ok := value.(string); ok && companyName != "" {
|
||||||
|
query = query.Where("ei.company_name LIKE ?", "%"+companyName+"%")
|
||||||
|
}
|
||||||
|
} else if key == "start_time" {
|
||||||
|
// 处理开始时间范围
|
||||||
|
if startTime, ok := value.(time.Time); ok {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.created_at >= ?", startTime)
|
||||||
|
} else {
|
||||||
|
query = query.Where("created_at >= ?", startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "end_time" {
|
||||||
|
// 处理结束时间范围
|
||||||
|
if endTime, ok := value.(time.Time); ok {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.created_at <= ?", endTime)
|
||||||
|
} else {
|
||||||
|
query = query.Where("created_at <= ?", endTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "min_amount" {
|
||||||
|
// 处理最小金额,支持string、int、int64类型
|
||||||
|
if amount, err := r.parseAmount(value); err == nil {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.amount >= ?", amount)
|
||||||
|
} else {
|
||||||
|
query = query.Where("amount >= ?", amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == "max_amount" {
|
||||||
|
// 处理最大金额,支持string、int、int64类型
|
||||||
|
if amount, err := r.parseAmount(value); err == nil {
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr.amount <= ?", amount)
|
||||||
|
} else {
|
||||||
|
query = query.Where("amount <= ?", amount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
query = query.Where(key+" = ?", value)
|
// 其他过滤器使用等值查询
|
||||||
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Where("rr."+key+" = ?", value)
|
||||||
|
} else {
|
||||||
|
query = query.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Search != "" {
|
if options.Search != "" {
|
||||||
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ?",
|
if hasCompanyNameFilter {
|
||||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
query = query.Where("rr.user_id LIKE ? OR rr.transfer_order_id LIKE ? OR rr.alipay_order_id LIKE ? OR rr.wechat_order_id LIKE ?",
|
||||||
|
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||||
|
} else {
|
||||||
|
query = query.Where("user_id LIKE ? OR transfer_order_id LIKE ? OR alipay_order_id LIKE ? OR wechat_order_id LIKE ?",
|
||||||
|
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Sort != "" {
|
if options.Sort != "" {
|
||||||
@@ -133,9 +280,17 @@ func (r *GormRechargeRecordRepository) List(ctx context.Context, options interfa
|
|||||||
if options.Order == "desc" || options.Order == "DESC" {
|
if options.Order == "desc" || options.Order == "DESC" {
|
||||||
order = "DESC"
|
order = "DESC"
|
||||||
}
|
}
|
||||||
query = query.Order(options.Sort + " " + order)
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Order("rr." + options.Sort + " " + order)
|
||||||
|
} else {
|
||||||
|
query = query.Order(options.Sort + " " + order)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
query = query.Order("created_at DESC")
|
if hasCompanyNameFilter {
|
||||||
|
query = query.Order("rr.created_at DESC")
|
||||||
|
} else {
|
||||||
|
query = query.Order("created_at DESC")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Page > 0 && options.PageSize > 0 {
|
if options.Page > 0 && options.PageSize > 0 {
|
||||||
@@ -271,16 +426,18 @@ func (r *GormRechargeRecordRepository) GetSystemTotalAmount(ctx context.Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemAmountByDateRange 获取系统指定时间范围内的充值金额(排除赠送)
|
// GetSystemAmountByDateRange 获取系统指定时间范围内的充值金额(排除赠送)
|
||||||
|
// endDate 应该是结束日期当天的次日00:00:00(日统计)或下个月1号00:00:00(月统计),使用 < 而不是 <=
|
||||||
func (r *GormRechargeRecordRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
func (r *GormRechargeRecordRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||||
var total float64
|
var total float64
|
||||||
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
err := r.GetDB(ctx).Model(&entities.RechargeRecord{}).
|
||||||
Where("status = ? AND recharge_type != ? AND created_at >= ? AND created_at <= ?", entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate, endDate).
|
Where("status = ? AND recharge_type != ? AND created_at >= ? AND created_at < ?", entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate, endDate).
|
||||||
Select("COALESCE(SUM(amount), 0)").
|
Select("COALESCE(SUM(amount), 0)").
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
return total, err
|
return total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemDailyStats 获取系统每日充值统计(排除赠送)
|
// GetSystemDailyStats 获取系统每日充值统计(排除赠送)
|
||||||
|
// startDate 和 endDate 应该是时间对象,endDate 应该是结束日期当天的次日00:00:00,使用 < 而不是 <=
|
||||||
func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||||
var results []map[string]interface{}
|
var results []map[string]interface{}
|
||||||
|
|
||||||
@@ -291,13 +448,13 @@ func (r *GormRechargeRecordRepository) GetSystemDailyStats(ctx context.Context,
|
|||||||
FROM recharge_records
|
FROM recharge_records
|
||||||
WHERE status = ?
|
WHERE status = ?
|
||||||
AND recharge_type != ?
|
AND recharge_type != ?
|
||||||
AND DATE(created_at) >= ?
|
AND created_at >= ?
|
||||||
AND DATE(created_at) <= ?
|
AND created_at < ?
|
||||||
GROUP BY DATE(created_at)
|
GROUP BY DATE(created_at)
|
||||||
ORDER BY date ASC
|
ORDER BY date ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
err := r.GetDB(ctx).Raw(sql, entities.RechargeStatusSuccess, entities.RechargeTypeGift, startDate, endDate).Scan(&results).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -317,7 +474,7 @@ func (r *GormRechargeRecordRepository) GetSystemMonthlyStats(ctx context.Context
|
|||||||
WHERE status = ?
|
WHERE status = ?
|
||||||
AND recharge_type != ?
|
AND recharge_type != ?
|
||||||
AND created_at >= ?
|
AND created_at >= ?
|
||||||
AND created_at <= ?
|
AND created_at < ?
|
||||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||||
ORDER BY month ASC
|
ORDER BY month ASC
|
||||||
`
|
`
|
||||||
@@ -329,3 +486,24 @@ func (r *GormRechargeRecordRepository) GetSystemMonthlyStats(ctx context.Context
|
|||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseAmount 解析金额值,支持string、int、int64类型,转换为decimal.Decimal
|
||||||
|
func (r *GormRechargeRecordRepository) parseAmount(value interface{}) (decimal.Decimal, error) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
if v == "" {
|
||||||
|
return decimal.Zero, fmt.Errorf("empty string")
|
||||||
|
}
|
||||||
|
return decimal.NewFromString(v)
|
||||||
|
case int:
|
||||||
|
return decimal.NewFromInt(int64(v)), nil
|
||||||
|
case int64:
|
||||||
|
return decimal.NewFromInt(v), nil
|
||||||
|
case float64:
|
||||||
|
return decimal.NewFromFloat(v), nil
|
||||||
|
case decimal.Decimal:
|
||||||
|
return v, nil
|
||||||
|
default:
|
||||||
|
return decimal.Zero, fmt.Errorf("unsupported type: %T", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -382,12 +382,26 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
|
|||||||
|
|
||||||
// 应用筛选条件
|
// 应用筛选条件
|
||||||
if filters != nil {
|
if filters != nil {
|
||||||
// 用户ID筛选
|
// 用户ID筛选(支持单个和多个)
|
||||||
if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
if userIds, ok := filters["user_ids"].(string); ok && userIds != "" {
|
||||||
|
// 多个用户ID,逗号分隔
|
||||||
|
userIdsList := strings.Split(userIds, ",")
|
||||||
|
whereCondition += " AND wt.user_id IN ?"
|
||||||
|
whereArgs = append(whereArgs, userIdsList)
|
||||||
|
} else if userId, ok := filters["user_id"].(string); ok && userId != "" {
|
||||||
|
// 单个用户ID
|
||||||
whereCondition += " AND wt.user_id = ?"
|
whereCondition += " AND wt.user_id = ?"
|
||||||
whereArgs = append(whereArgs, userId)
|
whereArgs = append(whereArgs, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 产品ID筛选(支持多个)
|
||||||
|
if productIds, ok := filters["product_ids"].(string); ok && productIds != "" {
|
||||||
|
// 多个产品ID,逗号分隔
|
||||||
|
productIdsList := strings.Split(productIds, ",")
|
||||||
|
whereCondition += " AND wt.product_id IN ?"
|
||||||
|
whereArgs = append(whereArgs, productIdsList)
|
||||||
|
}
|
||||||
|
|
||||||
// 时间范围筛选
|
// 时间范围筛选
|
||||||
if startTime, ok := filters["start_time"].(time.Time); ok {
|
if startTime, ok := filters["start_time"].(time.Time); ok {
|
||||||
whereCondition += " AND wt.created_at >= ?"
|
whereCondition += " AND wt.created_at >= ?"
|
||||||
@@ -410,6 +424,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
|
|||||||
whereArgs = append(whereArgs, "%"+productName+"%")
|
whereArgs = append(whereArgs, "%"+productName+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName, ok := filters["company_name"].(string); ok && companyName != "" {
|
||||||
|
whereCondition += " AND ei.company_name LIKE ?"
|
||||||
|
whereArgs = append(whereArgs, "%"+companyName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
// 金额范围筛选
|
// 金额范围筛选
|
||||||
if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
|
if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
|
||||||
whereCondition += " AND wt.amount >= ?"
|
whereCondition += " AND wt.amount >= ?"
|
||||||
@@ -422,9 +442,12 @@ func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建JOIN查询
|
// 构建JOIN查询
|
||||||
|
// 需要JOIN product表获取产品名称,JOIN users和enterprise_infos表获取企业名称
|
||||||
query := r.GetDB(ctx).Table("wallet_transactions wt").
|
query := r.GetDB(ctx).Table("wallet_transactions wt").
|
||||||
Select("wt.*, p.name as product_name").
|
Select("wt.*, p.name as product_name").
|
||||||
Joins("LEFT JOIN product p ON wt.product_id = p.id").
|
Joins("LEFT JOIN product p ON wt.product_id = p.id").
|
||||||
|
Joins("LEFT JOIN users u ON wt.user_id = u.id").
|
||||||
|
Joins("LEFT JOIN enterprise_infos ei ON u.id = ei.user_id").
|
||||||
Where(whereCondition, whereArgs...)
|
Where(whereCondition, whereArgs...)
|
||||||
|
|
||||||
// 获取总数
|
// 获取总数
|
||||||
@@ -563,10 +586,11 @@ func (r *GormWalletTransactionRepository) GetSystemTotalAmount(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemAmountByDateRange 获取系统指定时间范围内的消费金额
|
// GetSystemAmountByDateRange 获取系统指定时间范围内的消费金额
|
||||||
|
// endDate 应该是结束日期当天的次日00:00:00(日统计)或下个月1号00:00:00(月统计),使用 < 而不是 <=
|
||||||
func (r *GormWalletTransactionRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
func (r *GormWalletTransactionRepository) GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error) {
|
||||||
var total float64
|
var total float64
|
||||||
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
err := r.GetDB(ctx).Model(&entities.WalletTransaction{}).
|
||||||
Where("created_at >= ? AND created_at <= ?", startDate, endDate).
|
Where("created_at >= ? AND created_at < ?", startDate, endDate).
|
||||||
Select("COALESCE(SUM(amount), 0)").
|
Select("COALESCE(SUM(amount), 0)").
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
return total, err
|
return total, err
|
||||||
@@ -605,7 +629,7 @@ func (r *GormWalletTransactionRepository) GetSystemMonthlyStats(ctx context.Cont
|
|||||||
COALESCE(SUM(amount), 0) as amount
|
COALESCE(SUM(amount), 0) as amount
|
||||||
FROM wallet_transactions
|
FROM wallet_transactions
|
||||||
WHERE created_at >= ?
|
WHERE created_at >= ?
|
||||||
AND created_at <= ?
|
AND created_at < ?
|
||||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||||
ORDER BY month ASC
|
ORDER BY month ASC
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"tyapi-server/internal/domains/finance/entities"
|
||||||
|
domain_finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||||
|
"tyapi-server/internal/shared/database"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WechatOrdersTable = "typay_orders"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GormWechatOrderRepository struct {
|
||||||
|
*database.CachedBaseRepositoryImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ domain_finance_repo.WechatOrderRepository = (*GormWechatOrderRepository)(nil)
|
||||||
|
|
||||||
|
func NewGormWechatOrderRepository(db *gorm.DB, logger *zap.Logger) domain_finance_repo.WechatOrderRepository {
|
||||||
|
return &GormWechatOrderRepository{
|
||||||
|
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, WechatOrdersTable),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) Create(ctx context.Context, order entities.WechatOrder) (entities.WechatOrder, error) {
|
||||||
|
err := r.CreateEntity(ctx, &order)
|
||||||
|
return order, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) GetByID(ctx context.Context, id string) (entities.WechatOrder, error) {
|
||||||
|
var order entities.WechatOrder
|
||||||
|
err := r.SmartGetByID(ctx, id, &order)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return entities.WechatOrder{}, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return entities.WechatOrder{}, err
|
||||||
|
}
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.WechatOrder, error) {
|
||||||
|
var order entities.WechatOrder
|
||||||
|
err := r.GetDB(ctx).Where("out_trade_no = ?", outTradeNo).First(&order).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) GetByRechargeID(ctx context.Context, rechargeID string) (*entities.WechatOrder, error) {
|
||||||
|
var order entities.WechatOrder
|
||||||
|
err := r.GetDB(ctx).Where("recharge_id = ?", rechargeID).First(&order).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) GetByUserID(ctx context.Context, userID string) ([]entities.WechatOrder, error) {
|
||||||
|
var orders []entities.WechatOrder
|
||||||
|
// 需要通过充值记录关联查询,这里简化处理
|
||||||
|
err := r.GetDB(ctx).Find(&orders).Error
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) Update(ctx context.Context, order entities.WechatOrder) error {
|
||||||
|
return r.UpdateEntity(ctx, &order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) UpdateStatus(ctx context.Context, id string, status entities.WechatOrderStatus) error {
|
||||||
|
return r.GetDB(ctx).Model(&entities.WechatOrder{}).Where("id = ?", id).Update("status", status).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) Delete(ctx context.Context, id string) error {
|
||||||
|
return r.DeleteEntity(ctx, id, &entities.WechatOrder{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *GormWechatOrderRepository) Exists(ctx context.Context, id string) (bool, error) {
|
||||||
|
return r.ExistsEntity(ctx, id, &entities.WechatOrder{})
|
||||||
|
}
|
||||||
@@ -458,6 +458,54 @@ func (r *GormUserRepository) GetSystemMonthlyUserStats(ctx context.Context, star
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSystemDailyCertificationStats 获取系统每日认证用户统计(基于is_certified字段)
|
||||||
|
func (r *GormUserRepository) GetSystemDailyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||||
|
var results []map[string]interface{}
|
||||||
|
|
||||||
|
sql := `
|
||||||
|
SELECT
|
||||||
|
DATE(updated_at) as date,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM users
|
||||||
|
WHERE is_certified = true
|
||||||
|
AND DATE(updated_at) >= $1
|
||||||
|
AND DATE(updated_at) <= $2
|
||||||
|
GROUP BY DATE(updated_at)
|
||||||
|
ORDER BY date ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemMonthlyCertificationStats 获取系统每月认证用户统计(基于is_certified字段)
|
||||||
|
func (r *GormUserRepository) GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||||
|
var results []map[string]interface{}
|
||||||
|
|
||||||
|
sql := `
|
||||||
|
SELECT
|
||||||
|
TO_CHAR(updated_at, 'YYYY-MM') as month,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM users
|
||||||
|
WHERE is_certified = true
|
||||||
|
AND updated_at >= $1
|
||||||
|
AND updated_at <= $2
|
||||||
|
GROUP BY TO_CHAR(updated_at, 'YYYY-MM')
|
||||||
|
ORDER BY month ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserCallRankingByCalls 按调用次数获取用户排行
|
// GetUserCallRankingByCalls 按调用次数获取用户排行
|
||||||
func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||||
var sql string
|
var sql string
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type ZhichaResp struct {
|
|||||||
type ZhichaConfig struct {
|
type ZhichaConfig struct {
|
||||||
URL string
|
URL string
|
||||||
AppID string
|
AppID string
|
||||||
AppSecret string
|
AppSecret string
|
||||||
EncryptKey string
|
EncryptKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +133,10 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
|
|||||||
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||||
// 检查是否是网络超时错误
|
// 检查是否是网络超时错误
|
||||||
isTimeout = true
|
isTimeout = true
|
||||||
} else if errStr := err.Error();
|
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
|
||||||
errStr == "context deadline exceeded" ||
|
errStr == "timeout" ||
|
||||||
errStr == "timeout" ||
|
errStr == "Client.Timeout exceeded" ||
|
||||||
errStr == "Client.Timeout exceeded" ||
|
errStr == "net/http: request canceled" {
|
||||||
errStr == "net/http: request canceled" {
|
|
||||||
isTimeout = true
|
isTimeout = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
411
internal/infrastructure/http/handlers/announcement_handler.go
Normal file
411
internal/infrastructure/http/handlers/announcement_handler.go
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tyapi-server/internal/application/article"
|
||||||
|
"tyapi-server/internal/application/article/dto/commands"
|
||||||
|
appQueries "tyapi-server/internal/application/article/dto/queries"
|
||||||
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnouncementHandler 公告HTTP处理器
|
||||||
|
type AnnouncementHandler struct {
|
||||||
|
appService article.AnnouncementApplicationService
|
||||||
|
responseBuilder interfaces.ResponseBuilder
|
||||||
|
validator interfaces.RequestValidator
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnouncementHandler 创建公告HTTP处理器
|
||||||
|
func NewAnnouncementHandler(
|
||||||
|
appService article.AnnouncementApplicationService,
|
||||||
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
|
validator interfaces.RequestValidator,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *AnnouncementHandler {
|
||||||
|
return &AnnouncementHandler{
|
||||||
|
appService: appService,
|
||||||
|
responseBuilder: responseBuilder,
|
||||||
|
validator: validator,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAnnouncement 创建公告
|
||||||
|
// @Summary 创建公告
|
||||||
|
// @Description 创建新的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body commands.CreateAnnouncementCommand true "创建公告请求"
|
||||||
|
// @Success 201 {object} map[string]interface{} "公告创建成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements [post]
|
||||||
|
func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.CreateAnnouncementCommand
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户是否已登录
|
||||||
|
if _, exists := c.Get("user_id"); !exists {
|
||||||
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.CreateAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("创建公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Created(c, nil, "公告创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncementByID 获取公告详情
|
||||||
|
// @Summary 获取公告详情
|
||||||
|
// @Description 根据ID获取公告详情
|
||||||
|
// @Tags 公告管理-用户端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} responses.AnnouncementInfoResponse "获取公告详情成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 404 {object} map[string]interface{} "公告不存在"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/announcements/{id} [get]
|
||||||
|
func (h *AnnouncementHandler) GetAnnouncementByID(c *gin.Context) {
|
||||||
|
var query appQueries.GetAnnouncementQuery
|
||||||
|
|
||||||
|
// 绑定URI参数(公告ID)
|
||||||
|
if err := h.validator.ValidateParam(c, &query); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.appService.GetAnnouncementByID(c.Request.Context(), &query)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取公告详情失败", zap.Error(err))
|
||||||
|
h.responseBuilder.NotFound(c, "公告不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, response, "获取公告详情成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAnnouncements 获取公告列表
|
||||||
|
// @Summary 获取公告列表
|
||||||
|
// @Description 分页获取公告列表,支持多种筛选条件
|
||||||
|
// @Tags 公告管理-用户端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(10)
|
||||||
|
// @Param status query string false "公告状态"
|
||||||
|
// @Param title query string false "标题关键词"
|
||||||
|
// @Param order_by query string false "排序字段"
|
||||||
|
// @Param order_dir query string false "排序方向"
|
||||||
|
// @Success 200 {object} responses.AnnouncementListResponse "获取公告列表成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/announcements [get]
|
||||||
|
func (h *AnnouncementHandler) ListAnnouncements(c *gin.Context) {
|
||||||
|
var query appQueries.ListAnnouncementQuery
|
||||||
|
if err := h.validator.ValidateQuery(c, &query); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if query.Page <= 0 {
|
||||||
|
query.Page = 1
|
||||||
|
}
|
||||||
|
if query.PageSize <= 0 {
|
||||||
|
query.PageSize = 10
|
||||||
|
}
|
||||||
|
if query.PageSize > 100 {
|
||||||
|
query.PageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.appService.ListAnnouncements(c.Request.Context(), &query)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取公告列表失败", zap.Error(err))
|
||||||
|
h.responseBuilder.InternalError(c, "获取公告列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, response, "获取公告列表成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishAnnouncement 发布公告
|
||||||
|
// @Summary 发布公告
|
||||||
|
// @Description 发布指定的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "发布成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/publish [post]
|
||||||
|
func (h *AnnouncementHandler) PublishAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.PublishAnnouncementCommand
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.PublishAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("发布公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "发布成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithdrawAnnouncement 撤回公告
|
||||||
|
// @Summary 撤回公告
|
||||||
|
// @Description 撤回已发布的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "撤回成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/withdraw [post]
|
||||||
|
func (h *AnnouncementHandler) WithdrawAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.WithdrawAnnouncementCommand
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.WithdrawAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("撤回公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "撤回成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveAnnouncement 归档公告
|
||||||
|
// @Summary 归档公告
|
||||||
|
// @Description 归档指定的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "归档成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/archive [post]
|
||||||
|
func (h *AnnouncementHandler) ArchiveAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.ArchiveAnnouncementCommand
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.ArchiveAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("归档公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "归档成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAnnouncement 更新公告
|
||||||
|
// @Summary 更新公告
|
||||||
|
// @Description 更新指定的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Param request body commands.UpdateAnnouncementCommand true "更新公告请求"
|
||||||
|
// @Success 200 {object} map[string]interface{} "更新成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id} [put]
|
||||||
|
func (h *AnnouncementHandler) UpdateAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.UpdateAnnouncementCommand
|
||||||
|
|
||||||
|
// 先绑定URI参数(公告ID)
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再绑定JSON请求体(公告信息)
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.UpdateAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("更新公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "更新成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAnnouncement 删除公告
|
||||||
|
// @Summary 删除公告
|
||||||
|
// @Description 删除指定的公告
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "删除成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id} [delete]
|
||||||
|
func (h *AnnouncementHandler) DeleteAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.DeleteAnnouncementCommand
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.DeleteAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("删除公告失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchedulePublishAnnouncement 定时发布公告
|
||||||
|
// @Summary 定时发布公告
|
||||||
|
// @Description 设置公告的定时发布时间
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Param request body commands.SchedulePublishAnnouncementCommand true "定时发布请求"
|
||||||
|
// @Success 200 {object} map[string]interface{} "设置成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/schedule-publish [post]
|
||||||
|
func (h *AnnouncementHandler) SchedulePublishAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.SchedulePublishAnnouncementCommand
|
||||||
|
|
||||||
|
// 先绑定URI参数(公告ID)
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再绑定JSON请求体(定时发布时间)
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.SchedulePublishAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("设置定时发布失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "设置成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSchedulePublishAnnouncement 更新定时发布公告
|
||||||
|
// @Summary 更新定时发布公告
|
||||||
|
// @Description 修改公告的定时发布时间
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Param request body commands.UpdateSchedulePublishAnnouncementCommand true "更新定时发布请求"
|
||||||
|
// @Success 200 {object} map[string]interface{} "更新成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/update-schedule-publish [post]
|
||||||
|
func (h *AnnouncementHandler) UpdateSchedulePublishAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.UpdateSchedulePublishAnnouncementCommand
|
||||||
|
|
||||||
|
// 先绑定URI参数(公告ID)
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再绑定JSON请求体(定时发布时间)
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.UpdateSchedulePublishAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("更新定时发布时间失败", zap.String("id", cmd.ID), zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "更新成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSchedulePublishAnnouncement 取消定时发布公告
|
||||||
|
// @Summary 取消定时发布公告
|
||||||
|
// @Description 取消公告的定时发布
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "公告ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "取消成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/{id}/cancel-schedule [post]
|
||||||
|
func (h *AnnouncementHandler) CancelSchedulePublishAnnouncement(c *gin.Context) {
|
||||||
|
var cmd commands.CancelSchedulePublishAnnouncementCommand
|
||||||
|
if err := h.validator.ValidateParam(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.appService.CancelSchedulePublishAnnouncement(c.Request.Context(), &cmd); err != nil {
|
||||||
|
h.logger.Error("取消定时发布失败", zap.Error(err))
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "取消成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncementStats 获取公告统计信息
|
||||||
|
// @Summary 获取公告统计信息
|
||||||
|
// @Description 获取公告的统计数据
|
||||||
|
// @Tags 公告管理-管理端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Success 200 {object} responses.AnnouncementStatsResponse "获取统计信息成功"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/admin/announcements/stats [get]
|
||||||
|
func (h *AnnouncementHandler) GetAnnouncementStats(c *gin.Context) {
|
||||||
|
response, err := h.appService.GetAnnouncementStats(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取公告统计信息失败", zap.Error(err))
|
||||||
|
h.responseBuilder.InternalError(c, "获取统计信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, response, "获取统计信息成功")
|
||||||
|
}
|
||||||
@@ -110,7 +110,10 @@ func (h *ApiHandler) GetUserWhiteList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := h.appService.GetUserWhiteList(c.Request.Context(), userID)
|
// 获取查询参数
|
||||||
|
remarkKeyword := c.Query("remark") // 备注模糊查询关键词
|
||||||
|
|
||||||
|
result, err := h.appService.GetUserWhiteList(c.Request.Context(), userID, remarkKeyword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("获取用户白名单失败", zap.Error(err))
|
h.logger.Error("获取用户白名单失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, err.Error())
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
@@ -134,7 +137,7 @@ func (h *ApiHandler) AddWhiteListIP(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.appService.AddWhiteListIP(c.Request.Context(), userID, req.IPAddress)
|
err := h.appService.AddWhiteListIP(c.Request.Context(), userID, req.IPAddress, req.Remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("添加白名单IP失败", zap.Error(err))
|
h.logger.Error("添加白名单IP失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, err.Error())
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,12 +21,12 @@ import (
|
|||||||
|
|
||||||
// FinanceHandler 财务HTTP处理器
|
// FinanceHandler 财务HTTP处理器
|
||||||
type FinanceHandler struct {
|
type FinanceHandler struct {
|
||||||
appService finance.FinanceApplicationService
|
appService finance.FinanceApplicationService
|
||||||
invoiceAppService finance.InvoiceApplicationService
|
invoiceAppService finance.InvoiceApplicationService
|
||||||
adminInvoiceAppService finance.AdminInvoiceApplicationService
|
adminInvoiceAppService finance.AdminInvoiceApplicationService
|
||||||
responseBuilder interfaces.ResponseBuilder
|
responseBuilder interfaces.ResponseBuilder
|
||||||
validator interfaces.RequestValidator
|
validator interfaces.RequestValidator
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFinanceHandler 创建财务HTTP处理器
|
// NewFinanceHandler 创建财务HTTP处理器
|
||||||
@@ -201,6 +203,123 @@ func (h *FinanceHandler) HandleAlipayCallback(c *gin.Context) {
|
|||||||
c.String(200, "success")
|
c.String(200, "success")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleWechatPayCallback 处理微信支付回调
|
||||||
|
// @Summary 微信支付回调
|
||||||
|
// @Description 处理微信支付异步通知
|
||||||
|
// @Tags 支付管理
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce text/plain
|
||||||
|
// @Success 200 {string} string "success"
|
||||||
|
// @Failure 400 {string} string "fail"
|
||||||
|
// @Router /api/v1/pay/wechat/callback [post]
|
||||||
|
func (h *FinanceHandler) HandleWechatPayCallback(c *gin.Context) {
|
||||||
|
// 记录回调请求信息
|
||||||
|
h.logger.Info("收到微信支付回调请求",
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("url", c.Request.URL.String()),
|
||||||
|
zap.String("remote_addr", c.ClientIP()),
|
||||||
|
zap.String("user_agent", c.GetHeader("User-Agent")),
|
||||||
|
zap.String("content_type", c.GetHeader("Content-Type")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 读取请求体内容用于调试(注意:读取后需要重新设置,否则后续解析会失败)
|
||||||
|
bodyBytes, err := c.GetRawData()
|
||||||
|
if err == nil && len(bodyBytes) > 0 {
|
||||||
|
h.logger.Info("微信支付回调请求体",
|
||||||
|
zap.String("body", string(bodyBytes)),
|
||||||
|
zap.Int("body_size", len(bodyBytes)),
|
||||||
|
)
|
||||||
|
// 重新设置请求体,供后续解析使用
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过应用服务处理微信支付回调
|
||||||
|
err = h.appService.HandleWechatPayCallback(c.Request.Context(), c.Request)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("微信支付回调处理失败",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("remote_addr", c.ClientIP()),
|
||||||
|
)
|
||||||
|
c.String(400, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("微信支付回调处理成功", zap.String("remote_addr", c.ClientIP()))
|
||||||
|
// 返回成功响应(微信要求返回success)
|
||||||
|
c.String(200, "success")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWechatRefundCallback 处理微信退款回调
|
||||||
|
// @Summary 微信退款回调
|
||||||
|
// @Description 处理微信退款异步通知
|
||||||
|
// @Tags 支付管理
|
||||||
|
// @Accept application/json
|
||||||
|
// @Produce text/plain
|
||||||
|
// @Success 200 {string} string "success"
|
||||||
|
// @Failure 400 {string} string "fail"
|
||||||
|
// @Router /api/v1/wechat/refund_callback [post]
|
||||||
|
func (h *FinanceHandler) HandleWechatRefundCallback(c *gin.Context) {
|
||||||
|
// 记录回调请求信息
|
||||||
|
h.logger.Info("收到微信退款回调请求",
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("url", c.Request.URL.String()),
|
||||||
|
zap.String("remote_addr", c.ClientIP()),
|
||||||
|
zap.String("user_agent", c.GetHeader("User-Agent")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 通过应用服务处理微信退款回调
|
||||||
|
err := h.appService.HandleWechatRefundCallback(c.Request.Context(), c.Request)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("微信退款回调处理失败", zap.Error(err))
|
||||||
|
c.String(400, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功响应(微信要求返回success)
|
||||||
|
c.String(200, "success")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWechatOrderStatus 获取微信订单状态
|
||||||
|
// @Summary 获取微信订单状态
|
||||||
|
// @Description 根据商户订单号查询微信订单状态
|
||||||
|
// @Tags 钱包管理
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param out_trade_no query string true "商户订单号"
|
||||||
|
// @Success 200 {object} responses.WechatOrderStatusResponse "获取订单状态成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 404 {object} map[string]interface{} "订单不存在"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/finance/wallet/wechat-order-status [get]
|
||||||
|
func (h *FinanceHandler) GetWechatOrderStatus(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outTradeNo := c.Query("out_trade_no")
|
||||||
|
if outTradeNo == "" {
|
||||||
|
h.responseBuilder.BadRequest(c, "缺少商户订单号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.appService.GetWechatOrderStatus(c.Request.Context(), outTradeNo)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取微信订单状态失败",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("out_trade_no", outTradeNo),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
h.responseBuilder.BadRequest(c, "获取订单状态失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, result, "获取订单状态成功")
|
||||||
|
}
|
||||||
|
|
||||||
// HandleAlipayReturn 处理支付宝同步回调
|
// HandleAlipayReturn 处理支付宝同步回调
|
||||||
// @Summary 支付宝同步回调
|
// @Summary 支付宝同步回调
|
||||||
// @Description 处理支付宝同步支付通知,跳转到前端成功页面
|
// @Description 处理支付宝同步支付通知,跳转到前端成功页面
|
||||||
@@ -319,7 +438,6 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 调用应用服务进行完整的业务流程编排
|
// 调用应用服务进行完整的业务流程编排
|
||||||
result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd)
|
result, err := h.appService.CreateAlipayRechargeOrder(c.Request.Context(), &cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -343,6 +461,53 @@ func (h *FinanceHandler) CreateAlipayRecharge(c *gin.Context) {
|
|||||||
h.responseBuilder.Success(c, result, "支付宝充值订单创建成功")
|
h.responseBuilder.Success(c, result, "支付宝充值订单创建成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateWechatRecharge 创建微信充值订单
|
||||||
|
// @Summary 创建微信充值订单
|
||||||
|
// @Description 创建微信充值订单并返回预支付数据
|
||||||
|
// @Tags 钱包管理
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body commands.CreateWechatRechargeCommand true "微信充值请求"
|
||||||
|
// @Success 200 {object} responses.WechatRechargeOrderResponse "创建充值订单成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/finance/wallet/wechat-recharge [post]
|
||||||
|
func (h *FinanceHandler) CreateWechatRecharge(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd commands.CreateWechatRechargeCommand
|
||||||
|
cmd.UserID = userID
|
||||||
|
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.appService.CreateWechatRechargeOrder(c.Request.Context(), &cmd)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("创建微信充值订单失败",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("amount", cmd.Amount),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
h.responseBuilder.BadRequest(c, "创建微信充值订单失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("微信充值订单创建成功",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
zap.String("out_trade_no", result.OutTradeNo),
|
||||||
|
zap.String("amount", cmd.Amount),
|
||||||
|
zap.String("platform", cmd.Platform),
|
||||||
|
)
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, result, "微信充值订单创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
// TransferRecharge 管理员对公转账充值
|
// TransferRecharge 管理员对公转账充值
|
||||||
func (h *FinanceHandler) TransferRecharge(c *gin.Context) {
|
func (h *FinanceHandler) TransferRecharge(c *gin.Context) {
|
||||||
var cmd commands.TransferRechargeCommand
|
var cmd commands.TransferRechargeCommand
|
||||||
@@ -849,8 +1014,6 @@ func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
h.responseBuilder.Success(c, nil, "通过发票申请成功")
|
h.responseBuilder.Success(c, nil, "通过发票申请成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,8 +1100,8 @@ func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
|
|||||||
// 暂时返回基本信息
|
// 暂时返回基本信息
|
||||||
debugInfo := map[string]interface{}{
|
debugInfo := map[string]interface{}{
|
||||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
"message": "事件系统调试端点已启用",
|
"message": "事件系统调试端点已启用",
|
||||||
"handler": "FinanceHandler",
|
"handler": "FinanceHandler",
|
||||||
}
|
}
|
||||||
|
|
||||||
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
|
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tyapi-server/internal/application/api"
|
"tyapi-server/internal/application/api"
|
||||||
"tyapi-server/internal/application/finance"
|
"tyapi-server/internal/application/finance"
|
||||||
@@ -10,9 +13,6 @@ import (
|
|||||||
"tyapi-server/internal/application/product/dto/queries"
|
"tyapi-server/internal/application/product/dto/queries"
|
||||||
"tyapi-server/internal/application/product/dto/responses"
|
"tyapi-server/internal/application/product/dto/responses"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProductAdminHandler 产品管理员HTTP处理器
|
// ProductAdminHandler 产品管理员HTTP处理器
|
||||||
@@ -72,13 +72,14 @@ func (h *ProductAdminHandler) CreateProduct(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.productAppService.CreateProduct(c.Request.Context(), &cmd); err != nil {
|
result, err := h.productAppService.CreateProduct(c.Request.Context(), &cmd)
|
||||||
|
if err != nil {
|
||||||
h.logger.Error("创建产品失败", zap.Error(err))
|
h.logger.Error("创建产品失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, err.Error())
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.responseBuilder.Created(c, nil, "产品创建成功")
|
h.responseBuilder.Created(c, result, "产品创建成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProduct 更新产品
|
// UpdateProduct 更新产品
|
||||||
@@ -797,9 +798,9 @@ func (h *ProductAdminHandler) BatchUpdateSubscriptionPrices(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.responseBuilder.Success(c, map[string]interface{}{
|
h.responseBuilder.Success(c, map[string]interface{}{
|
||||||
"user_id": cmd.UserID,
|
"user_id": cmd.UserID,
|
||||||
"discount": cmd.Discount,
|
"discount": cmd.Discount,
|
||||||
"scope": cmd.Scope,
|
"scope": cmd.Scope,
|
||||||
}, "一键改价成功")
|
}, "一键改价成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,8 +1114,6 @@ func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) {
|
|||||||
h.responseBuilder.Success(c, nil, "文档删除成功")
|
h.responseBuilder.Success(c, nil, "文档删除成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// GetAdminWalletTransactions 获取管理端消费记录
|
// GetAdminWalletTransactions 获取管理端消费记录
|
||||||
// @Summary 获取管理端消费记录
|
// @Summary 获取管理端消费记录
|
||||||
// @Description 管理员获取消费记录,支持筛选和分页
|
// @Description 管理员获取消费记录,支持筛选和分页
|
||||||
@@ -1173,6 +1172,11 @@ func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
|
|||||||
filters["product_name"] = productName
|
filters["product_name"] = productName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName := c.Query("company_name"); companyName != "" {
|
||||||
|
filters["company_name"] = companyName
|
||||||
|
}
|
||||||
|
|
||||||
// 金额范围筛选
|
// 金额范围筛选
|
||||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||||
filters["min_amount"] = minAmount
|
filters["min_amount"] = minAmount
|
||||||
@@ -1234,13 +1238,27 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
|||||||
|
|
||||||
// 时间范围筛选
|
// 时间范围筛选
|
||||||
if startTime := c.Query("start_time"); startTime != "" {
|
if startTime := c.Query("start_time"); startTime != "" {
|
||||||
|
// 处理URL编码的+号,转换为空格
|
||||||
|
startTime = strings.ReplaceAll(startTime, "+", " ")
|
||||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||||
filters["start_time"] = t
|
filters["start_time"] = t
|
||||||
|
} else {
|
||||||
|
// 尝试其他格式
|
||||||
|
if t, err := time.Parse("2006-01-02T15:04:05", startTime); err == nil {
|
||||||
|
filters["start_time"] = t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endTime := c.Query("end_time"); endTime != "" {
|
if endTime := c.Query("end_time"); endTime != "" {
|
||||||
|
// 处理URL编码的+号,转换为空格
|
||||||
|
endTime = strings.ReplaceAll(endTime, "+", " ")
|
||||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||||
filters["end_time"] = t
|
filters["end_time"] = t
|
||||||
|
} else {
|
||||||
|
// 尝试其他格式
|
||||||
|
if t, err := time.Parse("2006-01-02T15:04:05", endTime); err == nil {
|
||||||
|
filters["end_time"] = t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1259,6 +1277,11 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
|||||||
filters["product_ids"] = productIds
|
filters["product_ids"] = productIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName := c.Query("company_name"); companyName != "" {
|
||||||
|
filters["company_name"] = companyName
|
||||||
|
}
|
||||||
|
|
||||||
// 金额范围筛选
|
// 金额范围筛选
|
||||||
if minAmount := c.Query("min_amount"); minAmount != "" {
|
if minAmount := c.Query("min_amount"); minAmount != "" {
|
||||||
filters["min_amount"] = minAmount
|
filters["min_amount"] = minAmount
|
||||||
@@ -1278,7 +1301,16 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
|||||||
fileData, err := h.financeAppService.ExportAdminWalletTransactions(c.Request.Context(), filters, format)
|
fileData, err := h.financeAppService.ExportAdminWalletTransactions(c.Request.Context(), filters, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("导出消费记录失败", zap.Error(err))
|
h.logger.Error("导出消费记录失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, "导出消费记录失败")
|
|
||||||
|
// 根据错误信息返回具体的提示
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") {
|
||||||
|
h.responseBuilder.NotFound(c, "没有找到符合筛选条件的数据,请调整筛选条件后重试")
|
||||||
|
} else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") {
|
||||||
|
h.responseBuilder.BadRequest(c, errMsg)
|
||||||
|
} else {
|
||||||
|
h.responseBuilder.BadRequest(c, "导出消费记录失败:"+errMsg)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1305,7 +1337,7 @@ func (h *ProductAdminHandler) ExportAdminWalletTransactions(c *gin.Context) {
|
|||||||
// @Param page query int false "页码" default(1)
|
// @Param page query int false "页码" default(1)
|
||||||
// @Param page_size query int false "每页数量" default(10)
|
// @Param page_size query int false "每页数量" default(10)
|
||||||
// @Param user_id query string false "用户ID"
|
// @Param user_id query string false "用户ID"
|
||||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift)
|
||||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||||
// @Param min_amount query string false "最小金额"
|
// @Param min_amount query string false "最小金额"
|
||||||
// @Param max_amount query string false "最大金额"
|
// @Param max_amount query string false "最大金额"
|
||||||
@@ -1361,6 +1393,11 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
|
|||||||
filters["max_amount"] = maxAmount
|
filters["max_amount"] = maxAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName := c.Query("company_name"); companyName != "" {
|
||||||
|
filters["company_name"] = companyName
|
||||||
|
}
|
||||||
|
|
||||||
// 构建分页选项
|
// 构建分页选项
|
||||||
options := interfaces.ListOptions{
|
options := interfaces.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
@@ -1387,7 +1424,7 @@ func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
|
|||||||
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
// @Param user_ids query string false "用户ID列表,逗号分隔"
|
||||||
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
|
// @Param recharge_type query string false "充值类型" Enums(alipay, wechat, transfer, gift)
|
||||||
// @Param status query string false "状态" Enums(pending, success, failed)
|
// @Param status query string false "状态" Enums(pending, success, failed)
|
||||||
// @Param start_time query string false "开始时间" format(date-time)
|
// @Param start_time query string false "开始时间" format(date-time)
|
||||||
// @Param end_time query string false "结束时间" format(date-time)
|
// @Param end_time query string false "结束时间" format(date-time)
|
||||||
@@ -1439,7 +1476,16 @@ func (h *ProductAdminHandler) ExportAdminRechargeRecords(c *gin.Context) {
|
|||||||
fileData, err := h.financeAppService.ExportAdminRechargeRecords(c.Request.Context(), filters, format)
|
fileData, err := h.financeAppService.ExportAdminRechargeRecords(c.Request.Context(), filters, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("导出充值记录失败", zap.Error(err))
|
h.logger.Error("导出充值记录失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, "导出充值记录失败")
|
|
||||||
|
// 根据错误信息返回具体的提示
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") {
|
||||||
|
h.responseBuilder.NotFound(c, "没有找到符合筛选条件的充值记录,请调整筛选条件后重试")
|
||||||
|
} else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") {
|
||||||
|
h.responseBuilder.BadRequest(c, errMsg)
|
||||||
|
} else {
|
||||||
|
h.responseBuilder.BadRequest(c, "导出充值记录失败:"+errMsg)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1465,7 +1511,10 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
|
|||||||
// 构建筛选条件
|
// 构建筛选条件
|
||||||
filters := make(map[string]interface{})
|
filters := make(map[string]interface{})
|
||||||
|
|
||||||
// 用户ID筛选
|
// 用户ID筛选(支持单个user_id和多个user_ids,根据需求使用)
|
||||||
|
if userId := c.Query("user_id"); userId != "" {
|
||||||
|
filters["user_id"] = userId
|
||||||
|
}
|
||||||
if userIds := c.Query("user_ids"); userIds != "" {
|
if userIds := c.Query("user_ids"); userIds != "" {
|
||||||
filters["user_ids"] = userIds
|
filters["user_ids"] = userIds
|
||||||
}
|
}
|
||||||
@@ -1475,15 +1524,57 @@ func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
|
|||||||
filters["product_ids"] = productIds
|
filters["product_ids"] = productIds
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间范围筛选
|
// 产品名称筛选
|
||||||
|
if productName := c.Query("product_name"); productName != "" {
|
||||||
|
filters["product_name"] = productName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业名称筛选
|
||||||
|
if companyName := c.Query("company_name"); companyName != "" {
|
||||||
|
filters["company_name"] = companyName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易ID筛选
|
||||||
|
if transactionId := c.Query("transaction_id"); transactionId != "" {
|
||||||
|
filters["transaction_id"] = transactionId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
filters["status"] = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选 - 增强错误处理和日志
|
||||||
if startTime := c.Query("start_time"); startTime != "" {
|
if startTime := c.Query("start_time"); startTime != "" {
|
||||||
|
// 处理URL编码的+号,转换为空格
|
||||||
|
startTime = strings.ReplaceAll(startTime, "+", " ")
|
||||||
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
|
||||||
filters["start_time"] = t
|
filters["start_time"] = t
|
||||||
|
h.logger.Debug("解析start_time成功", zap.String("原始值", c.Query("start_time")), zap.Time("解析后", t))
|
||||||
|
} else {
|
||||||
|
// 尝试其他格式(ISO格式)
|
||||||
|
if t, err := time.Parse("2006-01-02T15:04:05", startTime); err == nil {
|
||||||
|
filters["start_time"] = t
|
||||||
|
h.logger.Debug("解析start_time成功(ISO格式)", zap.String("原始值", c.Query("start_time")), zap.Time("解析后", t))
|
||||||
|
} else {
|
||||||
|
h.logger.Warn("解析start_time失败", zap.String("原始值", c.Query("start_time")), zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endTime := c.Query("end_time"); endTime != "" {
|
if endTime := c.Query("end_time"); endTime != "" {
|
||||||
|
// 处理URL编码的+号,转换为空格
|
||||||
|
endTime = strings.ReplaceAll(endTime, "+", " ")
|
||||||
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
|
||||||
filters["end_time"] = t
|
filters["end_time"] = t
|
||||||
|
h.logger.Debug("解析end_time成功", zap.String("原始值", c.Query("end_time")), zap.Time("解析后", t))
|
||||||
|
} else {
|
||||||
|
// 尝试其他格式(ISO格式)
|
||||||
|
if t, err := time.Parse("2006-01-02T15:04:05", endTime); err == nil {
|
||||||
|
filters["end_time"] = t
|
||||||
|
h.logger.Debug("解析end_time成功(ISO格式)", zap.String("原始值", c.Query("end_time")), zap.Time("解析后", t))
|
||||||
|
} else {
|
||||||
|
h.logger.Warn("解析end_time失败", zap.String("原始值", c.Query("end_time")), zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1543,7 +1634,16 @@ func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) {
|
|||||||
fileData, err := h.apiAppService.ExportAdminApiCalls(c.Request.Context(), filters, format)
|
fileData, err := h.apiAppService.ExportAdminApiCalls(c.Request.Context(), filters, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("导出API调用记录失败", zap.Error(err))
|
h.logger.Error("导出API调用记录失败", zap.Error(err))
|
||||||
h.responseBuilder.BadRequest(c, "导出API调用记录失败")
|
|
||||||
|
// 根据错误信息返回具体的提示
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "没有找到符合条件的数据") || strings.Contains(errMsg, "没有数据") {
|
||||||
|
h.responseBuilder.NotFound(c, "没有找到符合筛选条件的API调用记录,请调整筛选条件后重试")
|
||||||
|
} else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") {
|
||||||
|
h.responseBuilder.BadRequest(c, errMsg)
|
||||||
|
} else {
|
||||||
|
h.responseBuilder.BadRequest(c, "导出API调用记录失败:"+errMsg)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,35 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"tyapi-server/internal/application/product"
|
"tyapi-server/internal/application/product"
|
||||||
"tyapi-server/internal/application/product/dto/commands"
|
"tyapi-server/internal/application/product/dto/commands"
|
||||||
"tyapi-server/internal/application/product/dto/queries"
|
"tyapi-server/internal/application/product/dto/queries"
|
||||||
_ "tyapi-server/internal/application/product/dto/responses"
|
_ "tyapi-server/internal/application/product/dto/responses"
|
||||||
|
"tyapi-server/internal/domains/product/entities"
|
||||||
"tyapi-server/internal/shared/interfaces"
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
"tyapi-server/internal/shared/pdf"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProductHandler 产品相关HTTP处理器
|
// ProductHandler 产品相关HTTP处理器
|
||||||
type ProductHandler struct {
|
type ProductHandler struct {
|
||||||
appService product.ProductApplicationService
|
appService product.ProductApplicationService
|
||||||
apiConfigService product.ProductApiConfigApplicationService
|
apiConfigService product.ProductApiConfigApplicationService
|
||||||
categoryService product.CategoryApplicationService
|
categoryService product.CategoryApplicationService
|
||||||
subAppService product.SubscriptionApplicationService
|
subAppService product.SubscriptionApplicationService
|
||||||
documentationAppService product.DocumentationApplicationServiceInterface
|
documentationAppService product.DocumentationApplicationServiceInterface
|
||||||
responseBuilder interfaces.ResponseBuilder
|
responseBuilder interfaces.ResponseBuilder
|
||||||
validator interfaces.RequestValidator
|
validator interfaces.RequestValidator
|
||||||
logger *zap.Logger
|
pdfGenerator *pdf.PDFGenerator
|
||||||
|
pdfCacheManager *pdf.PDFCacheManager
|
||||||
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProductHandler 创建产品HTTP处理器
|
// NewProductHandler 创建产品HTTP处理器
|
||||||
@@ -34,17 +42,21 @@ func NewProductHandler(
|
|||||||
documentationAppService product.DocumentationApplicationServiceInterface,
|
documentationAppService product.DocumentationApplicationServiceInterface,
|
||||||
responseBuilder interfaces.ResponseBuilder,
|
responseBuilder interfaces.ResponseBuilder,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
|
pdfGenerator *pdf.PDFGenerator,
|
||||||
|
pdfCacheManager *pdf.PDFCacheManager,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) *ProductHandler {
|
) *ProductHandler {
|
||||||
return &ProductHandler{
|
return &ProductHandler{
|
||||||
appService: appService,
|
appService: appService,
|
||||||
apiConfigService: apiConfigService,
|
apiConfigService: apiConfigService,
|
||||||
categoryService: categoryService,
|
categoryService: categoryService,
|
||||||
subAppService: subAppService,
|
subAppService: subAppService,
|
||||||
documentationAppService: documentationAppService,
|
documentationAppService: documentationAppService,
|
||||||
responseBuilder: responseBuilder,
|
responseBuilder: responseBuilder,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
logger: logger,
|
pdfGenerator: pdfGenerator,
|
||||||
|
pdfCacheManager: pdfCacheManager,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,32 +586,77 @@ func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先获取订阅信息以验证权限
|
result, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取我的订阅使用情况失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
||||||
|
h.responseBuilder.NotFound(c, "订阅不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订阅是否属于当前用户(通过获取订阅详情来验证)
|
||||||
var query queries.GetSubscriptionQuery
|
var query queries.GetSubscriptionQuery
|
||||||
query.ID = subscriptionID
|
query.ID = subscriptionID
|
||||||
|
|
||||||
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
|
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("获取订阅信息失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
h.logger.Error("获取订阅详情失败", zap.Error(err), zap.String("subscription_id", subscriptionID))
|
||||||
h.responseBuilder.NotFound(c, "订阅不存在")
|
h.responseBuilder.NotFound(c, "订阅不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证订阅是否属于当前用户
|
// 验证订阅是否属于当前用户
|
||||||
if subscription.UserID != userID {
|
if subscription.UserID != userID {
|
||||||
h.logger.Error("用户尝试访问不属于自己的订阅使用情况", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID))
|
h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID))
|
||||||
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
h.responseBuilder.Forbidden(c, "无权访问此订阅")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
usage, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID)
|
h.responseBuilder.Success(c, result, "获取我的订阅使用情况成功")
|
||||||
if err != nil {
|
}
|
||||||
h.logger.Error("获取订阅使用情况失败", zap.Error(err))
|
|
||||||
h.responseBuilder.BadRequest(c, err.Error())
|
// CancelMySubscription 取消我的订阅
|
||||||
|
// @Summary 取消我的订阅
|
||||||
|
// @Description 取消指定的订阅(软删除)
|
||||||
|
// @Tags 我的订阅
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "订阅ID"
|
||||||
|
// @Success 200 {object} map[string]interface{} "取消订阅成功"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||||
|
// @Failure 403 {object} map[string]interface{} "无权操作"
|
||||||
|
// @Failure 404 {object} map[string]interface{} "订阅不存在"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/my/subscriptions/{id}/cancel [post]
|
||||||
|
func (h *ProductHandler) CancelMySubscription(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.responseBuilder.Success(c, usage, "获取使用情况成功")
|
subscriptionID := c.Param("id")
|
||||||
|
if subscriptionID == "" {
|
||||||
|
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.subAppService.CancelMySubscription(c.Request.Context(), userID, subscriptionID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("取消订阅失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
||||||
|
|
||||||
|
// 根据错误类型返回不同的响应
|
||||||
|
if err.Error() == "订阅不存在" {
|
||||||
|
h.responseBuilder.NotFound(c, "订阅不存在")
|
||||||
|
} else if err.Error() == "无权取消此订阅" {
|
||||||
|
h.responseBuilder.Forbidden(c, "无权取消此订阅")
|
||||||
|
} else {
|
||||||
|
h.responseBuilder.BadRequest(c, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.responseBuilder.Success(c, nil, "取消订阅成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProductDocumentation 获取产品文档
|
// GetProductDocumentation 获取产品文档
|
||||||
@@ -630,3 +687,329 @@ func (h *ProductHandler) GetProductDocumentation(c *gin.Context) {
|
|||||||
|
|
||||||
h.responseBuilder.Success(c, doc, "获取文档成功")
|
h.responseBuilder.Success(c, doc, "获取文档成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadProductDocumentation 下载产品接口文档(PDF文件)
|
||||||
|
// @Summary 下载产品接口文档
|
||||||
|
// @Description 根据产品ID从数据库获取产品信息和文档信息,动态生成PDF文档并下载。
|
||||||
|
// @Tags 数据大厅
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/pdf
|
||||||
|
// @Param id path string true "产品ID"
|
||||||
|
// @Success 200 {file} file "PDF文档文件"
|
||||||
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||||
|
// @Failure 404 {object} map[string]interface{} "产品不存在"
|
||||||
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||||
|
// @Router /api/v1/products/{id}/documentation/download [get]
|
||||||
|
func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) {
|
||||||
|
productID := c.Param("id")
|
||||||
|
if productID == "" {
|
||||||
|
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查PDF生成器是否可用
|
||||||
|
if h.pdfGenerator == nil {
|
||||||
|
h.logger.Error("PDF生成器未初始化")
|
||||||
|
h.responseBuilder.InternalError(c, "PDF生成器未初始化")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品信息
|
||||||
|
product, err := h.appService.GetProductByID(c.Request.Context(), &queries.GetProductQuery{ID: productID})
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("获取产品信息失败", zap.Error(err))
|
||||||
|
h.responseBuilder.NotFound(c, "产品不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查产品编码是否存在
|
||||||
|
if product.Code == "" {
|
||||||
|
h.logger.Warn("产品编码为空", zap.String("product_id", productID))
|
||||||
|
h.responseBuilder.BadRequest(c, "产品编码不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("开始生成PDF文档",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("product_code", product.Code),
|
||||||
|
zap.String("product_name", product.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取产品文档信息
|
||||||
|
doc, docErr := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
||||||
|
if docErr != nil {
|
||||||
|
h.logger.Warn("获取产品文档失败,将只生成产品基本信息",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Error(docErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将响应类型转换为entity类型
|
||||||
|
var docEntity *entities.ProductDocumentation
|
||||||
|
var docVersion string
|
||||||
|
if doc != nil {
|
||||||
|
docEntity = &entities.ProductDocumentation{
|
||||||
|
ID: doc.ID,
|
||||||
|
ProductID: doc.ProductID,
|
||||||
|
RequestURL: doc.RequestURL,
|
||||||
|
RequestMethod: doc.RequestMethod,
|
||||||
|
BasicInfo: doc.BasicInfo,
|
||||||
|
RequestParams: doc.RequestParams,
|
||||||
|
ResponseFields: doc.ResponseFields,
|
||||||
|
ResponseExample: doc.ResponseExample,
|
||||||
|
ErrorCodes: doc.ErrorCodes,
|
||||||
|
Version: doc.Version,
|
||||||
|
}
|
||||||
|
docVersion = doc.Version
|
||||||
|
} else {
|
||||||
|
// 如果没有文档,使用默认版本号
|
||||||
|
docVersion = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是组合包,获取子产品的文档信息
|
||||||
|
var subProductDocs []*entities.ProductDocumentation
|
||||||
|
if product.IsPackage && len(product.PackageItems) > 0 {
|
||||||
|
h.logger.Info("检测到组合包,开始获取子产品文档",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Int("sub_product_count", len(product.PackageItems)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 收集所有子产品的ID
|
||||||
|
subProductIDs := make([]string, 0, len(product.PackageItems))
|
||||||
|
for _, item := range product.PackageItems {
|
||||||
|
subProductIDs = append(subProductIDs, item.ProductID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量获取子产品的文档
|
||||||
|
subDocs, err := h.documentationAppService.GetDocumentationsByProductIDs(c.Request.Context(), subProductIDs)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("获取组合包子产品文档失败",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 转换为entity类型,并按PackageItems的顺序排序
|
||||||
|
docMap := make(map[string]*entities.ProductDocumentation)
|
||||||
|
for i := range subDocs {
|
||||||
|
docMap[subDocs[i].ProductID] = &entities.ProductDocumentation{
|
||||||
|
ID: subDocs[i].ID,
|
||||||
|
ProductID: subDocs[i].ProductID,
|
||||||
|
RequestURL: subDocs[i].RequestURL,
|
||||||
|
RequestMethod: subDocs[i].RequestMethod,
|
||||||
|
BasicInfo: subDocs[i].BasicInfo,
|
||||||
|
RequestParams: subDocs[i].RequestParams,
|
||||||
|
ResponseFields: subDocs[i].ResponseFields,
|
||||||
|
ResponseExample: subDocs[i].ResponseExample,
|
||||||
|
ErrorCodes: subDocs[i].ErrorCodes,
|
||||||
|
Version: subDocs[i].Version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按PackageItems的顺序构建子产品文档列表
|
||||||
|
for _, item := range product.PackageItems {
|
||||||
|
if subDoc, exists := docMap[item.ProductID]; exists {
|
||||||
|
subProductDocs = append(subProductDocs, subDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("成功获取组合包子产品文档",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Int("total_sub_products", len(product.PackageItems)),
|
||||||
|
zap.Int("docs_found", len(subProductDocs)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从缓存获取PDF
|
||||||
|
var pdfBytes []byte
|
||||||
|
var cacheHit bool
|
||||||
|
|
||||||
|
if h.pdfCacheManager != nil {
|
||||||
|
var cacheErr error
|
||||||
|
pdfBytes, cacheHit, cacheErr = h.pdfCacheManager.Get(productID, docVersion)
|
||||||
|
if cacheErr != nil {
|
||||||
|
h.logger.Warn("从缓存获取PDF失败,将重新生成",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Error(cacheErr),
|
||||||
|
)
|
||||||
|
} else if cacheHit {
|
||||||
|
h.logger.Info("PDF缓存命中",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("version", docVersion),
|
||||||
|
zap.Int("pdf_size", len(pdfBytes)),
|
||||||
|
)
|
||||||
|
// 直接返回缓存的PDF
|
||||||
|
fileName := fmt.Sprintf("%s_接口文档.pdf", product.Name)
|
||||||
|
if product.Name == "" {
|
||||||
|
fileName = fmt.Sprintf("%s_接口文档.pdf", product.Code)
|
||||||
|
}
|
||||||
|
// 清理文件名中的非法字符
|
||||||
|
fileName = strings.ReplaceAll(fileName, "/", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "\\", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, ":", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "*", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "?", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "\"", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "<", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, ">", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "|", "_")
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/pdf")
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||||
|
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
||||||
|
c.Header("X-Cache", "HIT") // 添加缓存命中标识
|
||||||
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,需要生成PDF
|
||||||
|
h.logger.Info("PDF缓存未命中,开始生成PDF",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("product_name", product.Name),
|
||||||
|
zap.String("version", docVersion),
|
||||||
|
zap.Bool("has_doc", docEntity != nil),
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
h.logger.Error("PDF生成过程中发生panic",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Any("panic_value", r),
|
||||||
|
)
|
||||||
|
// 确保在panic时也能返回响应
|
||||||
|
if !c.Writer.Written() {
|
||||||
|
h.responseBuilder.InternalError(c, fmt.Sprintf("生成PDF文档时发生错误: %v", r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 构建Product实体(用于PDF生成)
|
||||||
|
productEntity := &entities.Product{
|
||||||
|
ID: product.ID,
|
||||||
|
Name: product.Name,
|
||||||
|
Code: product.Code,
|
||||||
|
Description: product.Description,
|
||||||
|
Content: product.Content,
|
||||||
|
IsPackage: product.IsPackage,
|
||||||
|
Price: decimal.NewFromFloat(product.Price),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是组合包,添加子产品信息
|
||||||
|
if product.IsPackage && len(product.PackageItems) > 0 {
|
||||||
|
productEntity.PackageItems = make([]*entities.ProductPackageItem, len(product.PackageItems))
|
||||||
|
for i, item := range product.PackageItems {
|
||||||
|
productEntity.PackageItems[i] = &entities.ProductPackageItem{
|
||||||
|
ID: item.ID,
|
||||||
|
PackageID: product.ID,
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
SortOrder: item.SortOrder,
|
||||||
|
Product: &entities.Product{
|
||||||
|
ID: item.ProductID,
|
||||||
|
Code: item.ProductCode,
|
||||||
|
Name: item.ProductName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接调用PDF生成器(简化版本,不使用goroutine)
|
||||||
|
h.logger.Info("开始调用PDF生成器",
|
||||||
|
zap.Bool("is_package", product.IsPackage),
|
||||||
|
zap.Int("sub_product_docs_count", len(subProductDocs)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 使用重构后的生成器
|
||||||
|
refactoredGen := pdf.NewPDFGeneratorRefactored(h.logger)
|
||||||
|
var genErr error
|
||||||
|
|
||||||
|
if product.IsPackage && len(subProductDocs) > 0 {
|
||||||
|
// 组合包:使用支持子产品文档的方法
|
||||||
|
pdfBytes, genErr = refactoredGen.GenerateProductPDFWithSubProducts(
|
||||||
|
c.Request.Context(),
|
||||||
|
productEntity,
|
||||||
|
docEntity,
|
||||||
|
subProductDocs,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 普通产品:使用标准方法
|
||||||
|
pdfBytes, genErr = refactoredGen.GenerateProductPDFFromEntity(
|
||||||
|
c.Request.Context(),
|
||||||
|
productEntity,
|
||||||
|
docEntity,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
h.logger.Info("PDF生成器调用返回",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Bool("has_error", genErr != nil),
|
||||||
|
zap.Int("pdf_size", len(pdfBytes)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if genErr != nil {
|
||||||
|
h.logger.Error("生成PDF文档失败",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("product_code", product.Code),
|
||||||
|
zap.Error(genErr),
|
||||||
|
)
|
||||||
|
h.responseBuilder.InternalError(c, fmt.Sprintf("生成PDF文档失败: %s", genErr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("PDF生成器调用完成",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.Int("pdf_size", len(pdfBytes)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(pdfBytes) == 0 {
|
||||||
|
h.logger.Error("生成的PDF文档为空",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("product_code", product.Code),
|
||||||
|
)
|
||||||
|
h.responseBuilder.InternalError(c, "生成的PDF文档为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到缓存(异步,不阻塞响应)
|
||||||
|
if h.pdfCacheManager != nil {
|
||||||
|
go func() {
|
||||||
|
if err := h.pdfCacheManager.Set(productID, docVersion, pdfBytes); err != nil {
|
||||||
|
h.logger.Warn("保存PDF到缓存失败",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("version", docVersion),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件名(清理文件名中的非法字符)
|
||||||
|
fileName := fmt.Sprintf("%s_接口文档.pdf", product.Name)
|
||||||
|
if product.Name == "" {
|
||||||
|
fileName = fmt.Sprintf("%s_接口文档.pdf", product.Code)
|
||||||
|
}
|
||||||
|
// 清理文件名中的非法字符
|
||||||
|
fileName = strings.ReplaceAll(fileName, "/", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "\\", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, ":", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "*", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "?", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "\"", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "<", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, ">", "_")
|
||||||
|
fileName = strings.ReplaceAll(fileName, "|", "_")
|
||||||
|
|
||||||
|
h.logger.Info("成功生成PDF文档",
|
||||||
|
zap.String("product_id", productID),
|
||||||
|
zap.String("product_code", product.Code),
|
||||||
|
zap.String("file_name", fileName),
|
||||||
|
zap.Int("file_size", len(pdfBytes)),
|
||||||
|
zap.Bool("cached", false),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 设置响应头并返回PDF文件
|
||||||
|
c.Header("Content-Type", "application/pdf")
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||||
|
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
||||||
|
c.Header("X-Cache", "MISS") // 添加缓存未命中标识
|
||||||
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||||
|
}
|
||||||
|
|||||||
73
internal/infrastructure/http/routes/announcement_routes.go
Normal file
73
internal/infrastructure/http/routes/announcement_routes.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tyapi-server/internal/infrastructure/http/handlers"
|
||||||
|
sharedhttp "tyapi-server/internal/shared/http"
|
||||||
|
"tyapi-server/internal/shared/middleware"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnouncementRoutes 公告路由
|
||||||
|
type AnnouncementRoutes struct {
|
||||||
|
handler *handlers.AnnouncementHandler
|
||||||
|
auth *middleware.JWTAuthMiddleware
|
||||||
|
admin *middleware.AdminAuthMiddleware
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnouncementRoutes 创建公告路由
|
||||||
|
func NewAnnouncementRoutes(
|
||||||
|
handler *handlers.AnnouncementHandler,
|
||||||
|
auth *middleware.JWTAuthMiddleware,
|
||||||
|
admin *middleware.AdminAuthMiddleware,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *AnnouncementRoutes {
|
||||||
|
return &AnnouncementRoutes{
|
||||||
|
handler: handler,
|
||||||
|
auth: auth,
|
||||||
|
admin: admin,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 注册路由
|
||||||
|
func (r *AnnouncementRoutes) Register(router *sharedhttp.GinRouter) {
|
||||||
|
engine := router.GetEngine()
|
||||||
|
|
||||||
|
// ==================== 用户端路由 ====================
|
||||||
|
// 公告相关路由 - 用户端(只显示已发布的公告)
|
||||||
|
announcementGroup := engine.Group("/api/v1/announcements")
|
||||||
|
{
|
||||||
|
// 公开路由 - 不需要认证
|
||||||
|
announcementGroup.GET("/:id", r.handler.GetAnnouncementByID) // 获取公告详情
|
||||||
|
announcementGroup.GET("", r.handler.ListAnnouncements) // 获取公告列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 管理员端路由 ====================
|
||||||
|
// 管理员公告管理路由
|
||||||
|
adminAnnouncementGroup := engine.Group("/api/v1/admin/announcements")
|
||||||
|
adminAnnouncementGroup.Use(r.admin.Handle())
|
||||||
|
{
|
||||||
|
// 统计信息
|
||||||
|
adminAnnouncementGroup.GET("/stats", r.handler.GetAnnouncementStats) // 获取公告统计
|
||||||
|
|
||||||
|
// 公告列表查询
|
||||||
|
adminAnnouncementGroup.GET("", r.handler.ListAnnouncements) // 获取公告列表(管理员端,包含所有状态)
|
||||||
|
|
||||||
|
// 公告管理
|
||||||
|
adminAnnouncementGroup.POST("", r.handler.CreateAnnouncement) // 创建公告
|
||||||
|
adminAnnouncementGroup.PUT("/:id", r.handler.UpdateAnnouncement) // 更新公告
|
||||||
|
adminAnnouncementGroup.DELETE("/:id", r.handler.DeleteAnnouncement) // 删除公告
|
||||||
|
|
||||||
|
// 公告状态管理
|
||||||
|
adminAnnouncementGroup.POST("/:id/publish", r.handler.PublishAnnouncement) // 发布公告
|
||||||
|
adminAnnouncementGroup.POST("/:id/withdraw", r.handler.WithdrawAnnouncement) // 撤回公告
|
||||||
|
adminAnnouncementGroup.POST("/:id/archive", r.handler.ArchiveAnnouncement) // 归档公告
|
||||||
|
adminAnnouncementGroup.POST("/:id/schedule-publish", r.handler.SchedulePublishAnnouncement) // 定时发布公告
|
||||||
|
adminAnnouncementGroup.POST("/:id/update-schedule-publish", r.handler.UpdateSchedulePublishAnnouncement) // 修改定时发布时间
|
||||||
|
adminAnnouncementGroup.POST("/:id/cancel-schedule", r.handler.CancelSchedulePublishAnnouncement) // 取消定时发布
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("公告路由注册完成")
|
||||||
|
}
|
||||||
@@ -42,6 +42,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
alipayGroup.GET("/return", r.financeHandler.HandleAlipayReturn) // 支付宝同步回调
|
alipayGroup.GET("/return", r.financeHandler.HandleAlipayReturn) // 支付宝同步回调
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 微信支付回调路由(不需要认证)
|
||||||
|
wechatPayGroup := engine.Group("/api/v1/pay/wechat")
|
||||||
|
{
|
||||||
|
wechatPayGroup.POST("/callback", r.financeHandler.HandleWechatPayCallback) // 微信支付异步回调
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信退款回调路由(不需要认证)
|
||||||
|
wechatRefundGroup := engine.Group("/api/v1/wechat")
|
||||||
|
{
|
||||||
|
wechatRefundGroup.POST("/refund_callback", r.financeHandler.HandleWechatRefundCallback) // 微信退款异步回调
|
||||||
|
}
|
||||||
|
|
||||||
// 财务路由组,需要用户认证
|
// 财务路由组,需要用户认证
|
||||||
financeGroup := engine.Group("/api/v1/finance")
|
financeGroup := engine.Group("/api/v1/finance")
|
||||||
financeGroup.Use(r.authMiddleware.Handle())
|
financeGroup.Use(r.authMiddleware.Handle())
|
||||||
@@ -49,12 +61,14 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
// 钱包相关路由
|
// 钱包相关路由
|
||||||
walletGroup := financeGroup.Group("/wallet")
|
walletGroup := financeGroup.Group("/wallet")
|
||||||
{
|
{
|
||||||
walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息
|
walletGroup.GET("", r.financeHandler.GetWallet) // 获取钱包信息
|
||||||
walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录
|
walletGroup.GET("/transactions", r.financeHandler.GetUserWalletTransactions) // 获取钱包交易记录
|
||||||
walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置
|
walletGroup.GET("/recharge-config", r.financeHandler.GetRechargeConfig) // 获取充值配置
|
||||||
walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单
|
walletGroup.POST("/alipay-recharge", r.financeHandler.CreateAlipayRecharge) // 创建支付宝充值订单
|
||||||
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
walletGroup.POST("/wechat-recharge", r.financeHandler.CreateWechatRecharge) // 创建微信充值订单
|
||||||
|
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
||||||
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
|
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
|
||||||
|
walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +76,11 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
invoiceGroup := engine.Group("/api/v1/invoices")
|
invoiceGroup := engine.Group("/api/v1/invoices")
|
||||||
invoiceGroup.Use(r.authMiddleware.Handle())
|
invoiceGroup.Use(r.authMiddleware.Handle())
|
||||||
{
|
{
|
||||||
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
|
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
|
||||||
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
|
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
|
||||||
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
|
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
|
||||||
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
|
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
|
||||||
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
|
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
|
||||||
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件
|
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +88,8 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
adminFinanceGroup := engine.Group("/api/v1/admin/finance")
|
adminFinanceGroup := engine.Group("/api/v1/admin/finance")
|
||||||
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
|
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||||
{
|
{
|
||||||
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
|
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
|
||||||
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
||||||
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +97,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
adminInvoiceGroup := engine.Group("/api/v1/admin/invoices")
|
adminInvoiceGroup := engine.Group("/api/v1/admin/invoices")
|
||||||
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle())
|
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle())
|
||||||
{
|
{
|
||||||
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
|
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
|
||||||
adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
|
adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
|
||||||
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
|
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
|
||||||
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件
|
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
products.GET("/:id", r.productHandler.GetProductDetail)
|
products.GET("/:id", r.productHandler.GetProductDetail)
|
||||||
products.GET("/:id/api-config", r.productHandler.GetProductApiConfig)
|
products.GET("/:id/api-config", r.productHandler.GetProductApiConfig)
|
||||||
products.GET("/:id/documentation", r.productHandler.GetProductDocumentation)
|
products.GET("/:id/documentation", r.productHandler.GetProductDocumentation)
|
||||||
|
products.GET("/:id/documentation/download", r.productHandler.DownloadProductDocumentation)
|
||||||
|
|
||||||
// 订阅产品(需要认证)
|
// 订阅产品(需要认证)
|
||||||
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)
|
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)
|
||||||
@@ -82,6 +83,9 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
|
|
||||||
// 获取订阅使用情况
|
// 获取订阅使用情况
|
||||||
subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage)
|
subscriptions.GET("/:id/usage", r.productHandler.GetMySubscriptionUsage)
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
subscriptions.POST("/:id/cancel", r.productHandler.CancelMySubscription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,33 @@ func (f *TaskFactory) CreateArticlePublishTask(articleID string, publishAt time.
|
|||||||
return task, nil
|
return task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAnnouncementPublishTask 创建公告发布任务
|
||||||
|
func (f *TaskFactory) CreateAnnouncementPublishTask(announcementID string, publishAt time.Time, userID string) (*AsyncTask, error) {
|
||||||
|
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||||
|
task := &AsyncTask{
|
||||||
|
Type: string(types.TaskTypeAnnouncementPublish),
|
||||||
|
Status: TaskStatusPending,
|
||||||
|
ScheduledAt: &publishAt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在payload中添加任务ID(将在保存后更新)
|
||||||
|
payloadWithID := map[string]interface{}{
|
||||||
|
"announcement_id": announcementID,
|
||||||
|
"publish_at": publishAt,
|
||||||
|
"user_id": userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadDataWithID, err := json.Marshal(payloadWithID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Payload = string(payloadDataWithID)
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateArticleCancelTask 创建文章取消任务
|
// CreateArticleCancelTask 创建文章取消任务
|
||||||
func (f *TaskFactory) CreateArticleCancelTask(articleID string, userID string) (*AsyncTask, error) {
|
func (f *TaskFactory) CreateArticleCancelTask(articleID string, userID string) (*AsyncTask, error) {
|
||||||
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
// 创建任务实体,ID将由GORM的BeforeCreate钩子自动生成UUID
|
||||||
@@ -240,6 +267,32 @@ func (f *TaskFactory) CreateAndEnqueueArticlePublishTask(ctx context.Context, ar
|
|||||||
return fmt.Errorf("TaskManager类型不匹配")
|
return fmt.Errorf("TaskManager类型不匹配")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAndEnqueueAnnouncementPublishTask 创建并入队公告发布任务
|
||||||
|
func (f *TaskFactory) CreateAndEnqueueAnnouncementPublishTask(ctx context.Context, announcementID string, publishAt time.Time, userID string) error {
|
||||||
|
if f.taskManager == nil {
|
||||||
|
return fmt.Errorf("TaskManager未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := f.CreateAnnouncementPublishTask(announcementID, publishAt, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := publishAt.Sub(time.Now())
|
||||||
|
if delay < 0 {
|
||||||
|
delay = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用类型断言调用TaskManager方法
|
||||||
|
if tm, ok := f.taskManager.(interface {
|
||||||
|
CreateAndEnqueueDelayedTask(ctx context.Context, task *AsyncTask, delay time.Duration) error
|
||||||
|
}); ok {
|
||||||
|
return tm.CreateAndEnqueueDelayedTask(ctx, task, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("TaskManager类型不匹配")
|
||||||
|
}
|
||||||
|
|
||||||
// CreateAndEnqueueApiLogTask 创建并入队API日志任务
|
// CreateAndEnqueueApiLogTask 创建并入队API日志任务
|
||||||
func (f *TaskFactory) CreateAndEnqueueApiLogTask(ctx context.Context, transactionID string, userID string, apiName string, productID string) error {
|
func (f *TaskFactory) CreateAndEnqueueApiLogTask(ctx context.Context, transactionID string, userID string, apiName string, productID string) error {
|
||||||
if f.taskManager == nil {
|
if f.taskManager == nil {
|
||||||
|
|||||||
@@ -17,17 +17,24 @@ import (
|
|||||||
|
|
||||||
// ArticleTaskHandler 文章任务处理器
|
// ArticleTaskHandler 文章任务处理器
|
||||||
type ArticleTaskHandler struct {
|
type ArticleTaskHandler struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
articleApplicationService article.ArticleApplicationService
|
articleApplicationService article.ArticleApplicationService
|
||||||
asyncTaskRepo repositories.AsyncTaskRepository
|
announcementApplicationService article.AnnouncementApplicationService
|
||||||
|
asyncTaskRepo repositories.AsyncTaskRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewArticleTaskHandler 创建文章任务处理器
|
// NewArticleTaskHandler 创建文章任务处理器
|
||||||
func NewArticleTaskHandler(logger *zap.Logger, articleApplicationService article.ArticleApplicationService, asyncTaskRepo repositories.AsyncTaskRepository) *ArticleTaskHandler {
|
func NewArticleTaskHandler(
|
||||||
|
logger *zap.Logger,
|
||||||
|
articleApplicationService article.ArticleApplicationService,
|
||||||
|
announcementApplicationService article.AnnouncementApplicationService,
|
||||||
|
asyncTaskRepo repositories.AsyncTaskRepository,
|
||||||
|
) *ArticleTaskHandler {
|
||||||
return &ArticleTaskHandler{
|
return &ArticleTaskHandler{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
articleApplicationService: articleApplicationService,
|
articleApplicationService: articleApplicationService,
|
||||||
asyncTaskRepo: asyncTaskRepo,
|
announcementApplicationService: announcementApplicationService,
|
||||||
|
asyncTaskRepo: asyncTaskRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +119,47 @@ func (h *ArticleTaskHandler) HandleArticleModify(ctx context.Context, t *asynq.T
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleAnnouncementPublish 处理公告发布任务
|
||||||
|
func (h *ArticleTaskHandler) HandleAnnouncementPublish(ctx context.Context, t *asynq.Task) error {
|
||||||
|
h.logger.Info("开始处理公告发布任务")
|
||||||
|
|
||||||
|
var payload AnnouncementPublishPayload
|
||||||
|
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||||
|
h.logger.Error("解析公告发布任务载荷失败", zap.Error(err))
|
||||||
|
h.updateTaskStatus(ctx, t, "failed", "解析任务载荷失败")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("处理公告发布任务",
|
||||||
|
zap.String("announcement_id", payload.AnnouncementID),
|
||||||
|
zap.Time("publish_at", payload.PublishAt))
|
||||||
|
|
||||||
|
// 检查任务是否已被取消
|
||||||
|
if err := h.checkTaskStatus(ctx, t); err != nil {
|
||||||
|
h.logger.Info("任务已被取消,跳过执行", zap.String("announcement_id", payload.AnnouncementID))
|
||||||
|
return nil // 静默返回,不报错
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用公告应用服务发布公告
|
||||||
|
if h.announcementApplicationService != nil {
|
||||||
|
err := h.announcementApplicationService.PublishAnnouncementByID(ctx, payload.AnnouncementID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("公告发布失败", zap.String("announcement_id", payload.AnnouncementID), zap.Error(err))
|
||||||
|
h.updateTaskStatus(ctx, t, "failed", "公告发布失败: "+err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.logger.Warn("公告应用服务未初始化,跳过发布", zap.String("announcement_id", payload.AnnouncementID))
|
||||||
|
h.updateTaskStatus(ctx, t, "failed", "公告应用服务未初始化")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务状态为成功
|
||||||
|
h.updateTaskStatus(ctx, t, "completed", "")
|
||||||
|
h.logger.Info("公告发布任务处理完成", zap.String("announcement_id", payload.AnnouncementID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ArticlePublishPayload 文章发布任务载荷
|
// ArticlePublishPayload 文章发布任务载荷
|
||||||
type ArticlePublishPayload struct {
|
type ArticlePublishPayload struct {
|
||||||
ArticleID string `json:"article_id"`
|
ArticleID string `json:"article_id"`
|
||||||
@@ -157,9 +205,9 @@ func (p *ArticleCancelPayload) FromJSON(data []byte) error {
|
|||||||
|
|
||||||
// ArticleModifyPayload 文章修改任务载荷
|
// ArticleModifyPayload 文章修改任务载荷
|
||||||
type ArticleModifyPayload struct {
|
type ArticleModifyPayload struct {
|
||||||
ArticleID string `json:"article_id"`
|
ArticleID string `json:"article_id"`
|
||||||
NewPublishAt time.Time `json:"new_publish_at"`
|
NewPublishAt time.Time `json:"new_publish_at"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType 获取任务类型
|
// GetType 获取任务类型
|
||||||
@@ -177,6 +225,28 @@ func (p *ArticleModifyPayload) FromJSON(data []byte) error {
|
|||||||
return json.Unmarshal(data, p)
|
return json.Unmarshal(data, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnnouncementPublishPayload 公告发布任务载荷
|
||||||
|
type AnnouncementPublishPayload struct {
|
||||||
|
AnnouncementID string `json:"announcement_id"`
|
||||||
|
PublishAt time.Time `json:"publish_at"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType 获取任务类型
|
||||||
|
func (p *AnnouncementPublishPayload) GetType() types.TaskType {
|
||||||
|
return types.TaskTypeAnnouncementPublish
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 序列化为JSON
|
||||||
|
func (p *AnnouncementPublishPayload) ToJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromJSON 从JSON反序列化
|
||||||
|
func (p *AnnouncementPublishPayload) FromJSON(data []byte) error {
|
||||||
|
return json.Unmarshal(data, p)
|
||||||
|
}
|
||||||
|
|
||||||
// updateTaskStatus 更新任务状态
|
// updateTaskStatus 更新任务状态
|
||||||
func (h *ArticleTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task, status string, errorMsg string) {
|
func (h *ArticleTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task, status string, errorMsg string) {
|
||||||
// 从任务载荷中提取任务ID
|
// 从任务载荷中提取任务ID
|
||||||
@@ -189,9 +259,11 @@ func (h *ArticleTaskHandler) updateTaskStatus(ctx context.Context, t *asynq.Task
|
|||||||
// 尝试从payload中获取任务ID
|
// 尝试从payload中获取任务ID
|
||||||
taskID, ok := payload["task_id"].(string)
|
taskID, ok := payload["task_id"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
// 如果没有task_id,尝试从article_id生成
|
// 如果没有task_id,尝试从article_id或announcement_id生成
|
||||||
if articleID, ok := payload["article_id"].(string); ok {
|
if articleID, ok := payload["article_id"].(string); ok {
|
||||||
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
||||||
|
} else if announcementID, ok := payload["announcement_id"].(string); ok {
|
||||||
|
taskID = fmt.Sprintf("announcement-publish-%s", announcementID)
|
||||||
} else {
|
} else {
|
||||||
h.logger.Error("无法从任务载荷中获取任务ID")
|
h.logger.Error("无法从任务载荷中获取任务ID")
|
||||||
return
|
return
|
||||||
@@ -278,9 +350,11 @@ func (h *ArticleTaskHandler) checkTaskStatus(ctx context.Context, t *asynq.Task)
|
|||||||
// 尝试从payload中获取任务ID
|
// 尝试从payload中获取任务ID
|
||||||
taskID, ok := payload["task_id"].(string)
|
taskID, ok := payload["task_id"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
// 如果没有task_id,尝试从article_id生成
|
// 如果没有task_id,尝试从article_id或announcement_id生成
|
||||||
if articleID, ok := payload["article_id"].(string); ok {
|
if articleID, ok := payload["article_id"].(string); ok {
|
||||||
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
taskID = fmt.Sprintf("article-publish-%s", articleID)
|
||||||
|
} else if announcementID, ok := payload["announcement_id"].(string); ok {
|
||||||
|
taskID = fmt.Sprintf("announcement-publish-%s", announcementID)
|
||||||
} else {
|
} else {
|
||||||
h.logger.Error("无法从任务载荷中获取任务ID")
|
h.logger.Error("无法从任务载荷中获取任务ID")
|
||||||
return fmt.Errorf("无法获取任务ID")
|
return fmt.Errorf("无法获取任务ID")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func NewAsynqWorker(
|
|||||||
redisAddr string,
|
redisAddr string,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
articleApplicationService article.ArticleApplicationService,
|
articleApplicationService article.ArticleApplicationService,
|
||||||
|
announcementApplicationService article.AnnouncementApplicationService,
|
||||||
apiApplicationService api.ApiApplicationService,
|
apiApplicationService api.ApiApplicationService,
|
||||||
walletService finance_services.WalletAggregateService,
|
walletService finance_services.WalletAggregateService,
|
||||||
subscriptionService *product_services.ProductSubscriptionService,
|
subscriptionService *product_services.ProductSubscriptionService,
|
||||||
@@ -39,15 +40,16 @@ func NewAsynqWorker(
|
|||||||
asynq.Config{
|
asynq.Config{
|
||||||
Concurrency: 6, // 降低总并发数
|
Concurrency: 6, // 降低总并发数
|
||||||
Queues: map[string]int{
|
Queues: map[string]int{
|
||||||
"default": 2, // 2个goroutine
|
"default": 2, // 2个goroutine
|
||||||
"api": 3, // 3个goroutine (扣款任务)
|
"api": 3, // 3个goroutine (扣款任务)
|
||||||
"article": 1, // 1个goroutine
|
"article": 1, // 1个goroutine
|
||||||
|
"announcement": 1, // 1个goroutine
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建任务处理器
|
// 创建任务处理器
|
||||||
articleHandler := handlers.NewArticleTaskHandler(logger, articleApplicationService, asyncTaskRepo)
|
articleHandler := handlers.NewArticleTaskHandler(logger, articleApplicationService, announcementApplicationService, asyncTaskRepo)
|
||||||
apiHandler := handlers.NewApiTaskHandler(logger, apiApplicationService, walletService, subscriptionService, asyncTaskRepo)
|
apiHandler := handlers.NewApiTaskHandler(logger, apiApplicationService, walletService, subscriptionService, asyncTaskRepo)
|
||||||
|
|
||||||
// 创建ServeMux
|
// 创建ServeMux
|
||||||
@@ -105,6 +107,9 @@ func (w *AsynqWorker) registerAllHandlers() {
|
|||||||
w.mux.HandleFunc(string(types.TaskTypeArticleCancel), w.articleHandler.HandleArticleCancel)
|
w.mux.HandleFunc(string(types.TaskTypeArticleCancel), w.articleHandler.HandleArticleCancel)
|
||||||
w.mux.HandleFunc(string(types.TaskTypeArticleModify), w.articleHandler.HandleArticleModify)
|
w.mux.HandleFunc(string(types.TaskTypeArticleModify), w.articleHandler.HandleArticleModify)
|
||||||
|
|
||||||
|
// 注册公告任务处理器
|
||||||
|
w.mux.HandleFunc(string(types.TaskTypeAnnouncementPublish), w.articleHandler.HandleAnnouncementPublish)
|
||||||
|
|
||||||
// 注册API任务处理器
|
// 注册API任务处理器
|
||||||
w.mux.HandleFunc(string(types.TaskTypeApiCall), w.apiHandler.HandleApiCall)
|
w.mux.HandleFunc(string(types.TaskTypeApiCall), w.apiHandler.HandleApiCall)
|
||||||
w.mux.HandleFunc(string(types.TaskTypeApiLog), w.apiHandler.HandleApiLog)
|
w.mux.HandleFunc(string(types.TaskTypeApiLog), w.apiHandler.HandleApiLog)
|
||||||
@@ -116,6 +121,7 @@ func (w *AsynqWorker) registerAllHandlers() {
|
|||||||
zap.String("article_publish", string(types.TaskTypeArticlePublish)),
|
zap.String("article_publish", string(types.TaskTypeArticlePublish)),
|
||||||
zap.String("article_cancel", string(types.TaskTypeArticleCancel)),
|
zap.String("article_cancel", string(types.TaskTypeArticleCancel)),
|
||||||
zap.String("article_modify", string(types.TaskTypeArticleModify)),
|
zap.String("article_modify", string(types.TaskTypeArticleModify)),
|
||||||
|
zap.String("announcement_publish", string(types.TaskTypeAnnouncementPublish)),
|
||||||
zap.String("api_call", string(types.TaskTypeApiCall)),
|
zap.String("api_call", string(types.TaskTypeApiCall)),
|
||||||
zap.String("api_log", string(types.TaskTypeApiLog)),
|
zap.String("api_log", string(types.TaskTypeApiLog)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import (
|
|||||||
|
|
||||||
// TaskManagerImpl 任务管理器实现
|
// TaskManagerImpl 任务管理器实现
|
||||||
type TaskManagerImpl struct {
|
type TaskManagerImpl struct {
|
||||||
asynqClient *asynq.Client
|
asynqClient *asynq.Client
|
||||||
asyncTaskRepo repositories.AsyncTaskRepository
|
asyncTaskRepo repositories.AsyncTaskRepository
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
config *interfaces.TaskManagerConfig
|
config *interfaces.TaskManagerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTaskManager 创建任务管理器
|
// NewTaskManager 创建任务管理器
|
||||||
@@ -274,8 +274,7 @@ func (tm *TaskManagerImpl) updatePayloadTaskID(task *entities.AsyncTask) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findTask 查找任务(支持taskID、articleID和announcementID三重查找)
|
||||||
// findTask 查找任务(支持taskID和articleID双重查找)
|
|
||||||
func (tm *TaskManagerImpl) findTask(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
func (tm *TaskManagerImpl) findTask(ctx context.Context, taskID string) (*entities.AsyncTask, error) {
|
||||||
// 先尝试通过任务ID查找
|
// 先尝试通过任务ID查找
|
||||||
task, err := tm.asyncTaskRepo.GetByID(ctx, taskID)
|
task, err := tm.asyncTaskRepo.GetByID(ctx, taskID)
|
||||||
@@ -287,17 +286,30 @@ func (tm *TaskManagerImpl) findTask(ctx context.Context, taskID string) (*entiti
|
|||||||
tm.logger.Info("通过任务ID查找失败,尝试通过文章ID查找", zap.String("task_id", taskID))
|
tm.logger.Info("通过任务ID查找失败,尝试通过文章ID查找", zap.String("task_id", taskID))
|
||||||
|
|
||||||
tasks, err := tm.asyncTaskRepo.GetByArticleID(ctx, taskID)
|
tasks, err := tm.asyncTaskRepo.GetByArticleID(ctx, taskID)
|
||||||
if err != nil || len(tasks) == 0 {
|
if err == nil && len(tasks) > 0 {
|
||||||
tm.logger.Error("通过文章ID也找不到任务",
|
// 使用找到的第一个任务
|
||||||
|
task = tasks[0]
|
||||||
|
tm.logger.Info("通过文章ID找到任务",
|
||||||
zap.String("article_id", taskID),
|
zap.String("article_id", taskID),
|
||||||
|
zap.String("task_id", task.ID))
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果通过文章ID也找不到,尝试通过公告ID查找
|
||||||
|
tm.logger.Info("通过文章ID查找失败,尝试通过公告ID查找", zap.String("task_id", taskID))
|
||||||
|
|
||||||
|
announcementTasks, err := tm.asyncTaskRepo.GetByAnnouncementID(ctx, taskID)
|
||||||
|
if err != nil || len(announcementTasks) == 0 {
|
||||||
|
tm.logger.Error("通过公告ID也找不到任务",
|
||||||
|
zap.String("announcement_id", taskID),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, fmt.Errorf("获取任务信息失败: %w", err)
|
return nil, fmt.Errorf("获取任务信息失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用找到的第一个任务
|
// 使用找到的第一个任务
|
||||||
task = tasks[0]
|
task = announcementTasks[0]
|
||||||
tm.logger.Info("通过文章ID找到任务",
|
tm.logger.Info("通过公告ID找到任务",
|
||||||
zap.String("article_id", taskID),
|
zap.String("announcement_id", taskID),
|
||||||
zap.String("task_id", task.ID))
|
zap.String("task_id", task.ID))
|
||||||
|
|
||||||
return task, nil
|
return task, nil
|
||||||
@@ -362,7 +374,8 @@ func (tm *TaskManagerImpl) enqueueTaskWithDelay(ctx context.Context, task *entit
|
|||||||
// getQueueName 根据任务类型获取队列名称
|
// getQueueName 根据任务类型获取队列名称
|
||||||
func (tm *TaskManagerImpl) getQueueName(taskType string) string {
|
func (tm *TaskManagerImpl) getQueueName(taskType string) string {
|
||||||
switch taskType {
|
switch taskType {
|
||||||
case string(types.TaskTypeArticlePublish), string(types.TaskTypeArticleCancel), string(types.TaskTypeArticleModify):
|
case string(types.TaskTypeArticlePublish), string(types.TaskTypeArticleCancel), string(types.TaskTypeArticleModify),
|
||||||
|
string(types.TaskTypeAnnouncementPublish):
|
||||||
return "article"
|
return "article"
|
||||||
case string(types.TaskTypeApiCall), string(types.TaskTypeApiLog), string(types.TaskTypeDeduction), string(types.TaskTypeUsageStats):
|
case string(types.TaskTypeApiCall), string(types.TaskTypeApiLog), string(types.TaskTypeDeduction), string(types.TaskTypeUsageStats):
|
||||||
return "api"
|
return "api"
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ type AsyncTaskRepository interface {
|
|||||||
GetByArticleID(ctx context.Context, articleID string) ([]*entities.AsyncTask, error)
|
GetByArticleID(ctx context.Context, articleID string) ([]*entities.AsyncTask, error)
|
||||||
CancelArticlePublishTask(ctx context.Context, articleID string) error
|
CancelArticlePublishTask(ctx context.Context, articleID string) error
|
||||||
UpdateArticlePublishTaskSchedule(ctx context.Context, articleID string, newScheduledAt time.Time) error
|
UpdateArticlePublishTaskSchedule(ctx context.Context, articleID string, newScheduledAt time.Time) error
|
||||||
|
|
||||||
|
// 公告任务专用方法
|
||||||
|
GetByAnnouncementID(ctx context.Context, announcementID string) ([]*entities.AsyncTask, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncTaskRepositoryImpl 异步任务仓库实现
|
// AsyncTaskRepositoryImpl 异步任务仓库实现
|
||||||
@@ -265,3 +268,32 @@ func (r *AsyncTaskRepositoryImpl) UpdateArticlePublishTaskSchedule(ctx context.C
|
|||||||
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||||
Update("scheduled_at", newScheduledAt).Error
|
Update("scheduled_at", newScheduledAt).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPendingArticlePublishTaskByArticleID 根据公告ID获取待执行的公告发布任务
|
||||||
|
func (r *AsyncTaskRepositoryImpl) GetPendingAnnouncementPublishTaskByAnnouncementID(ctx context.Context, announcementID string) (*entities.AsyncTask, error) {
|
||||||
|
var task entities.AsyncTask
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("type = ? AND payload LIKE ? AND status IN ?",
|
||||||
|
types.TaskTypeAnnouncementPublish,
|
||||||
|
"%\"announcement_id\":\""+announcementID+"\"%",
|
||||||
|
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||||
|
First(&task).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByAnnouncementID 根据公告ID获取所有相关任务
|
||||||
|
func (r *AsyncTaskRepositoryImpl) GetByAnnouncementID(ctx context.Context, announcementID string) ([]*entities.AsyncTask, error) {
|
||||||
|
var tasks []*entities.AsyncTask
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("payload LIKE ? AND status IN ?",
|
||||||
|
"%\"announcement_id\":\""+announcementID+"\"%",
|
||||||
|
[]entities.TaskStatus{entities.TaskStatusPending, entities.TaskStatusRunning}).
|
||||||
|
Find(&tasks).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ const (
|
|||||||
TaskTypeArticleCancel TaskType = "article_cancel"
|
TaskTypeArticleCancel TaskType = "article_cancel"
|
||||||
TaskTypeArticleModify TaskType = "article_modify"
|
TaskTypeArticleModify TaskType = "article_modify"
|
||||||
|
|
||||||
|
// 公告相关任务
|
||||||
|
TaskTypeAnnouncementPublish TaskType = "announcement_publish"
|
||||||
|
|
||||||
// API相关任务
|
// API相关任务
|
||||||
TaskTypeApiCall TaskType = "api_call"
|
TaskTypeApiCall TaskType = "api_call"
|
||||||
TaskTypeApiLog TaskType = "api_log"
|
TaskTypeApiLog TaskType = "api_log"
|
||||||
|
|
||||||
// 财务相关任务
|
// 财务相关任务
|
||||||
TaskTypeDeduction TaskType = "deduction"
|
TaskTypeDeduction TaskType = "deduction"
|
||||||
TaskTypeCompensation TaskType = "compensation"
|
TaskTypeCompensation TaskType = "compensation"
|
||||||
|
|
||||||
// 产品相关任务
|
// 产品相关任务
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func TestWestDexDecryptOutput(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "测试数据1",
|
name: "测试数据1",
|
||||||
data: "DLrbtEki5o/5yTvQWR+dWWUZYEo5s58D8LTnhhlAl99SwZbECa34KpStmR+Qr0gbbKzh3y4t5+/vbFFZgv03DtnYlLQcQt+rSgtxkCN/PCBPaFE0QZRTufd7djJfUww0Eh6DMHD7NS9pcuCa0PHGVoE+Vwo2YSwOnh2gtx3Bt0Qhs+w76tfCwIeufZ8tcpFs/nb84HIZxk+0cH1bTfNE6VsXI6vMpKvnS02O3oE2642ozeHgglCNuiOFMcCL8Erw4FKPnfRCUYdeKc2dZ7OF2IZqt0t4WiJBxjB/6k4tgAj/HepE2gaulWU8RVvAF+vPF5i3ekHHq8T7226rNlVfuagodaRXiOqO5E1h6Mx9ygcDL0HXvQKsxxJdl/bUP+t/+rOjA+k/IR/vF1UJGrGrkSJVfkcWXPP85cgws18gE9rIs2Ji1HGjvOmnez370L0+",
|
data: "0IdH/7L/ybMY00dne6clsk7VYBXPHkFfDagilHTzSHt9wTxref38uX8cDe7fJCGksbDQnMGo8GfsyEIpiCfj+w==",
|
||||||
secretKey: "121a1e41fc1690dd6b90afbcacd80cf4",
|
secretKey: "121a1e41fc1690dd6b90afbcacd80cf4",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
25
internal/shared/payment/context.go
Normal file
25
internal/shared/payment/context.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUidFromCtx 从context中获取用户ID
|
||||||
|
func GetUidFromCtx(ctx context.Context) (string, error) {
|
||||||
|
userID := ctx.Value("user_id")
|
||||||
|
if userID == nil {
|
||||||
|
return "", fmt.Errorf("用户ID不存在于上下文中")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := userID.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("用户ID类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
return "", fmt.Errorf("用户ID为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user