f
This commit is contained in:
1
.cursorignore
Normal file
1
.cursorignore
Normal file
@@ -0,0 +1 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
51
.dockerignore
Normal file
51
.dockerignore
Normal 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
52
.gitignore
vendored
Normal 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
82
Dockerfile
Normal 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
64
Dockerfile.worker
Normal file
@@ -0,0 +1,64 @@
|
||||
# 第一阶段:构建阶段
|
||||
FROM golang:1.23.4-alpine AS builder
|
||||
|
||||
# 设置Go代理和Alpine镜像源
|
||||
ENV GOPROXY https://goproxy.cn,direct
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk add --no-cache git tzdata
|
||||
|
||||
# 复制模块文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用程序
|
||||
ARG VERSION=1.0.0
|
||||
ARG COMMIT=dev
|
||||
ARG BUILD_TIME
|
||||
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
|
||||
-a -installsuffix cgo \
|
||||
-o worker \
|
||||
cmd/worker/main.go
|
||||
|
||||
# 第二阶段:运行阶段
|
||||
FROM alpine:3.19
|
||||
|
||||
# 设置Alpine镜像源
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add tzdata curl
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/worker .
|
||||
|
||||
# 复制配置文件
|
||||
COPY --chown=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
512
Makefile
Normal 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
240
README.md
Normal 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) 文件了解详情。
|
||||
135
cmd/api/main.go
Normal file
135
cmd/api/main.go
Normal 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"
|
||||
}
|
||||
77
cmd/qygl_report_build/main.go
Normal file
77
cmd/qygl_report_build/main.go
Normal 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)
|
||||
}
|
||||
59
cmd/qygl_report_pdf/main.go
Normal file
59
cmd/qygl_report_pdf/main.go
Normal 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)
|
||||
}
|
||||
159
cmd/qygl_report_preview/main.go
Normal file
159
cmd/qygl_report_preview/main.go
Normal 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
658
config.yaml
Normal 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(默认)、aliyun;mock_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
|
||||
191
configs/env.development.yaml
Normal file
191
configs/env.development.yaml
Normal 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
172
configs/env.production.yaml
Normal 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
48
configs/env.testing.yaml
Normal 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
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: "default"
|
||||
orgId: 1
|
||||
folder: ""
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
109
deployments/docker/jaeger-sampling-prod.json
Normal file
109
deployments/docker/jaeger-sampling-prod.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
109
deployments/docker/jaeger-sampling.json
Normal file
109
deployments/docker/jaeger-sampling.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
46
deployments/docker/jaeger-ui-config.json
Normal file
46
deployments/docker/jaeger-ui-config.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
234
deployments/docker/nginx.conf
Normal file
234
deployments/docker/nginx.conf
Normal 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;
|
||||
# }
|
||||
# }
|
||||
}
|
||||
1
deployments/docker/pgadmin-passfile
Normal file
1
deployments/docker/pgadmin-passfile
Normal file
@@ -0,0 +1 @@
|
||||
postgres:5432:hyapi_dev:postgres:Qm8kZ3nR7pL4wT9y
|
||||
15
deployments/docker/pgadmin-servers.json
Normal file
15
deployments/docker/pgadmin-servers.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
deployments/docker/postgresql.conf
Normal file
28
deployments/docker/postgresql.conf
Normal 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
|
||||
39
deployments/docker/prometheus.yml
Normal file
39
deployments/docker/prometheus.yml
Normal 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
|
||||
104
deployments/docker/redis.conf
Normal file
104
deployments/docker/redis.conf
Normal 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
71
docker-compose.dev.yml
Normal 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
193
docker-compose.prod.yml
Normal 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
|
||||
603
docs/api/statistics/api_documentation.md
Normal file
603
docs/api/statistics/api_documentation.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# 统计功能API文档
|
||||
|
||||
## 概述
|
||||
|
||||
统计功能API提供了完整的统计数据分析和管理功能,包括指标管理、实时统计、历史统计、仪表板管理、报告生成、数据导出等功能。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: `/api/v1/statistics`
|
||||
- **认证方式**: Bearer Token
|
||||
- **内容类型**: `application/json`
|
||||
- **字符编码**: `UTF-8`
|
||||
|
||||
## 认证和权限
|
||||
|
||||
### 认证方式
|
||||
所有API请求都需要在请求头中包含有效的JWT令牌:
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
### 权限级别
|
||||
- **公开访问**: 无需认证的接口
|
||||
- **用户权限**: 需要用户或管理员权限
|
||||
- **管理员权限**: 仅管理员可访问
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 指标管理
|
||||
|
||||
#### 1.1 创建统计指标
|
||||
- **URL**: `POST /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 创建新的统计指标
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "realtime",
|
||||
"value": 100.0,
|
||||
"metadata": "{\"source\": \"api_gateway\"}",
|
||||
"date": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "指标创建成功",
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "realtime",
|
||||
"value": 100.0,
|
||||
"metadata": "{\"source\": \"api_gateway\"}",
|
||||
"date": "2024-01-01T00:00:00Z",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 更新统计指标
|
||||
- **URL**: `PUT /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 更新现有统计指标的值
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"value": 150.0
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 删除统计指标
|
||||
- **URL**: `DELETE /api/v1/statistics/metrics`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 删除指定的统计指标
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 获取单个指标
|
||||
- **URL**: `GET /api/v1/statistics/metrics/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计指标
|
||||
|
||||
#### 1.5 获取指标列表
|
||||
- **URL**: `GET /api/v1/statistics/metrics`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计指标列表
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string): 指标类型
|
||||
- `metric_name` (string): 指标名称
|
||||
- `dimension` (string): 统计维度
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `limit` (int): 限制数量 (默认20, 最大1000)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
|
||||
### 2. 实时统计
|
||||
|
||||
#### 2.1 获取实时指标
|
||||
- **URL**: `GET /api/v1/statistics/realtime`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定类型的实时统计指标
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `time_range` (string): 时间范围 (last_hour, last_day, last_week)
|
||||
- `dimension` (string): 统计维度
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取实时指标成功",
|
||||
"data": {
|
||||
"metric_type": "api_calls",
|
||||
"metrics": {
|
||||
"total_count": 1000,
|
||||
"success_count": 950,
|
||||
"failed_count": 50,
|
||||
"success_rate": 95.0
|
||||
},
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"metadata": {
|
||||
"time_range": "last_hour",
|
||||
"dimension": "realtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 历史统计
|
||||
|
||||
#### 3.1 获取历史指标
|
||||
- **URL**: `GET /api/v1/statistics/historical`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定时间范围的历史统计指标
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string): 指标名称
|
||||
- `dimension` (string): 统计维度
|
||||
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||
- `period` (string): 统计周期
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `aggregate_by` (string): 聚合维度
|
||||
- `group_by` (string): 分组维度
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取历史指标成功",
|
||||
"data": {
|
||||
"metric_type": "api_calls",
|
||||
"metric_name": "total_count",
|
||||
"dimension": "daily",
|
||||
"data_points": [
|
||||
{
|
||||
"date": "2024-01-01T00:00:00Z",
|
||||
"value": 1000,
|
||||
"label": "total_count"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 30000,
|
||||
"average": 1000,
|
||||
"max": 1500,
|
||||
"min": 500,
|
||||
"count": 30,
|
||||
"growth_rate": 5.2,
|
||||
"trend": "increasing"
|
||||
},
|
||||
"metadata": {
|
||||
"period": "daily",
|
||||
"aggregate_by": "day",
|
||||
"group_by": "metric_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 仪表板管理
|
||||
|
||||
#### 4.1 创建仪表板
|
||||
- **URL**: `POST /api/v1/statistics/dashboards`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 创建新的统计仪表板
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "用户仪表板",
|
||||
"description": "普通用户专用仪表板",
|
||||
"user_role": "user",
|
||||
"layout": "{\"columns\": 2, \"rows\": 3}",
|
||||
"widgets": "[{\"type\": \"api_calls\", \"position\": {\"x\": 0, \"y\": 0}}]",
|
||||
"settings": "{\"theme\": \"light\", \"auto_refresh\": false}",
|
||||
"refresh_interval": 600,
|
||||
"access_level": "private",
|
||||
"created_by": "user_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 获取仪表板列表
|
||||
- **URL**: `GET /api/v1/statistics/dashboards`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计仪表板列表
|
||||
|
||||
**查询参数**:
|
||||
- `user_role` (string): 用户角色
|
||||
- `is_default` (bool): 是否默认
|
||||
- `is_active` (bool): 是否激活
|
||||
- `access_level` (string): 访问级别
|
||||
- `created_by` (string): 创建者ID
|
||||
- `name` (string): 仪表板名称
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
|
||||
#### 4.3 获取单个仪表板
|
||||
- **URL**: `GET /api/v1/statistics/dashboards/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计仪表板
|
||||
|
||||
#### 4.4 获取仪表板数据
|
||||
- **URL**: `GET /api/v1/statistics/dashboards/data`
|
||||
- **权限**: 公开
|
||||
- **描述**: 获取指定角色的仪表板数据
|
||||
|
||||
**查询参数**:
|
||||
- `user_role` (string, 必需): 用户角色
|
||||
- `period` (string): 统计周期 (today, week, month)
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `metric_types` (string): 指标类型列表
|
||||
- `dimensions` (string): 统计维度列表
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取仪表板数据成功",
|
||||
"data": {
|
||||
"api_calls": {
|
||||
"total_count": 10000,
|
||||
"success_count": 9500,
|
||||
"failed_count": 500,
|
||||
"success_rate": 95.0,
|
||||
"avg_response_time": 150.5
|
||||
},
|
||||
"users": {
|
||||
"total_count": 1000,
|
||||
"certified_count": 800,
|
||||
"active_count": 750,
|
||||
"certification_rate": 80.0,
|
||||
"retention_rate": 75.0
|
||||
},
|
||||
"finance": {
|
||||
"total_amount": 50000.0,
|
||||
"recharge_amount": 60000.0,
|
||||
"deduct_amount": 10000.0,
|
||||
"net_amount": 50000.0
|
||||
},
|
||||
"period": {
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-01",
|
||||
"period": "today"
|
||||
},
|
||||
"metadata": {
|
||||
"generated_at": "2024-01-01 12:00:00",
|
||||
"user_role": "user",
|
||||
"data_version": "1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 报告管理
|
||||
|
||||
#### 5.1 生成报告
|
||||
- **URL**: `POST /api/v1/statistics/reports`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 生成指定类型的统计报告
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"report_type": "summary",
|
||||
"title": "月度汇总报告",
|
||||
"period": "month",
|
||||
"user_role": "admin",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-01-31T23:59:59Z",
|
||||
"filters": {
|
||||
"metric_types": ["api_calls", "users"],
|
||||
"dimensions": ["daily", "weekly"]
|
||||
},
|
||||
"generated_by": "admin_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 获取报告列表
|
||||
- **URL**: `GET /api/v1/statistics/reports`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据条件获取统计报告列表
|
||||
|
||||
**查询参数**:
|
||||
- `report_type` (string): 报告类型
|
||||
- `user_role` (string): 用户角色
|
||||
- `status` (string): 报告状态
|
||||
- `period` (string): 统计周期
|
||||
- `start_date` (string): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string): 结束日期 (YYYY-MM-DD)
|
||||
- `limit` (int): 限制数量 (默认20)
|
||||
- `offset` (int): 偏移量 (默认0)
|
||||
- `sort_by` (string): 排序字段 (默认created_at)
|
||||
- `sort_order` (string): 排序顺序 (默认desc)
|
||||
- `generated_by` (string): 生成者ID
|
||||
|
||||
#### 5.3 获取单个报告
|
||||
- **URL**: `GET /api/v1/statistics/reports/{id}`
|
||||
- **权限**: 用户
|
||||
- **描述**: 根据ID获取指定的统计报告
|
||||
|
||||
### 6. 统计分析
|
||||
|
||||
#### 6.1 计算增长率
|
||||
- **URL**: `GET /api/v1/statistics/analysis/growth-rate`
|
||||
- **权限**: 用户
|
||||
- **描述**: 计算指定指标的增长率
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string, 必需): 指标名称
|
||||
- `current_period` (string, 必需): 当前周期 (YYYY-MM-DD)
|
||||
- `previous_period` (string, 必需): 上一周期 (YYYY-MM-DD)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "计算增长率成功",
|
||||
"data": {
|
||||
"growth_rate": 15.5,
|
||||
"current_value": 1150,
|
||||
"previous_value": 1000,
|
||||
"period": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 计算趋势
|
||||
- **URL**: `GET /api/v1/statistics/analysis/trend`
|
||||
- **权限**: 用户
|
||||
- **描述**: 计算指定指标的趋势
|
||||
|
||||
**查询参数**:
|
||||
- `metric_type` (string, 必需): 指标类型
|
||||
- `metric_name` (string, 必需): 指标名称
|
||||
- `start_date` (string, 必需): 开始日期 (YYYY-MM-DD)
|
||||
- `end_date` (string, 必需): 结束日期 (YYYY-MM-DD)
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "计算趋势成功",
|
||||
"data": {
|
||||
"trend": "increasing",
|
||||
"trend_strength": 0.8,
|
||||
"data_points": 30,
|
||||
"correlation": 0.75
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 数据导出
|
||||
|
||||
#### 7.1 导出数据
|
||||
- **URL**: `POST /api/v1/statistics/export`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 导出指定格式的统计数据
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"format": "excel",
|
||||
"metric_type": "api_calls",
|
||||
"start_date": "2024-01-01T00:00:00Z",
|
||||
"end_date": "2024-01-31T23:59:59Z",
|
||||
"dimension": "daily",
|
||||
"group_by": "metric_name",
|
||||
"filters": {
|
||||
"status": "success"
|
||||
},
|
||||
"columns": ["date", "metric_name", "value"],
|
||||
"include_charts": true,
|
||||
"exported_by": "admin_id"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "数据导出成功",
|
||||
"data": {
|
||||
"download_url": "https://api.example.com/downloads/export_123.xlsx",
|
||||
"file_name": "api_calls_export_20240101_20240131.xlsx",
|
||||
"file_size": 1024000,
|
||||
"expires_at": "2024-01-02T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. 定时任务管理
|
||||
|
||||
#### 8.1 手动触发小时聚合
|
||||
- **URL**: `POST /api/v1/statistics/cron/hourly-aggregation`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发指定时间的小时级数据聚合
|
||||
|
||||
**查询参数**:
|
||||
- `target_hour` (string, 必需): 目标小时 (YYYY-MM-DDTHH:MM:SSZ)
|
||||
|
||||
#### 8.2 手动触发日聚合
|
||||
- **URL**: `POST /api/v1/statistics/cron/daily-aggregation`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发指定时间的日级数据聚合
|
||||
|
||||
**查询参数**:
|
||||
- `target_date` (string, 必需): 目标日期 (YYYY-MM-DD)
|
||||
|
||||
#### 8.3 手动触发数据清理
|
||||
- **URL**: `POST /api/v1/statistics/cron/data-cleanup`
|
||||
- **权限**: 管理员
|
||||
- **描述**: 手动触发过期数据清理任务
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | HTTP状态码 | 描述 |
|
||||
|--------|------------|------|
|
||||
| 400 | 400 Bad Request | 请求参数错误 |
|
||||
| 401 | 401 Unauthorized | 未认证或认证失败 |
|
||||
| 403 | 403 Forbidden | 权限不足 |
|
||||
| 404 | 404 Not Found | 资源不存在 |
|
||||
| 422 | 422 Unprocessable Entity | 参数验证失败 |
|
||||
| 429 | 429 Too Many Requests | 请求频率过高 |
|
||||
| 500 | 500 Internal Server Error | 服务器内部错误 |
|
||||
|
||||
## 响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
// 响应数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"error": "详细错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 列表响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "查询成功",
|
||||
"data": [
|
||||
// 数据列表
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 100,
|
||||
"pages": 5,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 统计指标 (StatisticsMetric)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"metric_type": "string",
|
||||
"metric_name": "string",
|
||||
"dimension": "string",
|
||||
"value": "number",
|
||||
"metadata": "string",
|
||||
"date": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 统计报告 (StatisticsReport)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"report_type": "string",
|
||||
"title": "string",
|
||||
"content": "string",
|
||||
"period": "string",
|
||||
"user_role": "string",
|
||||
"status": "string",
|
||||
"generated_by": "string",
|
||||
"generated_at": "string",
|
||||
"expires_at": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 统计仪表板 (StatisticsDashboard)
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"user_role": "string",
|
||||
"is_default": "boolean",
|
||||
"is_active": "boolean",
|
||||
"layout": "string",
|
||||
"widgets": "string",
|
||||
"settings": "string",
|
||||
"refresh_interval": "number",
|
||||
"created_by": "string",
|
||||
"access_level": "string",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 获取今日API调用统计
|
||||
```bash
|
||||
curl -X GET "https://api.example.com/api/v1/statistics/realtime?metric_type=api_calls&time_range=last_hour" \
|
||||
-H "Authorization: Bearer your-jwt-token"
|
||||
```
|
||||
|
||||
### 获取历史用户数据
|
||||
```bash
|
||||
curl -X GET "https://api.example.com/api/v1/statistics/historical?metric_type=users&start_date=2024-01-01&end_date=2024-01-31" \
|
||||
-H "Authorization: Bearer your-jwt-token"
|
||||
```
|
||||
|
||||
### 生成月度报告
|
||||
```bash
|
||||
curl -X POST "https://api.example.com/api/v1/statistics/reports" \
|
||||
-H "Authorization: Bearer your-jwt-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"report_type": "summary",
|
||||
"title": "月度汇总报告",
|
||||
"period": "month",
|
||||
"user_role": "admin",
|
||||
"generated_by": "admin_id"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **日期格式**: 所有日期参数都使用 `YYYY-MM-DD` 格式
|
||||
2. **时间戳**: 所有时间戳都使用 ISO 8601 格式
|
||||
3. **分页**: 默认每页20条记录,最大1000条
|
||||
4. **限流**: API有请求频率限制,超出限制会返回429错误
|
||||
5. **缓存**: 部分接口支持缓存,响应头会包含缓存信息
|
||||
6. **权限**: 不同接口需要不同的权限级别,请确保有相应权限
|
||||
7. **数据量**: 查询大量数据时建议使用分页和日期范围限制
|
||||
|
||||
9257
docs/docs.go
Normal file
9257
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
8546
docs/swagger.json
Normal file
8546
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
6209
docs/swagger.yaml
Normal file
6209
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
253
docs/swagger/README.md
Normal file
253
docs/swagger/README.md
Normal 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
9939
docs/swagger/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
9159
docs/swagger/swagger.json
Normal file
9159
docs/swagger/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
6670
docs/swagger/swagger.yaml
Normal file
6670
docs/swagger/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
373
docs/领域服务层设计.md
Normal file
373
docs/领域服务层设计.md
Normal 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
151
go.mod
Normal 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
630
go.sum
Normal 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
347
internal/app/app.go
Normal 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)
|
||||
}
|
||||
}
|
||||
1417
internal/application/api/api_application_service.go
Normal file
1417
internal/application/api/api_application_service.go
Normal file
File diff suppressed because it is too large
Load Diff
71
internal/application/api/commands/api_call_commands.go
Normal file
71
internal/application/api/commands/api_call_commands.go
Normal 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"`
|
||||
}
|
||||
104
internal/application/api/dto/api_call_validation.go
Normal file
104
internal/application/api/dto/api_call_validation.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
api_entities "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
|
||||
}
|
||||
103
internal/application/api/dto/api_response.go
Normal file
103
internal/application/api/dto/api_response.go
Normal 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: "",
|
||||
}
|
||||
}
|
||||
19
internal/application/api/dto/form_config_dto.go
Normal file
19
internal/application/api/dto/form_config_dto.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
// FormField 表单字段配置
|
||||
type FormField struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Validation string `json:"validation"`
|
||||
Description string `json:"description"`
|
||||
Example string `json:"example"`
|
||||
Placeholder string `json:"placeholder"`
|
||||
}
|
||||
|
||||
// FormConfigResponse 表单配置响应
|
||||
type FormConfigResponse struct {
|
||||
ApiCode string `json:"api_code"`
|
||||
Fields []FormField `json:"fields"`
|
||||
}
|
||||
61
internal/application/api/errors.go
Normal file
61
internal/application/api/errors.go
Normal 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()
|
||||
}
|
||||
40
internal/application/api/utils/error_translator.go
Normal file
40
internal/application/api/utils/error_translator.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
48
internal/application/article/article_application_service.go
Normal file
48
internal/application/article/article_application_service.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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)
|
||||
}
|
||||
836
internal/application/article/article_application_service_impl.go
Normal file
836
internal/application/article/article_application_service_impl.go
Normal file
@@ -0,0 +1,836 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateAnnouncementCommand 创建公告命令
|
||||
type CreateAnnouncementCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"公告标题"`
|
||||
Content string `json:"content" binding:"required" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// UpdateAnnouncementCommand 更新公告命令
|
||||
type UpdateAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
Title string `json:"title" comment:"公告标题"`
|
||||
Content string `json:"content" comment:"公告内容"`
|
||||
}
|
||||
|
||||
// DeleteAnnouncementCommand 删除公告命令
|
||||
type DeleteAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// PublishAnnouncementCommand 发布公告命令
|
||||
type PublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// WithdrawAnnouncementCommand 撤回公告命令
|
||||
type WithdrawAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// ArchiveAnnouncementCommand 归档公告命令
|
||||
type ArchiveAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
|
||||
// SchedulePublishAnnouncementCommand 定时发布公告命令
|
||||
type SchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// UpdateSchedulePublishAnnouncementCommand 更新定时发布公告命令
|
||||
type UpdateSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *UpdateSchedulePublishAnnouncementCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
|
||||
// CancelSchedulePublishAnnouncementCommand 取消定时发布公告命令
|
||||
type CancelSchedulePublishAnnouncementCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"公告ID"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package commands
|
||||
|
||||
// CreateArticleCommand 创建文章命令
|
||||
type CreateArticleCommand struct {
|
||||
Title string `json:"title" binding:"required" comment:"文章标题"`
|
||||
Content string `json:"content" binding:"required" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// UpdateArticleCommand 更新文章命令
|
||||
type UpdateArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
Title string `json:"title" comment:"文章标题"`
|
||||
Content string `json:"content" comment:"文章内容"`
|
||||
Summary string `json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `json:"cover_image" comment:"封面图片"`
|
||||
CategoryID string `json:"category_id" comment:"分类ID"`
|
||||
TagIDs []string `json:"tag_ids" comment:"标签ID列表"`
|
||||
IsFeatured bool `json:"is_featured" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
// DeleteArticleCommand 删除文章命令
|
||||
type DeleteArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// PublishArticleCommand 发布文章命令
|
||||
type PublishArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// ArchiveArticleCommand 归档文章命令
|
||||
type ArchiveArticleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
|
||||
// SetFeaturedCommand 设置推荐状态命令
|
||||
type SetFeaturedCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
IsFeatured bool `json:"is_featured" binding:"required" comment:"是否推荐"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// CancelScheduleCommand 取消定时发布命令
|
||||
type CancelScheduleCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateCategoryCommand 创建分类命令
|
||||
type CreateCategoryCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// UpdateCategoryCommand 更新分类命令
|
||||
type UpdateCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=50" message:"分类名称不能为空且长度在1-50之间"`
|
||||
Description string `json:"description" validate:"max=200" message:"分类描述长度不能超过200"`
|
||||
}
|
||||
|
||||
// DeleteCategoryCommand 删除分类命令
|
||||
type DeleteCategoryCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SchedulePublishCommand 定时发布文章命令
|
||||
type SchedulePublishCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"文章ID"`
|
||||
ScheduledTime string `json:"scheduled_time" binding:"required" comment:"定时发布时间"`
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取解析后的定时发布时间
|
||||
func (cmd *SchedulePublishCommand) GetScheduledTime() (time.Time, error) {
|
||||
// 定义中国东八区时区
|
||||
cst := time.FixedZone("CST", 8*3600)
|
||||
|
||||
// 支持多种时间格式
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // "2025-09-02 14:12:01"
|
||||
"2006-01-02T15:04:05", // "2025-09-02T14:12:01"
|
||||
"2006-01-02T15:04:05Z", // "2025-09-02T14:12:01Z"
|
||||
"2006-01-02 15:04", // "2025-09-02 14:12"
|
||||
time.RFC3339, // "2025-09-02T14:12:01+08:00"
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.ParseInLocation(format, cmd.ScheduledTime, cst); err == nil {
|
||||
// 确保返回的时间是东八区时区
|
||||
return t.In(cst), nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("不支持的时间格式: %s,请使用 YYYY-MM-DD HH:mm:ss 格式", cmd.ScheduledTime)
|
||||
}
|
||||
19
internal/application/article/dto/commands/tag_commands.go
Normal file
19
internal/application/article/dto/commands/tag_commands.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
// CreateTagCommand 创建标签命令
|
||||
type CreateTagCommand struct {
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// UpdateTagCommand 更新标签命令
|
||||
type UpdateTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
Name string `json:"name" binding:"required" validate:"required,min=1,max=30" message:"标签名称不能为空且长度在1-30之间"`
|
||||
Color string `json:"color" validate:"required,hexcolor" message:"标签颜色不能为空且必须是有效的十六进制颜色"`
|
||||
}
|
||||
|
||||
// DeleteTagCommand 删除标签命令
|
||||
type DeleteTagCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package queries
|
||||
|
||||
import "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"`
|
||||
}
|
||||
54
internal/application/article/dto/queries/article_queries.go
Normal file
54
internal/application/article/dto/queries/article_queries.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package queries
|
||||
|
||||
import "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:"每页数量"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetCategoryQuery 获取分类详情查询
|
||||
type GetCategoryQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"分类ID"`
|
||||
}
|
||||
6
internal/application/article/dto/queries/tag_queries.go
Normal file
6
internal/application/article/dto/queries/tag_queries.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package queries
|
||||
|
||||
// GetTagQuery 获取标签详情查询
|
||||
type GetTagQuery struct {
|
||||
ID string `json:"-" uri:"id" binding:"required" comment:"标签ID"`
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"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
|
||||
}
|
||||
219
internal/application/article/dto/responses/article_responses.go
Normal file
219
internal/application/article/dto/responses/article_responses.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
"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
|
||||
}
|
||||
126
internal/application/article/task_management_service.go
Normal file
126
internal/application/article/task_management_service.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package article
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"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
|
||||
}
|
||||
@@ -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
@@ -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列表"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
// GetContractSignURLCommand 获取合同签署链接命令
|
||||
type GetContractSignURLCommand struct {
|
||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
||||
}
|
||||
@@ -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"` // 法人姓名(模糊搜索)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"` // 过期时间
|
||||
}
|
||||
@@ -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"` // 高度
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
121
internal/application/finance/dto/invoice_responses.go
Normal file
121
internal/application/finance/dto/invoice_responses.go
Normal 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"`
|
||||
}
|
||||
21
internal/application/finance/dto/queries/finance_queries.go
Normal file
21
internal/application/finance/dto/queries/finance_queries.go
Normal 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"`
|
||||
}
|
||||
@@ -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"` // 是否可以重试
|
||||
}
|
||||
171
internal/application/finance/dto/responses/finance_responses.go
Normal file
171
internal/application/finance/dto/responses/finance_responses.go
Normal 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"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// WechatOrderStatusResponse 微信订单状态响应
|
||||
type WechatOrderStatusResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
TransactionID *string `json:"transaction_id"` // 微信支付交易号
|
||||
Status string `json:"status"` // 订单状态
|
||||
Amount decimal.Decimal `json:"amount"` // 订单金额
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
NotifyTime *time.Time `json:"notify_time"` // 异步通知时间
|
||||
ReturnTime *time.Time `json:"return_time"` // 同步返回时间
|
||||
ErrorCode *string `json:"error_code"` // 错误码
|
||||
ErrorMessage *string `json:"error_message"` // 错误信息
|
||||
IsProcessing bool `json:"is_processing"` // 是否处理中
|
||||
CanRetry bool `json:"can_retry"` // 是否可以重试
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package responses
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// WechatRechargeOrderResponse 微信充值下单响应
|
||||
type WechatRechargeOrderResponse struct {
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Amount decimal.Decimal `json:"amount"` // 充值金额
|
||||
Platform string `json:"platform"` // 支付平台
|
||||
Subject string `json:"subject"` // 订单标题
|
||||
PrepayData interface{} `json:"prepay_data"` // 预支付数据(APP预支付ID或JSAPI参数)
|
||||
}
|
||||
52
internal/application/finance/finance_application_service.go
Normal file
52
internal/application/finance/finance_application_service.go
Normal 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)
|
||||
}
|
||||
2119
internal/application/finance/finance_application_service_impl.go
Normal file
2119
internal/application/finance/finance_application_service_impl.go
Normal file
File diff suppressed because it is too large
Load Diff
786
internal/application/finance/invoice_application_service.go
Normal file
786
internal/application/finance/invoice_application_service.go
Normal 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
|
||||
}
|
||||
19
internal/application/product/category_application_service.go
Normal file
19
internal/application/product/category_application_service.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
1305
internal/application/product/component_report_order_service.go
Normal file
1305
internal/application/product/component_report_order_service.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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:"排序"`
|
||||
}
|
||||
@@ -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:"配置信息"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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:所有)"`
|
||||
}
|
||||
17
internal/application/product/dto/queries/category_queries.go
Normal file
17
internal/application/product/dto/queries/category_queries.go
Normal 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:"分类编号"`
|
||||
}
|
||||
10
internal/application/product/dto/queries/package_queries.go
Normal file
10
internal/application/product/dto/queries/package_queries.go
Normal 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:"每页数量"`
|
||||
}
|
||||
54
internal/application/product/dto/queries/product_queries.go
Normal file
54
internal/application/product/dto/queries/product_queries.go
Normal 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"`
|
||||
}
|
||||
@@ -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:"排序方向"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user