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

1
.cursorignore Normal file
View File

@@ -0,0 +1 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

51
.dockerignore Normal file
View File

@@ -0,0 +1,51 @@
# 开发环境文件
.env*
*.log
logs/
tmp/
# 构建产物
bin/
dist/
build/
*.exe
# 测试文件
*_test.go
coverage.out
coverage.html
test/
# 开发工具
.vscode/
.idea/
*.swp
*.swo
*~
# OS 文件
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Docker 相关
Dockerfile*
docker-compose*.yml
.dockerignore
# 文档和说明文件
README.md
docs/
*.md
api.mdc
# 开发依赖
node_modules/
# 临时文件
*.tmp
*.temp
coverage.out

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# 日志文件
logs/
*.log
file
# 编译输出
bin/
dist/
# IDE文件
.vscode/
.idea/
*.swp
*.swo
# 操作系统文件
.DS_Store
Thumbs.db
# 环境变量文件
.env
.env.local
.env.production
# 临时文件
tmp/
temp/
console
worker
# 依赖目录
vendor/
# 测试覆盖率文件
coverage.out
coverage.html
# 字体文件(大文件,不进行版本控制)
internal/shared/pdf/fonts/*.ttf
internal/shared/pdf/fonts/*.ttc
internal/shared/pdf/fonts/*.otf
# Pure Component 目录(用于持久化存储,不进行版本控制)
resources/Pure_Component/
# 其他
*.exe
*.exe*
*.dll
*.so
*.dylib
cmd/api/__debug_bin*

82
Dockerfile Normal file
View File

@@ -0,0 +1,82 @@
# 第一阶段:构建阶段
FROM golang:1.23.4-alpine AS builder
# 设置Go代理和Alpine镜像源
ENV GOPROXY https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 设置工作目录
WORKDIR /app
# 安装必要的包
RUN apk add --no-cache git tzdata
# 复制模块文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用程序
ARG VERSION=1.0.0
ARG COMMIT=dev
ARG BUILD_TIME
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
-a -installsuffix cgo \
-o hyapi-server \
cmd/api/main.go
# 第二阶段:运行阶段
FROM alpine:3.19
# 设置Alpine镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装必要的包(包含 headless Chrome 所需依赖)
# - tzdata: 时区
# - curl: 健康检查
# - chromium: 无头浏览器,用于 chromedp 生成 HTML 报告 PDF
# - nss、freetype、harfbuzz、ttf-freefont、font-noto-cjk: 字体及渲染依赖,避免中文/图标丢失和乱码
RUN apk --no-cache add \
tzdata \
curl \
chromium \
nss \
freetype \
harfbuzz \
ttf-freefont \
font-noto-cjk
# 设置时区
ENV TZ=Asia/Shanghai
# 为 chromedp 指定默认的 Chrome 路径Alpine 下 chromium 包的可执行文件)
ENV CHROME_BIN=/usr/bin/chromium-browser
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/hyapi-server .
# 复制配置文件
COPY config.yaml .
COPY configs/ ./configs/
# 复制资源文件报告模板、PDF、组件等
COPY resources ./resources
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动命令
CMD ["./hyapi-server", "-env=production"]

64
Dockerfile.worker Normal file
View File

@@ -0,0 +1,64 @@
# 第一阶段:构建阶段
FROM golang:1.23.4-alpine AS builder
# 设置Go代理和Alpine镜像源
ENV GOPROXY https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 设置工作目录
WORKDIR /app
# 安装必要的包
RUN apk add --no-cache git tzdata
# 复制模块文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用程序
ARG VERSION=1.0.0
ARG COMMIT=dev
ARG BUILD_TIME
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
-a -installsuffix cgo \
-o worker \
cmd/worker/main.go
# 第二阶段:运行阶段
FROM alpine:3.19
# 设置Alpine镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装必要的包
RUN apk --no-cache add tzdata curl
# 设置时区
ENV TZ=Asia/Shanghai
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/worker .
# 复制配置文件
COPY --chown=hyapi:hyapi config.yaml .
COPY --chown=hyapi:hyapi configs/ ./configs/
# 暴露端口(如果需要)
# EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动命令
CMD ["./worker", "-env=production"]

512
Makefile Normal file
View File

@@ -0,0 +1,512 @@
# HYAPI Server Makefile
# 应用信息
APP_NAME := hyapi-server
VERSION := 1.0.0
# 检测操作系统
ifeq ($(OS),Windows_NT)
# Windows 环境
BUILD_TIME := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'")
GIT_COMMIT := $(shell powershell -Command "try { git rev-parse --short HEAD } catch { 'dev' }")
GO_VERSION := $(shell go version)
MKDIR := mkdir
RM := del /f /q
RMDIR := rmdir /s /q
else
# Unix 环境
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo 'dev')
GO_VERSION := $(shell go version | awk '{print $$3}')
MKDIR := mkdir -p
RM := rm -f
RMDIR := rm -rf
endif
# 构建参数
LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_TIME)"
BUILD_DIR := bin
MAIN_PATH := cmd/api/main.go
# Go 相关
GOCMD := go
GOBUILD := $(GOCMD) build
GOCLEAN := $(GOCMD) clean
GOTEST := $(GOCMD) test
GOGET := $(GOCMD) get
GOMOD := $(GOCMD) mod
GOFMT := $(GOCMD) fmt
# Docker 相关
DOCKER_IMAGE := $(APP_NAME):$(VERSION)
DOCKER_LATEST := $(APP_NAME):;\
# 默认目标
.DEFAULT_GOAL := help
## 显示帮助信息
help:
@echo "HYAPI Server Makefile"
@echo ""
@echo "Usage: make [target]"
@echo ""
@echo "Development Basics:"
@echo " help Show this help message"
@echo " setup Setup development environment"
@echo " deps Install dependencies"
@echo " fmt Format code"
@echo " lint Lint code"
@echo " test Run tests"
@echo " coverage Generate test coverage report"
@echo ""
@echo "Build & Compile:"
@echo " build Build application"
@echo " build-prod Build production version"
@echo " build-all Cross compile for all platforms"
@echo " clean Clean build files"
@echo ""
@echo "Run & Manage:"
@echo " dev Run in development mode"
@echo " run Run compiled application"
@echo " migrate Run database migration"
@echo " version Show version info"
@echo " health Run health check"
@echo ""
@echo "Docker Containers:"
@echo " docker-build Build Docker image"
@echo " docker-build-prod Build production Docker image"
@echo " docker-push-prod Push image to registry"
@echo " docker-run Run Docker container"
@echo " docker-stop Stop Docker container"
@echo ""
@echo "Production Environment:"
@echo " deploy-prod Deploy to production"
@echo " prod-up Start production services"
@echo " prod-down Stop production services"
@echo " prod-logs View production logs"
@echo " prod-status Check production status"
@echo ""
@echo "Development Environment:"
@echo " services-up Start dev dependencies"
@echo " services-down Stop dev dependencies"
@echo " services-update Update dev dependencies (rebuild & restart)"
@echo " dev-up Alias for services-up"
@echo " dev-down Alias for services-down"
@echo " dev-update Alias for services-update"
@echo ""
@echo "Tools & Utilities:"
@echo " env Create .env file from template"
@echo " logs View application logs"
@echo " docs Generate API documentation"
@echo " bench Run performance benchmark"
@echo " race Run race condition detection"
@echo " security Run security scan"
@echo " mock Generate mock data"
@echo ""
@echo "CI/CD Pipeline:"
@echo " ci Run complete CI pipeline"
@echo " release Run complete release pipeline"
## Install dependencies
deps:
@echo "Installing dependencies..."
$(GOMOD) download
$(GOMOD) tidy
## Format code
fmt:
@echo "Formatting code..."
$(GOFMT) ./...
## Lint code
lint:
@echo "Linting code..."
ifeq ($(OS),Windows_NT)
@where golangci-lint >nul 2>&1 && golangci-lint run || echo "golangci-lint not installed, skipping lint check"
else
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
else \
echo "golangci-lint not installed, skipping lint check"; \
fi
endif
## Run tests
test:
@echo "Running tests..."
ifeq ($(OS),Windows_NT)
@where gcc >nul 2>&1 && $(GOTEST) -v -race -coverprofile=coverage.out ./... || $(GOTEST) -v -coverprofile=coverage.out ./...
else
$(GOTEST) -v -race -coverprofile=coverage.out ./...
endif
## Generate test coverage report
coverage: test
@echo "Generating coverage report..."
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
## Build application (development)
build:
@echo "Building application..."
ifeq ($(OS),Windows_NT)
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
else
@mkdir -p $(BUILD_DIR)
endif
$(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH)
## Build production version
build-prod:
@echo "Building production version..."
ifeq ($(OS),Windows_NT)
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
else
@mkdir -p $(BUILD_DIR)
endif
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
## Cross compile
build-all:
@echo "Cross compiling..."
ifeq ($(OS),Windows_NT)
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
else
@mkdir -p $(BUILD_DIR)
endif
# Linux AMD64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
# Linux ARM64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-arm64 $(MAIN_PATH)
# macOS AMD64
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-darwin-amd64 $(MAIN_PATH)
# macOS ARM64
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-darwin-arm64 $(MAIN_PATH)
# Windows AMD64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH)
## Run application
run: build
@echo "Starting application..."
./$(BUILD_DIR)/$(APP_NAME)
## Run in development mode
dev:
@echo "Starting development mode..."
$(GOCMD) run $(MAIN_PATH)
## Run database migration
migrate: build
@echo "Running database migration..."
./$(BUILD_DIR)/$(APP_NAME) -migrate
## Show version info
version: build
@echo "Version info:"
./$(BUILD_DIR)/$(APP_NAME) -version
## Health check
health: build
@echo "Running health check..."
./$(BUILD_DIR)/$(APP_NAME) -health
## Clean build files
clean:
@echo "Cleaning build files..."
$(GOCLEAN)
ifeq ($(OS),Windows_NT)
@if exist "$(BUILD_DIR)" $(RMDIR) "$(BUILD_DIR)" 2>nul || echo ""
@if exist "coverage.out" $(RM) "coverage.out" 2>nul || echo ""
@if exist "coverage.html" $(RM) "coverage.html" 2>nul || echo ""
else
$(RMDIR) $(BUILD_DIR) 2>/dev/null || true
$(RM) coverage.out coverage.html 2>/dev/null || true
endif
## Create .env file
env:
ifeq ($(OS),Windows_NT)
@if not exist ".env" ( \
echo Creating .env file from production template... && \
copy .env.production .env && \
echo .env file created, please modify configuration as needed \
) else ( \
echo .env file already exists \
)
else
@if [ ! -f .env ]; then \
echo "Creating .env file from production template..."; \
cp .env.production .env; \
echo ".env file created, please modify configuration as needed"; \
else \
echo ".env file already exists"; \
fi
endif
## Setup development environment
setup: deps env
@echo "Setting up development environment..."
@echo "1. [OK] Dependencies installed"
@echo "2. [OK] .env file created from production template"
@echo "3. [TODO] Please edit .env file and set your configuration"
@echo "4. [NEXT] Run 'make services-up' to start PostgreSQL + Redis"
@echo "5. [NEXT] Run 'make migrate' to create database tables"
@echo "6. [NEXT] Run 'make dev' to start development server"
@echo ""
@echo "Tip: Use 'make help' to see all available commands"
## Build Docker image
docker-build:
@echo "Building Docker image..."
docker build -t $(DOCKER_IMAGE) -t $(DOCKER_LATEST) .
## Build production Docker image with registry
docker-build-prod:
@echo "Building production Docker image..."
docker build \
--build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(GIT_COMMIT) \
--build-arg BUILD_TIME=$(BUILD_TIME) \
-t docker-registry.haiyudata.com/hyapi-server:$(VERSION) \
-t docker-registry.haiyudata.com/hyapi-server:latest \
.
## Push Docker image to registry
docker-push-prod:
@echo "Pushing Docker image to production registry..."
docker push docker-registry.haiyudata.com/hyapi-server:$(VERSION)
docker push docker-registry.haiyudata.com/hyapi-server:latest
## Deploy to production
deploy-prod:
@echo "Deploying to production environment..."
ifeq ($(OS),Windows_NT)
@if exist "scripts\\deploy.sh" ( \
bash scripts/deploy.sh $(VERSION) \
) else ( \
echo "Deploy script not found" \
)
else
@if [ -f scripts/deploy.sh ]; then \
./scripts/deploy.sh $(VERSION); \
else \
echo "Deploy script not found"; \
fi
endif
## Start production services
prod-up:
@echo "Starting production services..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.prod.yml" ( \
docker-compose -f docker-compose.prod.yml up -d \
) else ( \
echo docker-compose.prod.yml not found \
)
else
@if [ -f docker-compose.prod.yml ]; then \
docker-compose -f docker-compose.prod.yml up -d; \
else \
echo "docker-compose.prod.yml not found"; \
fi
endif
## Stop production services
prod-down:
@echo "Stopping production services..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.prod.yml" ( \
docker-compose -f docker-compose.prod.yml down \
) else ( \
echo docker-compose.prod.yml not found \
)
else
@if [ -f docker-compose.prod.yml ]; then \
docker-compose -f docker-compose.prod.yml down; \
else \
echo "docker-compose.prod.yml not found"; \
fi
endif
## View production logs
prod-logs:
@echo "Viewing production logs..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.prod.yml" ( \
docker-compose -f docker-compose.prod.yml logs -f \
) else ( \
echo docker-compose.prod.yml not found \
)
else
@if [ -f docker-compose.prod.yml ]; then \
docker-compose -f docker-compose.prod.yml logs -f; \
else \
echo "docker-compose.prod.yml not found"; \
fi
endif
## Check production status
prod-status:
@echo "Checking production status..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.prod.yml" ( \
docker-compose -f docker-compose.prod.yml ps \
) else ( \
echo docker-compose.prod.yml not found \
)
else
@if [ -f docker-compose.prod.yml ]; then \
docker-compose -f docker-compose.prod.yml ps; \
else \
echo "docker-compose.prod.yml not found"; \
fi
endif
## Run Docker container
docker-run:
@echo "Running Docker container..."
docker run -d --name $(APP_NAME) -p 8080:8080 --env-file .env $(DOCKER_LATEST)
## Stop Docker container
docker-stop:
@echo "Stopping Docker container..."
docker stop $(APP_NAME) || true
docker rm $(APP_NAME) || true
## Start development dependencies (Docker Compose)
services-up:
@echo "Starting development dependencies..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.dev.yml" ( \
docker-compose -f docker-compose.dev.yml up -d \
) else ( \
echo docker-compose.dev.yml not found \
)
else
@if [ -f docker-compose.dev.yml ]; then \
docker-compose -f docker-compose.dev.yml up -d; \
else \
echo "docker-compose.dev.yml not found"; \
fi
endif
## Stop development dependencies
services-down:
@echo "Stopping development dependencies..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.dev.yml" ( \
docker-compose -f docker-compose.dev.yml down \
) else ( \
echo docker-compose.dev.yml not found \
)
else
@if [ -f docker-compose.dev.yml ]; then \
docker-compose -f docker-compose.dev.yml down; \
else \
echo "docker-compose.dev.yml not found"; \
fi
endif
## Alias for dev-up (start development dependencies)
dev-up: services-up
## Alias for dev-down (stop development dependencies)
dev-down: services-down
## Update development dependencies (rebuild and restart)
services-update:
@echo "Updating development dependencies..."
ifeq ($(OS),Windows_NT)
@if exist "docker-compose.dev.yml" ( \
docker-compose -f docker-compose.dev.yml down && \
docker-compose -f docker-compose.dev.yml pull && \
docker-compose -f docker-compose.dev.yml up -d --build \
) else ( \
echo docker-compose.dev.yml not found \
)
else
@if [ -f docker-compose.dev.yml ]; then \
docker-compose -f docker-compose.dev.yml down && \
docker-compose -f docker-compose.dev.yml pull && \
docker-compose -f docker-compose.dev.yml up -d --build; \
else \
echo "docker-compose.dev.yml not found"; \
fi
endif
## Alias for services-update
dev-update: services-update
## View application logs
logs:
@echo "Viewing application logs..."
ifeq ($(OS),Windows_NT)
@if exist "logs\\app.log" ( \
powershell -Command "Get-Content logs\\app.log -Wait" \
) else ( \
echo "Log file does not exist" \
)
else
@if [ -f logs/app.log ]; then \
tail -f logs/app.log; \
else \
echo "Log file does not exist"; \
fi
endif
## Generate API documentation
docs:
@echo "Generating API documentation..."
ifeq ($(OS),Windows_NT)
@where swag >nul 2>&1 && swag init -g $(MAIN_PATH) -o docs/swagger || echo "swag not installed, skipping documentation generation"
else
@if command -v swag >/dev/null 2>&1; then \
swag init -g $(MAIN_PATH) -o docs/swagger; \
else \
echo "swag not installed, skipping documentation generation"; \
fi
endif
## Performance benchmark
bench:
@echo "Running performance benchmark..."
$(GOTEST) -bench=. -benchmem ./...
## Race condition detection
race:
@echo "Running race condition detection..."
$(GOTEST) -race ./...
## Security scan
security:
@echo "Running security scan..."
ifeq ($(OS),Windows_NT)
@where gosec >nul 2>&1 && gosec ./... || echo "gosec not installed, skipping security scan"
else
@if command -v gosec >/dev/null 2>&1; then \
gosec ./...; \
else \
echo "gosec not installed, skipping security scan"; \
fi
endif
## Generate mock data
mock:
@echo "Generating mock data..."
ifeq ($(OS),Windows_NT)
@where mockgen >nul 2>&1 && echo "Generating mock data..." || echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest"
else
@if command -v mockgen >/dev/null 2>&1; then \
echo "Generating mock data..."; \
else \
echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest"; \
fi
endif
## 完整的 CI 流程
ci: deps fmt lint test build
## 完整的发布流程
release: ci build-all docker-build
.PHONY: help deps fmt lint test coverage build build-prod build-all run dev migrate version health clean env setup docker-build docker-run docker-stop docker-push docker-build-prod docker-push-prod deploy-prod prod-up prod-down prod-logs prod-status services-up services-down services-update dev-up dev-down dev-update logs docs bench race security mock ci release

240
README.md Normal file
View File

@@ -0,0 +1,240 @@
# HYAPI Server
基于 DDD 和 Clean Architecture 的企业级后端 API 服务,采用 Gin 框架构建支持用户管理、JWT 认证、企业认证、财务管理等功能。
## 🚀 快速开始
### 启动服务
```bash
# 开发模式启动
make dev
# 或者直接运行
go run cmd/api/main.go
```
### API 文档
启动服务后,可以通过以下地址访问 API 文档:
- **Swagger UI**: http://localhost:8080/swagger/index.html
- **API 文档信息**: http://localhost:8080/api/docs
- **重定向地址**: http://localhost:8080/docs
详细的 API 文档使用说明请参考:[docs/swagger/README.md](docs/swagger/README.md)
## 📋 功能特性
- 🔐 **用户认证**: 支持密码登录和短信验证码登录
- 👤 **用户管理**: 用户注册、信息管理、密码修改
- 👨‍💼 **管理员系统**: 管理员认证和权限管理
- 🏢 **企业认证**: 企业信息认证、营业执照上传、人脸验证
- 💰 **财务管理**: 钱包管理、充值提现、交易记录
- 🔑 **密钥管理**: API 访问密钥的创建和管理
- 📊 **监控统计**: 完整的业务数据统计和分析
## 🏗️ 技术架构
- **框架**: Gin (Go Web Framework)
- **架构**: DDD + Clean Architecture
- **数据库**: PostgreSQL + GORM
- **缓存**: Redis
- **认证**: JWT
- **文档**: Swagger/OpenAPI
- **依赖注入**: Uber FX
- **日志**: Zap
- **配置**: Viper
## 📁 项目结构
```
hyapi-server/
├── cmd/api/ # 应用入口
├── internal/ # 内部包
│ ├── app/ # 应用层
│ ├── application/ # 应用服务层
│ ├── config/ # 配置管理
│ ├── container/ # 依赖注入容器
│ ├── domains/ # 领域层
│ ├── infrastructure/ # 基础设施层
│ └── shared/ # 共享组件
├── docs/swagger/ # API 文档
├── scripts/ # 脚本文件
└── deployments/ # 部署配置
```
## 🔧 开发指南
### 环境要求
- Go 1.23+
- PostgreSQL 12+
- Redis 6+
- Docker (可选)
### 安装依赖
```bash
# 安装 Go 依赖
go mod download
go mod tidy
# 安装开发工具
go install github.com/swaggo/swag/cmd/swag@latest
```
### 数据库迁移
```bash
# 运行数据库迁移
make migrate
```
### 更新 API 文档
```bash
# 使用 Makefile
make docs
# 使用 PowerShell 脚本 (Windows)
.\scripts\update-docs.ps1
# 更新文档并重启服务器
.\scripts\update-docs.ps1 -Restart
```
## 🐳 Docker 部署
```bash
# 开发环境
docker-compose -f docker-compose.dev.yml up -d
# 生产环境
docker-compose -f docker-compose.prod.yml up -d
```
## 📝 配置系统
HYAPI 服务端采用分层配置系统,使配置管理更加灵活和清晰:
1. **基础配置文件** (`config.yaml`): 包含所有配置项和默认值
2. **环境特定配置文件** (`configs/env.<环境>.yaml`): 包含特定环境需要覆盖的配置项
3. **环境变量**: 可以覆盖任何配置项,优先级最高
### 配置文件格式
所有配置文件采用 YAML 格式,保持相同的结构层次。
#### 基础配置文件 (config.yaml)
包含所有配置项和默认值,作为配置的基础。
#### 环境配置文件
环境配置文件只需包含需要覆盖的配置项,保持与基础配置相同的层次结构:
- `configs/env.development.yaml`: 开发环境配置
- `configs/env.testing.yaml`: 测试环境配置
- `configs/env.production.yaml`: 生产环境配置
### 配置加载顺序
系统按以下顺序加载配置,后加载的会覆盖先加载的:
1. 首先加载基础配置文件 `config.yaml`
2. 然后加载环境特定配置文件 `configs/env.<环境>.yaml`
3. 最后应用环境变量覆盖
### 环境确定方式
系统按以下优先级确定当前环境:
1. `CONFIG_ENV` 环境变量
2. `ENV` 环境变量
3. `APP_ENV` 环境变量
4. 默认值 `development`
### 统一配置项
某些配置项在所有环境中保持一致,直接在基础配置文件中设置:
1. **短信配置**: 所有环境使用相同的短信服务配置
2. **基础服务地址**: 如第三方服务端点等
### 使用示例
#### 基础配置 (config.yaml)
```yaml
app:
name: "HYAPI Server"
version: "1.0.0"
env: "development"
database:
host: "localhost"
port: "5432"
user: "postgres"
password: "default_password"
# 统一的短信配置
sms:
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
endpoint_url: "dysmsapi.aliyuncs.com"
```
#### 环境配置 (configs/env.production.yaml)
```yaml
app:
env: "production"
database:
host: "prod-db.example.com"
password: "prod_secure_password"
```
#### 运行时
```bash
# 使用开发环境配置
go run cmd/api/main.go
# 使用生产环境配置
ENV=production go run cmd/api/main.go
# 使用环境变量覆盖特定配置
ENV=production DB_PASSWORD=custom_password go run cmd/api/main.go
```
### 敏感信息处理
对于敏感信息(如密码、密钥等):
1. 开发环境:可以放在环境配置文件中
2. 生产环境:应通过环境变量注入,不应出现在配置文件中
### 配置验证
系统在启动时会验证必要的配置项,确保应用能够正常运行。如果缺少关键配置,系统将拒绝启动并提供明确的错误信息。
## 📚 相关文档
- [API 文档使用指南](docs/swagger/README.md)
- [开发指南](docs/开始指南/开发指南.md)
- [架构设计文档](docs/开始指南/架构设计文档.md)
- [部署指南](docs/开始指南/部署指南.md)
## 🤝 贡献指南
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 打开 Pull Request
## 📄 许可证
本项目采用 Apache 2.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。

2953
api.mdc Normal file

File diff suppressed because it is too large Load Diff

135
cmd/api/main.go Normal file
View File

@@ -0,0 +1,135 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"time"
"hyapi-server/internal/app"
)
// @title HYAPI Server API
// @version 1.0
// @description 基于DDD和Clean Architecture的企业级后端API服务
// @description 采用Gin框架构建支持用户管理、JWT认证、事件驱动等功能
// @contact.name API Support
// @contact.url https://github.com/your-org/hyapi-server
// @contact.email support@example.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
// 构建时注入的变量
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
// 设置时区为北京时间
time.Local = time.FixedZone("CST", 8*3600)
// 命令行参数
var (
showVersion = flag.Bool("version", false, "显示版本信息")
migrate = flag.Bool("migrate", false, "运行数据库迁移")
health = flag.Bool("health", false, "执行健康检查")
env = flag.String("env", "", "指定运行环境 (development|production|testing)")
)
flag.Parse()
// 处理版本信息显示 (不需要初始化完整应用)
if *showVersion {
fmt.Printf("HYAPI Server\n")
fmt.Printf("Version: %s\n", version)
fmt.Printf("Commit: %s\n", commit)
fmt.Printf("Build Date: %s\n", date)
os.Exit(0)
}
// 设置环境变量(如果通过命令行指定)
if *env != "" {
if err := validateEnvironment(*env); err != nil {
log.Fatalf("无效的环境参数: %v", err)
}
os.Setenv("ENV", *env)
fmt.Printf("🌍 通过命令行设置环境: %s\n", *env)
}
// 显示当前环境
currentEnv := getCurrentEnvironment()
fmt.Printf("🔧 当前运行环境: %s\n", currentEnv)
// 生产环境安全提示
if currentEnv == "production" {
fmt.Printf("⚠️ 生产环境模式 - 请确保配置正确\n")
}
// 创建应用程序实例
application, err := app.NewApplication()
if err != nil {
log.Fatalf("Failed to create application: %v", err)
}
// 处理命令行参数
if *migrate {
fmt.Println("Running database migrations...")
if err := application.RunCommand("migrate"); err != nil {
log.Fatalf("Migration failed: %v", err)
}
fmt.Println("Database migrations completed successfully")
os.Exit(0)
}
if *health {
fmt.Println("Performing health check...")
if err := application.RunCommand("health"); err != nil {
log.Fatalf("Health check failed: %v", err)
}
fmt.Println("Health check passed")
os.Exit(0)
}
// 启动应用程序 (使用完整的架构)
fmt.Printf("🚀 Starting HYAPI Server v%s (%s)\n", version, commit)
if err := application.Run(); err != nil {
log.Fatalf("Application failed to start: %v", err)
}
}
// validateEnvironment 验证环境参数
func validateEnvironment(env string) error {
validEnvs := []string{"development", "production", "testing"}
for _, validEnv := range validEnvs {
if env == validEnv {
return nil
}
}
return fmt.Errorf("环境必须是以下之一: %v", validEnvs)
}
// getCurrentEnvironment 获取当前环境与config包中的逻辑保持一致
func getCurrentEnvironment() string {
if env := os.Getenv("CONFIG_ENV"); env != "" {
return env
}
if env := os.Getenv("ENV"); env != "" {
return env
}
if env := os.Getenv("APP_ENV"); env != "" {
return env
}
return "development"
}

View File

@@ -0,0 +1,77 @@
// 将 raw fixture各处理器原始 JSON经 BuildReportFromRawSources 转化为与线上一致的完整报告 JSON。
//
// go run ./cmd/qygl_report_build -in resources/dev-report/fixture.raw.example.json -out resources/dev-report/built.json
// go run ./cmd/qygl_report_build -in raw.json -out - # 输出到 stdout
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"hyapi-server/internal/domains/api/services/processors/qygl"
)
type rawBundle struct {
Kind string `json:"kind"`
JiguangFull map[string]interface{} `json:"jiguangFull"`
JudicialCertFull map[string]interface{} `json:"judicialCertFull"`
EquityPanorama map[string]interface{} `json:"equityPanorama"`
AnnualReport map[string]interface{} `json:"annualReport"`
TaxViolation map[string]interface{} `json:"taxViolation"`
TaxArrears map[string]interface{} `json:"taxArrears"`
}
func main() {
inPath := flag.String("in", "", "raw fixture JSON 路径(含 jiguangFull 等字段,可参考 fixture.raw.example.json")
outPath := flag.String("out", "", "输出文件;- 或留空表示输出到 stdout")
flag.Parse()
if *inPath == "" {
log.Fatal("请指定 -in <raw.json>")
}
raw, err := os.ReadFile(*inPath)
if err != nil {
log.Fatalf("读取输入失败: %v", err)
}
var b rawBundle
if err := json.Unmarshal(raw, &b); err != nil {
log.Fatalf("解析 JSON 失败: %v", err)
}
if b.Kind == "full" {
log.Fatal("输入为 kind=full已是 build 结果),无需再转化;预览请用: go run ./cmd/qygl_report_preview")
}
if b.Kind != "" && b.Kind != "raw" {
log.Fatalf("若填写 kind仅支持 raw当前: %q", b.Kind)
}
report := qygl.BuildReportFromRawSources(
b.JiguangFull,
b.JudicialCertFull,
b.EquityPanorama,
b.AnnualReport,
b.TaxViolation,
b.TaxArrears,
)
out, err := json.MarshalIndent(report, "", " ")
if err != nil {
log.Fatalf("序列化报告失败: %v", err)
}
if *outPath == "" || *outPath == "-" {
if _, err := os.Stdout.Write(append(out, '\n')); err != nil {
log.Fatal(err)
}
return
}
if err := os.WriteFile(*outPath, append(out, '\n'), 0644); err != nil {
log.Fatalf("写入失败: %v", err)
}
fmt.Fprintf(os.Stderr, "已写入 %s\n", *outPath)
}

View File

@@ -0,0 +1,59 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"go.uber.org/zap"
"hyapi-server/internal/shared/pdf"
)
// 一个本地调试用的小工具:
// 从 JSON 文件(企业报告.json读取 QYGL 聚合结果,使用 gofpdf 生成企业全景报告 PDF输出到当前目录。
func main() {
var (
jsonPath string
outPath string
)
flag.StringVar(&jsonPath, "json", "企业报告.json", "企业报告 JSON 数据源文件路径")
flag.StringVar(&outPath, "out", "企业全景报告_gofpdf.pdf", "输出 PDF 文件路径")
flag.Parse()
logger, _ := zap.NewDevelopment()
defer logger.Sync()
absJSON, _ := filepath.Abs(jsonPath)
fmt.Printf("读取 JSON 数据源:%s\n", absJSON)
data, err := os.ReadFile(jsonPath)
if err != nil {
fmt.Printf("读取 JSON 文件失败: %v\n", err)
os.Exit(1)
}
var report map[string]interface{}
if err := json.Unmarshal(data, &report); err != nil {
fmt.Printf("解析 JSON 失败: %v\n", err)
os.Exit(1)
}
fmt.Println("开始使用 gofpdf 生成企业全景报告 PDF...")
pdfBytes, err := pdf.GenerateQYGLReportPDF(context.Background(), logger, report)
if err != nil {
fmt.Printf("生成 PDF 失败: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(outPath, pdfBytes, 0644); err != nil {
fmt.Printf("写入 PDF 文件失败: %v\n", err)
os.Exit(1)
}
absOut, _ := filepath.Abs(outPath)
fmt.Printf("PDF 生成完成:%s\n", absOut)
}

View File

@@ -0,0 +1,159 @@
// 仅读取 build 后的报告 JSON本地渲染 qiye.html不执行 BuildReportFromRawSources
//
// go run ./cmd/qygl_report_preview -in resources/dev-report/built.json
// go run ./cmd/qygl_report_preview -in built.json -addr :8899 -watch
//
// 每次打开/刷新页面都会重新读取 -in 文件;加 -watch 后保存 JSON 会自动刷新浏览器。
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
)
func parseBuiltReport(data []byte) (map[string]interface{}, error) {
var root map[string]interface{}
if err := json.Unmarshal(data, &root); err != nil {
return nil, err
}
if _, ok := root["jiguangFull"]; ok {
return nil, fmt.Errorf("检测到 raw 字段 jiguangFull请先执行: go run ./cmd/qygl_report_build -in <raw.json> -out built.json")
}
if k, _ := root["kind"].(string); k == "full" {
r, ok := root["report"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("kind=full 时缺少 report 对象")
}
return r, nil
}
if r, ok := root["report"].(map[string]interface{}); ok {
return r, nil
}
if root["entName"] != nil || root["basic"] != nil || root["reportTime"] != nil {
return root, nil
}
return nil, fmt.Errorf("不是有效的 build 后报告(根级应有 entName、basic、reportTime 之一,或 {\"report\":{...}} / kind=full")
}
func fileVersionTag(path string) (string, error) {
st, err := os.Stat(path)
if err != nil {
return "", err
}
return fmt.Sprintf("%d-%d", st.ModTime().UnixNano(), st.Size()), nil
}
func renderPage(tmpl *template.Template, report map[string]interface{}, injectLive bool) ([]byte, error) {
reportBytes, err := json.Marshal(report)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]interface{}{
"ReportJSON": template.JS(reportBytes),
}); err != nil {
return nil, err
}
b := buf.Bytes()
if !injectLive {
return b, nil
}
script := `<script>(function(){var v0=null;function tick(){fetch("/__version?="+Date.now(),{cache:"no-store"}).then(function(r){return r.text();}).then(function(v){if(v==="")return;if(v0===null)v0=v;else if(v0!==v){v0=v;location.reload();}}).catch(function(){});}setInterval(tick,600);tick();})();</script>`
closing := []byte("</body>")
idx := bytes.LastIndex(b, closing)
if idx < 0 {
return append(b, []byte(script)...), nil
}
out := make([]byte, 0, len(b)+len(script))
out = append(out, b[:idx]...)
out = append(out, script...)
out = append(out, b[idx:]...)
return out, nil
}
func main() {
addr := flag.String("addr", ":8899", "监听地址")
root := flag.String("root", ".", "项目根目录(含 resources/qiye.html")
inPath := flag.String("in", "", "build 后的 JSON由 qygl_report_build 生成,或 fixture.full 中的 report 形态)")
watch := flag.Bool("watch", false, "监听 -in 文件变化并自动刷新浏览器(轮询)")
flag.Parse()
if *inPath == "" {
log.Fatal("请指定 -in <built.json>")
}
rootAbs, err := filepath.Abs(*root)
if err != nil {
log.Fatalf("解析 root: %v", err)
}
tplPath := filepath.Join(rootAbs, "resources", "qiye.html")
if _, err := os.Stat(tplPath); err != nil {
log.Fatalf("未找到模板 %s: %v", tplPath, err)
}
var inAbs string
if filepath.IsAbs(*inPath) {
inAbs = *inPath
} else {
inAbs = filepath.Join(rootAbs, *inPath)
}
if _, err := os.Stat(inAbs); err != nil {
log.Fatalf("读取 %s: %v", inAbs, err)
}
tmpl, err := template.ParseFiles(tplPath)
if err != nil {
log.Fatalf("解析模板: %v", err)
}
http.HandleFunc("/__version", func(w http.ResponseWriter, r *http.Request) {
tag, err := fileVersionTag(inAbs)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(tag))
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
raw, err := os.ReadFile(inAbs)
if err != nil {
http.Error(w, "读取报告文件失败: "+err.Error(), http.StatusInternalServerError)
return
}
report, err := parseBuiltReport(raw)
if err != nil {
http.Error(w, "解析 JSON 失败: "+err.Error(), http.StatusInternalServerError)
return
}
html, err := renderPage(tmpl, report, *watch)
if err != nil {
http.Error(w, "渲染失败: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(html)
})
log.Printf("报告预览: http://127.0.0.1%s/ (每请求重读 %s", *addr, inAbs)
if *watch {
log.Printf("已启用 -watch保存 JSON 后约 0.6s 内自动刷新页面")
}
if err := http.ListenAndServe(*addr, nil); err != nil {
log.Fatal(err)
}
}

658
config.yaml Normal file
View File

@@ -0,0 +1,658 @@
# HYAPI Server Configuration
# 🎯 统一配置文件,包含所有默认配置值
app:
name: "HYAPI Server"
version: "1.0.0"
env: "development"
server:
host: "0.0.0.0"
port: "8080"
mode: "debug"
read_timeout: 30s
write_timeout: 30s
idle_timeout: 120s
database:
host: "localhost"
port: "15432"
user: "postgres"
password: "Qm8kZ3nR7pL4wT9y"
name: "hyapi_dev"
sslmode: "disable"
timezone: "Asia/Shanghai"
max_open_conns: 50
max_idle_conns: 20
conn_max_lifetime: 300s
auto_migrate: true
redis:
host: "localhost"
port: "16379"
password: ""
db: 0
pool_size: 10
min_idle_conns: 3
max_retries: 3
dial_timeout: 5s
read_timeout: 3s
write_timeout: 3s
cache:
default_ttl: 3600s
cleanup_interval: 600s
max_size: 1000
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger:
# 基础配置
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
format: "json" # 输出格式: json, console
output: "file" # 输出方式: stdout, stderr, file
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出仅console格式有效
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
enable_request_logging: true # 是否启用请求日志
enable_performance_log: true # 是否启用性能日志
# 开发环境配置
development: true # 是否为开发环境
sampling: false # 是否启用采样
# 各级别配置(按级别分文件时使用)
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
# 全面日志配置
comprehensive_logging:
enable_request_logging: true
enable_response_logging: true
enable_request_body_logging: true # 开发环境记录请求体
enable_error_logging: true
enable_business_logging: true
enable_performance_logging: true
max_body_size: 10240 # 10KB
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
jwt:
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
expires_in: 168h
refresh_expires_in: 168h
api:
domain: "api.haiyudata.com"
# public_base_url: "" # 可选,无尾斜杠;空则按 https://{domain} 推导;环境变量 API_PUBLIC_BASE_URL 优先
sms:
# 短信服务商tencent默认、aliyunmock_enabled=true 时不走云厂商
provider: "tencent"
tencent_cloud:
secret_id: "AKIDPCwVzUovoefbP4YzDEQOAi27wZx72i9h" # 腾讯云 API 密钥 SecretId建议用环境变量覆盖
secret_key: "MTu87tSv6NqX2JMAE4QYR4tVtCHBrHSO" # SecretKey
region: "ap-guangzhou"
endpoint: "sms.tencentcloudapi.com" # 可空,默认 sms.tencentcloudapi.com
sms_sdk_app_id: "1401111903" # 短信 SdkAppId
sign_name: "海宇数科广东横琴科技" # 短信签名
template_id: "2631130" # 验证码模板 ID单变量验证码
# 低余额与欠费为两套模板(变量顺序一般为:企业名、时间、金额)
low_balance_template_id: "2631956" # 余额不足预警
arrears_template_id: "2631956" # 欠费预警
balance_alert_template_id: "2631956" # 可选:若上面两项未配,则两类告警共用此模板 ID兼容旧配置
# 阿里云provider=aliyun 时使用)
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
endpoint_url: "dysmsapi.aliyuncs.com"
sign_name: "海南海宇大数据"
template_code: "SMS_302641455"
# 阿里云余额预警模板 CODE低余额与欠费共用可空则默认 SMS_500565339
balance_alert_template_code: ""
code_length: 6
expire_time: 5m
mock_enabled: false
# 签名验证配置(用于防止接口被刷)
signature_enabled: true # 是否启用签名验证
signature_secret: "HyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥加密模式时需要可选EKEY
captcha_endpoint: "captcha.cn-shanghai.aliyuncs.com" # 阿里云验证码服务Endpoint
scene_id: "wynt39to" # 阿里云验证码场景ID
rate_limit:
daily_limit: 10
hourly_limit: 5
min_interval: 60s
# 邮件服务配置 - QQ邮箱
email:
host: "smtp.qq.com"
port: 587
username: "1726850085@qq.com"
password: "kqnumdccomvlehjg"
from_email: "1726850085@qq.com"
use_ssl: true
timeout: 10s
domain: "console.haiyudata.com"
# 存储服务配置 - 七牛云
storage:
access_key: "your-qiniu-access-key"
secret_key: "your-qiniu-secret-key"
bucket: "your-bucket-name"
domain: "https://your-domain.com"
# OCR服务配置 - 百度智能云
ocr:
api_key: "your-baidu-api-key"
secret_key: "your-baidu-secret-key"
ratelimit:
requests: 7500
window: 70s
# 每日请求限制配置
daily_ratelimit:
max_requests_per_day: 300 # 每日最大请求次数
max_requests_per_ip: 15 # 每个IP每日最大请求次数
key_prefix: "daily_limit" # Redis键前缀
ttl: 24h # 键过期时间
max_concurrent: 8 # 最大并发请求数
# 安全配置
enable_ip_whitelist: false # 是否启用IP白名单
ip_whitelist: # IP白名单列表
- "192.168.1.*" # 内网IP段
- "10.0.0.*" # 内网IP段
- "127.0.0.1" # 本地回环
enable_ip_blacklist: true # 是否启用IP黑名单
ip_blacklist: # IP黑名单列表
- "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP
enable_user_agent: false # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人
- "crawler" # 爬虫
- "spider" # 蜘蛛
- "scraper" # 抓取器
- "curl" # curl工具
- "wget" # wget工具
- "python" # Python脚本
- "java" # Java脚本
- "go-http-client" # Go HTTP客户端
- "LangShen"
enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer
- "https://console.haiyudata.com" # 天元API控制台
- "https://consoletest.haiyudata.com" # 天元API测试控制台
enable_proxy_check: false # 是否检查代理
enable_geo_block: false # 是否启用地理位置阻止
blocked_countries: # 被阻止的国家/地区
- "XX" # 示例国家代码
monitoring:
metrics_enabled: true
metrics_port: "9090"
tracing_enabled: true
tracing_endpoint: "http://localhost:4317"
sample_rate: 0.1
health:
enabled: true
interval: 30s
timeout: 10s
resilience:
circuit_breaker_enabled: true
circuit_breaker_threshold: 5
circuit_breaker_timeout: 60s
retry_max_attempts: 3
retry_initial_delay: 100ms
retry_max_delay: 5s
development:
debug: true
enable_profiler: true
enable_cors: true
cors_allowed_origins: "http://localhost:5173,https://consoletest.haiyudata.com,https://console.haiyudata.com"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# 企业微信配置
wechat_work:
webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1fdabca0-298a-43d6-8794-6b4caf15e176"
secret: ""
# ===========================================
# 📝 e签宝服务配置
# ===========================================
esign:
app_id: "7439073138"
app_secret: "d76e27fdd169b391e09262a0959dac5c"
server_url: "https://smlopenapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
contract:
name: "海宇数据API合作协议"
expire_days: 7
retry_count: 3
auth:
org_auth_modes: ["PSN_MOBILE3"]
default_auth_mode: "PSN_MOBILE3"
psn_auth_modes: ["PSN_MOBILE3", "PSN_IDCARD"]
willingness_auth_modes: ["CODE_SMS"]
redirect_url: "https://console.haiyudata.com/certification/callback/auth"
sign:
auto_finish: true
sign_field_style: 1
client_type: "ALL"
redirect_url: "https://console.haiyudata.com/certification/callback/sign"
# ===========================================
# 💰 钱包配置
# ===========================================
wallet:
default_credit_limit: 50.00
min_amount: "100.00" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额
recharge_bonus_enabled: true # 是否启用充值赠送,设为 false 时仅展示商务洽谈提示
api_store_recharge_tip: "" # 关闭赠送时展示的提示文案,为空则使用默认文案
# 支付宝充值赠送配置
alipay_recharge_bonus:
- recharge_amount: 1000.00 # 充值1000元
bonus_amount: 50.00 # 赠送50元
- recharge_amount: 5000.00 # 充值5000元
bonus_amount: 300.00 # 赠送300元
- recharge_amount: 10000.00 # 充值10000元
bonus_amount: 800.00 # 赠送800元
# 余额预警配置
balance_alert:
default_enabled: true # 默认启用余额预警
default_threshold: 200.00 # 默认预警阈值
alert_cooldown_hours: 24 # 预警冷却时间(小时)
# ===========================================
# 🌍 西部数据配置
# ===========================================
westdex:
url: "https://apimaster.westdex.com.cn/api/invoke"
key: "121a1e41fc1690dd6b90afbcacd80cf4"
secret_id: "449159"
secret_second_id: "296804"
# 西部数据日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "westdex"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌍 羽山配置
# ===========================================
yushan:
url: https://api.yushanshuju.com/credit-gw/service
api_key: "4c566c4a4b543164535455685655316c"
acct_id: "YSSJ843926726"
# 羽山日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "yushan"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 💰 支付宝支付配置
# ===========================================
alipay:
app_id: "2021004181633376"
private_key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCC2GNEWrQUg6FVHBdlDcgL1SA1KmRI8IHgSJGvXEsgfo3g62aa5usFXHVz5bMzpIcDu0N+jGtZQIBuuq7TxGwhDvWBygEDXN17p00uwqik/3TsyFvJ4FfbkaS7pRIGfeO/cBTzjqznanPUdHJZ9L39QmTqTefIQQvGOCvgntKxPa/LdS24+ZLA2RNh3TsRzbSxOOJPmUrwvCX8U13F9jH250hvf+Tewz4hyG8CkiMM4d1UpGMndQNr8oTY0vwFbWAG0ZDGgkxjg0iRJ02fgxwShQS1TgY5NxPhpKBiN5C/WG15qCqEw0F3GlpfWZwzUhv1uMiy+xbZ2bGLo1YCtwUtAgMBAAECggEAQ8uk25T3u61cWYH9qTGT1nWug32ciqJ7WN+hBLCYiJSqJMEz380INzXp8Ywx5u83ubo8xYQyVwNxyG3YCge7UwGyOXaWQczLQbe06SaZRSzLw6gozxf7zdvP9B4akdyGtfl4EZ56fkmNDKbtXSjPjDrrmO+Wyg7R7/nI2lDQsF6dXTKD0YiHtTKz40amKgbIYX+qc3yVS0slkVjcfnRczr+PKM5RMsV3Jk2pr6IYeq3E24LnbuVtV76priTqJN3hVSy2Y6JqmAYkI0HCoCuaFGE8ud3J859jjMcUXTRFJyDsKKooa+FZCoEx2ToVMqnb4vjfr1gZifUrw4ZNd5cPoQKBgQC4v/fNTXuA21pb+l4fnqK0o3wFhiNJh920yIlF4Vd0Nsi2/TwqFK6cVhrUFAmKr88hTzY1vkOhd/HLlkWjNDR5OGx1K1BKUAZjWIfProv8lDSckADEI29lro9WzFGy0o4szlEJ2uuUfO/j9Qn2lmx5oFPsz0TI+HoSNFE0q/SlxQKBgQC1ToMLuh0OkucZm1SL6xcjudBX7U0ElZ/TIxRzfxQ/sN911/BRlxrSdCcDMXNuuFpV2ACjDNWWLJM1sRVsOWNA/oXzZf6VTvUDIAv8XrNUt/B87genBVuMTZ2RYmMWCrgW0PE1OrpKGuQCKVsn242B2Xpmee9OnHhBF2uTASDASQKBgBALvD38iMl8Q7DRYfNlF8SQnmjsaYwtXLgi4qlLFQlm6K/b9qnA+hlh8RqSUvHUqyy9cHvidoVDoaCJAKtYEWal2+WhSWvq32MpgUIsasQZKyid6TMf0MEIFDL5s+7QEsEZejhc5zESWNN3qNHd5rX5ktBygArkadXC7XqhpLHxAoGBAJ0dJEKNTZDLjKiMCoAVgT/cTcdkRFGst4tn4tkTTqDCzWJ5di++Geg173i86aMQ7ndlb2fcP1qb1hW5Fy9pq7Eu3zVFNZB9k6TZqIlSJ2VK4IPiYY9C/UpgGCNcdzEqqMxc1Cmkcrq1AtE8tVmc0Mutgnw7Pj2JKkx91yLU32TBAoGAKxssUdTLuf5Z5oFgzpoSES9qwc1h6jlMfsouDzHcZf0aYintD6Vby7SVul5540qYkDkNs0YZ3uZu74LHfoBaWJjYIIVAMSMX+3AtBpQUyYluex64V/g60t+0sFuDWqMvSPU7mZcv6+KIP6vW56GeYdhHf4JqttdIHm9SgkoJjjY="
alipay_public_key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2CqoCp95w/JV3RT/gzF4/8QmVT1HQNaeW7yUp+mA7x9AbjvlTW/+eRn6oGAL/XhZLjvHD0XjKLVKX0MJVS1aUQHEHEbOJN4Eu8II45OavD4iZISa7Kp9V6AM+i4qTyaeV2wNDnGxHQBaLVUGCfMR+56EK2YpORdE1H9uy72SSQseVb3bmpsV9EW/IJNmcVL/ut3uA1JWAoRmzlQ7ekxg7p8AYXzYPEHQr1tl7W+M4zv9wO9GKZCxIqMA8U3RP5npPfRaCfIRGzXzCqFEEUvWuidOB7frsvN4jiPD07qpL2Bi9LM1X/ee2kC/oM8Uhd7ERZhG8MbZfijZKxgrsDKBcwIDAQAB"
is_production: true
notify_url: "https://console.haiyudata.com/api/v1/finance/alipay/callback"
return_url: "https://console.haiyudata.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.haiyudata.com/api/v1/pay/wechat/callback"
refund_notify_url: "https://console.haiyudata.com/api/v1/wechat/refund_callback"
# 微信小程序配置
WechatMini:
app_id: "wxa581992dc74d860e"
# 微信H5配置
WechatH5:
app_id: "wxa581992dc74d860e"
# ===========================================
# 🔍 天眼查配置
# ===========================================
tianyancha:
base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
# ===========================================
# ☁️ 阿里云配置
# ===========================================
alicloud:
host: "https://kzidcardv1.market.alicloudapi.com"
app_code: "d55b58829efb41c8aa8e86769cba4844"
# ===========================================
# 🔍 智查金控配置
# ===========================================
zhicha:
url: "https://www.zhichajinkong.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
# 智查金控日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "zhicha"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌐 木子数据配置
# ===========================================
muzi:
url: "https://carv.m0101.com/magic/carv/pubin/service"
app_id: "713014138179585"
app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d"
timeout: 60s
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "muzi"
use_daily: true
enable_level_separation: true
level_configs:
info:
max_size: 50
max_backups: 3
max_age: 7
compress: true
error:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🎯 行为数据配置
# ===========================================
xingwei:
url: "https://sjztyh.chengdaoji.cn/dataCenterManageApi/manage/interface/doc/api/handle"
api_id: "jGtqla2FQv1zuXuH"
api_key: "iR1qS9725N4JA70gwlwohqT3ogl2zBf3"
# 行为数据日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "xingwei"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# ✨ 极光配置
# ===========================================
jiguang:
url: "http://api.jiguangcloud.com/jg-open-api-gateway/api"
app_id: "66ZA28w5" # 请替换为实际的 appId
app_secret: "e5261d0f6f003ae7b9fc1b0255b21761bb618d56" # 请替换为实际的 appSecret
sign_method: "hmac" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 极光日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "jiguang"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 📄 PDF生成服务配置
# ===========================================
pdfgen:
# 服务地址配置
development_url: "http://pdfg.haiyudata.com" # 开发环境服务地址
production_url: "http://101.43.41.217:15990" # 生产环境服务地址
# API路径配置
api_path: "/api/v1/generate/guangzhou" # PDF生成API路径
# 超时配置
timeout: 120s # 请求超时时间120秒
# 缓存配置
cache:
ttl: 24h # 缓存过期时间24小时
cache_dir: "" # 缓存目录(空则使用默认目录)
max_size: 0 # 最大缓存大小0表示不限制单位字节
# ===========================================
# ✨ 数脉配置走实时接口
# ===========================================
shumai:
url: "https://api.shumaidata.com"
app_id: "pIfqx8MsoTOjhbB762qi5BfkjJ4D7w0O"
app_secret: "BnJWo61hUgNEa5fqBCueiT1IZ1e0DxPU"
# ===========================================
# ✨ 数脉子账号配置走政务
# ===========================================
# 走政务接口使用这个
app_id2: "AwZZRzWkArtFDO2lDcT2jHfuoo9n35Tq"
app_secret2: "nCXN6fKLImjfvzI12hj8O1CMl1gJeaWh"
sign_method: "md5" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 数脉日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "shumai"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# ✨ 数据宝配置走实时接口
# ===========================================
shujubao:
url: "https://api.chinadatapay.com"
app_secret: "iOk0ALBX0BSdTSTf"
sign_method: "md5" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 数据宝日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "shujubao"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true

View File

@@ -0,0 +1,191 @@
# 🔧 开发环境配置
# 只包含与默认配置不同的配置项
# ===========================================
# 🌍 环境标识
# ===========================================
app:
env: development
# ===========================================
# 🗄️ 数据库配置
# ===========================================
database:
password: Qm8kZ3nR7pL4wT9y
name: "hyapi_dev"
# ===========================================
# 🔐 JWT配置
# ===========================================
jwt:
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
# 本地联调:企业报告链接与 headless PDF 需能访问到本机服务;端口与 server 监听一致。环境变量 API_PUBLIC_BASE_URL 可覆盖。
api:
public_base_url: "http://127.0.0.1:8080"
# ===========================================
# 📁 存储服务配置 - 七牛云
# ===========================================
storage:
access_key: "AO6u6sDWi6L9TsPfr4awC7FYP85JTjt3bodZACCM"
secret_key: "2fjxweGtSAEaUdVgDkWEmN7JbBxHBQDv1cLORb9_"
bucket: "tianyuanapi"
domain: "https://file.haiyudata.com"
# ===========================================
# 🔍 OCR服务配置 - 百度智能云
# ===========================================
ocr:
api_key: "aMsrBNGUJxgcgqdm3SEdcumm"
secret_key: "sWlv2h2AWA3aAt5bjXCkE6WeA5AzpAAD"
# ===========================================
# 📝 e签宝服务配置
# ===========================================
esign:
app_id: "7439073713"
app_secret: "c7d8cb0d701f7890601d221e9b6edfef"
server_url: "https://smlopenapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
contract:
name: "海宇数据API合作协议"
expire_days: 7
retry_count: 3
auth:
org_auth_modes: ["PSN_MOBILE3"]
default_auth_mode: "PSN_MOBILE3"
psn_auth_modes: ["PSN_MOBILE3", "PSN_IDCARD"]
willingness_auth_modes: ["CODE_SMS"]
redirect_url: "http://localhost:5173/certification/callback/auth"
sign:
auto_finish: true
sign_field_style: 1
client_type: "ALL"
redirect_url: "http://localhost:5173/certification/callback/sign"
# ===========================================
# 🌍 西部数据配置
# ===========================================
westdex:
url: "http://proxy.haiyudata.com/api/invoke"
key: "121a1e41fc1690dd6b90afbcacd80cf4"
secret_id: "449159"
secret_second_id: "296804"
yushan:
url: https://api2.yushanshuju.com/credit-gw/service
# ===========================================
# 💰 支付宝支付配置
# ===========================================
alipay:
app_id: "2021004181633376"
private_key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCC2GNEWrQUg6FVHBdlDcgL1SA1KmRI8IHgSJGvXEsgfo3g62aa5usFXHVz5bMzpIcDu0N+jGtZQIBuuq7TxGwhDvWBygEDXN17p00uwqik/3TsyFvJ4FfbkaS7pRIGfeO/cBTzjqznanPUdHJZ9L39QmTqTefIQQvGOCvgntKxPa/LdS24+ZLA2RNh3TsRzbSxOOJPmUrwvCX8U13F9jH250hvf+Tewz4hyG8CkiMM4d1UpGMndQNr8oTY0vwFbWAG0ZDGgkxjg0iRJ02fgxwShQS1TgY5NxPhpKBiN5C/WG15qCqEw0F3GlpfWZwzUhv1uMiy+xbZ2bGLo1YCtwUtAgMBAAECggEAQ8uk25T3u61cWYH9qTGT1nWug32ciqJ7WN+hBLCYiJSqJMEz380INzXp8Ywx5u83ubo8xYQyVwNxyG3YCge7UwGyOXaWQczLQbe06SaZRSzLw6gozxf7zdvP9B4akdyGtfl4EZ56fkmNDKbtXSjPjDrrmO+Wyg7R7/nI2lDQsF6dXTKD0YiHtTKz40amKgbIYX+qc3yVS0slkVjcfnRczr+PKM5RMsV3Jk2pr6IYeq3E24LnbuVtV76priTqJN3hVSy2Y6JqmAYkI0HCoCuaFGE8ud3J859jjMcUXTRFJyDsKKooa+FZCoEx2ToVMqnb4vjfr1gZifUrw4ZNd5cPoQKBgQC4v/fNTXuA21pb+l4fnqK0o3wFhiNJh920yIlF4Vd0Nsi2/TwqFK6cVhrUFAmKr88hTzY1vkOhd/HLlkWjNDR5OGx1K1BKUAZjWIfProv8lDSckADEI29lro9WzFGy0o4szlEJ2uuUfO/j9Qn2lmx5oFPsz0TI+HoSNFE0q/SlxQKBgQC1ToMLuh0OkucZm1SL6xcjudBX7U0ElZ/TIxRzfxQ/sN911/BRlxrSdCcDMXNuuFpV2ACjDNWWLJM1sRVsOWNA/oXzZf6VTvUDIAv8XrNUt/B87genBVuMTZ2RYmMWCrgW0PE1OrpKGuQCKVsn242B2Xpmee9OnHhBF2uTASDASQKBgBALvD38iMl8Q7DRYfNlF8SQnmjsaYwtXLgi4qlLFQlm6K/b9qnA+hlh8RqSUvHUqyy9cHvidoVDoaCJAKtYEWal2+WhSWvq32MpgUIsasQZKyid6TMf0MEIFDL5s+7QEsEZejhc5zESWNN3qNHd5rX5ktBygArkadXC7XqhpLHxAoGBAJ0dJEKNTZDLjKiMCoAVgT/cTcdkRFGst4tn4tkTTqDCzWJ5di++Geg173i86aMQ7ndlb2fcP1qb1hW5Fy9pq7Eu3zVFNZB9k6TZqIlSJ2VK4IPiYY9C/UpgGCNcdzEqqMxc1Cmkcrq1AtE8tVmc0Mutgnw7Pj2JKkx91yLU32TBAoGAKxssUdTLuf5Z5oFgzpoSES9qwc1h6jlMfsouDzHcZf0aYintD6Vby7SVul5540qYkDkNs0YZ3uZu74LHfoBaWJjYIIVAMSMX+3AtBpQUyYluex64V/g60t+0sFuDWqMvSPU7mZcv6+KIP6vW56GeYdhHf4JqttdIHm9SgkoJjjY="
alipay_public_key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2CqoCp95w/JV3RT/gzF4/8QmVT1HQNaeW7yUp+mA7x9AbjvlTW/+eRn6oGAL/XhZLjvHD0XjKLVKX0MJVS1aUQHEHEbOJN4Eu8II45OavD4iZISa7Kp9V6AM+i4qTyaeV2wNDnGxHQBaLVUGCfMR+56EK2YpORdE1H9uy72SSQseVb3bmpsV9EW/IJNmcVL/ut3uA1JWAoRmzlQ7ekxg7p8AYXzYPEHQr1tl7W+M4zv9wO9GKZCxIqMA8U3RP5npPfRaCfIRGzXzCqFEEUvWuidOB7frsvN4jiPD07qpL2Bi9LM1X/ee2kC/oM8Uhd7ERZhG8MbZfijZKxgrsDKBcwIDAQAB"
is_production: true
notify_url: "https://6m4685017o.goho.co/api/v1/finance/alipay/callback"
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:
default_credit_limit: 0.01
min_amount: "0.01" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额
recharge_bonus_enabled: false # 开发环境可设为 true 测试赠送
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
# 支付宝充值赠送配置
alipay_recharge_bonus:
- recharge_amount: 0.01 # 充值1000元
bonus_amount: 50.00 # 赠送50元
- recharge_amount: 0.05 # 充值5000元
bonus_amount: 300.00 # 赠送300元
- recharge_amount: 0.1 # 充值10000元
bonus_amount: 800.00 # 赠送800元
# ===========================================
# 🔍 天眼查配置
# ===========================================
tianyancha:
base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
# 智查金控配置示例
zhicha:
url: "http://proxy.haiyudata.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
development:
enable_cors: true
cors_allowed_origins: "http://localhost:5173,http://localhost:8080"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# ===========================================
# 🚦 开发环境全局限流(放宽或近似关闭)
# ===========================================
ratelimit:
requests: 1000000 # 每窗口允许的请求数,足够大,相当于关闭
window: 1s # 时间窗口
burst: 1000000 # 令牌桶突发容量
# ===========================================
# 🚀 开发环境频率限制配置(放宽限制)
# ===========================================
daily_ratelimit:
max_requests_per_day: 1000000 # 开发环境每日最大请求次数
max_requests_per_ip: 10000000 # 开发环境每个IP每日最大请求次数
max_concurrent: 50 # 开发环境最大并发请求数
# 排除频率限制的路径
exclude_paths:
- "/health" # 健康检查接口
- "/metrics" # 监控指标接口
# 排除频率限制的域名
exclude_domains:
- "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名
# 开发环境安全配置(放宽限制)
enable_ip_whitelist: true # 启用IP白名单
ip_whitelist: # 开发环境IP白名单
- "127.0.0.1" # 本地回环
- "localhost" # 本地主机
- "192.168.*" # 内网IP段
- "10.*" # 内网IP段
- "172.16.*" # 内网IP段
enable_ip_blacklist: false # 开发环境禁用IP黑名单
enable_user_agent: false # 开发环境禁用User-Agent检查
enable_referer: false # 开发环境禁用Referer检查
enable_proxy_check: false # 开发环境禁用代理检查
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

172
configs/env.production.yaml Normal file
View File

@@ -0,0 +1,172 @@
# 🏭 生产环境配置
# 只包含与默认配置不同的配置项
# ===========================================
# 🌍 环境标识
# ===========================================
app:
env: production
# ===========================================
# 🌐 服务器配置
# ===========================================
server:
mode: release
# ===========================================
# 🔒 CORS配置 - 生产环境
# ===========================================
development:
enable_cors: true
cors_allowed_origins: "http://localhost:5173,https://consoletest.haiyudata.com,https://console.haiyudata.com"
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With,Access-Id"
# ===========================================
# 🗄️ 数据库配置
# ===========================================
# 敏感信息通过外部环境变量注入
database:
host: "hyapi-postgres-prod"
port: "5432"
user: "hyapi_user"
password: "Qm8kZ3nR7pL4wT9y"
name: "hyapi"
sslmode: "disable"
timezone: "Asia/Shanghai"
max_open_conns: 25
max_idle_conns: 10
conn_max_lifetime: 300s
auto_migrate: true
redis:
host: "hyapi-redis-prod"
port: "6379"
password: ""
db: 0
# ===========================================
# 🔐 JWT配置
# ===========================================
jwt:
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
api:
domain: "api.haiyudata.com"
# 可选:对外可访问的 API 完整基址(无尾斜杠),用于企业报告 reportUrl、PDF 预生成等;不设则按 https://{domain} 推导。环境变量 API_PUBLIC_BASE_URL 优先于本项。
# public_base_url: "https://api.haiyudata.com"
# ===========================================
# 📁 存储服务配置 - 七牛云
# ===========================================
storage:
access_key: "AO6u6sDWi6L9TsPfr4awC7FYP85JTjt3bodZACCM"
secret_key: "2fjxweGtSAEaUdVgDkWEmN7JbBxHBQDv1cLORb9_"
bucket: "tianyuanapi"
domain: "https://file.haiyudata.com"
# ===========================================
# 🔍 OCR服务配置 - 百度智能云
# ===========================================
ocr:
api_key: "aMsrBNGUJxgcgqdm3SEdcumm"
secret_key: "sWlv2h2AWA3aAt5bjXCkE6WeA5AzpAAD"
# ===========================================
# 📝 e签宝服务配置
# ===========================================
esign:
app_id: "5112008003"
app_secret: "d487672273e7aa70c800804a1d9499b9"
server_url: "https://openapi.esign.cn"
template_id: "9f7a3f63cc5a48b085b127ba027d234d"
contract:
name: "海宇数据API合作协议"
expire_days: 7
retry_count: 3
auth:
org_auth_modes: ["PSN_MOBILE3"]
default_auth_mode: "PSN_MOBILE3"
psn_auth_modes: ["PSN_MOBILE3", "PSN_IDCARD"]
willingness_auth_modes: ["CODE_SMS"]
redirect_url: "https://console.haiyudata.com/certification/callback/auth"
sign:
auto_finish: true
sign_field_style: 1
client_type: "ALL"
redirect_url: "https://console.haiyudata.com/certification/callback/sign"
# ===========================================
# 💰 支付宝支付配置
# ===========================================
alipay:
app_id: "2021004181633376"
private_key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCC2GNEWrQUg6FVHBdlDcgL1SA1KmRI8IHgSJGvXEsgfo3g62aa5usFXHVz5bMzpIcDu0N+jGtZQIBuuq7TxGwhDvWBygEDXN17p00uwqik/3TsyFvJ4FfbkaS7pRIGfeO/cBTzjqznanPUdHJZ9L39QmTqTefIQQvGOCvgntKxPa/LdS24+ZLA2RNh3TsRzbSxOOJPmUrwvCX8U13F9jH250hvf+Tewz4hyG8CkiMM4d1UpGMndQNr8oTY0vwFbWAG0ZDGgkxjg0iRJ02fgxwShQS1TgY5NxPhpKBiN5C/WG15qCqEw0F3GlpfWZwzUhv1uMiy+xbZ2bGLo1YCtwUtAgMBAAECggEAQ8uk25T3u61cWYH9qTGT1nWug32ciqJ7WN+hBLCYiJSqJMEz380INzXp8Ywx5u83ubo8xYQyVwNxyG3YCge7UwGyOXaWQczLQbe06SaZRSzLw6gozxf7zdvP9B4akdyGtfl4EZ56fkmNDKbtXSjPjDrrmO+Wyg7R7/nI2lDQsF6dXTKD0YiHtTKz40amKgbIYX+qc3yVS0slkVjcfnRczr+PKM5RMsV3Jk2pr6IYeq3E24LnbuVtV76priTqJN3hVSy2Y6JqmAYkI0HCoCuaFGE8ud3J859jjMcUXTRFJyDsKKooa+FZCoEx2ToVMqnb4vjfr1gZifUrw4ZNd5cPoQKBgQC4v/fNTXuA21pb+l4fnqK0o3wFhiNJh920yIlF4Vd0Nsi2/TwqFK6cVhrUFAmKr88hTzY1vkOhd/HLlkWjNDR5OGx1K1BKUAZjWIfProv8lDSckADEI29lro9WzFGy0o4szlEJ2uuUfO/j9Qn2lmx5oFPsz0TI+HoSNFE0q/SlxQKBgQC1ToMLuh0OkucZm1SL6xcjudBX7U0ElZ/TIxRzfxQ/sN911/BRlxrSdCcDMXNuuFpV2ACjDNWWLJM1sRVsOWNA/oXzZf6VTvUDIAv8XrNUt/B87genBVuMTZ2RYmMWCrgW0PE1OrpKGuQCKVsn242B2Xpmee9OnHhBF2uTASDASQKBgBALvD38iMl8Q7DRYfNlF8SQnmjsaYwtXLgi4qlLFQlm6K/b9qnA+hlh8RqSUvHUqyy9cHvidoVDoaCJAKtYEWal2+WhSWvq32MpgUIsasQZKyid6TMf0MEIFDL5s+7QEsEZejhc5zESWNN3qNHd5rX5ktBygArkadXC7XqhpLHxAoGBAJ0dJEKNTZDLjKiMCoAVgT/cTcdkRFGst4tn4tkTTqDCzWJ5di++Geg173i86aMQ7ndlb2fcP1qb1hW5Fy9pq7Eu3zVFNZB9k6TZqIlSJ2VK4IPiYY9C/UpgGCNcdzEqqMxc1Cmkcrq1AtE8tVmc0Mutgnw7Pj2JKkx91yLU32TBAoGAKxssUdTLuf5Z5oFgzpoSES9qwc1h6jlMfsouDzHcZf0aYintD6Vby7SVul5540qYkDkNs0YZ3uZu74LHfoBaWJjYIIVAMSMX+3AtBpQUyYluex64V/g60t+0sFuDWqMvSPU7mZcv6+KIP6vW56GeYdhHf4JqttdIHm9SgkoJjjY="
alipay_public_key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2CqoCp95w/JV3RT/gzF4/8QmVT1HQNaeW7yUp+mA7x9AbjvlTW/+eRn6oGAL/XhZLjvHD0XjKLVKX0MJVS1aUQHEHEbOJN4Eu8II45OavD4iZISa7Kp9V6AM+i4qTyaeV2wNDnGxHQBaLVUGCfMR+56EK2YpORdE1H9uy72SSQseVb3bmpsV9EW/IJNmcVL/ut3uA1JWAoRmzlQ7ekxg7p8AYXzYPEHQr1tl7W+M4zv9wO9GKZCxIqMA8U3RP5npPfRaCfIRGzXzCqFEEUvWuidOB7frsvN4jiPD07qpL2Bi9LM1X/ee2kC/oM8Uhd7ERZhG8MbZfijZKxgrsDKBcwIDAQAB"
is_production: true
notify_url: "https://console.haiyudata.com/api/v1/finance/alipay/callback"
return_url: "https://console.haiyudata.com/api/v1/finance/alipay/return"
# ===========================================
# 💰 钱包配置
# ===========================================
wallet:
default_credit_limit: 50.00
min_amount: "100.00" # 生产环境最低充值金额
max_amount: "100000.00" # 单次最高充值金额
recharge_bonus_enabled: false # 暂不赠送,展示商务洽谈提示
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
# 支付宝充值赠送配置recharge_bonus_enabled 为 true 时生效)
alipay_recharge_bonus:
- recharge_amount: 1000.00 # 充值1000元
bonus_amount: 50.00 # 赠送50元
- recharge_amount: 5000.00 # 充值5000元
bonus_amount: 300.00 # 赠送300元
- recharge_amount: 10000.00 # 充值10000元
bonus_amount: 800.00 # 赠送800元
# ===========================================
# 🚦 频率限制配置 - 生产环境
# ===========================================
daily_ratelimit:
max_requests_per_day: 50000 # 生产环境每日最大请求次数
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
max_concurrent: 200 # 生产环境最大并发请求数
# 排除频率限制的路径
exclude_paths:
- "/health" # 健康检查接口
- "/metrics" # 监控指标接口
# 排除频率限制的域名
exclude_domains:
- "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名
# 生产环境安全配置(严格限制)
enable_ip_whitelist: false # 生产环境不启用IP白名单
enable_ip_blacklist: true # 启用IP黑名单
ip_blacklist: # 生产环境IP黑名单
- "192.168.1.100" # 示例被禁止的IP
- "10.0.0.50" # 示例被禁止的IP
enable_user_agent: true # 启用User-Agent检查
blocked_user_agents: # 被阻止的User-Agent
- "curl" # 阻止curl请求
- "wget" # 阻止wget请求
- "python-requests" # 阻止Python requests
- "LangShen" # 阻止LangShen请求
enable_referer: true # 启用Referer检查
allowed_referers: # 允许的Referer
- "https://console.haiyudata.com"
- "https://consoletest.haiyudata.com"
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
enable_proxy_check: true # 启用代理检查
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

48
configs/env.testing.yaml Normal file
View File

@@ -0,0 +1,48 @@
# 🧪 测试环境配置
# 只包含与默认配置不同的配置项
# ===========================================
# 🌍 环境标识
# ===========================================
app:
env: testing
# ===========================================
# 🌐 服务器配置
# ===========================================
server:
mode: test
# ===========================================
# 🗄️ 数据库配置
# ===========================================
database:
password: Hk4nR9mP3qW7vL2x
name: hyapi_test
# ===========================================
# 📦 Redis配置
# ===========================================
redis:
db: 15
# ===========================================
# 📝 日志配置
# ===========================================
logger:
level: debug
# ===========================================
# 🔐 JWT配置
# ===========================================
jwt:
secret: test-jwt-secret-key-for-testing-only
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

View File

@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: "default"
orgId: 1
folder: ""
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards

View File

@@ -0,0 +1,198 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": "Jaeger",
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "7.5.0",
"targets": [
{
"query": "hyapi-server",
"refId": "A"
}
],
"title": "HYAPI服务链路追踪",
"type": "jaeger"
},
{
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {
"align": null,
"filterable": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"textMode": "auto"
},
"pluginVersion": "7.5.0",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"interval": "",
"legendFormat": "{{method}} {{path}}",
"refId": "A"
}
],
"title": "HTTP请求速率",
"type": "stat"
},
{
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 0.5
},
{
"color": "red",
"value": 1
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"displayMode": "list",
"orientation": "horizontal",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"showUnfilled": true
},
"pluginVersion": "7.5.0",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"interval": "",
"legendFormat": "95th percentile - {{method}} {{path}}",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"interval": "",
"legendFormat": "50th percentile - {{method}} {{path}}",
"refId": "B"
}
],
"title": "HTTP请求延迟分布",
"type": "bargauge"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": ["jaeger", "tracing", "hyapi"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "HYAPI链路追踪监控",
"uid": "hyapi-tracing",
"version": 1
}

View File

@@ -0,0 +1,36 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
- name: Jaeger
type: jaeger
access: proxy
url: http://jaeger:16686
isDefault: false
editable: true
jsonData:
httpMethod: GET
# 启用节点图功能
nodeGraph:
enabled: true
# 启用追踪链路图
traceQuery:
timeShiftEnabled: true
spanStartTimeShift: "1h"
spanEndTimeShift: "1h"
# 配置标签
tracesToLogs:
datasourceUid: "loki"
tags: ["job", "instance", "pod", "namespace"]
mappedTags: [{ key: "service.name", value: "service" }]
mapTagNamesEnabled: false
spanStartTimeShift: "1h"
spanEndTimeShift: "1h"
filterByTraceID: false
filterBySpanID: false

View File

@@ -0,0 +1,109 @@
{
"service_strategies": [
{
"service": "hyapi-server",
"type": "probabilistic",
"param": 0.01,
"max_traces_per_second": 50,
"operation_strategies": [
{
"operation": "GET /health",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "GET /metrics",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "GET /api/v1/health",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "POST /api/v1/users/register",
"type": "probabilistic",
"param": 0.2
},
{
"operation": "POST /api/v1/users/login",
"type": "probabilistic",
"param": 0.1
},
{
"operation": "POST /api/v1/users/logout",
"type": "probabilistic",
"param": 0.05
},
{
"operation": "POST /api/v1/users/refresh",
"type": "probabilistic",
"param": 0.05
},
{
"operation": "GET /api/v1/users/profile",
"type": "probabilistic",
"param": 0.02
},
{
"operation": "PUT /api/v1/users/profile",
"type": "probabilistic",
"param": 0.1
},
{
"operation": "POST /api/v1/sms/send",
"type": "probabilistic",
"param": 0.3
},
{
"operation": "POST /api/v1/sms/verify",
"type": "probabilistic",
"param": 0.3
},
{
"operation": "error",
"type": "probabilistic",
"param": 1.0
}
]
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.01,
"max_traces_per_second": 50
},
"per_operation_strategies": [
{
"operation": "health_check",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "metrics",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "database_query",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "redis_operation",
"type": "probabilistic",
"param": 0.005
},
{
"operation": "external_api_call",
"type": "probabilistic",
"param": 0.2
},
{
"operation": "error",
"type": "probabilistic",
"param": 1.0
}
]
}

View File

@@ -0,0 +1,109 @@
{
"service_strategies": [
{
"service": "hyapi-server",
"type": "probabilistic",
"param": 0.1,
"max_traces_per_second": 200,
"operation_strategies": [
{
"operation": "GET /health",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "GET /metrics",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "GET /api/v1/health",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "POST /api/v1/users/register",
"type": "probabilistic",
"param": 0.8
},
{
"operation": "POST /api/v1/users/login",
"type": "probabilistic",
"param": 0.8
},
{
"operation": "POST /api/v1/users/logout",
"type": "probabilistic",
"param": 0.3
},
{
"operation": "POST /api/v1/users/refresh",
"type": "probabilistic",
"param": 0.3
},
{
"operation": "GET /api/v1/users/profile",
"type": "probabilistic",
"param": 0.2
},
{
"operation": "PUT /api/v1/users/profile",
"type": "probabilistic",
"param": 0.6
},
{
"operation": "POST /api/v1/sms/send",
"type": "probabilistic",
"param": 0.9
},
{
"operation": "POST /api/v1/sms/verify",
"type": "probabilistic",
"param": 0.9
},
{
"operation": "error",
"type": "probabilistic",
"param": 1.0
}
]
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.1,
"max_traces_per_second": 200
},
"per_operation_strategies": [
{
"operation": "health_check",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "metrics",
"type": "probabilistic",
"param": 0.01
},
{
"operation": "database_query",
"type": "probabilistic",
"param": 0.1
},
{
"operation": "redis_operation",
"type": "probabilistic",
"param": 0.05
},
{
"operation": "external_api_call",
"type": "probabilistic",
"param": 0.8
},
{
"operation": "error",
"type": "probabilistic",
"param": 1.0
}
]
}

View File

@@ -0,0 +1,46 @@
{
"monitor": {
"menuEnabled": true
},
"dependencies": {
"menuEnabled": true
},
"archiveEnabled": true,
"tracking": {
"gaID": null,
"trackErrors": false
},
"menu": [
{
"label": "HYAPI 文档",
"url": "http://localhost:3000/docs",
"anchorTarget": "_blank"
},
{
"label": "Grafana 监控",
"url": "http://localhost:3000",
"anchorTarget": "_blank"
},
{
"label": "Prometheus 指标",
"url": "http://localhost:9090",
"anchorTarget": "_blank"
}
],
"search": {
"maxLookback": {
"label": "2 days",
"value": "2d"
},
"maxLimit": 1500
},
"scripts": [],
"linkPatterns": [
{
"type": "process",
"key": "jaeger.version",
"url": "https://github.com/jaegertracing/jaeger/releases/tag/#{jaeger.version}",
"text": "#{jaeger.version} release notes"
}
]
}

View File

@@ -0,0 +1,234 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# 基本设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# 客户端设置
client_max_body_size 10M;
client_body_timeout 60s;
client_header_timeout 60s;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml;
# 上游服务器配置
upstream hyapi_backend {
server hyapi-app:8080;
keepalive 32;
}
upstream grafana_backend {
server grafana:3000;
keepalive 16;
}
upstream prometheus_backend {
server prometheus:9090;
keepalive 16;
}
upstream minio_backend {
server minio:9000;
keepalive 16;
}
upstream minio_console_backend {
server minio:9001;
keepalive 16;
}
upstream jaeger_backend {
server jaeger:16686;
keepalive 16;
}
upstream pgadmin_backend {
server pgadmin:80;
keepalive 16;
}
# HTTP 服务器配置
server {
listen 80;
server_name _;
# 健康检查端点
location /health {
proxy_pass http://hyapi_backend/health;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API 路由
location /api/ {
proxy_pass http://hyapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Swagger 文档
location /swagger/ {
proxy_pass http://hyapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 根路径重定向到API文档
location = / {
return 301 /swagger/index.html;
}
# Grafana 仪表盘
location /grafana/ {
proxy_pass http://grafana_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Prometheus 监控
location /prometheus/ {
proxy_pass http://prometheus_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Jaeger 链路追踪
location /jaeger/ {
proxy_pass http://jaeger_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# MinIO 对象存储 API
location /minio/ {
proxy_pass http://minio_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# MinIO 需要的特殊头
proxy_set_header X-Forwarded-Host $host;
client_max_body_size 1000M;
}
# MinIO 控制台
location /minio-console/ {
proxy_pass http://minio_console_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# pgAdmin 数据库管理
location /pgadmin/ {
proxy_pass http://pgadmin_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Script-Name /pgadmin;
}
# 限制某些路径的访问
location ~* \.(git|env|log)$ {
deny all;
return 404;
}
}
# HTTPS 服务器配置 (可选需要SSL证书)
# server {
# listen 443 ssl http2;
# server_name your-domain.com;
# ssl_certificate /etc/nginx/ssl/server.crt;
# ssl_certificate_key /etc/nginx/ssl/server.key;
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
# ssl_prefer_server_ciphers off;
# # HSTS
# add_header Strict-Transport-Security "max-age=63072000" always;
# location / {
# proxy_pass http://hyapi_backend;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
}

View File

@@ -0,0 +1 @@
postgres:5432:hyapi_dev:postgres:Qm8kZ3nR7pL4wT9y

View File

@@ -0,0 +1,15 @@
{
"Servers": {
"1": {
"Name": "HYAPI PostgreSQL",
"Group": "Development Servers",
"Host": "postgres",
"Port": 5432,
"MaintenanceDB": "hyapi_dev",
"Username": "postgres",
"PassFile": "/var/lib/pgadmin/passfile",
"SSLMode": "prefer",
"Comment": "HYAPI Development Database"
}
}
}

View File

@@ -0,0 +1,28 @@
# PostgreSQL配置文件
# 时区设置
timezone = 'Asia/Shanghai'
log_timezone = 'Asia/Shanghai'
# 字符编码
client_encoding = 'UTF8'
# 连接设置
max_connections = 100
shared_buffers = 128MB
# 日志设置
log_destination = 'stderr'
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = 1d
log_rotation_size = 100MB
# 性能设置
effective_cache_size = 1GB
work_mem = 4MB
maintenance_work_mem = 64MB
# 查询优化
random_page_cost = 1.1
effective_io_concurrency = 200

View File

@@ -0,0 +1,39 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
# Prometheus 自身监控
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
# HYAPI 应用监控
- job_name: "hyapi-server"
static_configs:
- targets: ["host.docker.internal:8080"]
metrics_path: "/metrics"
scrape_interval: 10s
# PostgreSQL 监控 (如果启用了 postgres_exporter)
- job_name: "postgres"
static_configs:
- targets: ["postgres:5432"]
scrape_interval: 30s
# Redis 监控 (如果启用了 redis_exporter)
- job_name: "redis"
static_configs:
- targets: ["redis:6379"]
scrape_interval: 30s
# Docker 容器监控 (如果启用了 cadvisor)
- job_name: "docker"
static_configs:
- targets: ["host.docker.internal:8080"]
metrics_path: "/docker/metrics"
scrape_interval: 30s

View File

@@ -0,0 +1,104 @@
# Redis Configuration for HYAPI Server Development
# Network
bind 0.0.0.0
port 6379
timeout 0
tcp-keepalive 300
# General
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
# Snapshotting
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
# Replication
# slaveof <masterip> <masterport>
# masterauth <master-password>
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-ping-slave-period 10
repl-timeout 60
repl-disable-tcp-nodelay no
repl-backlog-size 1mb
repl-backlog-ttl 3600
slave-priority 100
# Security
# requirepass foobared
# rename-command FLUSHDB ""
# rename-command FLUSHALL ""
# Limits
maxclients 10000
maxmemory 256mb
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Append only file
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
# Lua scripting
lua-time-limit 5000
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Latency monitor
latency-monitor-threshold 100
# Event notification
notify-keyspace-events Ex
# Hash
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# List
list-max-ziplist-size -2
list-compress-depth 0
# Set
set-max-intset-entries 512
# Sorted set
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
# HyperLogLog
hll-sparse-max-bytes 3000
# Active rehashing
activerehashing yes
# Client output buffer limits
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# Hz
hz 10
# AOF rewrite
aof-rewrite-incremental-fsync yes

71
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,71 @@
services:
# PostgreSQL 数据库
postgres:
image: postgres:16.9
container_name: hyapi-postgres
environment:
POSTGRES_DB: hyapi_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: Qm8kZ3nR7pL4wT9y
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
TZ: Asia/Shanghai
PGTZ: Asia/Shanghai
ports:
- "15432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
- ./scripts/set_timezone.sql:/docker-entrypoint-initdb.d/set_timezone.sql
- ./deployments/docker/postgresql.conf:/etc/postgresql/postgresql.conf
networks:
- hyapi-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:8.0.2
container_name: hyapi-redis
ports:
- "16379:6379"
volumes:
- redis_data:/data
- ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
environment:
TZ: Asia/Shanghai
networks:
- hyapi-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# Asynq 任务监控
asynq-monitor:
image: hibiken/asynqmon:latest
container_name: hyapi-asynq-monitor
environment:
TZ: Asia/Shanghai
ports:
- "18081:8080"
command: --redis-addr=hyapi-redis:6379
networks:
- hyapi-network
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
hyapi-network:
driver: bridge

193
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,193 @@
version: "3.8"
services:
# PostgreSQL 数据库 (生产环境)
postgres:
image: postgres:16.9
container_name: hyapi-postgres-prod
environment:
TZ: Asia/Shanghai
PGTZ: Asia/Shanghai
POSTGRES_DB: hyapi
POSTGRES_USER: hyapi_user
POSTGRES_PASSWORD: Qm8kZ3nR7pL4wT9y
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
# 性能优化配置
POSTGRES_SHARED_PRELOAD_LIBRARIES: pg_stat_statements
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- hyapi-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hyapi_user -d hyapi -h localhost"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
restart: unless-stopped
deploy:
resources:
limits:
memory: 2G
cpus: "1.0"
reservations:
memory: 512M
cpus: "0.5"
# 生产环境暴露数据库端口到主机
ports:
- "25010:5432"
# Redis 缓存 (生产环境)
redis:
image: redis:8.0.2
container_name: hyapi-redis-prod
environment:
TZ: Asia/Shanghai
REDIS_PASSWORD: ""
volumes:
- redis_data:/data
- ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
networks:
- hyapi-network
healthcheck:
test: redis-cli ping
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
cpus: "0.5"
reservations:
memory: 256M
cpus: "0.2"
# 生产环境不暴露端口到主机
# ports:
# - "6379:6379"
# HYAPI 应用程序
hyapi-app:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: 1.0.0
COMMIT: dev
BUILD_TIME: ""
container_name: hyapi-app-prod
environment:
# 时区配置
TZ: Asia/Shanghai
# 环境设置
ENV: production
ports:
- "25000:8080"
volumes:
- ./logs:/app/logs
# 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
- ./resources:/app/resources
# 持久化PDF缓存目录确保生成的PDF在容器重启后仍然存在
- ./storage/pdfg-cache:/app/storage/pdfg-cache
# user: "1001:1001" # 注释掉使用root权限运行
networks:
- hyapi-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
restart: unless-stopped
# HYAPI Worker 服务
hyapi-worker:
build:
context: .
dockerfile: Dockerfile.worker
args:
VERSION: 1.0.0
COMMIT: dev
BUILD_TIME: ""
container_name: hyapi-worker-prod
environment:
# 时区配置
TZ: Asia/Shanghai
# 环境设置
ENV: production
volumes:
- ./logs:/root/logs
# user: "1001:1001" # 注释掉使用root权限运行
networks:
- hyapi-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "ps", "aux", "|", "grep", "worker"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
restart: unless-stopped
# Asynq 任务监控 (生产环境)
asynq-monitor:
image: hibiken/asynqmon:latest
container_name: hyapi-asynq-monitor-prod
environment:
TZ: Asia/Shanghai
ports:
- "25080:8080"
command: --redis-addr=hyapi-redis-prod:6379
networks:
- hyapi-network
depends_on:
redis:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: "0.3"
reservations:
memory: 64M
cpus: "0.1"
volumes:
postgres_data:
driver: local
redis_data:
driver: local
pure_component:
driver: local
networks:
hyapi-network:
driver: bridge

View File

@@ -0,0 +1,603 @@
# 统计功能API文档
## 概述
统计功能API提供了完整的统计数据分析和管理功能包括指标管理、实时统计、历史统计、仪表板管理、报告生成、数据导出等功能。
## 基础信息
- **基础URL**: `/api/v1/statistics`
- **认证方式**: Bearer Token
- **内容类型**: `application/json`
- **字符编码**: `UTF-8`
## 认证和权限
### 认证方式
所有API请求都需要在请求头中包含有效的JWT令牌
```
Authorization: Bearer <your-jwt-token>
```
### 权限级别
- **公开访问**: 无需认证的接口
- **用户权限**: 需要用户或管理员权限
- **管理员权限**: 仅管理员可访问
## API接口
### 1. 指标管理
#### 1.1 创建统计指标
- **URL**: `POST /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 创建新的统计指标
**请求体**:
```json
{
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "realtime",
"value": 100.0,
"metadata": "{\"source\": \"api_gateway\"}",
"date": "2024-01-01T00:00:00Z"
}
```
**响应**:
```json
{
"success": true,
"message": "指标创建成功",
"data": {
"id": "uuid",
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "realtime",
"value": 100.0,
"metadata": "{\"source\": \"api_gateway\"}",
"date": "2024-01-01T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
}
```
#### 1.2 更新统计指标
- **URL**: `PUT /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 更新现有统计指标的值
**请求体**:
```json
{
"id": "uuid",
"value": 150.0
}
```
#### 1.3 删除统计指标
- **URL**: `DELETE /api/v1/statistics/metrics`
- **权限**: 管理员
- **描述**: 删除指定的统计指标
**请求体**:
```json
{
"id": "uuid"
}
```
#### 1.4 获取单个指标
- **URL**: `GET /api/v1/statistics/metrics/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计指标
#### 1.5 获取指标列表
- **URL**: `GET /api/v1/statistics/metrics`
- **权限**: 用户
- **描述**: 根据条件获取统计指标列表
**查询参数**:
- `metric_type` (string): 指标类型
- `metric_name` (string): 指标名称
- `dimension` (string): 统计维度
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `limit` (int): 限制数量 (默认20, 最大1000)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
### 2. 实时统计
#### 2.1 获取实时指标
- **URL**: `GET /api/v1/statistics/realtime`
- **权限**: 公开
- **描述**: 获取指定类型的实时统计指标
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `time_range` (string): 时间范围 (last_hour, last_day, last_week)
- `dimension` (string): 统计维度
**响应**:
```json
{
"success": true,
"message": "获取实时指标成功",
"data": {
"metric_type": "api_calls",
"metrics": {
"total_count": 1000,
"success_count": 950,
"failed_count": 50,
"success_rate": 95.0
},
"timestamp": "2024-01-01T12:00:00Z",
"metadata": {
"time_range": "last_hour",
"dimension": "realtime"
}
}
}
```
### 3. 历史统计
#### 3.1 获取历史指标
- **URL**: `GET /api/v1/statistics/historical`
- **权限**: 公开
- **描述**: 获取指定时间范围的历史统计指标
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string): 指标名称
- `dimension` (string): 统计维度
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
- `period` (string): 统计周期
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `aggregate_by` (string): 聚合维度
- `group_by` (string): 分组维度
**响应**:
```json
{
"success": true,
"message": "获取历史指标成功",
"data": {
"metric_type": "api_calls",
"metric_name": "total_count",
"dimension": "daily",
"data_points": [
{
"date": "2024-01-01T00:00:00Z",
"value": 1000,
"label": "total_count"
}
],
"summary": {
"total": 30000,
"average": 1000,
"max": 1500,
"min": 500,
"count": 30,
"growth_rate": 5.2,
"trend": "increasing"
},
"metadata": {
"period": "daily",
"aggregate_by": "day",
"group_by": "metric_name"
}
}
}
```
### 4. 仪表板管理
#### 4.1 创建仪表板
- **URL**: `POST /api/v1/statistics/dashboards`
- **权限**: 管理员
- **描述**: 创建新的统计仪表板
**请求体**:
```json
{
"name": "用户仪表板",
"description": "普通用户专用仪表板",
"user_role": "user",
"layout": "{\"columns\": 2, \"rows\": 3}",
"widgets": "[{\"type\": \"api_calls\", \"position\": {\"x\": 0, \"y\": 0}}]",
"settings": "{\"theme\": \"light\", \"auto_refresh\": false}",
"refresh_interval": 600,
"access_level": "private",
"created_by": "user_id"
}
```
#### 4.2 获取仪表板列表
- **URL**: `GET /api/v1/statistics/dashboards`
- **权限**: 用户
- **描述**: 根据条件获取统计仪表板列表
**查询参数**:
- `user_role` (string): 用户角色
- `is_default` (bool): 是否默认
- `is_active` (bool): 是否激活
- `access_level` (string): 访问级别
- `created_by` (string): 创建者ID
- `name` (string): 仪表板名称
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
#### 4.3 获取单个仪表板
- **URL**: `GET /api/v1/statistics/dashboards/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计仪表板
#### 4.4 获取仪表板数据
- **URL**: `GET /api/v1/statistics/dashboards/data`
- **权限**: 公开
- **描述**: 获取指定角色的仪表板数据
**查询参数**:
- `user_role` (string, 必需): 用户角色
- `period` (string): 统计周期 (today, week, month)
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `metric_types` (string): 指标类型列表
- `dimensions` (string): 统计维度列表
**响应**:
```json
{
"success": true,
"message": "获取仪表板数据成功",
"data": {
"api_calls": {
"total_count": 10000,
"success_count": 9500,
"failed_count": 500,
"success_rate": 95.0,
"avg_response_time": 150.5
},
"users": {
"total_count": 1000,
"certified_count": 800,
"active_count": 750,
"certification_rate": 80.0,
"retention_rate": 75.0
},
"finance": {
"total_amount": 50000.0,
"recharge_amount": 60000.0,
"deduct_amount": 10000.0,
"net_amount": 50000.0
},
"period": {
"start_date": "2024-01-01",
"end_date": "2024-01-01",
"period": "today"
},
"metadata": {
"generated_at": "2024-01-01 12:00:00",
"user_role": "user",
"data_version": "1.0"
}
}
}
```
### 5. 报告管理
#### 5.1 生成报告
- **URL**: `POST /api/v1/statistics/reports`
- **权限**: 管理员
- **描述**: 生成指定类型的统计报告
**请求体**:
```json
{
"report_type": "summary",
"title": "月度汇总报告",
"period": "month",
"user_role": "admin",
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-01-31T23:59:59Z",
"filters": {
"metric_types": ["api_calls", "users"],
"dimensions": ["daily", "weekly"]
},
"generated_by": "admin_id"
}
```
#### 5.2 获取报告列表
- **URL**: `GET /api/v1/statistics/reports`
- **权限**: 用户
- **描述**: 根据条件获取统计报告列表
**查询参数**:
- `report_type` (string): 报告类型
- `user_role` (string): 用户角色
- `status` (string): 报告状态
- `period` (string): 统计周期
- `start_date` (string): 开始日期 (YYYY-MM-DD)
- `end_date` (string): 结束日期 (YYYY-MM-DD)
- `limit` (int): 限制数量 (默认20)
- `offset` (int): 偏移量 (默认0)
- `sort_by` (string): 排序字段 (默认created_at)
- `sort_order` (string): 排序顺序 (默认desc)
- `generated_by` (string): 生成者ID
#### 5.3 获取单个报告
- **URL**: `GET /api/v1/statistics/reports/{id}`
- **权限**: 用户
- **描述**: 根据ID获取指定的统计报告
### 6. 统计分析
#### 6.1 计算增长率
- **URL**: `GET /api/v1/statistics/analysis/growth-rate`
- **权限**: 用户
- **描述**: 计算指定指标的增长率
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string, 必需): 指标名称
- `current_period` (string, 必需): 当前周期 (YYYY-MM-DD)
- `previous_period` (string, 必需): 上一周期 (YYYY-MM-DD)
**响应**:
```json
{
"success": true,
"message": "计算增长率成功",
"data": {
"growth_rate": 15.5,
"current_value": 1150,
"previous_value": 1000,
"period": "daily"
}
}
```
#### 6.2 计算趋势
- **URL**: `GET /api/v1/statistics/analysis/trend`
- **权限**: 用户
- **描述**: 计算指定指标的趋势
**查询参数**:
- `metric_type` (string, 必需): 指标类型
- `metric_name` (string, 必需): 指标名称
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
**响应**:
```json
{
"success": true,
"message": "计算趋势成功",
"data": {
"trend": "increasing",
"trend_strength": 0.8,
"data_points": 30,
"correlation": 0.75
}
}
```
### 7. 数据导出
#### 7.1 导出数据
- **URL**: `POST /api/v1/statistics/export`
- **权限**: 管理员
- **描述**: 导出指定格式的统计数据
**请求体**:
```json
{
"format": "excel",
"metric_type": "api_calls",
"start_date": "2024-01-01T00:00:00Z",
"end_date": "2024-01-31T23:59:59Z",
"dimension": "daily",
"group_by": "metric_name",
"filters": {
"status": "success"
},
"columns": ["date", "metric_name", "value"],
"include_charts": true,
"exported_by": "admin_id"
}
```
**响应**:
```json
{
"success": true,
"message": "数据导出成功",
"data": {
"download_url": "https://api.example.com/downloads/export_123.xlsx",
"file_name": "api_calls_export_20240101_20240131.xlsx",
"file_size": 1024000,
"expires_at": "2024-01-02T12:00:00Z"
}
}
```
### 8. 定时任务管理
#### 8.1 手动触发小时聚合
- **URL**: `POST /api/v1/statistics/cron/hourly-aggregation`
- **权限**: 管理员
- **描述**: 手动触发指定时间的小时级数据聚合
**查询参数**:
- `target_hour` (string, 必需): 目标小时 (YYYY-MM-DDTHH:MM:SSZ)
#### 8.2 手动触发日聚合
- **URL**: `POST /api/v1/statistics/cron/daily-aggregation`
- **权限**: 管理员
- **描述**: 手动触发指定时间的日级数据聚合
**查询参数**:
- `target_date` (string, 必需): 目标日期 (YYYY-MM-DD)
#### 8.3 手动触发数据清理
- **URL**: `POST /api/v1/statistics/cron/data-cleanup`
- **权限**: 管理员
- **描述**: 手动触发过期数据清理任务
## 错误码
| 错误码 | HTTP状态码 | 描述 |
|--------|------------|------|
| 400 | 400 Bad Request | 请求参数错误 |
| 401 | 401 Unauthorized | 未认证或认证失败 |
| 403 | 403 Forbidden | 权限不足 |
| 404 | 404 Not Found | 资源不存在 |
| 422 | 422 Unprocessable Entity | 参数验证失败 |
| 429 | 429 Too Many Requests | 请求频率过高 |
| 500 | 500 Internal Server Error | 服务器内部错误 |
## 响应格式
### 成功响应
```json
{
"success": true,
"message": "操作成功",
"data": {
// 响应数据
}
}
```
### 错误响应
```json
{
"success": false,
"message": "错误描述",
"error": "详细错误信息"
}
```
### 列表响应
```json
{
"success": true,
"message": "查询成功",
"data": [
// 数据列表
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 100,
"pages": 5,
"has_next": true,
"has_prev": false
}
}
```
## 数据模型
### 统计指标 (StatisticsMetric)
```json
{
"id": "string",
"metric_type": "string",
"metric_name": "string",
"dimension": "string",
"value": "number",
"metadata": "string",
"date": "string",
"created_at": "string",
"updated_at": "string"
}
```
### 统计报告 (StatisticsReport)
```json
{
"id": "string",
"report_type": "string",
"title": "string",
"content": "string",
"period": "string",
"user_role": "string",
"status": "string",
"generated_by": "string",
"generated_at": "string",
"expires_at": "string",
"created_at": "string",
"updated_at": "string"
}
```
### 统计仪表板 (StatisticsDashboard)
```json
{
"id": "string",
"name": "string",
"description": "string",
"user_role": "string",
"is_default": "boolean",
"is_active": "boolean",
"layout": "string",
"widgets": "string",
"settings": "string",
"refresh_interval": "number",
"created_by": "string",
"access_level": "string",
"created_at": "string",
"updated_at": "string"
}
```
## 使用示例
### 获取今日API调用统计
```bash
curl -X GET "https://api.example.com/api/v1/statistics/realtime?metric_type=api_calls&time_range=last_hour" \
-H "Authorization: Bearer your-jwt-token"
```
### 获取历史用户数据
```bash
curl -X GET "https://api.example.com/api/v1/statistics/historical?metric_type=users&start_date=2024-01-01&end_date=2024-01-31" \
-H "Authorization: Bearer your-jwt-token"
```
### 生成月度报告
```bash
curl -X POST "https://api.example.com/api/v1/statistics/reports" \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"report_type": "summary",
"title": "月度汇总报告",
"period": "month",
"user_role": "admin",
"generated_by": "admin_id"
}'
```
## 注意事项
1. **日期格式**: 所有日期参数都使用 `YYYY-MM-DD` 格式
2. **时间戳**: 所有时间戳都使用 ISO 8601 格式
3. **分页**: 默认每页20条记录最大1000条
4. **限流**: API有请求频率限制超出限制会返回429错误
5. **缓存**: 部分接口支持缓存,响应头会包含缓存信息
6. **权限**: 不同接口需要不同的权限级别,请确保有相应权限
7. **数据量**: 查询大量数据时建议使用分页和日期范围限制

9257
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

8546
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

6209
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

253
docs/swagger/README.md Normal file
View File

@@ -0,0 +1,253 @@
# HYAPI Server Swagger 文档
## 📖 概述
本项目使用 [Swaggo](https://github.com/swaggo/swag) 自动生成 Swagger/OpenAPI 文档,提供完整的 API 接口文档和在线测试功能。
## 🚀 快速开始
### 1. 启动服务器
```bash
# 开发模式启动
make dev
# 或者直接运行
go run cmd/api/main.go
```
### 2. 访问文档
启动服务器后,可以通过以下地址访问 API 文档:
- **Swagger UI**: http://localhost:8080/swagger/index.html
- **API 文档信息**: http://localhost:8080/api/docs
- **重定向地址**: http://localhost:8080/docs
## 📝 文档更新
### 自动更新
使用提供的脚本快速更新文档:
```powershell
# Windows PowerShell
.\scripts\update-docs.ps1
# 更新文档并重启服务器
.\scripts\update-docs.ps1 -Restart
# 查看帮助
.\scripts\update-docs.ps1 -Help
```
### 手动更新
```bash
# 使用 Makefile
make docs
# 或者直接使用 swag 命令
swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal
```
## 🔧 开发指南
### 添加新的 API 接口
1. **在 Handler 方法上添加 Swagger 注释**
```go
// @Summary 接口简短描述
// @Description 接口详细描述
// @Tags 标签分组
// @Accept json
// @Produce json
// @Security Bearer # 如果需要认证
// @Param request body dto.RequestStruct true "请求参数描述"
// @Param id path string true "路径参数描述"
// @Param page query int false "查询参数描述"
// @Success 200 {object} dto.ResponseStruct "成功响应描述"
// @Failure 400 {object} map[string]interface{} "错误响应描述"
// @Router /api/v1/your-endpoint [post]
func (h *YourHandler) YourMethod(c *gin.Context) {
// Handler实现
}
```
2. **为 DTO 结构体添加注释和示例**
```go
// @Description 请求参数描述
type RequestStruct struct {
Name string `json:"name" example:"张三"`
Age int `json:"age" example:"25"`
}
// @Description 响应参数描述
type ResponseStruct struct {
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
Name string `json:"name" example:"张三"`
}
```
3. **重新生成文档**
```bash
make docs
```
### Swagger 注释语法
#### 基础注释
- `@Summary`: 接口摘要(在文档列表中显示)
- `@Description`: 详细描述(支持多行)
- `@Tags`: 标签分组(用于在 UI 中分组显示)
#### 请求/响应格式
- `@Accept`: 接受的内容类型json, xml, plain, html, mpfd, x-www-form-urlencoded
- `@Produce`: 响应的内容类型json, xml, plain, html
#### 安全认证
- `@Security Bearer`: JWT 认证
- `@Security ApiKeyAuth`: API Key 认证
#### 参数定义
- `@Param`: 定义请求参数
- 路径参数:`@Param id path string true "用户ID"`
- 查询参数:`@Param page query int false "页码"`
- 请求体:`@Param request body dto.RequestStruct true "请求参数"`
#### 响应定义
- `@Success`: 成功响应
- `@Failure`: 错误响应
## 📋 API 分组
当前文档按以下标签分组:
### 🔐 用户认证
- 用户注册
- 用户登录(密码/短信验证码)
- 发送验证码
### 👤 用户管理
- 获取用户信息
- 修改密码
### 👨‍💼 管理员认证
- 管理员登录
### 🏢 管理员管理
- 创建管理员
- 更新管理员信息
- 修改管理员密码
- 获取管理员列表
- 获取管理员详情
- 删除管理员
- 获取管理员统计
### 🏢 企业认证
- 创建认证申请
- 上传营业执照
- 获取认证状态
- 获取进度统计
- 提交企业信息
- 发起人脸验证
- 申请合同
- 获取认证详情
- 重试认证步骤
### 💰 钱包管理
- 创建钱包
- 获取钱包信息
- 更新钱包信息
- 钱包充值
- 钱包提现
- 钱包交易
- 获取钱包统计
### 🔑 用户密钥管理
- 创建用户密钥
- 获取用户密钥
- 重新生成访问密钥
- 停用用户密钥
## 🔐 认证说明
### JWT 认证
大部分 API 接口需要 JWT 认证,在 Swagger UI 中:
1. 点击右上角的 "Authorize" 按钮
2. 在 "Bearer" 输入框中输入 JWT Token
3. 点击 "Authorize" 确认
JWT Token 格式:`Bearer <your-jwt-token>`
### 获取 Token
通过以下接口获取 JWT Token
- **用户登录**: `POST /api/v1/users/login-password`
- **用户短信登录**: `POST /api/v1/users/login-sms`
- **管理员登录**: `POST /api/v1/admin/login`
## 🛠️ 故障排除
### 常见问题
1. **文档生成失败**
- 检查 Swagger 注释语法是否正确
- 确保所有引用的类型都已定义
- 检查 HTTP 方法是否正确(必须小写)
2. **模型没有正确显示**
- 确保结构体有正确的 `json` 标签
- 确保包被正确解析
- 使用 `--parseDependency --parseInternal` 参数
3. **认证测试失败**
- 确保安全定义正确
- 检查 JWT Token 格式是否正确
- 确认 Token 未过期
### 调试命令
```bash
# 详细输出生成过程
swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal -v
# 检查 swag 版本
swag version
# 重新安装 swag
go install github.com/swaggo/swag/cmd/swag@latest
```
## 📚 相关资源
- [Swaggo 官方文档](https://github.com/swaggo/swag)
- [Swagger UI 文档](https://swagger.io/tools/swagger-ui/)
- [OpenAPI 规范](https://swagger.io/specification/)
## 🤝 贡献指南
1. 添加新接口时,请同时添加完整的 Swagger 注释
2. 确保所有参数都有合适的 `example` 标签
3. 使用中文描述,符合项目的中文化规范
4. 更新文档后,请运行 `make docs` 重新生成文档

9939
docs/swagger/docs.go Normal file

File diff suppressed because it is too large Load Diff

9159
docs/swagger/swagger.json Normal file

File diff suppressed because it is too large Load Diff

6670
docs/swagger/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,373 @@
# 领域服务层设计文档
## 概述
本文档描述了认证域和用户域的领域服务层设计,包括核心业务逻辑、服务接口和职责分工。
## 认证域领域服务 (CertificationService)
### 服务职责
认证域领域服务负责管理企业认证流程的核心业务逻辑,包括:
- 认证申请的生命周期管理
- 状态机驱动的流程控制
- 人脸识别验证流程
- 合同申请和审核流程
- 认证完成和失败处理
- 进度跟踪和状态查询
### 核心方法
#### 1. 认证申请管理
- `CreateCertification(ctx, userID)` - 创建认证申请
- `GetCertificationByUserID(ctx, userID)` - 根据用户 ID 获取认证申请
- `GetCertificationByID(ctx, certificationID)` - 根据 ID 获取认证申请
- `GetCertificationWithDetails(ctx, certificationID)` - 获取认证申请详细信息(包含关联记录)
#### 2. 企业信息提交
- `SubmitEnterpriseInfo(ctx, certificationID)` - 提交企业信息,触发状态转换
#### 3. 人脸识别验证
- `InitiateFaceVerify(ctx, certificationID, realName, idCardNumber)` - 发起人脸识别验证
- `CompleteFaceVerify(ctx, faceVerifyID, isSuccess)` - 完成人脸识别验证
- `RetryFaceVerify(ctx, certificationID)` - 重试人脸识别
#### 4. 合同流程管理
- `ApplyContract(ctx, certificationID)` - 申请合同
- `ApproveContract(ctx, certificationID, adminID, signingURL, approvalNotes)` - 管理员审核通过
- `RejectContract(ctx, certificationID, adminID, rejectReason)` - 管理员拒绝
- `CompleteContractSign(ctx, certificationID, contractURL)` - 完成合同签署
#### 5. 认证完成
- `CompleteCertification(ctx, certificationID)` - 完成认证流程
- `RestartCertification(ctx, certificationID)` - 重新开始认证流程
#### 6. 记录查询
- `GetFaceVerifyRecords(ctx, certificationID)` - 获取人脸识别记录
- `GetContractRecords(ctx, certificationID)` - 获取合同记录
#### 7. 进度和状态管理
- `GetCertificationProgress(ctx, certificationID)` - 获取认证进度信息
- `UpdateOCRResult(ctx, certificationID, ocrRequestID, confidence)` - 更新 OCR 识别结果
### 状态机集成
认证服务与状态机紧密集成,所有状态转换都通过状态机进行:
- 确保状态转换的合法性
- 自动更新相关时间戳
- 记录状态转换日志
- 支持权限控制(用户/管理员)
## 认证状态机 (CertificationStateMachine)
### 状态机职责
认证状态机负责管理认证流程的状态转换,包括:
- 状态转换规则定义
- 转换验证和权限控制
- 时间戳自动更新
- 元数据管理
- 流程完整性验证
### 核心方法
#### 1. 状态转换管理
- `CanTransition(from, to, isUser, isAdmin)` - 检查是否可以转换到指定状态
- `TransitionTo(ctx, certificationID, targetStatus, isUser, isAdmin, metadata)` - 执行状态转换
- `GetValidNextStatuses(currentStatus, isUser, isAdmin)` - 获取当前状态可以转换到的下一个状态列表
#### 2. 转换规则管理
- `GetTransitionAction(from, to)` - 获取状态转换对应的操作名称
- `initializeTransitions()` - 初始化状态转换规则
#### 3. 流程验证和历史
- `GetTransitionHistory(ctx, certificationID)` - 获取状态转换历史
- `ValidateCertificationFlow(ctx, certificationID)` - 验证认证流程的完整性
### 状态转换规则
#### 正常流程转换
- `PENDING``INFO_SUBMITTED` (用户提交企业信息)
- `INFO_SUBMITTED``FACE_VERIFIED` (人脸识别成功)
- `FACE_VERIFIED``CONTRACT_APPLIED` (申请合同)
- `CONTRACT_APPLIED``CONTRACT_PENDING` (系统处理)
- `CONTRACT_PENDING``CONTRACT_APPROVED` (管理员审核通过)
- `CONTRACT_APPROVED``CONTRACT_SIGNED` (用户签署)
- `CONTRACT_SIGNED``COMPLETED` (系统完成)
#### 失败和重试转换
- `INFO_SUBMITTED``FACE_FAILED` (人脸识别失败)
- `FACE_FAILED``FACE_VERIFIED` (重试人脸识别)
- `CONTRACT_PENDING``REJECTED` (管理员拒绝)
- `REJECTED``INFO_SUBMITTED` (重新开始流程)
- `CONTRACT_APPROVED``SIGN_FAILED` (签署失败)
- `SIGN_FAILED``CONTRACT_SIGNED` (重试签署)
## 用户域领域服务
### 用户基础服务 (UserService)
#### 服务职责
用户基础服务负责用户核心信息的管理,包括:
- 用户基础信息的 CRUD 操作
- 用户密码管理
- 用户状态验证
- 用户统计信息
#### 核心方法
- `IsPhoneRegistered(ctx, phone)` - 检查手机号是否已注册
- `GetUserByID(ctx, userID)` - 根据 ID 获取用户信息
- `GetUserByPhone(ctx, phone)` - 根据手机号获取用户信息
- `GetUserWithEnterpriseInfo(ctx, userID)` - 获取用户信息(包含企业信息)
- `UpdateUser(ctx, user)` - 更新用户信息
- `ChangePassword(ctx, userID, oldPassword, newPassword)` - 修改用户密码
- `ValidateUser(ctx, userID)` - 验证用户信息
- `GetUserStats(ctx)` - 获取用户统计信息
### 企业信息服务 (EnterpriseService)
#### 服务职责
企业信息服务专门负责企业信息的管理,包括:
- 企业信息的创建、更新和查询
- 企业认证状态管理
- 数据验证和业务规则检查
- 认证状态跟踪
#### 核心方法
##### 1. 企业信息管理
- `CreateEnterpriseInfo(ctx, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID)` - 创建企业信息
- `GetEnterpriseInfo(ctx, userID)` - 获取企业信息
- `UpdateEnterpriseInfo(ctx, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID)` - 更新企业信息
- `GetEnterpriseInfoByUnifiedSocialCode(ctx, unifiedSocialCode)` - 根据统一社会信用代码获取企业信息
##### 2. 认证状态管理
- `UpdateOCRVerification(ctx, userID, isVerified, rawData, confidence)` - 更新 OCR 验证状态
- `UpdateFaceVerification(ctx, userID, isVerified)` - 更新人脸识别验证状态
- `CompleteEnterpriseCertification(ctx, userID)` - 完成企业认证
##### 3. 数据验证和查询
- `CheckUnifiedSocialCodeExists(ctx, unifiedSocialCode, excludeUserID)` - 检查统一社会信用代码唯一性
- `ValidateEnterpriseInfo(ctx, userID)` - 验证企业信息完整性
- `IsEnterpriseCertified(ctx, userID)` - 检查用户是否已完成企业认证
- `GetEnterpriseCertificationStatus(ctx, userID)` - 获取企业认证状态
##### 4. 用户信息集成
- `GetUserWithEnterpriseInfo(ctx, userID)` - 获取用户信息(包含企业信息)
### 短信验证码服务 (SMSCodeService)
#### 服务职责
短信验证码服务负责短信验证码的完整生命周期管理,包括:
- 验证码生成和发送
- 验证码验证
- 频率限制控制
- 安全策略执行
#### 核心方法
- `SendCode(ctx, phone, scene, clientIP, userAgent)` - 发送验证码
- `VerifyCode(ctx, phone, code, scene)` - 验证验证码
- `CanResendCode(ctx, phone, scene)` - 检查是否可以重新发送
- `GetCodeStatus(ctx, phone, scene)` - 获取验证码状态
- `CheckRateLimit(ctx, phone, scene)` - 检查发送频率限制
## 服务协作模式
### 服务依赖关系
```
CertificationService
├── 依赖 CertificationRepository
├── 依赖 FaceVerifyRecordRepository
├── 依赖 ContractRecordRepository
├── 依赖 LicenseUploadRecordRepository
├── 依赖 CertificationStateMachine
└── 提供认证流程管理功能
CertificationStateMachine
├── 依赖 CertificationRepository
├── 管理状态转换规则
└── 提供流程验证功能
UserService
├── 依赖 EnterpriseService (用于获取包含企业信息的用户数据)
└── 依赖 UserRepository
EnterpriseService
├── 依赖 UserRepository (用于验证用户存在性)
├── 依赖 EnterpriseInfoRepository
└── 提供企业信息相关功能
SMSCodeService
├── 依赖 SMSCodeRepository
├── 依赖 AliSMSService
├── 依赖 CacheService
└── 独立运行,不依赖其他领域服务
```
### 跨域调用
1. **认证域调用用户域**
- 认证服务在需要企业信息时调用企业服务
- 通过依赖注入获取企业服务实例
- 保持领域边界清晰
2. **应用服务层协调**
- 应用服务层负责协调不同领域服务
- 处理跨域事务和一致性
- 发布领域事件
### 事件驱动
1. **领域事件发布**
- 认证状态变更时发布事件
- 企业信息创建/更新时发布事件
- 用户注册/更新时发布事件
- 支持异步处理和集成
2. **事件处理**
- 事件处理器响应领域事件
- 执行副作用操作(如通知、日志)
- 维护数据一致性
## 业务规则
### 企业信息只读规则
- 认证完成后企业信息不可修改
- 通过 `IsReadOnly()` 方法检查
- 在更新操作中强制执行
### 统一社会信用代码唯一性
- 每个统一社会信用代码只能对应一个用户
- 更新时排除当前用户 ID
- 支持并发检查
### 认证完成条件
- OCR 验证必须通过
- 人脸识别验证必须通过
- 两个条件都满足才能完成认证
### 状态转换规则
- 严格按照状态机定义的转换规则执行
- 支持用户和管理员权限控制
- 自动记录转换历史和时间戳
### 短信验证码规则
- 支持多种场景(注册、登录、修改密码等)
- 频率限制(最小间隔、每小时限制、每日限制)
- 开发模式跳过验证
- 自动过期和清理
## 错误处理
### 业务错误
- 使用有意义的错误消息
- 包含业务上下文信息
- 支持错误分类和处理
### 日志记录
- 记录关键业务操作
- 包含操作上下文(用户 ID、操作类型等
- 支持问题排查和审计
## 性能考虑
### 数据库查询优化
- 合理使用索引
- 避免 N+1 查询问题
- 支持分页和缓存
### 并发控制
- 使用乐观锁或悲观锁
- 防止数据竞争条件
- 保证数据一致性
## 扩展性设计
### 新功能扩展
- 通过接口扩展新功能
- 保持向后兼容性
- 支持插件化架构
### 多租户支持
- 预留租户字段
- 支持数据隔离
- 便于未来扩展
## 测试策略
### 单元测试
- 测试核心业务逻辑
- 模拟外部依赖
- 覆盖边界条件
### 集成测试
- 测试服务间协作
- 验证数据一致性
- 测试完整业务流程
## 总结
领域服务层设计遵循 DDD 原则,实现了:
1. **职责分离**:
- 认证域负责流程管理和状态控制
- 用户域分为基础服务和企业服务,各司其职
- 短信服务独立运行
2. **业务封装**: 核心业务逻辑封装在相应的领域服务中
3. **状态管理**: 通过状态机管理复杂流程,支持历史追踪和流程验证
4. **数据一致性**: 通过业务规则保证数据完整性
5. **可扩展性**: 支持未来功能扩展和架构演进
6. **服务协作**: 通过依赖注入实现服务间的松耦合协作
7. **流程完整性**: 通过状态机验证和流程完整性检查确保业务逻辑正确性
这种设计为应用服务层提供了清晰的接口,便于实现复杂的业务流程协调,同时保持了良好的可维护性和可测试性。

151
go.mod Normal file
View File

@@ -0,0 +1,151 @@
module hyapi-server
go 1.23.4
require (
github.com/alibabacloud-go/captcha-20230305 v1.1.3
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/tea v1.3.13
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
github.com/chromedp/chromedp v0.13.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/jung-kurt/gofpdf/v2 v2.17.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220
github.com/prometheus/client_golang v1.22.0
github.com/qiniu/go-sdk/v7 v7.25.4
github.com/redis/go-redis/v9 v9.11.0
github.com/robfig/cron/v3 v3.0.1
github.com/shopspring/decimal v1.4.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/smartwalle/alipay/v3 v3.2.25
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.81
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.80
github.com/tidwall/gjson v1.18.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
github.com/xuri/excelize/v2 v2.9.1
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/fx v1.24.0
go.uber.org/zap v1.27.0
golang.org/x/time v0.12.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/dig v1.19.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/fileutil v1.0.0 // indirect
)

630
go.sum Normal file
View File

@@ -0,0 +1,630 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.2 h1:f6sZFFzCzPLvWSzeuXQBgONKG7zPq54YfEyEj0EplOY=
github.com/chromedp/chromedp v0.13.2/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 h1:FLQyP/6tTsTEtAhcIq/kS/zkDEMdOMon0I70pXVehOU=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220/go.mod h1:+mNMTBuDMdEGhWzoQgc6kBdqeaQpWh5ba8zqmp2MxCU=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.25.4 h1:ulCKlTEyrZzmNytXweOrnva49+Q4+ASjYBCSXhkRWTo=
github.com/qiniu/go-sdk/v7 v7.25.4/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw=
github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.80/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.81 h1:OStEffmKajZTNWo8BuTDaA5p1IMtrOlVPjMetWZbMQY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.81/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.80 h1:4mNiA2sDlvctnGNf78sNAveCtcO+S8LnXH9kBoqSGLE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.80/go.mod h1:z2hbqHG55lSo85QKqn3xDUAhXG1iIVPJnkMhwJzk4i0=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

347
internal/app/app.go Normal file
View File

@@ -0,0 +1,347 @@
package app
import (
"fmt"
"os"
"os/signal"
"syscall"
"go.uber.org/zap"
"gorm.io/gorm"
"hyapi-server/internal/config"
"hyapi-server/internal/container"
"hyapi-server/internal/domains/user/entities"
// 认证域实体
certEntities "hyapi-server/internal/domains/certification/entities"
// 财务域实体
financeEntities "hyapi-server/internal/domains/finance/entities"
// 产品域实体
productEntities "hyapi-server/internal/domains/product/entities"
// 文章域实体
articleEntities "hyapi-server/internal/domains/article/entities"
// 统计域实体
securityEntities "hyapi-server/internal/domains/security/entities"
statisticsEntities "hyapi-server/internal/domains/statistics/entities"
apiEntities "hyapi-server/internal/domains/api/entities"
"hyapi-server/internal/infrastructure/database"
taskEntities "hyapi-server/internal/infrastructure/task/entities"
)
// Application 应用程序结构
type Application struct {
container *container.Container
config *config.Config
logger *zap.Logger
}
// NewApplication 创建新的应用程序实例
func NewApplication() (*Application, error) {
// 加载配置
cfg, err := config.LoadConfig()
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
// 创建日志器
logger, err := createLogger(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
}
// 创建容器
cont := container.NewContainer()
return &Application{
container: cont,
config: cfg,
logger: logger,
}, nil
}
// Run 运行应用程序
func (a *Application) Run() error {
// 打印启动信息
a.printBanner()
// 检查是否需要自动迁移
if a.config.Database.AutoMigrate {
a.logger.Info("Auto migration is enabled, running database migrations...")
if err := a.RunMigrations(); err != nil {
a.logger.Error("Auto migration failed", zap.Error(err))
return fmt.Errorf("auto migration failed: %w", err)
}
a.logger.Info("Auto migration completed successfully")
}
// 启动容器
a.logger.Info("Starting application container...")
if err := a.container.Start(); err != nil {
a.logger.Error("Failed to start container", zap.Error(err))
return err
}
a.logger.Info("Container started successfully, setting up graceful shutdown...")
// 设置优雅关闭
a.setupGracefulShutdown()
a.logger.Info("Application started successfully",
zap.String("version", a.config.App.Version),
zap.String("environment", a.config.App.Env),
zap.String("port", a.config.Server.Port))
// 等待信号
return a.waitForShutdown()
}
// RunMigrations 运行数据库迁移
func (a *Application) RunMigrations() error {
a.logger.Info("Running database migrations...")
// 创建数据库连接
db, err := a.createDatabaseConnection()
if err != nil {
return fmt.Errorf("failed to create database connection: %w", err)
}
// 自动迁移
if err := a.autoMigrate(db); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
a.logger.Info("Database migrations completed successfully")
return nil
}
// printBanner 打印启动横幅
func (a *Application) printBanner() {
banner := fmt.Sprintf(`
╔══════════════════════════════════════════════════════════════╗
║ %s
║ Version: %s
║ Environment: %s
║ Port: %s
╚══════════════════════════════════════════════════════════════╝
`,
a.config.App.Name,
a.config.App.Version,
a.config.App.Env,
a.config.Server.Port,
)
fmt.Println(banner)
}
// setupGracefulShutdown 设置优雅关闭
func (a *Application) setupGracefulShutdown() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
a.logger.Info("Received shutdown signal, starting graceful shutdown...")
// 停止容器
if err := a.container.Stop(); err != nil {
a.logger.Error("Error during container shutdown", zap.Error(err))
}
a.logger.Info("Application shutdown completed")
os.Exit(0)
}()
}
// waitForShutdown 等待关闭信号
func (a *Application) waitForShutdown() error {
// 创建一个通道来等待关闭
done := make(chan bool, 1)
// 启动一个协程来监听信号
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
done <- true
}()
// 等待关闭信号
<-done
return nil
}
// createDatabaseConnection 创建数据库连接
func (a *Application) createDatabaseConnection() (*gorm.DB, error) {
dbCfg := database.Config{
Host: a.config.Database.Host,
Port: a.config.Database.Port,
User: a.config.Database.User,
Password: a.config.Database.Password,
Name: a.config.Database.Name,
SSLMode: a.config.Database.SSLMode,
Timezone: a.config.Database.Timezone,
MaxOpenConns: a.config.Database.MaxOpenConns,
MaxIdleConns: a.config.Database.MaxIdleConns,
ConnMaxLifetime: a.config.Database.ConnMaxLifetime,
}
db, err := database.NewConnection(dbCfg)
if err != nil {
return nil, err
}
return db.DB, nil
}
// autoMigrate 自动迁移
func (a *Application) autoMigrate(db *gorm.DB) error {
a.logger.Info("Starting database auto migration...")
// 如果需要删除某些表,可以在这里手动删除
// 注意:这会永久删除数据,请谨慎使用!
/*
// 删除不再需要的表(示例,请根据实际情况使用)
if err := db.Migrator().DropTable(&entities.FavoriteItem{}); err != nil {
a.logger.Warn("Failed to drop table", zap.Error(err))
// 继续执行,不阻断迁移
}
*/
// 自动迁移所有实体
return db.AutoMigrate(
// 用户域
&entities.User{},
&entities.SMSCode{},
&entities.EnterpriseInfo{},
&entities.ContractInfo{},
// 认证域
&certEntities.Certification{},
&certEntities.EnterpriseInfoSubmitRecord{},
&certEntities.EsignContractGenerateRecord{},
&certEntities.EsignContractSignRecord{},
// 财务域
&financeEntities.Wallet{},
&financeEntities.WalletTransaction{},
&financeEntities.RechargeRecord{},
&financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{},
&financeEntities.PurchaseOrder{}, //购买组件订单表
// 产品域
&productEntities.Product{},
&productEntities.ProductPackageItem{},
&productEntities.ProductCategory{},
&productEntities.Subscription{},
&productEntities.ProductDocumentation{},
&productEntities.ProductApiConfig{},
&productEntities.ComponentReportDownload{},
&productEntities.UIComponent{},
&productEntities.ProductUIComponent{},
// 文章域
&articleEntities.Article{},
&articleEntities.Category{},
&articleEntities.Tag{},
&articleEntities.ScheduledTask{},
// 公告
&articleEntities.Announcement{},
// 统计域
&statisticsEntities.StatisticsMetric{},
&statisticsEntities.StatisticsDashboard{},
&statisticsEntities.StatisticsReport{},
&securityEntities.SuspiciousIPRecord{},
// api
&apiEntities.ApiUser{},
&apiEntities.ApiCall{},
&apiEntities.Report{},
// 任务域
&taskEntities.AsyncTask{},
)
}
// createLogger 创建日志器
func createLogger(cfg *config.Config) (*zap.Logger, error) {
level, err := zap.ParseAtomicLevel(cfg.Logger.Level)
if err != nil {
level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
config := zap.Config{
Level: level,
Development: cfg.App.IsDevelopment(),
Encoding: cfg.Logger.Format,
EncoderConfig: zap.NewProductionEncoderConfig(),
OutputPaths: []string{cfg.Logger.Output},
ErrorOutputPaths: []string{"stderr"},
}
if cfg.Logger.Format == "" {
config.Encoding = "json"
}
if cfg.Logger.Output == "" {
config.OutputPaths = []string{"stdout"}
}
return config.Build()
}
// GetConfig 获取配置
func (a *Application) GetConfig() *config.Config {
return a.config
}
// GetLogger 获取日志器
func (a *Application) GetLogger() *zap.Logger {
return a.logger
}
// HealthCheck 应用程序健康检查
func (a *Application) HealthCheck() error {
// 这里可以添加应用程序级别的健康检查逻辑
return nil
}
// GetVersion 获取版本信息
func (a *Application) GetVersion() map[string]string {
return map[string]string{
"name": a.config.App.Name,
"version": a.config.App.Version,
"environment": a.config.App.Env,
"go_version": "1.23.4+",
}
}
// RunCommand 运行特定命令
func (a *Application) RunCommand(command string, args ...string) error {
switch command {
case "migrate":
return a.RunMigrations()
case "version":
version := a.GetVersion()
fmt.Printf("Name: %s\n", version["name"])
fmt.Printf("Version: %s\n", version["version"])
fmt.Printf("Environment: %s\n", version["environment"])
fmt.Printf("Go Version: %s\n", version["go_version"])
return nil
case "health":
if err := a.HealthCheck(); err != nil {
fmt.Printf("Health check failed: %v\n", err)
return err
}
fmt.Println("Application is healthy")
return nil
default:
return fmt.Errorf("unknown command: %s", command)
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,104 @@
package dto
import (
api_entities "hyapi-server/internal/domains/api/entities"
product_entities "hyapi-server/internal/domains/product/entities"
"github.com/shopspring/decimal"
)
// ApiCallValidationResult API调用验证结果
type ApiCallValidationResult struct {
UserID string `json:"user_id"`
ProductID string `json:"product_id"`
SubscriptionID string `json:"subscription_id"`
Amount decimal.Decimal `json:"amount"`
SecretKey string `json:"secret_key"`
IsValid bool `json:"is_valid"`
ErrorMessage string `json:"error_message"`
// 新增字段
ContractCode string `json:"contract_code"`
ApiCall *api_entities.ApiCall `json:"api_call"`
RequestParams map[string]interface{} `json:"request_params"`
Product *product_entities.Product `json:"product"`
Subscription *product_entities.Subscription `json:"subscription"`
}
// GetUserID 获取用户ID
func (r *ApiCallValidationResult) GetUserID() string {
return r.UserID
}
// GetProductID 获取产品ID
func (r *ApiCallValidationResult) GetProductID() string {
return r.ProductID
}
// GetSubscriptionID 获取订阅ID
func (r *ApiCallValidationResult) GetSubscriptionID() string {
return r.SubscriptionID
}
// GetAmount 获取金额
func (r *ApiCallValidationResult) GetAmount() decimal.Decimal {
return r.Amount
}
// GetSecretKey 获取密钥
func (r *ApiCallValidationResult) GetSecretKey() string {
return r.SecretKey
}
// IsValidResult 检查是否有效
func (r *ApiCallValidationResult) IsValidResult() bool {
return r.IsValid
}
// GetErrorMessage 获取错误消息
func (r *ApiCallValidationResult) GetErrorMessage() string {
return r.ErrorMessage
}
// NewApiCallValidationResult 创建新的API调用验证结果
func NewApiCallValidationResult() *ApiCallValidationResult {
return &ApiCallValidationResult{
IsValid: true,
RequestParams: make(map[string]interface{}),
}
}
// SetApiUser 设置API用户
func (r *ApiCallValidationResult) SetApiUser(apiUser *api_entities.ApiUser) {
r.UserID = apiUser.UserId
r.SecretKey = apiUser.SecretKey
}
// SetProduct 设置产品
func (r *ApiCallValidationResult) SetProduct(product *product_entities.Product) {
r.ProductID = product.ID
r.Product = product
// 注意这里不设置Amount应该通过SetSubscription来设置实际的扣费金额
}
// SetApiCall 设置API调用
func (r *ApiCallValidationResult) SetApiCall(apiCall *api_entities.ApiCall) {
r.ApiCall = apiCall
}
// SetRequestParams 设置请求参数
func (r *ApiCallValidationResult) SetRequestParams(params map[string]interface{}) {
r.RequestParams = params
}
// SetContractCode 设置合同代码
func (r *ApiCallValidationResult) SetContractCode(code string) {
r.ContractCode = code
}
// SetSubscription 设置订阅信息(包含实际扣费金额)
func (r *ApiCallValidationResult) SetSubscription(subscription *product_entities.Subscription) {
r.SubscriptionID = subscription.ID
r.Amount = subscription.Price // 使用订阅价格作为扣费金额
r.Subscription = subscription
}

View File

@@ -0,0 +1,103 @@
package dto
import "time"
// ApiCallResponse API调用响应结构
type ApiCallResponse struct {
Code int `json:"code"`
Message string `json:"message"`
TransactionId string `json:"transaction_id"`
Data string `json:"data,omitempty"`
}
// ApiKeysResponse API密钥响应结构
type ApiKeysResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AccessID string `json:"access_id"`
SecretKey string `json:"secret_key"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 白名单相关DTO
type WhiteListResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
IPAddress string `json:"ip_address"`
Remark string `json:"remark"` // 备注
CreatedAt time.Time `json:"created_at"`
}
type WhiteListRequest struct {
IPAddress string `json:"ip_address" binding:"required,ip"`
Remark string `json:"remark"` // 备注(可选)
}
type WhiteListListResponse struct {
Items []WhiteListResponse `json:"items"`
Total int `json:"total"`
}
// API调用记录相关DTO
type ApiCallRecordResponse struct {
ID string `json:"id"`
AccessId string `json:"access_id"`
UserId string `json:"user_id"`
ProductId *string `json:"product_id,omitempty"`
ProductName *string `json:"product_name,omitempty"`
TransactionId string `json:"transaction_id"`
ClientIp string `json:"client_ip"`
RequestParams string `json:"request_params"`
Status string `json:"status"`
StartAt string `json:"start_at"`
EndAt *string `json:"end_at,omitempty"`
Cost *string `json:"cost,omitempty"`
ErrorType *string `json:"error_type,omitempty"`
ErrorMsg *string `json:"error_msg,omitempty"`
TranslatedErrorMsg *string `json:"translated_error_msg,omitempty"`
CompanyName *string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// UserSimpleResponse 用户简单信息响应
type UserSimpleResponse struct {
ID string `json:"id"`
CompanyName string `json:"company_name"`
Phone string `json:"phone"`
}
type ApiCallListResponse struct {
Items []ApiCallRecordResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// EncryptResponse 加密响应
type EncryptResponse struct {
EncryptedData string `json:"encrypted_data"`
}
// NewSuccessResponse 创建成功响应
func NewSuccessResponse(transactionId, data string) *ApiCallResponse {
return &ApiCallResponse{
Code: 0,
Message: "业务成功",
TransactionId: transactionId,
Data: data,
}
}
// NewErrorResponse 创建错误响应
func NewErrorResponse(code int, message, transactionId string) *ApiCallResponse {
return &ApiCallResponse{
Code: code,
Message: message,
TransactionId: transactionId,
Data: "",
}
}

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package utils
// TranslateErrorMsg 翻译错误信息
func TranslateErrorMsg(errorType, errorMsg *string) *string {
if errorType == nil || errorMsg == nil {
return nil
}
// 错误类型到中文描述的映射
errorTypeTranslations := map[string]string{
"invalid_access": "无效的访问凭证",
"frozen_account": "账户已被冻结",
"invalid_ip": "IP地址未授权",
"arrears": "账户余额不足",
"not_subscribed": "未订阅该产品",
"product_not_found": "产品不存在",
"product_disabled": "产品已停用",
"system_error": "接口异常",
"datasource_error": "数据源异常",
"invalid_param": "参数校验失败",
"decrypt_fail": "参数解密失败",
"query_empty": "查询结果为空",
}
// 获取错误类型的中文描述
translatedType, exists := errorTypeTranslations[*errorType]
if !exists {
// 如果没有找到对应的翻译,返回原始错误信息
return errorMsg
}
// 构建翻译后的错误信息
translatedMsg := translatedType
if *errorMsg != "" && *errorMsg != *errorType {
// 如果原始错误信息不是错误类型本身,则组合显示
translatedMsg = translatedType + "" + *errorMsg
}
return &translatedMsg
}

View File

@@ -0,0 +1,30 @@
package article
import (
"context"
"hyapi-server/internal/application/article/dto/commands"
appQueries "hyapi-server/internal/application/article/dto/queries"
"hyapi-server/internal/application/article/dto/responses"
)
// AnnouncementApplicationService 公告应用服务接口
type AnnouncementApplicationService interface {
// 公告管理
CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error
UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error
DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error
GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error)
ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error)
// 公告状态管理
PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error
PublishAnnouncementByID(ctx context.Context, announcementID string) error // 通过ID发布公告 (用于定时任务)
WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error
ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error
SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error
UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error
CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error
// 统计信息
GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error)
}

View File

@@ -0,0 +1,484 @@
package article
import (
"context"
"fmt"
"hyapi-server/internal/application/article/dto/commands"
appQueries "hyapi-server/internal/application/article/dto/queries"
"hyapi-server/internal/application/article/dto/responses"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
"hyapi-server/internal/domains/article/services"
task_entities "hyapi-server/internal/infrastructure/task/entities"
task_interfaces "hyapi-server/internal/infrastructure/task/interfaces"
"go.uber.org/zap"
)
// AnnouncementApplicationServiceImpl 公告应用服务实现
type AnnouncementApplicationServiceImpl struct {
announcementRepo repositories.AnnouncementRepository
announcementService *services.AnnouncementService
taskManager task_interfaces.TaskManager
logger *zap.Logger
}
// NewAnnouncementApplicationService 创建公告应用服务
func NewAnnouncementApplicationService(
announcementRepo repositories.AnnouncementRepository,
announcementService *services.AnnouncementService,
taskManager task_interfaces.TaskManager,
logger *zap.Logger,
) AnnouncementApplicationService {
return &AnnouncementApplicationServiceImpl{
announcementRepo: announcementRepo,
announcementService: announcementService,
taskManager: taskManager,
logger: logger,
}
}
// CreateAnnouncement 创建公告
func (s *AnnouncementApplicationServiceImpl) CreateAnnouncement(ctx context.Context, cmd *commands.CreateAnnouncementCommand) error {
// 1. 创建公告实体
announcement := &entities.Announcement{
Title: cmd.Title,
Content: cmd.Content,
Status: entities.AnnouncementStatusDraft,
}
// 2. 调用领域服务验证
if err := s.announcementService.ValidateAnnouncement(announcement); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 3. 保存公告
_, err := s.announcementRepo.Create(ctx, *announcement)
if err != nil {
s.logger.Error("创建公告失败", zap.Error(err))
return fmt.Errorf("创建公告失败: %w", err)
}
s.logger.Info("创建公告成功", zap.String("id", announcement.ID), zap.String("title", announcement.Title))
return nil
}
// UpdateAnnouncement 更新公告
func (s *AnnouncementApplicationServiceImpl) UpdateAnnouncement(ctx context.Context, cmd *commands.UpdateAnnouncementCommand) error {
// 1. 获取原公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以编辑
if err := s.announcementService.CanEdit(&announcement); err != nil {
return fmt.Errorf("公告状态不允许编辑: %w", err)
}
// 3. 更新字段
if cmd.Title != "" {
announcement.Title = cmd.Title
}
if cmd.Content != "" {
announcement.Content = cmd.Content
}
// 4. 验证更新后的公告
if err := s.announcementService.ValidateAnnouncement(&announcement); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("更新公告失败: %w", err)
}
s.logger.Info("更新公告成功", zap.String("id", announcement.ID))
return nil
}
// DeleteAnnouncement 删除公告
func (s *AnnouncementApplicationServiceImpl) DeleteAnnouncement(ctx context.Context, cmd *commands.DeleteAnnouncementCommand) error {
// 1. 检查公告是否存在
_, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 删除公告
if err := s.announcementRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除公告失败: %w", err)
}
s.logger.Info("删除公告成功", zap.String("id", cmd.ID))
return nil
}
// GetAnnouncementByID 获取公告详情
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementByID(ctx context.Context, query *appQueries.GetAnnouncementQuery) (*responses.AnnouncementInfoResponse, error) {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("公告不存在: %w", err)
}
// 2. 转换为响应对象
response := responses.FromAnnouncementEntity(&announcement)
return response, nil
}
// ListAnnouncements 获取公告列表
func (s *AnnouncementApplicationServiceImpl) ListAnnouncements(ctx context.Context, query *appQueries.ListAnnouncementQuery) (*responses.AnnouncementListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListAnnouncementQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
Title: query.Title,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
announcements, total, err := s.announcementRepo.ListAnnouncements(ctx, repoQuery)
if err != nil {
s.logger.Error("获取公告列表失败", zap.Error(err))
return nil, fmt.Errorf("获取公告列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromAnnouncementEntityList(announcements)
response := &responses.AnnouncementListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取公告列表成功", zap.Int64("total", total))
return response, nil
}
// PublishAnnouncement 发布公告
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncement(ctx context.Context, cmd *commands.PublishAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以发布
if err := s.announcementService.CanPublish(&announcement); err != nil {
return fmt.Errorf("无法发布公告: %w", err)
}
// 3. 发布公告
if err := announcement.Publish(); err != nil {
return fmt.Errorf("发布公告失败: %w", err)
}
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("发布公告失败: %w", err)
}
s.logger.Info("发布公告成功", zap.String("id", announcement.ID))
return nil
}
// PublishAnnouncementByID 通过ID发布公告 (用于定时任务)
func (s *AnnouncementApplicationServiceImpl) PublishAnnouncementByID(ctx context.Context, announcementID string) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, announcementID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", announcementID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否已取消定时发布
if !announcement.IsScheduled() {
s.logger.Info("公告定时发布已取消,跳过执行",
zap.String("id", announcementID),
zap.String("status", string(announcement.Status)))
return nil // 静默返回,不报错
}
// 3. 检查定时发布时间是否匹配
if announcement.ScheduledAt == nil {
s.logger.Info("公告没有定时发布时间,跳过执行",
zap.String("id", announcementID))
return nil
}
// 4. 发布公告
if err := announcement.Publish(); err != nil {
return fmt.Errorf("发布公告失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("发布公告失败: %w", err)
}
s.logger.Info("定时发布公告成功", zap.String("id", announcement.ID))
return nil
}
// WithdrawAnnouncement 撤回公告
func (s *AnnouncementApplicationServiceImpl) WithdrawAnnouncement(ctx context.Context, cmd *commands.WithdrawAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以撤回
if err := s.announcementService.CanWithdraw(&announcement); err != nil {
return fmt.Errorf("无法撤回公告: %w", err)
}
// 3. 撤回公告
if err := announcement.Withdraw(); err != nil {
return fmt.Errorf("撤回公告失败: %w", err)
}
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("撤回公告失败: %w", err)
}
s.logger.Info("撤回公告成功", zap.String("id", announcement.ID))
return nil
}
// ArchiveAnnouncement 归档公告
func (s *AnnouncementApplicationServiceImpl) ArchiveAnnouncement(ctx context.Context, cmd *commands.ArchiveAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否可以归档
if err := s.announcementService.CanArchive(&announcement); err != nil {
return fmt.Errorf("无法归档公告: %w", err)
}
// 3. 归档公告
announcement.Status = entities.AnnouncementStatusArchived
// 4. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("归档公告失败: %w", err)
}
s.logger.Info("归档公告成功", zap.String("id", announcement.ID))
return nil
}
// SchedulePublishAnnouncement 定时发布公告
func (s *AnnouncementApplicationServiceImpl) SchedulePublishAnnouncement(ctx context.Context, cmd *commands.SchedulePublishAnnouncementCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 3. 检查是否可以定时发布
if err := s.announcementService.CanSchedulePublish(&announcement, scheduledTime); err != nil {
return fmt.Errorf("无法设置定时发布: %w", err)
}
// 4. 取消旧任务(如果存在)
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
}
// 5. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 6. 创建并异步入队公告发布任务
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
return fmt.Errorf("创建定时发布任务失败: %w", err)
}
// 7. 设置定时发布
if err := announcement.SchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("设置定时发布失败: %w", err)
}
// 8. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("设置定时发布失败: %w", err)
}
s.logger.Info("设置定时发布成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
return nil
}
// UpdateSchedulePublishAnnouncement 更新定时发布公告
func (s *AnnouncementApplicationServiceImpl) UpdateSchedulePublishAnnouncement(ctx context.Context, cmd *commands.UpdateSchedulePublishAnnouncementCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 3. 检查是否已设置定时发布
if !announcement.IsScheduled() {
return fmt.Errorf("公告未设置定时发布,无法修改时间")
}
// 4. 取消旧任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
}
// 5. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 6. 创建并异步入队新的公告发布任务
if err := taskFactory.CreateAndEnqueueAnnouncementPublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队公告发布任务失败", zap.Error(err))
return fmt.Errorf("创建定时发布任务失败: %w", err)
}
// 7. 更新定时发布时间
if err := announcement.UpdateSchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("更新定时发布时间失败: %w", err)
}
// 8. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
s.logger.Info("修改定时发布时间成功", zap.String("id", announcement.ID), zap.Time("scheduled_at", scheduledTime))
return nil
}
// CancelSchedulePublishAnnouncement 取消定时发布公告
func (s *AnnouncementApplicationServiceImpl) CancelSchedulePublishAnnouncement(ctx context.Context, cmd *commands.CancelSchedulePublishAnnouncementCommand) error {
// 1. 获取公告
announcement, err := s.announcementRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取公告失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("公告不存在: %w", err)
}
// 2. 检查是否已设置定时发布
if !announcement.IsScheduled() {
return fmt.Errorf("公告未设置定时发布,无需取消")
}
// 3. 取消任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消任务失败", zap.String("announcement_id", cmd.ID), zap.Error(err))
// 继续执行,即使取消任务失败也尝试取消定时发布状态
}
// 4. 取消定时发布
if err := announcement.CancelSchedulePublish(); err != nil {
return fmt.Errorf("取消定时发布失败: %w", err)
}
// 5. 保存更新
if err := s.announcementRepo.Update(ctx, announcement); err != nil {
s.logger.Error("更新公告失败", zap.String("id", announcement.ID), zap.Error(err))
return fmt.Errorf("取消定时发布失败: %w", err)
}
s.logger.Info("取消定时发布成功", zap.String("id", announcement.ID))
return nil
}
// GetAnnouncementStats 获取公告统计信息
func (s *AnnouncementApplicationServiceImpl) GetAnnouncementStats(ctx context.Context) (*responses.AnnouncementStatsResponse, error) {
// 1. 统计总数
total, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
if err != nil {
s.logger.Error("统计公告总数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
// 2. 统计各状态数量
published, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusPublished)
if err != nil {
s.logger.Error("统计已发布公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
draft, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusDraft)
if err != nil {
s.logger.Error("统计草稿公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
archived, err := s.announcementRepo.CountByStatus(ctx, entities.AnnouncementStatusArchived)
if err != nil {
s.logger.Error("统计归档公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
// 3. 统计定时发布数量需要查询有scheduled_at的草稿
scheduled, err := s.announcementRepo.FindScheduled(ctx)
if err != nil {
s.logger.Error("统计定时发布公告数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
response := &responses.AnnouncementStatsResponse{
TotalAnnouncements: total + published + archived,
PublishedAnnouncements: published,
DraftAnnouncements: draft,
ArchivedAnnouncements: archived,
ScheduledAnnouncements: int64(len(scheduled)),
}
return response, nil
}

View File

@@ -0,0 +1,48 @@
package article
import (
"context"
"hyapi-server/internal/application/article/dto/commands"
appQueries "hyapi-server/internal/application/article/dto/queries"
"hyapi-server/internal/application/article/dto/responses"
)
// ArticleApplicationService 文章应用服务接口
type ArticleApplicationService interface {
// 文章管理
CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error
UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error
DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error
GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error)
ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error)
// 文章状态管理
PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error
PublishArticleByID(ctx context.Context, articleID string) error
SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error
CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error
ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error
SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error
// 文章交互
RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error
// 统计信息
GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error)
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context) (*responses.CategoryListResponse, error)
// 标签管理
CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error
UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error
DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error
GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error)
ListTags(ctx context.Context) (*responses.TagListResponse, error)
}

View File

@@ -0,0 +1,836 @@
package article
import (
"context"
"fmt"
"hyapi-server/internal/application/article/dto/commands"
appQueries "hyapi-server/internal/application/article/dto/queries"
"hyapi-server/internal/application/article/dto/responses"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
repoQueries "hyapi-server/internal/domains/article/repositories/queries"
"hyapi-server/internal/domains/article/services"
task_entities "hyapi-server/internal/infrastructure/task/entities"
task_interfaces "hyapi-server/internal/infrastructure/task/interfaces"
shared_interfaces "hyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
)
// ArticleApplicationServiceImpl 文章应用服务实现
type ArticleApplicationServiceImpl struct {
articleRepo repositories.ArticleRepository
categoryRepo repositories.CategoryRepository
tagRepo repositories.TagRepository
articleService *services.ArticleService
taskManager task_interfaces.TaskManager
logger *zap.Logger
}
// NewArticleApplicationService 创建文章应用服务
func NewArticleApplicationService(
articleRepo repositories.ArticleRepository,
categoryRepo repositories.CategoryRepository,
tagRepo repositories.TagRepository,
articleService *services.ArticleService,
taskManager task_interfaces.TaskManager,
logger *zap.Logger,
) ArticleApplicationService {
return &ArticleApplicationServiceImpl{
articleRepo: articleRepo,
categoryRepo: categoryRepo,
tagRepo: tagRepo,
articleService: articleService,
taskManager: taskManager,
logger: logger,
}
}
// CreateArticle 创建文章
func (s *ArticleApplicationServiceImpl) CreateArticle(ctx context.Context, cmd *commands.CreateArticleCommand) error {
// 1. 参数验证
if err := s.validateCreateArticle(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建文章实体
article := &entities.Article{
Title: cmd.Title,
Content: cmd.Content,
Summary: cmd.Summary,
CoverImage: cmd.CoverImage,
CategoryID: cmd.CategoryID,
IsFeatured: cmd.IsFeatured,
Status: entities.ArticleStatusDraft,
}
// 3. 调用领域服务验证
if err := s.articleService.ValidateArticle(article); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 4. 保存文章
_, err := s.articleRepo.Create(ctx, *article)
if err != nil {
s.logger.Error("创建文章失败", zap.Error(err))
return fmt.Errorf("创建文章失败: %w", err)
}
// 5. 处理标签关联
if len(cmd.TagIDs) > 0 {
for _, tagID := range cmd.TagIDs {
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
}
}
}
s.logger.Info("创建文章成功", zap.String("id", article.ID), zap.String("title", article.Title))
return nil
}
// UpdateArticle 更新文章
func (s *ArticleApplicationServiceImpl) UpdateArticle(ctx context.Context, cmd *commands.UpdateArticleCommand) error {
// 1. 获取原文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否可以编辑
if !article.CanEdit() {
return fmt.Errorf("文章状态不允许编辑")
}
// 3. 更新字段
if cmd.Title != "" {
article.Title = cmd.Title
}
if cmd.Content != "" {
article.Content = cmd.Content
}
if cmd.Summary != "" {
article.Summary = cmd.Summary
}
if cmd.CoverImage != "" {
article.CoverImage = cmd.CoverImage
}
if cmd.CategoryID != "" {
article.CategoryID = cmd.CategoryID
}
article.IsFeatured = cmd.IsFeatured
// 4. 验证更新后的文章
if err := s.articleService.ValidateArticle(&article); err != nil {
return fmt.Errorf("业务验证失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("更新文章失败: %w", err)
}
// 6. 处理标签关联
// 先清除现有标签
existingTags, _ := s.tagRepo.GetArticleTags(ctx, article.ID)
for _, tag := range existingTags {
s.tagRepo.RemoveTagFromArticle(ctx, article.ID, tag.ID)
}
// 添加新标签
for _, tagID := range cmd.TagIDs {
if err := s.tagRepo.AddTagToArticle(ctx, article.ID, tagID); err != nil {
s.logger.Warn("添加标签失败", zap.String("article_id", article.ID), zap.String("tag_id", tagID), zap.Error(err))
}
}
s.logger.Info("更新文章成功", zap.String("id", article.ID))
return nil
}
// DeleteArticle 删除文章
func (s *ArticleApplicationServiceImpl) DeleteArticle(ctx context.Context, cmd *commands.DeleteArticleCommand) error {
// 1. 检查文章是否存在
_, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 删除文章
if err := s.articleRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除文章失败: %w", err)
}
s.logger.Info("删除文章成功", zap.String("id", cmd.ID))
return nil
}
// GetArticleByID 根据ID获取文章
func (s *ArticleApplicationServiceImpl) GetArticleByID(ctx context.Context, query *appQueries.GetArticleQuery) (*responses.ArticleInfoResponse, error) {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("文章不存在: %w", err)
}
// 2. 转换为响应对象
response := responses.FromArticleEntity(&article)
s.logger.Info("获取文章成功", zap.String("id", article.ID))
return response, nil
}
// ListArticles 获取文章列表
func (s *ArticleApplicationServiceImpl) ListArticles(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListArticleQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
CategoryID: query.CategoryID,
TagID: query.TagID,
Title: query.Title,
Summary: query.Summary,
IsFeatured: query.IsFeatured,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
articles, total, err := s.articleRepo.ListArticles(ctx, repoQuery)
if err != nil {
s.logger.Error("获取文章列表失败", zap.Error(err))
return nil, fmt.Errorf("获取文章列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromArticleEntitiesToListItemList(articles)
response := &responses.ArticleListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
return response, nil
}
// ListArticlesForAdmin 获取文章列表(管理员端)
func (s *ArticleApplicationServiceImpl) ListArticlesForAdmin(ctx context.Context, query *appQueries.ListArticleQuery) (*responses.ArticleListResponse, error) {
// 1. 构建仓储查询
repoQuery := &repoQueries.ListArticleQuery{
Page: query.Page,
PageSize: query.PageSize,
Status: query.Status,
CategoryID: query.CategoryID,
TagID: query.TagID,
Title: query.Title,
Summary: query.Summary,
IsFeatured: query.IsFeatured,
OrderBy: query.OrderBy,
OrderDir: query.OrderDir,
}
// 2. 调用仓储
articles, total, err := s.articleRepo.ListArticlesForAdmin(ctx, repoQuery)
if err != nil {
s.logger.Error("获取文章列表失败", zap.Error(err))
return nil, fmt.Errorf("获取文章列表失败: %w", err)
}
// 3. 转换为响应对象
items := responses.FromArticleEntitiesToListItemList(articles)
response := &responses.ArticleListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}
s.logger.Info("获取文章列表成功", zap.Int64("total", total))
return response, nil
}
// PublishArticle 发布文章
func (s *ArticleApplicationServiceImpl) PublishArticle(ctx context.Context, cmd *commands.PublishArticleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 发布文章
if err := article.Publish(); err != nil {
return fmt.Errorf("发布文章失败: %w", err)
}
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("发布文章失败: %w", err)
}
s.logger.Info("发布文章成功", zap.String("id", article.ID))
return nil
}
// PublishArticleByID 通过ID发布文章 (用于定时任务)
func (s *ArticleApplicationServiceImpl) PublishArticleByID(ctx context.Context, articleID string) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, articleID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", articleID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否已取消定时发布
if !article.IsScheduled() {
s.logger.Info("文章定时发布已取消,跳过执行",
zap.String("id", articleID),
zap.String("status", string(article.Status)))
return nil // 静默返回,不报错
}
// 3. 检查定时发布时间是否匹配
if article.ScheduledAt == nil {
s.logger.Info("文章没有定时发布时间,跳过执行",
zap.String("id", articleID))
return nil
}
// 4. 发布文章
if err := article.Publish(); err != nil {
return fmt.Errorf("发布文章失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("发布文章失败: %w", err)
}
s.logger.Info("定时发布文章成功", zap.String("id", article.ID))
return nil
}
// SchedulePublishArticle 定时发布文章
func (s *ArticleApplicationServiceImpl) SchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 3. 取消旧任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消旧任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
}
// 4. 创建任务工厂
taskFactory := task_entities.NewTaskFactoryWithManager(s.taskManager)
// 5. 创建并异步入队文章发布任务
if err := taskFactory.CreateAndEnqueueArticlePublishTask(
ctx,
cmd.ID,
scheduledTime,
"system", // 暂时使用系统用户ID
); err != nil {
s.logger.Error("创建并入队文章发布任务失败", zap.Error(err))
return err
}
// 6. 设置定时发布
if err := article.SchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("设置定时发布失败: %w", err)
}
// 7. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("设置定时发布失败: %w", err)
}
s.logger.Info("设置定时发布成功", zap.String("id", article.ID), zap.Time("scheduled_time", scheduledTime))
return nil
}
// CancelSchedulePublishArticle 取消定时发布文章
func (s *ArticleApplicationServiceImpl) CancelSchedulePublishArticle(ctx context.Context, cmd *commands.CancelScheduleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 检查是否已设置定时发布
if !article.IsScheduled() {
return fmt.Errorf("文章未设置定时发布")
}
// 3. 取消定时任务
if err := s.taskManager.CancelTask(ctx, cmd.ID); err != nil {
s.logger.Warn("取消定时任务失败", zap.String("article_id", cmd.ID), zap.Error(err))
// 不返回错误,继续执行取消定时发布
}
// 4. 取消定时发布
if err := article.CancelSchedulePublish(); err != nil {
return fmt.Errorf("取消定时发布失败: %w", err)
}
// 5. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("取消定时发布失败: %w", err)
}
s.logger.Info("取消定时发布成功", zap.String("id", article.ID))
return nil
}
// ArchiveArticle 归档文章
func (s *ArticleApplicationServiceImpl) ArchiveArticle(ctx context.Context, cmd *commands.ArchiveArticleCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 归档文章
if err := article.Archive(); err != nil {
return fmt.Errorf("归档文章失败: %w", err)
}
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("归档文章失败: %w", err)
}
s.logger.Info("归档文章成功", zap.String("id", article.ID))
return nil
}
// SetFeatured 设置推荐状态
func (s *ArticleApplicationServiceImpl) SetFeatured(ctx context.Context, cmd *commands.SetFeaturedCommand) error {
// 1. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 2. 设置推荐状态
article.SetFeatured(cmd.IsFeatured)
// 3. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("设置推荐状态失败: %w", err)
}
s.logger.Info("设置推荐状态成功", zap.String("id", article.ID), zap.Bool("is_featured", cmd.IsFeatured))
return nil
}
// RecordView 记录阅读
func (s *ArticleApplicationServiceImpl) RecordView(ctx context.Context, articleID string, userID string, ipAddress string, userAgent string) error {
// 1. 增加阅读量
if err := s.articleRepo.IncrementViewCount(ctx, articleID); err != nil {
s.logger.Error("增加阅读量失败", zap.String("id", articleID), zap.Error(err))
return fmt.Errorf("记录阅读失败: %w", err)
}
s.logger.Info("记录阅读成功", zap.String("id", articleID))
return nil
}
// GetArticleStats 获取文章统计
func (s *ArticleApplicationServiceImpl) GetArticleStats(ctx context.Context) (*responses.ArticleStatsResponse, error) {
// 1. 获取各种统计
totalArticles, err := s.articleRepo.CountByStatus(ctx, "")
if err != nil {
s.logger.Error("获取文章总数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
publishedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusPublished)
if err != nil {
s.logger.Error("获取已发布文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
draftArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusDraft)
if err != nil {
s.logger.Error("获取草稿文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
archivedArticles, err := s.articleRepo.CountByStatus(ctx, entities.ArticleStatusArchived)
if err != nil {
s.logger.Error("获取归档文章数失败", zap.Error(err))
return nil, fmt.Errorf("获取统计信息失败: %w", err)
}
response := &responses.ArticleStatsResponse{
TotalArticles: totalArticles,
PublishedArticles: publishedArticles,
DraftArticles: draftArticles,
ArchivedArticles: archivedArticles,
TotalViews: 0, // TODO: 实现总阅读量统计
}
s.logger.Info("获取文章统计成功")
return response, nil
}
// validateCreateArticle 验证创建文章参数
func (s *ArticleApplicationServiceImpl) validateCreateArticle(cmd *commands.CreateArticleCommand) error {
if cmd.Title == "" {
return fmt.Errorf("文章标题不能为空")
}
if cmd.Content == "" {
return fmt.Errorf("文章内容不能为空")
}
return nil
}
// ==================== 分类相关方法 ====================
// CreateCategory 创建分类
func (s *ArticleApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateCategory(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建分类实体
category := &entities.Category{
Name: cmd.Name,
Description: cmd.Description,
}
// 3. 保存分类
_, err := s.categoryRepo.Create(ctx, *category)
if err != nil {
s.logger.Error("创建分类失败", zap.Error(err))
return fmt.Errorf("创建分类失败: %w", err)
}
s.logger.Info("创建分类成功", zap.String("id", category.ID), zap.String("name", category.Name))
return nil
}
// UpdateCategory 更新分类
func (s *ArticleApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
// 1. 获取原分类
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 更新字段
category.Name = cmd.Name
category.Description = cmd.Description
// 3. 保存更新
if err := s.categoryRepo.Update(ctx, category); err != nil {
s.logger.Error("更新分类失败", zap.String("id", category.ID), zap.Error(err))
return fmt.Errorf("更新分类失败: %w", err)
}
s.logger.Info("更新分类成功", zap.String("id", category.ID))
return nil
}
// DeleteCategory 删除分类
func (s *ArticleApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
// 1. 检查分类是否存在
category, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 检查是否有文章使用此分类
count, err := s.articleRepo.CountByCategoryID(ctx, cmd.ID)
if err != nil {
s.logger.Error("检查分类使用情况失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除分类失败: %w", err)
}
if count > 0 {
return fmt.Errorf("该分类下还有 %d 篇文章,无法删除", count)
}
// 3. 删除分类
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除分类失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除分类失败: %w", err)
}
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("name", category.Name))
return nil
}
// GetCategoryByID 获取分类详情
func (s *ArticleApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *appQueries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
// 1. 获取分类
category, err := s.categoryRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取分类失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("分类不存在: %w", err)
}
// 2. 转换为响应对象
response := &responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
SortOrder: category.SortOrder,
CreatedAt: category.CreatedAt,
}
return response, nil
}
// ListCategories 获取分类列表
func (s *ArticleApplicationServiceImpl) ListCategories(ctx context.Context) (*responses.CategoryListResponse, error) {
// 1. 获取分类列表
categories, err := s.categoryRepo.List(ctx, shared_interfaces.ListOptions{})
if err != nil {
s.logger.Error("获取分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取分类列表失败: %w", err)
}
// 2. 转换为响应对象
items := make([]responses.CategoryInfoResponse, len(categories))
for i, category := range categories {
items[i] = responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
SortOrder: category.SortOrder,
CreatedAt: category.CreatedAt,
}
}
response := &responses.CategoryListResponse{
Items: items,
Total: len(items),
}
return response, nil
}
// ==================== 标签相关方法 ====================
// CreateTag 创建标签
func (s *ArticleApplicationServiceImpl) CreateTag(ctx context.Context, cmd *commands.CreateTagCommand) error {
// 1. 参数验证
if err := s.validateCreateTag(cmd); err != nil {
return fmt.Errorf("参数验证失败: %w", err)
}
// 2. 创建标签实体
tag := &entities.Tag{
Name: cmd.Name,
Color: cmd.Color,
}
// 3. 保存标签
_, err := s.tagRepo.Create(ctx, *tag)
if err != nil {
s.logger.Error("创建标签失败", zap.Error(err))
return fmt.Errorf("创建标签失败: %w", err)
}
s.logger.Info("创建标签成功", zap.String("id", tag.ID), zap.String("name", tag.Name))
return nil
}
// UpdateTag 更新标签
func (s *ArticleApplicationServiceImpl) UpdateTag(ctx context.Context, cmd *commands.UpdateTagCommand) error {
// 1. 获取原标签
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("标签不存在: %w", err)
}
// 2. 更新字段
tag.Name = cmd.Name
tag.Color = cmd.Color
// 3. 保存更新
if err := s.tagRepo.Update(ctx, tag); err != nil {
s.logger.Error("更新标签失败", zap.String("id", tag.ID), zap.Error(err))
return fmt.Errorf("更新标签失败: %w", err)
}
s.logger.Info("更新标签成功", zap.String("id", tag.ID))
return nil
}
// DeleteTag 删除标签
func (s *ArticleApplicationServiceImpl) DeleteTag(ctx context.Context, cmd *commands.DeleteTagCommand) error {
// 1. 检查标签是否存在
tag, err := s.tagRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("标签不存在: %w", err)
}
// 2. 删除标签
if err := s.tagRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除标签失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("删除标签失败: %w", err)
}
s.logger.Info("删除标签成功", zap.String("id", cmd.ID), zap.String("name", tag.Name))
return nil
}
// GetTagByID 获取标签详情
func (s *ArticleApplicationServiceImpl) GetTagByID(ctx context.Context, query *appQueries.GetTagQuery) (*responses.TagInfoResponse, error) {
// 1. 获取标签
tag, err := s.tagRepo.GetByID(ctx, query.ID)
if err != nil {
s.logger.Error("获取标签失败", zap.String("id", query.ID), zap.Error(err))
return nil, fmt.Errorf("标签不存在: %w", err)
}
// 2. 转换为响应对象
response := &responses.TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
return response, nil
}
// ListTags 获取标签列表
func (s *ArticleApplicationServiceImpl) ListTags(ctx context.Context) (*responses.TagListResponse, error) {
// 1. 获取标签列表
tags, err := s.tagRepo.List(ctx, shared_interfaces.ListOptions{})
if err != nil {
s.logger.Error("获取标签列表失败", zap.Error(err))
return nil, fmt.Errorf("获取标签列表失败: %w", err)
}
// 2. 转换为响应对象
items := make([]responses.TagInfoResponse, len(tags))
for i, tag := range tags {
items[i] = responses.TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
response := &responses.TagListResponse{
Items: items,
Total: len(items),
}
return response, nil
}
// UpdateSchedulePublishArticle 修改定时发布时间
func (s *ArticleApplicationServiceImpl) UpdateSchedulePublishArticle(ctx context.Context, cmd *commands.SchedulePublishCommand) error {
// 1. 解析定时发布时间
scheduledTime, err := cmd.GetScheduledTime()
if err != nil {
s.logger.Error("解析定时发布时间失败", zap.String("scheduled_time", cmd.ScheduledTime), zap.Error(err))
return fmt.Errorf("定时发布时间格式错误: %w", err)
}
// 2. 获取文章
article, err := s.articleRepo.GetByID(ctx, cmd.ID)
if err != nil {
s.logger.Error("获取文章失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("文章不存在: %w", err)
}
// 3. 检查是否已设置定时发布
if !article.IsScheduled() {
return fmt.Errorf("文章未设置定时发布,无法修改时间")
}
// 4. 更新数据库中的任务调度时间
if err := s.taskManager.UpdateTaskSchedule(ctx, cmd.ID, scheduledTime); err != nil {
s.logger.Error("更新任务调度时间失败", zap.String("id", cmd.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
// 5. 更新定时发布
if err := article.UpdateSchedulePublish(scheduledTime); err != nil {
return fmt.Errorf("更新定时发布失败: %w", err)
}
// 6. 保存更新
if err := s.articleRepo.Update(ctx, article); err != nil {
s.logger.Error("更新文章失败", zap.String("id", article.ID), zap.Error(err))
return fmt.Errorf("修改定时发布时间失败: %w", err)
}
s.logger.Info("修改定时发布时间成功",
zap.String("id", article.ID),
zap.Time("new_scheduled_time", scheduledTime))
return nil
}
// ==================== 验证方法 ====================
// validateCreateCategory 验证创建分类参数
func (s *ArticleApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
if cmd.Name == "" {
return fmt.Errorf("分类名称不能为空")
}
if len(cmd.Name) > 50 {
return fmt.Errorf("分类名称长度不能超过50个字符")
}
if len(cmd.Description) > 200 {
return fmt.Errorf("分类描述长度不能超过200个字符")
}
return nil
}
// validateCreateTag 验证创建标签参数
func (s *ArticleApplicationServiceImpl) validateCreateTag(cmd *commands.CreateTagCommand) error {
if cmd.Name == "" {
return fmt.Errorf("标签名称不能为空")
}
if len(cmd.Name) > 30 {
return fmt.Errorf("标签名称长度不能超过30个字符")
}
if cmd.Color == "" {
return fmt.Errorf("标签颜色不能为空")
}
// TODO: 添加十六进制颜色格式验证
return nil
}

View File

@@ -0,0 +1,104 @@
package commands
import (
"fmt"
"time"
)
// CreateAnnouncementCommand 创建公告命令
type CreateAnnouncementCommand struct {
Title string `json:"title" binding:"required" comment:"公告标题"`
Content string `json:"content" binding:"required" comment:"公告内容"`
}
// UpdateAnnouncementCommand 更新公告命令
type UpdateAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
}
// DeleteAnnouncementCommand 删除公告命令
type DeleteAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// PublishAnnouncementCommand 发布公告命令
type PublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// WithdrawAnnouncementCommand 撤回公告命令
type WithdrawAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// ArchiveAnnouncementCommand 归档公告命令
type ArchiveAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}
// SchedulePublishAnnouncementCommand 定时发布公告命令
type SchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
type UpdateSchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
}
// GetScheduledTime 获取解析后的定时发布时间
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
// 定义中国东八区时区
cst := time.FixedZone("CST", 8*3600)
// 支持多种时间格式
formats := []string{
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
"2006-01-02 15:04", // "2025-09-02 14:12"
time.RFC3339, // "2025-09-02T14:12:01+08:00"
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
// 确保返回的时间是东八区时区
return t.In(cst), nil
}
}
return time.Time{}, fmt.Errorf("不支持的时间格式: %s请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
}
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
type CancelSchedulePublishAnnouncementCommand struct {
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
package queries
import "hyapi-server/internal/domains/article/entities"
// ListAnnouncementQuery 公告列表查询
type ListAnnouncementQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Status entities.AnnouncementStatus `form:"status" comment:"公告状态"`
Title string `form:"title" comment:"标题关键词"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// GetAnnouncementQuery 获取公告详情查询
type GetAnnouncementQuery struct {
ID string `uri:"id" binding:"required" comment:"公告ID"`
}

View File

@@ -0,0 +1,54 @@
package queries
import "hyapi-server/internal/domains/article/entities"
// ListArticleQuery 文章列表查询
type ListArticleQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
CategoryID string `form:"category_id" comment:"分类ID"`
TagID string `form:"tag_id" comment:"标签ID"`
Title string `form:"title" comment:"标题关键词"`
Summary string `form:"summary" comment:"摘要关键词"`
IsFeatured *bool `form:"is_featured" comment:"是否推荐"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// SearchArticleQuery 文章搜索查询
type SearchArticleQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" comment:"搜索关键词"`
CategoryID string `form:"category_id" comment:"分类ID"`
AuthorID string `form:"author_id" comment:"作者ID"`
Status entities.ArticleStatus `form:"status" comment:"文章状态"`
OrderBy string `form:"order_by" comment:"排序字段"`
OrderDir string `form:"order_dir" comment:"排序方向"`
}
// GetArticleQuery 获取文章详情查询
type GetArticleQuery struct {
ID string `uri:"id" binding:"required" comment:"文章ID"`
}
// GetArticlesByAuthorQuery 获取作者文章查询
type GetArticlesByAuthorQuery struct {
AuthorID string `uri:"author_id" binding:"required" comment:"作者ID"`
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}
// GetArticlesByCategoryQuery 获取分类文章查询
type GetArticlesByCategoryQuery struct {
CategoryID string `uri:"category_id" binding:"required" comment:"分类ID"`
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}
// GetFeaturedArticlesQuery 获取推荐文章查询
type GetFeaturedArticlesQuery struct {
Page int `form:"page" binding:"min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
}

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
package responses
import (
"time"
"hyapi-server/internal/domains/article/entities"
)
// AnnouncementInfoResponse 公告详情响应
type AnnouncementInfoResponse struct {
ID string `json:"id" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
Status string `json:"status" comment:"公告状态"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// AnnouncementListItemResponse 公告列表项响应
type AnnouncementListItemResponse struct {
ID string `json:"id" comment:"公告ID"`
Title string `json:"title" comment:"公告标题"`
Content string `json:"content" comment:"公告内容"`
Status string `json:"status" comment:"公告状态"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// AnnouncementListResponse 公告列表响应
type AnnouncementListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []AnnouncementListItemResponse `json:"items" comment:"公告列表"`
}
// AnnouncementStatsResponse 公告统计响应
type AnnouncementStatsResponse struct {
TotalAnnouncements int64 `json:"total_announcements" comment:"公告总数"`
PublishedAnnouncements int64 `json:"published_announcements" comment:"已发布公告数"`
DraftAnnouncements int64 `json:"draft_announcements" comment:"草稿公告数"`
ArchivedAnnouncements int64 `json:"archived_announcements" comment:"归档公告数"`
ScheduledAnnouncements int64 `json:"scheduled_announcements" comment:"定时发布公告数"`
}
// FromAnnouncementEntity 从公告实体转换为响应对象
func FromAnnouncementEntity(announcement *entities.Announcement) *AnnouncementInfoResponse {
if announcement == nil {
return nil
}
return &AnnouncementInfoResponse{
ID: announcement.ID,
Title: announcement.Title,
Content: announcement.Content,
Status: string(announcement.Status),
ScheduledAt: announcement.ScheduledAt,
CreatedAt: announcement.CreatedAt,
UpdatedAt: announcement.UpdatedAt,
}
}
// FromAnnouncementEntityList 从公告实体列表转换为列表项响应
func FromAnnouncementEntityList(announcements []*entities.Announcement) []AnnouncementListItemResponse {
items := make([]AnnouncementListItemResponse, 0, len(announcements))
for _, announcement := range announcements {
items = append(items, AnnouncementListItemResponse{
ID: announcement.ID,
Title: announcement.Title,
Content: announcement.Content,
Status: string(announcement.Status),
ScheduledAt: announcement.ScheduledAt,
CreatedAt: announcement.CreatedAt,
UpdatedAt: announcement.UpdatedAt,
})
}
return items
}

View File

@@ -0,0 +1,219 @@
package responses
import (
"time"
"hyapi-server/internal/domains/article/entities"
)
// ArticleInfoResponse 文章详情响应
type ArticleInfoResponse struct {
ID string `json:"id" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Content string `json:"content" comment:"文章内容"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
Status string `json:"status" comment:"文章状态"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
ViewCount int `json:"view_count" comment:"阅读量"`
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// ArticleListItemResponse 文章列表项响应不包含content
type ArticleListItemResponse struct {
ID string `json:"id" comment:"文章ID"`
Title string `json:"title" comment:"文章标题"`
Summary string `json:"summary" comment:"文章摘要"`
CoverImage string `json:"cover_image" comment:"封面图片"`
CategoryID string `json:"category_id" comment:"分类ID"`
Category *CategoryInfoResponse `json:"category,omitempty" comment:"分类信息"`
Status string `json:"status" comment:"文章状态"`
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
ViewCount int `json:"view_count" comment:"阅读量"`
Tags []TagInfoResponse `json:"tags" comment:"标签列表"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// ArticleListResponse 文章列表响应
type ArticleListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []ArticleListItemResponse `json:"items" comment:"文章列表"`
}
// CategoryInfoResponse 分类信息响应
type CategoryInfoResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Description string `json:"description" comment:"分类描述"`
SortOrder int `json:"sort_order" comment:"排序"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
}
// TagInfoResponse 标签信息响应
type TagInfoResponse struct {
ID string `json:"id" comment:"标签ID"`
Name string `json:"name" comment:"标签名称"`
Color string `json:"color" comment:"标签颜色"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
Total int `json:"total" comment:"总数"`
}
// TagListResponse 标签列表响应
type TagListResponse struct {
Items []TagInfoResponse `json:"items" comment:"标签列表"`
Total int `json:"total" comment:"总数"`
}
// ArticleStatsResponse 文章统计响应
type ArticleStatsResponse struct {
TotalArticles int64 `json:"total_articles" comment:"文章总数"`
PublishedArticles int64 `json:"published_articles" comment:"已发布文章数"`
DraftArticles int64 `json:"draft_articles" comment:"草稿文章数"`
ArchivedArticles int64 `json:"archived_articles" comment:"归档文章数"`
TotalViews int64 `json:"total_views" comment:"总阅读量"`
}
// FromArticleEntity 从文章实体转换为响应对象
func FromArticleEntity(article *entities.Article) *ArticleInfoResponse {
if article == nil {
return nil
}
response := &ArticleInfoResponse{
ID: article.ID,
Title: article.Title,
Content: article.Content,
Summary: article.Summary,
CoverImage: article.CoverImage,
CategoryID: article.CategoryID,
Status: string(article.Status),
IsFeatured: article.IsFeatured,
PublishedAt: article.PublishedAt,
ScheduledAt: article.ScheduledAt,
ViewCount: article.ViewCount,
CreatedAt: article.CreatedAt,
UpdatedAt: article.UpdatedAt,
}
// 转换分类信息
if article.Category != nil {
response.Category = &CategoryInfoResponse{
ID: article.Category.ID,
Name: article.Category.Name,
Description: article.Category.Description,
SortOrder: article.Category.SortOrder,
CreatedAt: article.Category.CreatedAt,
}
}
// 转换标签信息
if len(article.Tags) > 0 {
response.Tags = make([]TagInfoResponse, len(article.Tags))
for i, tag := range article.Tags {
response.Tags[i] = TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
}
return response
}
// FromArticleEntityToListItem 从文章实体转换为列表项响应对象不包含content
func FromArticleEntityToListItem(article *entities.Article) *ArticleListItemResponse {
if article == nil {
return nil
}
response := &ArticleListItemResponse{
ID: article.ID,
Title: article.Title,
Summary: article.Summary,
CoverImage: article.CoverImage,
CategoryID: article.CategoryID,
Status: string(article.Status),
IsFeatured: article.IsFeatured,
PublishedAt: article.PublishedAt,
ScheduledAt: article.ScheduledAt,
ViewCount: article.ViewCount,
CreatedAt: article.CreatedAt,
UpdatedAt: article.UpdatedAt,
}
// 转换分类信息
if article.Category != nil {
response.Category = &CategoryInfoResponse{
ID: article.Category.ID,
Name: article.Category.Name,
Description: article.Category.Description,
SortOrder: article.Category.SortOrder,
CreatedAt: article.Category.CreatedAt,
}
}
// 转换标签信息
if len(article.Tags) > 0 {
response.Tags = make([]TagInfoResponse, len(article.Tags))
for i, tag := range article.Tags {
response.Tags[i] = TagInfoResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
CreatedAt: tag.CreatedAt,
}
}
}
return response
}
// FromArticleEntities 从文章实体列表转换为响应对象列表
func FromArticleEntities(articles []*entities.Article) []ArticleInfoResponse {
if len(articles) == 0 {
return []ArticleInfoResponse{}
}
responses := make([]ArticleInfoResponse, len(articles))
for i, article := range articles {
if response := FromArticleEntity(article); response != nil {
responses[i] = *response
}
}
return responses
}
// FromArticleEntitiesToListItemList 从文章实体列表转换为列表项响应对象列表不包含content
func FromArticleEntitiesToListItemList(articles []*entities.Article) []ArticleListItemResponse {
if len(articles) == 0 {
return []ArticleListItemResponse{}
}
responses := make([]ArticleListItemResponse, len(articles))
for i, article := range articles {
if response := FromArticleEntityToListItem(article); response != nil {
responses[i] = *response
}
}
return responses
}

View File

@@ -0,0 +1,126 @@
package article
import (
"context"
"fmt"
"time"
"hyapi-server/internal/domains/article/entities"
"hyapi-server/internal/domains/article/repositories"
"go.uber.org/zap"
)
// TaskManagementService 任务管理服务
type TaskManagementService struct {
scheduledTaskRepo repositories.ScheduledTaskRepository
logger *zap.Logger
}
// NewTaskManagementService 创建任务管理服务
func NewTaskManagementService(
scheduledTaskRepo repositories.ScheduledTaskRepository,
logger *zap.Logger,
) *TaskManagementService {
return &TaskManagementService{
scheduledTaskRepo: scheduledTaskRepo,
logger: logger,
}
}
// GetTaskStatus 获取任务状态
func (s *TaskManagementService) GetTaskStatus(ctx context.Context, taskID string) (*entities.ScheduledTask, error) {
task, err := s.scheduledTaskRepo.GetByTaskID(ctx, taskID)
if err != nil {
return nil, fmt.Errorf("获取任务状态失败: %w", err)
}
return &task, nil
}
// GetArticleTaskStatus 获取文章的定时任务状态
func (s *TaskManagementService) GetArticleTaskStatus(ctx context.Context, articleID string) (*entities.ScheduledTask, error) {
task, err := s.scheduledTaskRepo.GetByArticleID(ctx, articleID)
if err != nil {
return nil, fmt.Errorf("获取文章定时任务状态失败: %w", err)
}
return &task, nil
}
// CancelTask 取消任务
func (s *TaskManagementService) CancelTask(ctx context.Context, taskID string) error {
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, taskID); err != nil {
return fmt.Errorf("取消任务失败: %w", err)
}
s.logger.Info("任务已取消", zap.String("task_id", taskID))
return nil
}
// GetActiveTasks 获取活动任务列表
func (s *TaskManagementService) GetActiveTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
tasks, err := s.scheduledTaskRepo.GetActiveTasks(ctx)
if err != nil {
return nil, fmt.Errorf("获取活动任务列表失败: %w", err)
}
return tasks, nil
}
// GetExpiredTasks 获取过期任务列表
func (s *TaskManagementService) GetExpiredTasks(ctx context.Context) ([]entities.ScheduledTask, error) {
tasks, err := s.scheduledTaskRepo.GetExpiredTasks(ctx)
if err != nil {
return nil, fmt.Errorf("获取过期任务列表失败: %w", err)
}
return tasks, nil
}
// CleanupExpiredTasks 清理过期任务
func (s *TaskManagementService) CleanupExpiredTasks(ctx context.Context) error {
expiredTasks, err := s.GetExpiredTasks(ctx)
if err != nil {
return err
}
for _, task := range expiredTasks {
if err := s.scheduledTaskRepo.MarkAsCancelled(ctx, task.TaskID); err != nil {
s.logger.Warn("清理过期任务失败", zap.String("task_id", task.TaskID), zap.Error(err))
continue
}
s.logger.Info("已清理过期任务", zap.String("task_id", task.TaskID))
}
return nil
}
// GetTaskStats 获取任务统计信息
func (s *TaskManagementService) GetTaskStats(ctx context.Context) (map[string]interface{}, error) {
activeTasks, err := s.GetActiveTasks(ctx)
if err != nil {
return nil, err
}
expiredTasks, err := s.GetExpiredTasks(ctx)
if err != nil {
return nil, err
}
stats := map[string]interface{}{
"active_tasks_count": len(activeTasks),
"expired_tasks_count": len(expiredTasks),
"total_tasks_count": len(activeTasks) + len(expiredTasks),
"next_task_time": nil,
"last_cleanup_time": time.Now(),
}
// 计算下一个任务时间
if len(activeTasks) > 0 {
nextTask := activeTasks[0]
for _, task := range activeTasks {
if task.ScheduledAt.Before(nextTask.ScheduledAt) {
nextTask = task
}
}
stats["next_task_time"] = nextTask.ScheduledAt
}
return stats, nil
}

View File

@@ -0,0 +1,55 @@
package certification
import (
"context"
"hyapi-server/internal/application/certification/dto/commands"
"hyapi-server/internal/application/certification/dto/queries"
"hyapi-server/internal/application/certification/dto/responses"
)
// CertificationApplicationService 认证应用服务接口
// 负责用例协调,提供精简的应用层接口
type CertificationApplicationService interface {
// ================ 用户操作用例 ================
// 提交企业信息
SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error)
// 确认状态
ConfirmAuth(ctx context.Context, cmd *queries.ConfirmAuthCommand) (*responses.ConfirmAuthResponse, error)
// 确认签署
ConfirmSign(ctx context.Context, cmd *queries.ConfirmSignCommand) (*responses.ConfirmSignResponse, error)
// 申请合同签署
ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error)
// OCR营业执照识别
RecognizeBusinessLicense(ctx context.Context, imageBytes []byte) (*responses.BusinessLicenseResult, error)
// ================ 查询用例 ================
// 获取认证详情
GetCertification(ctx context.Context, query *queries.GetCertificationQuery) (*responses.CertificationResponse, error)
// 获取认证列表(管理员)
ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error)
// ================ 管理员后台操作用例 ================
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
AdminCompleteCertificationWithoutContract(ctx context.Context, cmd *commands.AdminCompleteCertificationCommand) (*responses.CertificationResponse, error)
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
AdminListSubmitRecords(ctx context.Context, query *queries.AdminListSubmitRecordsQuery) (*responses.AdminSubmitRecordsListResponse, error)
// AdminGetSubmitRecordByID 管理端获取单条提交记录详情
AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error)
// AdminApproveSubmitRecord 管理端审核通过(按提交记录 ID
AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminRejectSubmitRecord 管理端审核拒绝(按提交记录 ID
AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error
// AdminTransitionCertificationStatus 管理端按用户变更认证状态以状态机为准info_submitted=通过 / info_rejected=拒绝)
AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error
// ================ e签宝回调处理 ================
// 处理e签宝回调
HandleEsignCallback(ctx context.Context, cmd *commands.EsignCallbackCommand) error
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,130 @@
package commands
import (
"hyapi-server/internal/domains/certification/enums"
)
// CreateCertificationCommand 创建认证申请命令
type CreateCertificationCommand struct {
UserID string `json:"-"`
}
// ApplyContractCommand 申请合同命令
type ApplyContractCommand struct {
UserID string `json:"user_id" validate:"required"`
}
// RetryOperationCommand 重试操作命令
type RetryOperationCommand struct {
CertificationID string `json:"certification_id" validate:"required"`
UserID string `json:"user_id" validate:"required"`
Operation string `json:"operation" validate:"required,oneof=enterprise_verification contract_application"`
Reason string `json:"reason,omitempty"`
}
// EsignCallbackCommand e签宝回调命令
type EsignCallbackCommand struct {
Data *EsignCallbackData `json:"data"`
Headers map[string]string `json:"headers"`
QueryParams map[string]string `json:"query_params"`
}
// EsignCallbackData e签宝回调数据结构
type EsignCallbackData struct {
Action string `json:"action"`
Timestamp int64 `json:"timestamp"`
AuthFlowId string `json:"authFlowId,omitempty"`
SignFlowId string `json:"signFlowId,omitempty"`
CustomBizNum string `json:"customBizNum,omitempty"`
SignOrder int `json:"signOrder,omitempty"`
OperateTime int64 `json:"operateTime,omitempty"`
SignResult int `json:"signResult,omitempty"`
ResultDescription string `json:"resultDescription,omitempty"`
AuthType string `json:"authType,omitempty"`
SignFlowStatus string `json:"signFlowStatus,omitempty"`
Operator *EsignOperator `json:"operator,omitempty"`
PsnInfo *EsignPsnInfo `json:"psnInfo,omitempty"`
Organization *EsignOrganization `json:"organization,omitempty"`
}
// EsignOperator 签署人信息
type EsignOperator struct {
PsnId string `json:"psnId"`
PsnAccount *EsignPsnAccount `json:"psnAccount"`
}
// EsignPsnInfo 个人认证信息
type EsignPsnInfo struct {
PsnId string `json:"psnId"`
PsnAccount *EsignPsnAccount `json:"psnAccount"`
}
// EsignPsnAccount 个人账户信息
type EsignPsnAccount struct {
AccountMobile string `json:"accountMobile"`
AccountEmail string `json:"accountEmail"`
}
// EsignOrganization 企业信息
type EsignOrganization struct {
OrgName string `json:"orgName"`
// 可以根据需要添加更多企业信息字段
}
// AdminCompleteCertificationCommand 管理员代用户完成认证命令(可不关联合同)
type AdminCompleteCertificationCommand struct {
// AdminID 从JWT中获取不从请求体传递因此不做必填校验
AdminID string `json:"-"`
UserID string `json:"user_id" validate:"required"`
CompanyName string `json:"company_name" validate:"required,min=2,max=100"`
UnifiedSocialCode string `json:"unified_social_code" validate:"required"`
LegalPersonName string `json:"legal_person_name" validate:"required,min=2,max=20"`
LegalPersonID string `json:"legal_person_id" validate:"required"`
LegalPersonPhone string `json:"legal_person_phone" validate:"required"`
EnterpriseAddress string `json:"enterprise_address" validate:"required"`
// 备注信息,用于记录后台操作原因
Reason string `json:"reason" validate:"required"`
}
// ForceTransitionStatusCommand 强制状态转换命令(管理员)
type ForceTransitionStatusCommand struct {
CertificationID string `json:"certification_id" validate:"required"`
AdminID string `json:"admin_id" validate:"required"`
TargetStatus enums.CertificationStatus `json:"target_status" validate:"required"`
Reason string `json:"reason" validate:"required"`
Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证
}
// AdminTransitionCertificationStatusCommand 管理端变更认证状态(以状态机为准,用于审核通过/拒绝等)
type AdminTransitionCertificationStatusCommand struct {
AdminID string `json:"-"`
UserID string `json:"user_id" validate:"required"`
TargetStatus string `json:"target_status" validate:"required,oneof=info_submitted info_rejected"` // 审核通过 -> info_submitted审核拒绝 -> info_rejected
Remark string `json:"remark"`
}
// SubmitEnterpriseInfoCommand 提交企业信息命令
type SubmitEnterpriseInfoCommand struct {
UserID string `json:"-" comment:"用户唯一标识从JWT token获取不在JSON中暴露"`
CompanyName string `json:"company_name" binding:"required,min=2,max=100" comment:"企业名称,如:北京科技有限公司"`
UnifiedSocialCode string `json:"unified_social_code" binding:"required,social_credit_code" comment:"统一社会信用代码18位企业唯一标识91110000123456789X"`
LegalPersonName string `json:"legal_person_name" binding:"required,min=2,max=20" comment:"法定代表人姓名,如:张三"`
LegalPersonID string `json:"legal_person_id" binding:"required,id_card" comment:"法定代表人身份证号码18位110101199001011234"`
LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号11位13800138000"`
EnterpriseAddress string `json:"enterprise_address" binding:"required,enterprise_address" comment:"企业地址,如:北京市海淀区"`
VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"`
// 营业执照图片 URL单张
BusinessLicenseImageURL string `json:"business_license_image_url" binding:"omitempty,url" comment:"营业执照图片URL"`
// 办公场地图片 URL 列表(前端传 string 数组)
OfficePlaceImageURLs []string `json:"office_place_image_urls" binding:"omitempty,dive,url" comment:"办公场地图片URL列表"`
// 授权代表信息(与前端 authorized_rep_* 及表字段一致)
AuthorizedRepName string `json:"authorized_rep_name" binding:"omitempty,min=2,max=20" comment:"授权代表姓名"`
AuthorizedRepID string `json:"authorized_rep_id" binding:"omitempty,id_card" comment:"授权代表身份证号"`
AuthorizedRepPhone string `json:"authorized_rep_phone" binding:"omitempty,phone" comment:"授权代表手机号"`
AuthorizedRepIDImageURLs []string `json:"authorized_rep_id_image_urls" binding:"omitempty,dive,url" comment:"授权代表身份证正反面图片URL"`
// 应用场景
APIUsage string `json:"api_usage" binding:"omitempty,min=5,max=500" comment:"接口用途及业务场景说明"`
ScenarioAttachmentURLs []string `json:"scenario_attachment_urls" binding:"omitempty,dive,url" comment:"场景附件图片URL列表"`
}

View File

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

View File

@@ -0,0 +1,204 @@
package queries
import (
"time"
"hyapi-server/internal/domains/certification/enums"
domainQueries "hyapi-server/internal/domains/certification/repositories/queries"
)
// GetCertificationQuery 获取认证详情查询
type GetCertificationQuery struct {
UserID string `json:"user_id,omitempty"` // 用于权限验证
}
// ConfirmAuthCommand 确认认证状态命令
type ConfirmAuthCommand struct {
UserID string `json:"-"`
}
// ConfirmSignCommand 确认签署状态命令
type ConfirmSignCommand struct {
UserID string `json:"-"`
}
// GetUserCertificationsQuery 获取用户认证列表查询
type GetUserCertificationsQuery struct {
UserID string `json:"user_id" validate:"required"`
Status enums.CertificationStatus `json:"status,omitempty"`
IncludeCompleted bool `json:"include_completed,omitempty"`
IncludeFailed bool `json:"include_failed,omitempty"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ToDomainQuery 转换为领域查询对象
func (q *GetUserCertificationsQuery) ToDomainQuery() *domainQueries.UserCertificationsQuery {
domainQuery := &domainQueries.UserCertificationsQuery{
UserID: q.UserID,
Status: q.Status,
IncludeCompleted: q.IncludeCompleted,
IncludeFailed: q.IncludeFailed,
Page: q.Page,
PageSize: q.PageSize,
}
domainQuery.DefaultValues()
return domainQuery
}
// ListCertificationsQuery 认证列表查询(管理员)
type ListCertificationsQuery struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
UserID string `json:"user_id,omitempty"`
Status enums.CertificationStatus `json:"status,omitempty"`
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
FailureReason enums.FailureReason `json:"failure_reason,omitempty"`
CreatedAfter *time.Time `json:"created_after,omitempty"`
CreatedBefore *time.Time `json:"created_before,omitempty"`
CompanyName string `json:"company_name,omitempty"`
LegalPersonName string `json:"legal_person_name,omitempty"`
SearchKeyword string `json:"search_keyword,omitempty"`
}
// ToDomainQuery 转换为领域查询对象
func (q *ListCertificationsQuery) ToDomainQuery() *domainQueries.ListCertificationsQuery {
domainQuery := &domainQueries.ListCertificationsQuery{
Page: q.Page,
PageSize: q.PageSize,
SortBy: q.SortBy,
SortOrder: q.SortOrder,
UserID: q.UserID,
Status: q.Status,
Statuses: q.Statuses,
FailureReason: q.FailureReason,
CreatedAfter: q.CreatedAfter,
CreatedBefore: q.CreatedBefore,
CompanyName: q.CompanyName,
LegalPersonName: q.LegalPersonName,
SearchKeyword: q.SearchKeyword,
}
domainQuery.DefaultValues()
return domainQuery
}
// SearchCertificationsQuery 搜索认证查询
type SearchCertificationsQuery struct {
Keyword string `json:"keyword" validate:"required,min=2"`
SearchFields []string `json:"search_fields,omitempty"`
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
UserID string `json:"user_id,omitempty"`
Page int `json:"page"`
PageSize int `json:"page_size"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
ExactMatch bool `json:"exact_match,omitempty"`
}
// ToDomainQuery 转换为领域查询对象
func (q *SearchCertificationsQuery) ToDomainQuery() *domainQueries.SearchCertificationsQuery {
domainQuery := &domainQueries.SearchCertificationsQuery{
Keyword: q.Keyword,
SearchFields: q.SearchFields,
Statuses: q.Statuses,
UserID: q.UserID,
Page: q.Page,
PageSize: q.PageSize,
SortBy: q.SortBy,
SortOrder: q.SortOrder,
ExactMatch: q.ExactMatch,
}
domainQuery.DefaultValues()
return domainQuery
}
// GetCertificationStatisticsQuery 认证统计查询
type GetCertificationStatisticsQuery struct {
StartDate time.Time `json:"start_date" validate:"required"`
EndDate time.Time `json:"end_date" validate:"required"`
Period string `json:"period" validate:"oneof=daily weekly monthly yearly"`
GroupBy []string `json:"group_by,omitempty"`
UserIDs []string `json:"user_ids,omitempty"`
Statuses []enums.CertificationStatus `json:"statuses,omitempty"`
IncludeProgressStats bool `json:"include_progress_stats,omitempty"`
IncludeRetryStats bool `json:"include_retry_stats,omitempty"`
IncludeTimeStats bool `json:"include_time_stats,omitempty"`
}
// ToDomainQuery 转换为领域查询对象
func (q *GetCertificationStatisticsQuery) ToDomainQuery() *domainQueries.CertificationStatisticsQuery {
return &domainQueries.CertificationStatisticsQuery{
StartDate: q.StartDate,
EndDate: q.EndDate,
Period: q.Period,
GroupBy: q.GroupBy,
UserIDs: q.UserIDs,
Statuses: q.Statuses,
IncludeProgressStats: q.IncludeProgressStats,
IncludeRetryStats: q.IncludeRetryStats,
IncludeTimeStats: q.IncludeTimeStats,
}
}
// GetSystemMonitoringQuery 系统监控查询
type GetSystemMonitoringQuery struct {
TimeRange string `json:"time_range" validate:"oneof=1h 6h 24h 7d 30d"`
Metrics []string `json:"metrics,omitempty"` // 指定要获取的指标类型
}
// GetAvailableMetrics 获取可用的监控指标
func (q *GetSystemMonitoringQuery) GetAvailableMetrics() []string {
return []string{
"certification_count",
"success_rate",
"failure_rate",
"avg_processing_time",
"status_distribution",
"retry_count",
"esign_callback_success_rate",
}
}
// GetTimeRangeDuration 获取时间范围对应的持续时间
func (q *GetSystemMonitoringQuery) GetTimeRangeDuration() time.Duration {
switch q.TimeRange {
case "1h":
return time.Hour
case "6h":
return 6 * time.Hour
case "24h":
return 24 * time.Hour
case "7d":
return 7 * 24 * time.Hour
case "30d":
return 30 * 24 * time.Hour
default:
return 24 * time.Hour // 默认24小时
}
}
// ShouldIncludeMetric 检查是否应该包含指定指标
func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool {
if len(q.Metrics) == 0 {
return true // 如果没有指定,包含所有指标
}
for _, m := range q.Metrics {
if m == metric {
return true
}
}
return false
}
// AdminListSubmitRecordsQuery 管理端企业信息提交记录列表查询(以状态机 certification_status 为准,不做审核状态筛选)
type AdminListSubmitRecordsQuery struct {
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
CertificationStatus string `json:"certification_status" form:"certification_status"` // 按认证状态筛选,如 info_pending_review / info_submitted / info_rejected空为全部
CompanyName string `json:"company_name" form:"company_name"` // 企业名称(模糊搜索)
LegalPersonPhone string `json:"legal_person_phone" form:"legal_person_phone"` // 法人手机号
LegalPersonName string `json:"legal_person_name" form:"legal_person_name"` // 法人姓名(模糊搜索)
}

View File

@@ -0,0 +1,236 @@
package responses
import (
"time"
"hyapi-server/internal/domains/certification/entities/value_objects"
"hyapi-server/internal/domains/certification/enums"
)
// CertificationResponse 认证响应
type CertificationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Status enums.CertificationStatus `json:"status"`
StatusName string `json:"status_name"`
Progress int `json:"progress"`
// 企业信息
EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info,omitempty"`
// 合同信息
ContractInfo *value_objects.ContractInfo `json:"contract_info,omitempty"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"`
EnterpriseVerifiedAt *time.Time `json:"enterprise_verified_at,omitempty"`
ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"`
ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
// 业务状态
IsCompleted bool `json:"is_completed"`
IsFailed bool `json:"is_failed"`
IsUserActionRequired bool `json:"is_user_action_required"`
// 失败信息
FailureReason enums.FailureReason `json:"failure_reason,omitempty"`
FailureReasonName string `json:"failure_reason_name,omitempty"`
FailureMessage string `json:"failure_message,omitempty"`
CanRetry bool `json:"can_retry,omitempty"`
RetryCount int `json:"retry_count,omitempty"`
// 用户操作提示
NextAction string `json:"next_action,omitempty"`
AvailableActions []string `json:"available_actions,omitempty"`
// 元数据
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ConfirmAuthResponse 确认认证状态响应
type ConfirmAuthResponse struct {
Status enums.CertificationStatus `json:"status"`
Reason string `json:"reason"`
}
// ConfirmSignResponse 确认签署状态响应
type ConfirmSignResponse struct {
Status enums.CertificationStatus `json:"status"`
Reason string `json:"reason"`
}
// CertificationListResponse 认证列表响应
type CertificationListResponse struct {
Items []*CertificationResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ContractSignUrlResponse 合同签署URL响应
type ContractSignUrlResponse struct {
CertificationID string `json:"certification_id"`
ContractSignURL string `json:"contract_sign_url"`
ContractURL string `json:"contract_url,omitempty"`
ExpireAt *time.Time `json:"expire_at,omitempty"`
NextAction string `json:"next_action"`
Message string `json:"message"`
}
// SystemMonitoringResponse 系统监控响应
type SystemMonitoringResponse struct {
TimeRange string `json:"time_range"`
Metrics map[string]interface{} `json:"metrics"`
Alerts []SystemAlert `json:"alerts,omitempty"`
SystemHealth SystemHealthStatus `json:"system_health"`
LastUpdatedAt time.Time `json:"last_updated_at"`
}
// SystemAlert 系统警告
type SystemAlert struct {
Level string `json:"level"` // info, warning, error, critical
Type string `json:"type"` // 警告类型
Message string `json:"message"` // 警告消息
Metric string `json:"metric"` // 相关指标
Value interface{} `json:"value"` // 当前值
Threshold interface{} `json:"threshold"` // 阈值
CreatedAt time.Time `json:"created_at"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// SystemHealthStatus 系统健康状态
type SystemHealthStatus struct {
Overall string `json:"overall"` // healthy, warning, critical
Components map[string]string `json:"components"` // 各组件状态
LastCheck time.Time `json:"last_check"`
Details map[string]interface{} `json:"details,omitempty"`
}
// AdminSubmitRecordItem 管理端提交记录列表项
type AdminSubmitRecordItem struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
SubmitAt time.Time `json:"submit_at"`
Status string `json:"status"`
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准info_pending_review/info_submitted/info_rejected 等
}
// AdminSubmitRecordDetail 管理端提交记录详情(含完整信息与图片 URL
type AdminSubmitRecordDetail struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CompanyName string `json:"company_name"`
UnifiedSocialCode string `json:"unified_social_code"`
LegalPersonName string `json:"legal_person_name"`
LegalPersonID string `json:"legal_person_id"`
LegalPersonPhone string `json:"legal_person_phone"`
EnterpriseAddress string `json:"enterprise_address"`
AuthorizedRepName string `json:"authorized_rep_name"`
AuthorizedRepID string `json:"authorized_rep_id"`
AuthorizedRepPhone string `json:"authorized_rep_phone"`
AuthorizedRepIDImageURLs string `json:"authorized_rep_id_image_urls"` // JSON 字符串或解析后数组
BusinessLicenseImageURL string `json:"business_license_image_url"`
OfficePlaceImageURLs string `json:"office_place_image_urls"` // JSON 数组字符串
APIUsage string `json:"api_usage"`
ScenarioAttachmentURLs string `json:"scenario_attachment_urls"`
Status string `json:"status"`
SubmitAt time.Time `json:"submit_at"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
FailedAt *time.Time `json:"failed_at,omitempty"`
FailureReason string `json:"failure_reason,omitempty"`
CertificationStatus string `json:"certification_status,omitempty"` // 以状态机为准
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AdminSubmitRecordsListResponse 管理端提交记录列表响应
type AdminSubmitRecordsListResponse struct {
Items []*AdminSubmitRecordItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ================ 响应构建辅助方法 ================
// NewCertificationListResponse 创建认证列表响应
func NewCertificationListResponse(items []*CertificationResponse, total int64, page, pageSize int) *CertificationListResponse {
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
if totalPages == 0 {
totalPages = 1
}
return &CertificationListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}
}
// NewContractSignUrlResponse 创建合同签署URL响应
func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextAction, message string) *ContractSignUrlResponse {
response := &ContractSignUrlResponse{
CertificationID: certificationID,
ContractSignURL: signURL,
ContractURL: contractURL,
NextAction: nextAction,
Message: message,
}
// 设置过期时间默认24小时
expireAt := time.Now().Add(24 * time.Hour)
response.ExpireAt = &expireAt
return response
}
// NewSystemAlert 创建系统警告
func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert {
return &SystemAlert{
Level: level,
Type: alertType,
Message: message,
Metric: metric,
Value: value,
Threshold: threshold,
CreatedAt: time.Now(),
Metadata: make(map[string]interface{}),
}
}
// IsHealthy 检查系统是否健康
func (r *SystemMonitoringResponse) IsHealthy() bool {
return r.SystemHealth.Overall == "healthy"
}
// GetCriticalAlerts 获取严重警告
func (r *SystemMonitoringResponse) GetCriticalAlerts() []*SystemAlert {
var criticalAlerts []*SystemAlert
for i := range r.Alerts {
if r.Alerts[i].Level == "critical" {
criticalAlerts = append(criticalAlerts, &r.Alerts[i])
}
}
return criticalAlerts
}
// HasAlerts 检查是否有警告
func (r *SystemMonitoringResponse) HasAlerts() bool {
return len(r.Alerts) > 0
}
// GetMetricValue 获取指标值
func (r *SystemMonitoringResponse) GetMetricValue(metric string) (interface{}, bool) {
value, exists := r.Metrics[metric]
return value, exists
}

View File

@@ -0,0 +1,9 @@
package responses
// ContractSignURLResponse 合同签署链接响应
type ContractSignURLResponse struct {
SignURL string `json:"sign_url"` // 签署链接
ShortURL string `json:"short_url"` // 短链接
SignFlowID string `json:"sign_flow_id"` // 签署流程ID
ExpireAt string `json:"expire_at"` // 过期时间
}

View File

@@ -0,0 +1,55 @@
package responses
import "time"
// BusinessLicenseResult 营业执照识别结果
type BusinessLicenseResult struct {
CompanyName string `json:"company_name"` // 企业名称
UnifiedSocialCode string `json:"unified_social_code"` // 统一社会信用代码
LegalPersonName string `json:"legal_person_name"` // 法定代表人姓名
LegalPersonID string `json:"legal_person_id"` // 法定代表人身份证号
RegisteredCapital string `json:"registered_capital"` // 注册资本
BusinessScope string `json:"business_scope"` // 经营范围
Address string `json:"address"` // 企业地址
IssueDate string `json:"issue_date"` // 发证日期
ValidPeriod string `json:"valid_period"` // 有效期
Confidence float64 `json:"confidence"` // 识别置信度
ProcessedAt time.Time `json:"processed_at"` // 处理时间
}
// IDCardResult 身份证识别结果
type IDCardResult struct {
Name string `json:"name"` // 姓名
IDCardNumber string `json:"id_card_number"` // 身份证号
Gender string `json:"gender"` // 性别
Nation string `json:"nation"` // 民族
Birthday string `json:"birthday"` // 出生日期
Address string `json:"address"` // 住址
IssuingAgency string `json:"issuing_agency"` // 签发机关
ValidPeriod string `json:"valid_period"` // 有效期限
Side string `json:"side"` // 身份证面front/back
Confidence float64 `json:"confidence"` // 识别置信度
ProcessedAt time.Time `json:"processed_at"` // 处理时间
}
// GeneralTextResult 通用文字识别结果
type GeneralTextResult struct {
Words []TextLine `json:"words"` // 识别的文字行
Confidence float64 `json:"confidence"` // 整体置信度
ProcessedAt time.Time `json:"processed_at"` // 处理时间
}
// TextLine 文字行
type TextLine struct {
Text string `json:"text"` // 文字内容
Confidence float64 `json:"confidence"` // 置信度
Position Position `json:"position"` // 位置信息
}
// Position 位置信息
type Position struct {
X int `json:"x"` // X坐标
Y int `json:"y"` // Y坐标
Width int `json:"width"` // 宽度
Height int `json:"height"` // 高度
}

View File

@@ -0,0 +1,38 @@
package commands
// CreateWalletCommand 创建钱包命令
type CreateWalletCommand struct {
UserID string `json:"user_id" binding:"required,uuid"`
}
// TransferRechargeCommand 对公转账充值命令
type TransferRechargeCommand struct {
UserID string `json:"user_id" binding:"required,uuid"`
Amount string `json:"amount" binding:"required"`
TransferOrderID string `json:"transfer_order_id" binding:"required" comment:"转账订单号"`
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
}
// GiftRechargeCommand 赠送充值命令
type GiftRechargeCommand struct {
UserID string `json:"user_id" binding:"required,uuid"`
Amount string `json:"amount" binding:"required"`
Notes string `json:"notes" binding:"omitempty,max=500" comment:"备注信息"`
}
// CreateAlipayRechargeCommand 创建支付宝充值订单命令
type CreateAlipayRechargeCommand struct {
UserID string `json:"-"` // 用户ID从token获取
Amount string `json:"amount" binding:"required"` // 充值金额
Subject string `json:"-"` // 订单标题
Platform string `json:"platform" binding:"required,oneof=app h5 pc"` // 支付平台app/h5/pc
}
// CreateWechatRechargeCommand 创建微信充值订单命令
type CreateWechatRechargeCommand struct {
UserID string `json:"-"` // 用户ID从token获取
Amount string `json:"amount" binding:"required"` // 充值金额
Subject string `json:"-"` // 订单标题
Platform string `json:"platform" binding:"required,oneof=wx_native native wx_h5 h5"` // 仅支持微信Native扫码兼容传入native/wx_h5/h5
OpenID string `json:"openid" binding:"omitempty"` // 前端可直接传入的 openid用于小程序/H5
}

View File

@@ -0,0 +1,121 @@
package dto
import (
"time"
"hyapi-server/internal/domains/finance/entities"
"hyapi-server/internal/domains/finance/value_objects"
"github.com/shopspring/decimal"
)
// InvoiceApplicationResponse 发票申请响应
type InvoiceApplicationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
InvoiceInfo *value_objects.InvoiceInfo `json:"invoice_info"`
CreatedAt time.Time `json:"created_at"`
}
// InvoiceInfoResponse 发票信息响应
type InvoiceInfoResponse struct {
CompanyName string `json:"company_name"` // 从企业认证信息获取,只读
TaxpayerID string `json:"taxpayer_id"` // 从企业认证信息获取,只读
BankName string `json:"bank_name"` // 用户可编辑
BankAccount string `json:"bank_account"` // 用户可编辑
CompanyAddress string `json:"company_address"` // 用户可编辑
CompanyPhone string `json:"company_phone"` // 用户可编辑
ReceivingEmail string `json:"receiving_email"` // 用户可编辑
IsComplete bool `json:"is_complete"`
MissingFields []string `json:"missing_fields,omitempty"`
// 字段权限标识
CompanyNameReadOnly bool `json:"company_name_read_only"` // 公司名称是否只读
TaxpayerIDReadOnly bool `json:"taxpayer_id_read_only"` // 纳税人识别号是否只读
}
// InvoiceRecordResponse 发票记录响应
type InvoiceRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
// 开票信息(快照数据)
CompanyName string `json:"company_name"` // 公司名称
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号
BankName string `json:"bank_name"` // 开户银行
BankAccount string `json:"bank_account"` // 银行账号
CompanyAddress string `json:"company_address"` // 企业地址
CompanyPhone string `json:"company_phone"` // 企业电话
ReceivingEmail string `json:"receiving_email"` // 接收邮箱
// 文件信息
FileName *string `json:"file_name,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
FileURL *string `json:"file_url,omitempty"`
// 时间信息
ProcessedAt *time.Time `json:"processed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
// 拒绝原因
RejectReason *string `json:"reject_reason,omitempty"`
}
// InvoiceRecordsResponse 发票记录列表响应
type InvoiceRecordsResponse struct {
Records []*InvoiceRecordResponse `json:"records"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// FileDownloadResponse 文件下载响应
type FileDownloadResponse struct {
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
FileURL string `json:"file_url"`
FileContent []byte `json:"file_content"`
}
// AvailableAmountResponse 可开票金额响应
type AvailableAmountResponse struct {
AvailableAmount decimal.Decimal `json:"available_amount"` // 可开票金额
TotalRecharged decimal.Decimal `json:"total_recharged"` // 总充值金额
TotalGifted decimal.Decimal `json:"total_gifted"` // 总赠送金额
TotalInvoiced decimal.Decimal `json:"total_invoiced"` // 已开票金额
PendingApplications decimal.Decimal `json:"pending_applications"` // 待处理申请金额
}
// PendingApplicationResponse 待处理申请响应
type PendingApplicationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
CompanyName string `json:"company_name"`
TaxpayerID string `json:"taxpayer_id"`
BankName string `json:"bank_name"`
BankAccount string `json:"bank_account"`
CompanyAddress string `json:"company_address"`
CompanyPhone string `json:"company_phone"`
ReceivingEmail string `json:"receiving_email"`
FileName *string `json:"file_name,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
FileURL *string `json:"file_url,omitempty"`
ProcessedAt *time.Time `json:"processed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
RejectReason *string `json:"reject_reason,omitempty"`
}
// PendingApplicationsResponse 待处理申请列表响应
type PendingApplicationsResponse struct {
Applications []*PendingApplicationResponse `json:"applications"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}

View File

@@ -0,0 +1,21 @@
package queries
// GetWalletInfoQuery 获取钱包信息查询
type GetWalletInfoQuery struct {
UserID string `form:"user_id" binding:"required"`
}
// GetWalletQuery 获取钱包查询
type GetWalletQuery struct {
UserID string `form:"user_id" binding:"required"`
}
// GetWalletStatsQuery 获取钱包统计查询
type GetWalletStatsQuery struct {
UserID string `form:"user_id" binding:"required"`
}
// GetUserSecretsQuery 获取用户密钥查询
type GetUserSecretsQuery struct {
UserID string `form:"user_id" binding:"required"`
}

View File

@@ -0,0 +1,25 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
)
// AlipayOrderStatusResponse 支付宝订单状态响应
type AlipayOrderStatusResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
TradeNo *string `json:"trade_no"` // 支付宝交易号
Status string `json:"status"` // 订单状态
Amount decimal.Decimal `json:"amount"` // 订单金额
Subject string `json:"subject"` // 订单标题
Platform string `json:"platform"` // 支付平台
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
ErrorCode *string `json:"error_code"` // 错误码
ErrorMessage *string `json:"error_message"` // 错误信息
IsProcessing bool `json:"is_processing"` // 是否处理中
CanRetry bool `json:"can_retry"` // 是否可以重试
}

View File

@@ -0,0 +1,171 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
)
// WalletResponse 钱包响应
type WalletResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
IsActive bool `json:"is_active"`
Balance decimal.Decimal `json:"balance"`
BalanceStatus string `json:"balance_status"` // normal, low, arrears
IsArrears bool `json:"is_arrears"` // 是否欠费
IsLowBalance bool `json:"is_low_balance"` // 是否余额较低
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TransactionResponse 交易响应
type TransactionResponse struct {
TransactionID string `json:"transaction_id"`
Amount decimal.Decimal `json:"amount"`
}
// UserSecretsResponse 用户密钥响应
type UserSecretsResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AccessID string `json:"access_id"`
AccessKey string `json:"access_key"`
IsActive bool `json:"is_active"`
LastUsedAt *time.Time `json:"last_used_at"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WalletStatsResponse 钱包统计响应
type WalletStatsResponse struct {
TotalWallets int64 `json:"total_wallets"`
ActiveWallets int64 `json:"active_wallets"`
TotalBalance decimal.Decimal `json:"total_balance"`
TodayTransactions int64 `json:"today_transactions"`
TodayVolume decimal.Decimal `json:"today_volume"`
}
// RechargeRecordResponse 充值记录响应
type RechargeRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Amount decimal.Decimal `json:"amount"`
RechargeType string `json:"recharge_type"`
Status string `json:"status"`
AlipayOrderID string `json:"alipay_order_id,omitempty"`
WechatOrderID string `json:"wechat_order_id,omitempty"`
TransferOrderID string `json:"transfer_order_id,omitempty"`
Platform string `json:"platform,omitempty"` // 支付平台pc/wx_native等
Notes string `json:"notes,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WalletTransactionResponse 钱包交易记录响应
type WalletTransactionResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
ApiCallID string `json:"api_call_id"`
TransactionID string `json:"transaction_id"`
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Amount decimal.Decimal `json:"amount"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// WalletTransactionListResponse 钱包交易记录列表响应
type WalletTransactionListResponse struct {
Items []WalletTransactionResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// RechargeRecordListResponse 充值记录列表响应
type RechargeRecordListResponse struct {
Items []RechargeRecordResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// AlipayRechargeOrderResponse 支付宝充值订单响应
type AlipayRechargeOrderResponse struct {
PayURL string `json:"pay_url"` // 支付链接
OutTradeNo string `json:"out_trade_no"` // 商户订单号
Amount decimal.Decimal `json:"amount"` // 充值金额
Platform string `json:"platform"` // 支付平台
Subject string `json:"subject"` // 订单标题
}
// RechargeConfigResponse 充值配置响应
type RechargeConfigResponse struct {
MinAmount string `json:"min_amount"` // 最低充值金额
MaxAmount string `json:"max_amount"` // 最高充值金额
RechargeBonusEnabled bool `json:"recharge_bonus_enabled"` // 是否启用充值赠送
ApiStoreRechargeTip string `json:"api_store_recharge_tip"` // API 商店充值提示(大额/批量联系商务)
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
}
// AlipayRechargeBonusRuleResponse 支付宝充值赠送规则响应
type AlipayRechargeBonusRuleResponse struct {
RechargeAmount float64 `json:"recharge_amount"`
BonusAmount float64 `json:"bonus_amount"`
}
// UserSimpleResponse 用户简单信息响应
type UserSimpleResponse struct {
ID string `json:"id"`
CompanyName string `json:"company_name"`
Phone string `json:"phone"`
}
// PurchaseRecordResponse 购买记录响应
type PurchaseRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
OrderNo string `json:"order_no"`
TradeNo *string `json:"trade_no,omitempty"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ProductName string `json:"product_name"`
Category string `json:"category,omitempty"`
Subject string `json:"subject"`
Amount decimal.Decimal `json:"amount"`
PayAmount *decimal.Decimal `json:"pay_amount,omitempty"`
Status string `json:"status"`
Platform string `json:"platform"`
PayChannel string `json:"pay_channel"`
PaymentType string `json:"payment_type"`
BuyerID string `json:"buyer_id,omitempty"`
SellerID string `json:"seller_id,omitempty"`
ReceiptAmount decimal.Decimal `json:"receipt_amount,omitempty"`
NotifyTime *time.Time `json:"notify_time,omitempty"`
ReturnTime *time.Time `json:"return_time,omitempty"`
PayTime *time.Time `json:"pay_time,omitempty"`
FilePath *string `json:"file_path,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
Remark string `json:"remark,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PurchaseRecordListResponse 购买记录列表响应
type PurchaseRecordListResponse struct {
Items []PurchaseRecordResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}

View File

@@ -0,0 +1,25 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
)
// WechatOrderStatusResponse 微信订单状态响应
type WechatOrderStatusResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
TransactionID *string `json:"transaction_id"` // 微信支付交易号
Status string `json:"status"` // 订单状态
Amount decimal.Decimal `json:"amount"` // 订单金额
Subject string `json:"subject"` // 订单标题
Platform string `json:"platform"` // 支付平台
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
ErrorCode *string `json:"error_code"` // 错误码
ErrorMessage *string `json:"error_message"` // 错误信息
IsProcessing bool `json:"is_processing"` // 是否处理中
CanRetry bool `json:"can_retry"` // 是否可以重试
}

View File

@@ -0,0 +1,12 @@
package responses
import "github.com/shopspring/decimal"
// WechatRechargeOrderResponse 微信充值下单响应
type WechatRechargeOrderResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
Amount decimal.Decimal `json:"amount"` // 充值金额
Platform string `json:"platform"` // 支付平台
Subject string `json:"subject"` // 订单标题
PrepayData interface{} `json:"prepay_data"` // 预支付数据APP预支付ID或JSAPI参数
}

View File

@@ -0,0 +1,52 @@
package finance
import (
"context"
"net/http"
"hyapi-server/internal/application/finance/dto/commands"
"hyapi-server/internal/application/finance/dto/queries"
"hyapi-server/internal/application/finance/dto/responses"
"hyapi-server/internal/shared/interfaces"
)
// FinanceApplicationService 财务应用服务接口
type FinanceApplicationService interface {
// 钱包管理
CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error)
GetWallet(ctx context.Context, query *queries.GetWalletInfoQuery) (*responses.WalletResponse, error)
// 充值管理
CreateAlipayRechargeOrder(ctx context.Context, cmd *commands.CreateAlipayRechargeCommand) (*responses.AlipayRechargeOrderResponse, error)
CreateWechatRechargeOrder(ctx context.Context, cmd *commands.CreateWechatRechargeCommand) (*responses.WechatRechargeOrderResponse, error)
TransferRecharge(ctx context.Context, cmd *commands.TransferRechargeCommand) (*responses.RechargeRecordResponse, error)
GiftRecharge(ctx context.Context, cmd *commands.GiftRechargeCommand) (*responses.RechargeRecordResponse, error)
// 交易记录
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 导出功能
ExportAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
ExportAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, format string) ([]byte, error)
// 支付宝回调处理
HandleAlipayCallback(ctx context.Context, r *http.Request) error
HandleAlipayReturn(ctx context.Context, outTradeNo string) (string, error)
GetAlipayOrderStatus(ctx context.Context, outTradeNo string) (*responses.AlipayOrderStatusResponse, error)
// 微信支付回调处理
HandleWechatPayCallback(ctx context.Context, r *http.Request) error
HandleWechatRefundCallback(ctx context.Context, r *http.Request) error
GetWechatOrderStatus(ctx context.Context, outTradeNo string) (*responses.WechatOrderStatusResponse, error)
// 充值记录
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
// 购买记录
GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
// 获取充值配置
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,786 @@
package finance
import (
"context"
"fmt"
"mime/multipart"
"time"
"hyapi-server/internal/application/finance/dto"
"hyapi-server/internal/config"
"hyapi-server/internal/domains/finance/entities"
finance_repo "hyapi-server/internal/domains/finance/repositories"
"hyapi-server/internal/domains/finance/services"
"hyapi-server/internal/domains/finance/value_objects"
user_repo "hyapi-server/internal/domains/user/repositories"
user_service "hyapi-server/internal/domains/user/services"
"hyapi-server/internal/infrastructure/external/notification"
"hyapi-server/internal/infrastructure/external/storage"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
// ==================== 用户端发票应用服务 ====================
// InvoiceApplicationService 发票应用服务接口
// 职责:跨域协调、数据聚合、事务管理、外部服务调用
type InvoiceApplicationService interface {
// ApplyInvoice 申请开票
ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error)
// GetUserInvoiceInfo 获取用户发票信息
GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error)
// UpdateUserInvoiceInfo 更新用户发票信息
UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error
// GetUserInvoiceRecords 获取用户开票记录
GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error)
// DownloadInvoiceFile 下载发票文件
DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error)
// GetAvailableAmount 获取可开票金额
GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error)
}
// InvoiceApplicationServiceImpl 发票应用服务实现
type InvoiceApplicationServiceImpl struct {
// 仓储层依赖
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
rechargeRecordRepo finance_repo.RechargeRecordRepository
walletRepo finance_repo.WalletRepository
// 领域服务依赖
invoiceDomainService services.InvoiceDomainService
invoiceAggregateService services.InvoiceAggregateService
userInvoiceInfoService services.UserInvoiceInfoService
userAggregateService user_service.UserAggregateService
// 外部服务依赖
storageService *storage.QiNiuStorageService
logger *zap.Logger
wechatWorkServer *notification.WeChatWorkService
}
// NewInvoiceApplicationService 创建发票应用服务
func NewInvoiceApplicationService(
invoiceRepo finance_repo.InvoiceApplicationRepository,
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
userRepo user_repo.UserRepository,
userAggregateService user_service.UserAggregateService,
rechargeRecordRepo finance_repo.RechargeRecordRepository,
walletRepo finance_repo.WalletRepository,
invoiceDomainService services.InvoiceDomainService,
invoiceAggregateService services.InvoiceAggregateService,
userInvoiceInfoService services.UserInvoiceInfoService,
storageService *storage.QiNiuStorageService,
logger *zap.Logger,
cfg *config.Config,
) InvoiceApplicationService {
var wechatSvc *notification.WeChatWorkService
if cfg != nil && cfg.WechatWork.WebhookURL != "" {
wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger)
}
return &InvoiceApplicationServiceImpl{
invoiceRepo: invoiceRepo,
userInvoiceInfoRepo: userInvoiceInfoRepo,
userRepo: userRepo,
userAggregateService: userAggregateService,
rechargeRecordRepo: rechargeRecordRepo,
walletRepo: walletRepo,
invoiceDomainService: invoiceDomainService,
invoiceAggregateService: invoiceAggregateService,
userInvoiceInfoService: userInvoiceInfoService,
storageService: storageService,
logger: logger,
wechatWorkServer: wechatSvc,
}
}
// ApplyInvoice 申请开票
func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 验证发票类型
invoiceType := value_objects.InvoiceType(req.InvoiceType)
if !invoiceType.IsValid() {
return nil, fmt.Errorf("无效的发票类型")
}
// 3. 获取用户企业认证信息
userWithEnterprise, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 4. 检查用户是否有企业认证信息
if userWithEnterprise.EnterpriseInfo == nil {
return nil, fmt.Errorf("用户未完成企业认证,无法申请开票")
}
// 5. 获取用户开票信息
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
userWithEnterprise.EnterpriseInfo.CompanyName,
userWithEnterprise.EnterpriseInfo.UnifiedSocialCode,
)
if err != nil {
return nil, err
}
// 6. 验证开票信息完整性
invoiceInfo := value_objects.NewInvoiceInfo(
userInvoiceInfo.CompanyName,
userInvoiceInfo.TaxpayerID,
userInvoiceInfo.BankName,
userInvoiceInfo.BankAccount,
userInvoiceInfo.CompanyAddress,
userInvoiceInfo.CompanyPhone,
userInvoiceInfo.ReceivingEmail,
)
if err := s.userInvoiceInfoService.ValidateInvoiceInfo(ctx, invoiceInfo, invoiceType); err != nil {
return nil, err
}
// 7. 计算可开票金额
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
if err != nil {
return nil, fmt.Errorf("计算可开票金额失败: %w", err)
}
// 8. 验证开票金额
amount, err := decimal.NewFromString(req.Amount)
if err != nil {
return nil, fmt.Errorf("无效的金额格式: %w", err)
}
if err := s.invoiceDomainService.ValidateInvoiceAmount(ctx, amount, availableAmount); err != nil {
return nil, err
}
// 9. 调用聚合服务申请开票
aggregateReq := services.ApplyInvoiceRequest{
InvoiceType: invoiceType,
Amount: req.Amount,
InvoiceInfo: invoiceInfo,
}
application, err := s.invoiceAggregateService.ApplyInvoice(ctx, userID, aggregateReq)
if err != nil {
return nil, err
}
// 10. 构建响应DTO
resp := &dto.InvoiceApplicationResponse{
ID: application.ID,
UserID: application.UserID,
InvoiceType: application.InvoiceType,
Amount: application.Amount,
Status: application.Status,
InvoiceInfo: invoiceInfo,
CreatedAt: application.CreatedAt,
}
// 11. 企业微信通知(忽略发送错误),只使用企业名称和联系电话
if s.wechatWorkServer != nil {
companyName := userWithEnterprise.EnterpriseInfo.CompanyName
phone := user.Phone
if userWithEnterprise.EnterpriseInfo.LegalPersonPhone != "" {
phone = userWithEnterprise.EnterpriseInfo.LegalPersonPhone
}
content := fmt.Sprintf(
"### 【海宇数据】用户申请开发票\n"+
"> 企业名称:%s\n"+
"> 联系手机:%s\n"+
"> 申请开票金额:%s 元\n"+
"> 发票类型:%s\n"+
"> 申请时间:%s\n",
companyName,
phone,
application.Amount.String(),
string(application.InvoiceType),
time.Now().Format("2006-01-02 15:04:05"),
)
_ = s.wechatWorkServer.SendMarkdownMessage(ctx, content)
}
return resp, nil
}
// GetUserInvoiceInfo 获取用户发票信息
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
// 1. 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 获取企业认证信息
var companyName, taxpayerID string
var companyNameReadOnly, taxpayerIDReadOnly bool
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
companyNameReadOnly = true
taxpayerIDReadOnly = true
}
// 3. 获取用户开票信息(包含企业认证信息)
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
if err != nil {
return nil, err
}
// 4. 构建响应DTO
return &dto.InvoiceInfoResponse{
CompanyName: userInvoiceInfo.CompanyName,
TaxpayerID: userInvoiceInfo.TaxpayerID,
BankName: userInvoiceInfo.BankName,
BankAccount: userInvoiceInfo.BankAccount,
CompanyAddress: userInvoiceInfo.CompanyAddress,
CompanyPhone: userInvoiceInfo.CompanyPhone,
ReceivingEmail: userInvoiceInfo.ReceivingEmail,
IsComplete: userInvoiceInfo.IsComplete(),
MissingFields: userInvoiceInfo.GetMissingFields(),
// 字段权限标识
CompanyNameReadOnly: companyNameReadOnly, // 公司名称只读(从企业认证信息获取)
TaxpayerIDReadOnly: taxpayerIDReadOnly, // 纳税人识别号只读(从企业认证信息获取)
}, nil
}
// UpdateUserInvoiceInfo 更新用户发票信息
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
// 1. 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 检查用户是否有企业认证信息
if user.EnterpriseInfo == nil {
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
}
// 3. 创建开票信息对象,公司名称和纳税人识别号从企业认证信息中获取
invoiceInfo := value_objects.NewInvoiceInfo(
"", // 公司名称将由服务层从企业认证信息中获取
"", // 纳税人识别号将由服务层从企业认证信息中获取
req.BankName,
req.BankAccount,
req.CompanyAddress,
req.CompanyPhone,
req.ReceivingEmail,
)
// 4. 使用包含企业认证信息的方法
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
invoiceInfo,
user.EnterpriseInfo.CompanyName,
user.EnterpriseInfo.UnifiedSocialCode,
)
return err
}
// GetUserInvoiceRecords 获取用户开票记录
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 获取发票申请记录
var status entities.ApplicationStatus
if req.Status != "" {
status = entities.ApplicationStatus(req.Status)
}
// 3. 解析时间范围
var startTime, endTime *time.Time
if req.StartTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
startTime = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
endTime = &t
}
}
// 4. 获取发票申请记录(需要更新仓储层方法以支持时间筛选)
applications, total, err := s.invoiceRepo.FindByUserIDAndStatusWithTimeRange(ctx, userID, status, startTime, endTime, req.Page, req.PageSize)
if err != nil {
return nil, err
}
// 5. 构建响应DTO
records := make([]*dto.InvoiceRecordResponse, len(applications))
for i, app := range applications {
// 使用快照信息(申请时的开票信息)
records[i] = &dto.InvoiceRecordResponse{
ID: app.ID,
UserID: app.UserID,
InvoiceType: app.InvoiceType,
Amount: app.Amount,
Status: app.Status,
CompanyName: app.CompanyName, // 使用快照的公司名称
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
BankName: app.BankName, // 使用快照的银行名称
BankAccount: app.BankAccount, // 使用快照的银行账号
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
FileName: app.FileName,
FileSize: app.FileSize,
FileURL: app.FileURL,
ProcessedAt: app.ProcessedAt,
CreatedAt: app.CreatedAt,
RejectReason: app.RejectReason,
}
}
return &dto.InvoiceRecordsResponse{
Records: records,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
}, nil
}
// DownloadInvoiceFile 下载发票文件
func (s *InvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error) {
// 1. 查找申请记录
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return nil, err
}
if application == nil {
return nil, fmt.Errorf("申请记录不存在")
}
// 2. 验证权限(只能下载自己的发票)
if application.UserID != userID {
return nil, fmt.Errorf("无权访问此发票")
}
// 3. 验证状态(只能下载已完成的发票)
if application.Status != entities.ApplicationStatusCompleted {
return nil, fmt.Errorf("发票尚未通过审核")
}
// 4. 验证文件信息
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 5. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err)
}
// 6. 构建响应DTO
return &dto.FileDownloadResponse{
FileID: *application.FileID,
FileName: *application.FileName,
FileSize: *application.FileSize,
FileURL: *application.FileURL,
FileContent: fileContent,
}, nil
}
// GetAvailableAmount 获取可开票金额
func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 计算可开票金额
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
if err != nil {
return nil, err
}
// 3. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil {
return nil, err
}
// 4. 获取待处理申请金额
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
if err != nil {
return nil, err
}
// 5. 构建响应DTO
return &dto.AvailableAmountResponse{
AvailableAmount: availableAmount,
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+微信充值+对公转账)
TotalGifted: totalGifted,
TotalInvoiced: totalInvoiced,
PendingApplications: pendingAmount,
}, nil
}
// calculateAvailableAmount 计算可开票金额(私有方法)
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 1. 获取真实充值金额(支付宝充值+微信充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil {
return decimal.Zero, err
}
// 2. 获取待处理中的申请金额
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
if err != nil {
return decimal.Zero, err
}
fmt.Println("realRecharged", realRecharged)
fmt.Println("totalGifted", totalGifted)
fmt.Println("totalInvoiced", totalInvoiced)
fmt.Println("pendingAmount", pendingAmount)
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
// 可开票金额 = 真实充值金额(支付宝充值+微信充值+对公转账) - 已开票金额 - 待处理申请金额
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
fmt.Println("availableAmount", availableAmount)
// 确保可开票金额不为负数
if availableAmount.LessThan(decimal.Zero) {
availableAmount = decimal.Zero
}
return availableAmount, nil
}
// getAmountSummary 获取金额汇总(私有方法)
func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, userID string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
// 1. 获取用户所有成功的充值记录
rechargeRecords, err := s.rechargeRecordRepo.GetByUserID(ctx, userID)
if err != nil {
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
}
// 2. 计算真实充值金额(支付宝充值 + 微信充值 + 对公转账)和总赠送金额
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 微信充值 + 对公转账
var totalGifted decimal.Decimal // 总赠送金额
for _, record := range rechargeRecords {
if record.IsSuccess() {
if record.RechargeType == entities.RechargeTypeGift {
// 赠送金额不计入可开票金额
totalGifted = totalGifted.Add(record.Amount)
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeWechat || record.RechargeType == entities.RechargeTypeTransfer {
// 支付宝充值、微信充值和对公转账计入可开票金额
realRecharged = realRecharged.Add(record.Amount)
}
}
}
// 3. 获取用户所有发票申请记录(包括待处理、已完成、已拒绝)
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000) // 获取所有记录
if err != nil {
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
}
var totalInvoiced decimal.Decimal
for _, application := range applications {
// 计算已完成的发票申请金额
if application.IsCompleted() {
totalInvoiced = totalInvoiced.Add(application.Amount)
}
// 注意:待处理中的申请金额不计算在已开票金额中,但会在可开票金额计算时被扣除
}
return realRecharged, totalGifted, totalInvoiced, nil
}
// getPendingApplicationsAmount 获取待处理申请的总金额(私有方法)
func (s *InvoiceApplicationServiceImpl) getPendingApplicationsAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 获取用户所有发票申请记录
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000)
if err != nil {
return decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
}
var pendingAmount decimal.Decimal
for _, application := range applications {
// 只计算待处理状态的申请金额
if application.Status == entities.ApplicationStatusPending {
pendingAmount = pendingAmount.Add(application.Amount)
}
}
return pendingAmount, nil
}
// ==================== 管理员端发票应用服务 ====================
// AdminInvoiceApplicationService 管理员发票应用服务接口
type AdminInvoiceApplicationService interface {
// GetPendingApplications 获取发票申请列表(支持筛选)
GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error)
// ApproveInvoiceApplication 通过发票申请
ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
// RejectInvoiceApplication 拒绝发票申请
RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
// DownloadInvoiceFile 下载发票文件(管理员)
DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error)
}
// AdminInvoiceApplicationServiceImpl 管理员发票应用服务实现
type AdminInvoiceApplicationServiceImpl struct {
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
invoiceAggregateService services.InvoiceAggregateService
storageService *storage.QiNiuStorageService
logger *zap.Logger
}
// NewAdminInvoiceApplicationService 创建管理员发票应用服务
func NewAdminInvoiceApplicationService(
invoiceRepo finance_repo.InvoiceApplicationRepository,
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
userRepo user_repo.UserRepository,
invoiceAggregateService services.InvoiceAggregateService,
storageService *storage.QiNiuStorageService,
logger *zap.Logger,
) AdminInvoiceApplicationService {
return &AdminInvoiceApplicationServiceImpl{
invoiceRepo: invoiceRepo,
userInvoiceInfoRepo: userInvoiceInfoRepo,
userRepo: userRepo,
invoiceAggregateService: invoiceAggregateService,
storageService: storageService,
logger: logger,
}
}
// GetPendingApplications 获取发票申请列表(支持筛选)
func (s *AdminInvoiceApplicationServiceImpl) GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error) {
// 1. 解析状态筛选
var status entities.ApplicationStatus
if req.Status != "" {
status = entities.ApplicationStatus(req.Status)
}
// 2. 解析时间范围
var startTime, endTime *time.Time
if req.StartTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
startTime = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
endTime = &t
}
}
// 3. 获取发票申请记录(支持筛选)
var applications []*entities.InvoiceApplication
var total int64
var err error
if status != "" {
// 按状态筛选
applications, total, err = s.invoiceRepo.FindByStatusWithTimeRange(ctx, status, startTime, endTime, req.Page, req.PageSize)
} else {
// 获取所有记录(按时间筛选)
applications, total, err = s.invoiceRepo.FindAllWithTimeRange(ctx, startTime, endTime, req.Page, req.PageSize)
}
if err != nil {
return nil, err
}
// 4. 构建响应DTO
pendingApplications := make([]*dto.PendingApplicationResponse, len(applications))
for i, app := range applications {
// 使用快照信息
pendingApplications[i] = &dto.PendingApplicationResponse{
ID: app.ID,
UserID: app.UserID,
InvoiceType: app.InvoiceType,
Amount: app.Amount,
Status: app.Status,
CompanyName: app.CompanyName, // 使用快照的公司名称
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
BankName: app.BankName, // 使用快照的银行名称
BankAccount: app.BankAccount, // 使用快照的银行账号
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
FileName: app.FileName,
FileSize: app.FileSize,
FileURL: app.FileURL,
ProcessedAt: app.ProcessedAt,
CreatedAt: app.CreatedAt,
RejectReason: app.RejectReason,
}
}
return &dto.PendingApplicationsResponse{
Applications: pendingApplications,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
}, nil
}
// ApproveInvoiceApplication 通过发票申请
func (s *AdminInvoiceApplicationServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error {
// 1. 验证申请是否存在
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return err
}
if application == nil {
return fmt.Errorf("发票申请不存在")
}
// 2. 验证申请状态
if application.Status != entities.ApplicationStatusPending {
return fmt.Errorf("发票申请状态不允许处理")
}
// 3. 调用聚合服务处理申请
aggregateReq := services.ApproveInvoiceRequest{
AdminNotes: req.AdminNotes,
}
return s.invoiceAggregateService.ApproveInvoiceApplication(ctx, applicationID, file, aggregateReq)
}
// RejectInvoiceApplication 拒绝发票申请
func (s *AdminInvoiceApplicationServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error {
// 1. 验证申请是否存在
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return err
}
if application == nil {
return fmt.Errorf("发票申请不存在")
}
// 2. 验证申请状态
if application.Status != entities.ApplicationStatusPending {
return fmt.Errorf("发票申请状态不允许处理")
}
// 3. 调用聚合服务处理申请
aggregateReq := services.RejectInvoiceRequest{
Reason: req.Reason,
}
return s.invoiceAggregateService.RejectInvoiceApplication(ctx, applicationID, aggregateReq)
}
// ==================== 请求和响应DTO ====================
type ApplyInvoiceRequest struct {
InvoiceType string `json:"invoice_type" binding:"required"` // 发票类型general/special
Amount string `json:"amount" binding:"required"` // 开票金额
}
type UpdateInvoiceInfoRequest struct {
CompanyName string `json:"company_name"` // 公司名称(从企业认证信息获取,用户不可修改)
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号(从企业认证信息获取,用户不可修改)
BankName string `json:"bank_name"` // 银行名称
CompanyAddress string `json:"company_address"` // 公司地址
BankAccount string `json:"bank_account"` // 银行账户
CompanyPhone string `json:"company_phone"` // 企业注册电话
ReceivingEmail string `json:"receiving_email" binding:"required,email"` // 发票接收邮箱
}
type GetInvoiceRecordsRequest struct {
Page int `json:"page"` // 页码
PageSize int `json:"page_size"` // 每页数量
Status string `json:"status"` // 状态筛选
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
}
type GetPendingApplicationsRequest struct {
Page int `json:"page"` // 页码
PageSize int `json:"page_size"` // 每页数量
Status string `json:"status"` // 状态筛选pending/completed/rejected
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
}
type ApproveInvoiceRequest struct {
AdminNotes string `json:"admin_notes"` // 管理员备注
}
type RejectInvoiceRequest struct {
Reason string `json:"reason" binding:"required"` // 拒绝原因
}
// DownloadInvoiceFile 下载发票文件(管理员)
func (s *AdminInvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error) {
// 1. 查找申请记录
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return nil, err
}
if application == nil {
return nil, fmt.Errorf("申请记录不存在")
}
// 2. 验证状态(只能下载已完成的发票)
if application.Status != entities.ApplicationStatusCompleted {
return nil, fmt.Errorf("发票尚未通过审核")
}
// 3. 验证文件信息
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 4. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err)
}
// 5. 构建响应DTO
return &dto.FileDownloadResponse{
FileID: *application.FileID,
FileName: *application.FileName,
FileSize: *application.FileSize,
FileURL: *application.FileURL,
FileContent: fileContent,
}, nil
}

View File

@@ -0,0 +1,19 @@
package product
import (
"context"
"hyapi-server/internal/application/product/dto/commands"
"hyapi-server/internal/application/product/dto/queries"
"hyapi-server/internal/application/product/dto/responses"
)
// CategoryApplicationService 分类应用服务接口
type CategoryApplicationService interface {
// 分类管理
CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error
UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error
DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error
GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error)
ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error)
}

View File

@@ -0,0 +1,236 @@
package product
import (
"context"
"errors"
"fmt"
"hyapi-server/internal/application/product/dto/commands"
"hyapi-server/internal/application/product/dto/queries"
"hyapi-server/internal/application/product/dto/responses"
"hyapi-server/internal/domains/product/entities"
"hyapi-server/internal/domains/product/repositories"
repoQueries "hyapi-server/internal/domains/product/repositories/queries"
"go.uber.org/zap"
)
// CategoryApplicationServiceImpl 分类应用服务实现
type CategoryApplicationServiceImpl struct {
categoryRepo repositories.ProductCategoryRepository
logger *zap.Logger
}
// NewCategoryApplicationService 创建分类应用服务
func NewCategoryApplicationService(
categoryRepo repositories.ProductCategoryRepository,
logger *zap.Logger,
) CategoryApplicationService {
return &CategoryApplicationServiceImpl{
categoryRepo: categoryRepo,
logger: logger,
}
}
func (s *CategoryApplicationServiceImpl) CreateCategory(ctx context.Context, cmd *commands.CreateCategoryCommand) error {
// 1. 参数验证
if err := s.validateCreateCategory(cmd); err != nil {
return err
}
// 2. 验证分类编号唯一性
if err := s.validateCategoryCode(cmd.Code, ""); err != nil {
return err
}
// 3. 创建分类实体
category := &entities.ProductCategory{
Name: cmd.Name,
Code: cmd.Code,
Description: cmd.Description,
Sort: cmd.Sort,
IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible,
}
// 4. 保存到仓储
createdCategory, err := s.categoryRepo.Create(ctx, *category)
if err != nil {
s.logger.Error("创建分类失败", zap.Error(err), zap.String("code", cmd.Code))
return fmt.Errorf("创建分类失败: %w", err)
}
s.logger.Info("创建分类成功", zap.String("id", createdCategory.ID), zap.String("code", cmd.Code))
return nil
}
// UpdateCategory 更新分类
func (s *CategoryApplicationServiceImpl) UpdateCategory(ctx context.Context, cmd *commands.UpdateCategoryCommand) error {
// 1. 参数验证
if err := s.validateUpdateCategory(cmd); err != nil {
return err
}
// 2. 获取现有分类
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
// 3. 验证分类编号唯一性(排除当前分类)
if err := s.validateCategoryCode(cmd.Code, cmd.ID); err != nil {
return err
}
// 4. 更新分类信息
existingCategory.Name = cmd.Name
existingCategory.Code = cmd.Code
existingCategory.Description = cmd.Description
existingCategory.Sort = cmd.Sort
existingCategory.IsEnabled = cmd.IsEnabled
existingCategory.IsVisible = cmd.IsVisible
// 5. 保存到仓储
if err := s.categoryRepo.Update(ctx, existingCategory); err != nil {
s.logger.Error("更新分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("更新分类失败: %w", err)
}
s.logger.Info("更新分类成功", zap.String("id", cmd.ID), zap.String("code", cmd.Code))
return nil
}
// DeleteCategory 删除分类
func (s *CategoryApplicationServiceImpl) DeleteCategory(ctx context.Context, cmd *commands.DeleteCategoryCommand) error {
// 1. 检查分类是否存在
existingCategory, err := s.categoryRepo.GetByID(ctx, cmd.ID)
if err != nil {
return fmt.Errorf("分类不存在: %w", err)
}
// 2. 检查是否有产品(可选,根据业务需求决定)
// 这里可以添加检查逻辑,如果有产品则不允许删除
// 3. 删除分类
if err := s.categoryRepo.Delete(ctx, cmd.ID); err != nil {
s.logger.Error("删除分类失败", zap.Error(err), zap.String("id", cmd.ID))
return fmt.Errorf("删除分类失败: %w", err)
}
s.logger.Info("删除分类成功", zap.String("id", cmd.ID), zap.String("code", existingCategory.Code))
return nil
}
// GetCategoryByID 根据ID获取分类
func (s *CategoryApplicationServiceImpl) GetCategoryByID(ctx context.Context, query *queries.GetCategoryQuery) (*responses.CategoryInfoResponse, error) {
var category entities.ProductCategory
var err error
if query.ID != "" {
category, err = s.categoryRepo.GetByID(ctx, query.ID)
} else {
return nil, fmt.Errorf("分类ID不能为空")
}
if err != nil {
return nil, fmt.Errorf("分类不存在: %w", err)
}
// 转换为响应对象
response := s.convertToCategoryInfoResponse(&category)
return response, nil
}
// ListCategories 获取分类列表
func (s *CategoryApplicationServiceImpl) ListCategories(ctx context.Context, query *queries.ListCategoriesQuery) (*responses.CategoryListResponse, error) {
// 构建仓储查询
repoQuery := &repoQueries.ListCategoriesQuery{
Page: query.Page,
PageSize: query.PageSize,
IsEnabled: query.IsEnabled,
IsVisible: query.IsVisible,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
}
// 调用仓储
categories, total, err := s.categoryRepo.ListCategories(ctx, repoQuery)
if err != nil {
s.logger.Error("获取分类列表失败", zap.Error(err))
return nil, fmt.Errorf("获取分类列表失败: %w", err)
}
// 转换为响应对象
items := make([]responses.CategoryInfoResponse, len(categories))
for i, category := range categories {
items[i] = *s.convertToCategoryInfoResponse(category)
}
return &responses.CategoryListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// convertToCategoryInfoResponse 转换为分类信息响应
func (s *CategoryApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
return &responses.CategoryInfoResponse{
ID: category.ID,
Name: category.Name,
Code: category.Code,
Description: category.Description,
Sort: category.Sort,
IsEnabled: category.IsEnabled,
IsVisible: category.IsVisible,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
}
}
// convertToCategorySimpleResponse 转换为分类简单信息响应
func (s *CategoryApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
return &responses.CategorySimpleResponse{
ID: category.ID,
Name: category.Name,
Code: category.Code,
}
}
// validateCreateCategory 验证创建分类参数
func (s *CategoryApplicationServiceImpl) validateCreateCategory(cmd *commands.CreateCategoryCommand) error {
if cmd.Name == "" {
return errors.New("分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("分类编号不能为空")
}
return nil
}
// validateUpdateCategory 验证更新分类参数
func (s *CategoryApplicationServiceImpl) validateUpdateCategory(cmd *commands.UpdateCategoryCommand) error {
if cmd.ID == "" {
return errors.New("分类ID不能为空")
}
if cmd.Name == "" {
return errors.New("分类名称不能为空")
}
if cmd.Code == "" {
return errors.New("分类编号不能为空")
}
return nil
}
// validateCategoryCode 验证分类编号唯一性
func (s *CategoryApplicationServiceImpl) validateCategoryCode(code, excludeID string) error {
if code == "" {
return errors.New("分类编号不能为空")
}
existingCategory, err := s.categoryRepo.FindByCode(context.Background(), code)
if err == nil && existingCategory != nil && existingCategory.ID != excludeID {
return errors.New("分类编号已存在")
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
package product
import (
"context"
"fmt"
"strings"
"hyapi-server/internal/application/product/dto/commands"
"hyapi-server/internal/application/product/dto/responses"
"hyapi-server/internal/domains/product/entities"
"hyapi-server/internal/domains/product/services"
)
// DocumentationApplicationServiceInterface 文档应用服务接口
type DocumentationApplicationServiceInterface interface {
// CreateDocumentation 创建文档
CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error)
// UpdateDocumentation 更新文档
UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error)
// GetDocumentation 获取文档
GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error)
// GetDocumentationByProductID 通过产品ID获取文档
GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error)
// DeleteDocumentation 删除文档
DeleteDocumentation(ctx context.Context, id string) error
// GetDocumentationsByProductIDs 批量获取文档
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
// GenerateFullDocumentation 生成完整的接口文档Markdown格式
GenerateFullDocumentation(ctx context.Context, productID string) (string, error)
}
// DocumentationApplicationService 文档应用服务
type DocumentationApplicationService struct {
docService *services.ProductDocumentationService
}
// NewDocumentationApplicationService 创建文档应用服务实例
func NewDocumentationApplicationService(docService *services.ProductDocumentationService) *DocumentationApplicationService {
return &DocumentationApplicationService{
docService: docService,
}
}
// CreateDocumentation 创建文档
func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error) {
// 创建文档实体
doc := &entities.ProductDocumentation{
RequestURL: cmd.RequestURL,
RequestMethod: cmd.RequestMethod,
BasicInfo: cmd.BasicInfo,
RequestParams: cmd.RequestParams,
ResponseFields: cmd.ResponseFields,
ResponseExample: cmd.ResponseExample,
ErrorCodes: cmd.ErrorCodes,
PDFFilePath: cmd.PDFFilePath,
}
// 调用领域服务创建文档
err := s.docService.CreateDocumentation(ctx, cmd.ProductID, doc)
if err != nil {
return nil, err
}
// 返回响应
resp := responses.NewDocumentationResponse(doc)
return &resp, nil
}
// UpdateDocumentation 更新文档
func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error) {
// 调用领域服务更新文档
err := s.docService.UpdateDocumentation(ctx, id,
cmd.RequestURL,
cmd.RequestMethod,
cmd.BasicInfo,
cmd.RequestParams,
cmd.ResponseFields,
cmd.ResponseExample,
cmd.ErrorCodes,
)
if err != nil {
return nil, err
}
// 获取更新后的文档
doc, err := s.docService.GetDocumentation(ctx, id)
if err != nil {
return nil, err
}
// 更新PDF文件路径如果提供
if cmd.PDFFilePath != "" {
doc.PDFFilePath = cmd.PDFFilePath
err = s.docService.UpdateDocumentationEntity(ctx, doc)
if err != nil {
return nil, fmt.Errorf("更新PDF文件路径失败: %w", err)
}
// 重新获取更新后的文档以确保获取最新数据
doc, err = s.docService.GetDocumentation(ctx, id)
if err != nil {
return nil, err
}
}
// 返回响应
resp := responses.NewDocumentationResponse(doc)
return &resp, nil
}
// GetDocumentation 获取文档
func (s *DocumentationApplicationService) GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error) {
doc, err := s.docService.GetDocumentation(ctx, id)
if err != nil {
return nil, err
}
// 返回响应
resp := responses.NewDocumentationResponse(doc)
return &resp, nil
}
// GetDocumentationByProductID 通过产品ID获取文档
func (s *DocumentationApplicationService) GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error) {
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
if err != nil {
return nil, err
}
// 返回响应
resp := responses.NewDocumentationResponse(doc)
return &resp, nil
}
// DeleteDocumentation 删除文档
func (s *DocumentationApplicationService) DeleteDocumentation(ctx context.Context, id string) error {
return s.docService.DeleteDocumentation(ctx, id)
}
// GetDocumentationsByProductIDs 批量获取文档
func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error) {
docs, err := s.docService.GetDocumentationsByProductIDs(ctx, productIDs)
if err != nil {
return nil, err
}
var docResponses []responses.DocumentationResponse
for _, doc := range docs {
docResponses = append(docResponses, responses.NewDocumentationResponse(doc))
}
return docResponses, nil
}
// GenerateFullDocumentation 生成完整的接口文档Markdown格式
func (s *DocumentationApplicationService) GenerateFullDocumentation(ctx context.Context, productID string) (string, error) {
// 通过产品ID获取文档
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
if err != nil {
return "", fmt.Errorf("获取文档失败: %w", err)
}
// 获取文档时已经包含了产品信息通过GetDocumentationWithProduct
// 如果没有产品信息通过文档ID获取
if doc.Product == nil && doc.ID != "" {
docWithProduct, err := s.docService.GetDocumentationWithProduct(ctx, doc.ID)
if err == nil && docWithProduct != nil {
doc = docWithProduct
}
}
var markdown strings.Builder
// 添加文档标题
productName := "产品"
if doc.Product != nil {
productName = doc.Product.Name
}
markdown.WriteString(fmt.Sprintf("# %s 接口文档\n\n", productName))
// 添加产品基本信息
if doc.Product != nil {
markdown.WriteString("## 产品信息\n\n")
markdown.WriteString(fmt.Sprintf("- **产品名称**: %s\n", doc.Product.Name))
markdown.WriteString(fmt.Sprintf("- **产品编号**: %s\n", doc.Product.Code))
if doc.Product.Description != "" {
markdown.WriteString(fmt.Sprintf("- **产品描述**: %s\n", doc.Product.Description))
}
markdown.WriteString("\n")
}
// 添加请求方式
markdown.WriteString("## 请求方式\n\n")
if doc.RequestURL != "" {
markdown.WriteString(fmt.Sprintf("- **请求方法**: %s\n", doc.RequestMethod))
markdown.WriteString(fmt.Sprintf("- **请求地址**: %s\n", doc.RequestURL))
markdown.WriteString("\n")
}
// 添加请求方式详细说明
if doc.BasicInfo != "" {
markdown.WriteString("### 请求方式说明\n\n")
markdown.WriteString(doc.BasicInfo)
markdown.WriteString("\n\n")
}
// 添加请求参数
if doc.RequestParams != "" {
markdown.WriteString("## 请求参数\n\n")
markdown.WriteString(doc.RequestParams)
markdown.WriteString("\n\n")
}
// 添加返回字段说明
if doc.ResponseFields != "" {
markdown.WriteString("## 返回字段说明\n\n")
markdown.WriteString(doc.ResponseFields)
markdown.WriteString("\n\n")
}
// 添加响应示例
if doc.ResponseExample != "" {
markdown.WriteString("## 响应示例\n\n")
markdown.WriteString(doc.ResponseExample)
markdown.WriteString("\n\n")
}
// 添加错误代码
if doc.ErrorCodes != "" {
markdown.WriteString("## 错误代码\n\n")
markdown.WriteString(doc.ErrorCodes)
markdown.WriteString("\n\n")
}
// 添加文档版本信息
markdown.WriteString("---\n\n")
markdown.WriteString(fmt.Sprintf("**文档版本**: %s\n\n", doc.Version))
if doc.UpdatedAt.Year() > 1900 {
markdown.WriteString(fmt.Sprintf("**更新时间**: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05")))
}
return markdown.String(), nil
}

View File

@@ -0,0 +1,27 @@
package commands
// CreateCategoryCommand 创建分类命令
type CreateCategoryCommand struct {
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// UpdateCategoryCommand 更新分类命令
type UpdateCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
Name string `json:"name" binding:"required,min=2,max=50" comment:"分类名称"`
Code string `json:"code" binding:"required,product_code" comment:"分类编号"`
Description string `json:"description" binding:"omitempty,max=200" comment:"分类描述"`
Sort int `json:"sort" binding:"min=0,max=9999" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// DeleteCategoryCommand 删除分类命令
type DeleteCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"分类ID"`
}

View File

@@ -0,0 +1,26 @@
package commands
// CreateDocumentationCommand 创建文档命令
type CreateDocumentationCommand struct {
ProductID string `json:"product_id" binding:"required" validate:"required"`
RequestURL string `json:"request_url" binding:"required" validate:"required"`
RequestMethod string `json:"request_method" binding:"required" validate:"required"`
BasicInfo string `json:"basic_info" validate:"required"`
RequestParams string `json:"request_params" validate:"required"`
ResponseFields string `json:"response_fields"`
ResponseExample string `json:"response_example"`
ErrorCodes string `json:"error_codes"`
PDFFilePath string `json:"pdf_file_path,omitempty"`
}
// UpdateDocumentationCommand 更新文档命令
type UpdateDocumentationCommand struct {
RequestURL string `json:"request_url"`
RequestMethod string `json:"request_method"`
BasicInfo string `json:"basic_info"`
RequestParams string `json:"request_params"`
ResponseFields string `json:"response_fields"`
ResponseExample string `json:"response_example"`
ErrorCodes string `json:"error_codes"`
PDFFilePath string `json:"pdf_file_path,omitempty"`
}

View File

@@ -0,0 +1,27 @@
package commands
// AddPackageItemCommand 添加组合包子产品命令
type AddPackageItemCommand struct {
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
}
// UpdatePackageItemCommand 更新组合包子产品命令
type UpdatePackageItemCommand struct {
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
}
// ReorderPackageItemsCommand 重新排序组合包子产品命令
type ReorderPackageItemsCommand struct {
ItemIDs []string `json:"item_ids" binding:"required,dive,uuid" comment:"子产品ID列表"`
}
// UpdatePackageItemsCommand 批量更新组合包子产品命令
type UpdatePackageItemsCommand struct {
Items []PackageItemData `json:"items" binding:"required,dive" comment:"子产品列表"`
}
// PackageItemData 组合包子产品数据
type PackageItemData struct {
ProductID string `json:"product_id" binding:"required,uuid" comment:"子产品ID"`
SortOrder int `json:"sort_order" binding:"required,min=0" comment:"排序"`
}

View File

@@ -0,0 +1,75 @@
package commands
// CreateProductCommand 创建产品命令
type CreateProductCommand struct {
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
}
// UpdateProductCommand 更新产品命令
type UpdateProductCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
Name string `json:"name" binding:"required,min=2,max=100" comment:"产品名称"`
Code string `json:"code" binding:"required,product_code" comment:"产品编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"产品描述"`
Content string `json:"content" binding:"omitempty,max=5000" comment:"产品内容"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
SubCategoryID *string `json:"sub_category_id" binding:"omitempty,uuid" comment:"二级分类ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"产品价格"`
CostPrice float64 `json:"cost_price" binding:"omitempty,min=0" comment:"成本价"`
Remark string `json:"remark" binding:"omitempty,max=1000" comment:"备注"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
SEOKeywords string `json:"seo_keywords" binding:"omitempty,max=200" comment:"SEO关键词"`
}
// DeleteProductCommand 删除产品命令
type DeleteProductCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
}
// CreateProductApiConfigCommand 创建产品API配置命令
type CreateProductApiConfigCommand struct {
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
}
// UpdateProductApiConfigCommand 更新产品API配置命令
type UpdateProductApiConfigCommand struct {
ProductID string `json:"product_id" binding:"required,uuid" comment:"产品ID"`
ApiEndpoint string `json:"api_endpoint" binding:"required,url" comment:"API端点"`
ApiKey string `json:"api_key" binding:"required" comment:"API密钥"`
ApiSecret string `json:"api_secret" binding:"required" comment:"API密钥"`
Config string `json:"config" binding:"omitempty" comment:"配置信息"`
}

View File

@@ -0,0 +1,29 @@
package commands
// CreateSubCategoryCommand 创建二级分类命令
type CreateSubCategoryCommand struct {
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// UpdateSubCategoryCommand 更新二级分类命令
type UpdateSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
Name string `json:"name" binding:"required,min=2,max=100" comment:"二级分类名称"`
Code string `json:"code" binding:"required,min=2,max=50" comment:"二级分类编号"`
Description string `json:"description" binding:"omitempty,max=500" comment:"二级分类描述"`
CategoryID string `json:"category_id" binding:"required,uuid" comment:"一级分类ID"`
Sort int `json:"sort" binding:"min=0" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
}
// DeleteSubCategoryCommand 删除二级分类命令
type DeleteSubCategoryCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"二级分类ID"`
}

View File

@@ -0,0 +1,23 @@
package commands
// CreateSubscriptionCommand 创建订阅命令
type CreateSubscriptionCommand struct {
UserID string `json:"-" comment:"用户ID"`
ProductID string `json:"-" uri:"id" binding:"required,uuid" comment:"产品ID"`
}
// UpdateSubscriptionPriceCommand 更新订阅价格命令
type UpdateSubscriptionPriceCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格组合包使用"`
}
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
type BatchUpdateSubscriptionPricesCommand struct {
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"`
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
}

View File

@@ -0,0 +1,17 @@
package queries
// ListCategoriesQuery 分类列表查询
type ListCategoriesQuery struct {
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code sort created_at updated_at" comment:"排序字段"`
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
}
// GetCategoryQuery 获取分类详情查询
type GetCategoryQuery struct {
ID string `uri:"id" binding:"omitempty,uuid" comment:"分类ID"`
Code string `form:"code" binding:"omitempty,product_code" comment:"分类编号"`
}

View File

@@ -0,0 +1,10 @@
package queries
// GetAvailableProductsQuery 获取可选子产品查询
type GetAvailableProductsQuery struct {
ExcludePackageID string `form:"exclude_package_id" binding:"omitempty,uuid" comment:"排除的组合包ID"`
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
}

View File

@@ -0,0 +1,54 @@
package queries
// ListProductsQuery 产品列表查询
type ListProductsQuery struct {
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
IsPackage *bool `form:"is_package" comment:"是否组合包"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
}
// SearchProductsQuery 产品搜索查询
type SearchProductsQuery struct {
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
CategoryID string `form:"category_id" binding:"omitempty,uuid" comment:"分类ID"`
MinPrice *float64 `form:"min_price" binding:"omitempty,min=0" comment:"最低价格"`
MaxPrice *float64 `form:"max_price" binding:"omitempty,min=0" comment:"最高价格"`
IsEnabled *bool `form:"is_enabled" comment:"是否启用"`
IsVisible *bool `form:"is_visible" comment:"是否展示"`
IsPackage *bool `form:"is_package" comment:"是否组合包"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=name code price created_at updated_at" comment:"排序字段"`
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
}
// GetProductQuery 获取产品详情查询
type GetProductQuery struct {
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
}
// GetProductDetailQuery 获取产品详情查询(支持可选文档)
type GetProductDetailQuery struct {
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
WithDocument *bool `form:"with_document" comment:"是否包含文档信息"`
}
// GetProductsByIDsQuery 根据ID列表获取产品查询
type GetProductsByIDsQuery struct {
IDs []string `form:"ids" binding:"required,dive,uuid" comment:"产品ID列表"`
}
// GetSubscribableProductsQuery 获取可订阅产品查询
type GetSubscribableProductsQuery struct {
UserID string `form:"user_id" binding:"required" comment:"用户ID"`
}

View File

@@ -0,0 +1,17 @@
package queries
// GetSubCategoryQuery 获取二级分类查询
type GetSubCategoryQuery struct {
ID string `json:"id" form:"id" binding:"omitempty,uuid" comment:"二级分类ID"`
}
// ListSubCategoriesQuery 获取二级分类列表查询
type ListSubCategoriesQuery struct {
Page int `json:"page" form:"page" binding:"min=1" comment:"页码"`
PageSize int `json:"page_size" form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
CategoryID string `json:"category_id" form:"category_id" binding:"omitempty,uuid" comment:"一级分类ID"`
IsEnabled *bool `json:"is_enabled" form:"is_enabled" comment:"是否启用"`
IsVisible *bool `json:"is_visible" form:"is_visible" comment:"是否展示"`
SortBy string `json:"sort_by" form:"sort_by" comment:"排序字段"`
SortOrder string `json:"sort_order" form:"sort_order" comment:"排序方向"`
}

View File

@@ -0,0 +1,37 @@
package queries
// ListSubscriptionsQuery 订阅列表查询
type ListSubscriptionsQuery struct {
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
UserID string `form:"user_id" binding:"omitempty" comment:"用户ID"`
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at updated_at price" comment:"排序字段"`
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
// 新增筛选字段
CompanyName string `form:"company_name" binding:"omitempty,max=100" comment:"企业名称"`
ProductName string `form:"product_name" binding:"omitempty,max=100" comment:"产品名称"`
StartTime string `form:"start_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅开始时间"`
EndTime string `form:"end_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅结束时间"`
}
// GetSubscriptionQuery 获取订阅详情查询
type GetSubscriptionQuery struct {
ID string `uri:"id" binding:"required,uuid" comment:"订阅ID"`
}
// GetUserSubscriptionsQuery 获取用户订阅查询
type GetUserSubscriptionsQuery struct {
UserID string `form:"user_id" binding:"required,uuid" comment:"用户ID"`
}
// GetProductSubscriptionsQuery 获取产品订阅查询
type GetProductSubscriptionsQuery struct {
ProductID string `form:"product_id" binding:"required,uuid" comment:"产品ID"`
}
// GetActiveSubscriptionsQuery 获取活跃订阅查询
type GetActiveSubscriptionsQuery struct {
UserID string `form:"user_id" binding:"omitempty,uuid" comment:"用户ID"`
}

View File

@@ -0,0 +1,66 @@
package responses
import "time"
// CategoryInfoResponse 分类详情响应
type CategoryInfoResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"`
Description string `json:"description" comment:"分类描述"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []CategoryInfoResponse `json:"items" comment:"分类列表"`
}
// CategorySimpleResponse 分类简单信息响应
type CategorySimpleResponse struct {
ID string `json:"id" comment:"分类ID"`
Name string `json:"name" comment:"分类名称"`
Code string `json:"code" comment:"分类编号"`
}
// SubCategoryInfoResponse 二级分类详情响应
type SubCategoryInfoResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
Description string `json:"description" comment:"二级分类描述"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
Sort int `json:"sort" comment:"排序"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsVisible bool `json:"is_visible" comment:"是否展示"`
// 关联信息
Category *CategoryInfoResponse `json:"category,omitempty" comment:"一级分类信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
}
// SubCategoryListResponse 二级分类列表响应
type SubCategoryListResponse struct {
Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"`
Items []SubCategoryInfoResponse `json:"items" comment:"二级分类列表"`
}
// SubCategorySimpleResponse 二级分类简单信息响应
type SubCategorySimpleResponse struct {
ID string `json:"id" comment:"二级分类ID"`
Name string `json:"name" comment:"二级分类名称"`
Code string `json:"code" comment:"二级分类编号"`
CategoryID string `json:"category_id" comment:"一级分类ID"`
}

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