I第一次提交
41
.dockerignore
Normal file
@@ -0,0 +1,41 @@
|
||||
# Maven
|
||||
# target/ # 注释掉,让Docker能访问JAR文件
|
||||
pom.xml
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
# Test files
|
||||
test.txt
|
||||
|
||||
# Upload and extract directories (will be created at runtime)
|
||||
uploads/
|
||||
extracts/
|
||||
|
||||
# Documentation
|
||||
HELP.md
|
||||
SMART_QA_README.md
|
||||
README.md
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
BIN
3.3 同类项目经验.pdf
Normal file
26
3.3 同类项目经验_catalog_test.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"source": "filename_fallback",
|
||||
"total_pages": 39,
|
||||
"flat_chapters": [
|
||||
{
|
||||
"chapterId": "chap-0",
|
||||
"title": "3.3 同类项目经验",
|
||||
"level": 1,
|
||||
"page": "1",
|
||||
"startPage": 1,
|
||||
"endPage": 39,
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"hierarchical_chapters": [
|
||||
{
|
||||
"chapterId": "chap-0",
|
||||
"title": "3.3 同类项目经验",
|
||||
"level": 1,
|
||||
"page": "1",
|
||||
"startPage": 1,
|
||||
"endPage": 39,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
# 使用Eclipse Temurin OpenJDK 17作为基础镜像(离线部署)
|
||||
FROM eclipse-temurin:17-jre-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置时区为东八区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 安装curl用于健康检查和7-zip用于文件解压(可选)
|
||||
RUN apk add --no-cache curl p7zip
|
||||
|
||||
# 设置构建参数
|
||||
ARG VERSION=1.0.0
|
||||
ARG BUILD_TIME=unknown
|
||||
ENV APP_VERSION=${VERSION}
|
||||
ENV BUILD_TIME=${BUILD_TIME}
|
||||
|
||||
# 复制Maven构建的JAR文件
|
||||
COPY target/*.jar app.jar
|
||||
|
||||
# 创建非root用户 (Alpine Linux)
|
||||
RUN addgroup -g 1001 -S appuser && adduser -S -D -H -u 1001 -s /sbin/nologin -G appuser -g appuser appuser
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p /app/uploads /app/extracts /app/logs /app/config
|
||||
|
||||
# 设置目录权限
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 设置JVM参数
|
||||
ENV JAVA_OPTS="-Xmx1g -Xms512m -XX:+UseG1GC -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||
279
build-docker-bundle.sh
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 构建离线 Docker 交付包:包含应用镜像、pgvector(PostgreSQL)、Redis 及统一的 docker-compose
|
||||
# 使用方式:
|
||||
# chmod +x build-docker-bundle.sh
|
||||
# ./build-docker-bundle.sh # 使用默认版本号(时间戳)
|
||||
# VERSION=v1.2.3 ./build-docker-bundle.sh # 指定版本号
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
APP_NAME="gdyd-zhpb-zgf"
|
||||
VERSION="${VERSION:-${TIMESTAMP}}"
|
||||
OUTPUT_DIR="${ROOT_DIR}/docker-bundle-${VERSION}"
|
||||
IMAGES_DIR="${OUTPUT_DIR}/images"
|
||||
CONFIG_DIR="${OUTPUT_DIR}/config"
|
||||
INIT_DIR="${OUTPUT_DIR}/pgvector-init"
|
||||
|
||||
APP_IMAGE="${APP_NAME}-app:${VERSION}"
|
||||
PGVECTOR_IMAGE="${PGVECTOR_IMAGE:-pgvector/pgvector:pg16-trixie}"
|
||||
REDIS_IMAGE="${REDIS_IMAGE:-redis:7-alpine}"
|
||||
|
||||
# 默认账号密码,可在执行脚本前通过环境变量覆盖
|
||||
POSTGRES_DB="${POSTGRES_DB:-vector_db}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-vectoruser}"
|
||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-vectorpass}"
|
||||
POSTGRES_PORT="${POSTGRES_PORT:-5433}"
|
||||
|
||||
REDIS_PASSWORD="${REDIS_PASSWORD:-TNh8jiy8WpEweGLZ}"
|
||||
REDIS_PORT="${REDIS_PORT:-26739}"
|
||||
|
||||
APP_PORT="${APP_PORT:-8000}"
|
||||
|
||||
print_step() {
|
||||
printf '\n\033[1;34m==> %s\033[0m\n' "$1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
printf ' %s\n' "$1"
|
||||
}
|
||||
|
||||
abort() {
|
||||
printf '\033[1;31m[错误]\033[0m %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || abort "未找到命令:$1,请先安装。"
|
||||
}
|
||||
|
||||
APT_UPDATED=0
|
||||
SUDO_CMD=""
|
||||
if command -v sudo >/dev/null 2>&1 && [[ ${EUID:-0} -ne 0 ]]; then
|
||||
SUDO_CMD="sudo"
|
||||
fi
|
||||
|
||||
update_apt_once() {
|
||||
if [[ ${APT_UPDATED} -eq 0 ]]; then
|
||||
print_step "更新 apt 软件源"
|
||||
${SUDO_CMD} env DEBIAN_FRONTEND=noninteractive apt-get update -y
|
||||
APT_UPDATED=1
|
||||
fi
|
||||
}
|
||||
|
||||
install_with_apt() {
|
||||
local package="$1"
|
||||
local friendly="$2"
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
abort "缺少 ${friendly},但当前环境不支持自动安装(未检测到 apt-get)。"
|
||||
fi
|
||||
update_apt_once
|
||||
print_step "安装 ${friendly}"
|
||||
${SUDO_CMD} env DEBIAN_FRONTEND=noninteractive apt-get install -y "${package}"
|
||||
}
|
||||
|
||||
ensure_maven() {
|
||||
if ! command -v mvn >/dev/null 2>&1; then
|
||||
install_with_apt "maven" "Maven"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_java21() {
|
||||
local major=""
|
||||
if command -v java >/dev/null 2>&1; then
|
||||
local version_line
|
||||
version_line="$(java -version 2>&1 | head -n 1 | cut -d'"' -f2)"
|
||||
major="${version_line%%.*}"
|
||||
if [[ "${major}" == "1" ]]; then
|
||||
major="$(echo "${version_line}" | cut -d'.' -f2)"
|
||||
fi
|
||||
if [[ -n "${major}" && "${major}" -ge 21 ]]; then
|
||||
return
|
||||
fi
|
||||
print_info "检测到 Java 版本 ${major:-unknown},低于 21,准备自动安装 OpenJDK 21。"
|
||||
else
|
||||
print_info "未检测到 Java,准备自动安装 OpenJDK 21。"
|
||||
fi
|
||||
install_with_apt "openjdk-21-jdk" "OpenJDK 21"
|
||||
}
|
||||
|
||||
ensure_clean_dir() {
|
||||
if [[ -d "$1" ]]; then
|
||||
rm -rf "$1"
|
||||
fi
|
||||
mkdir -p "$1"
|
||||
}
|
||||
|
||||
ensure_java21
|
||||
ensure_maven
|
||||
check_command docker
|
||||
|
||||
print_step "编译 Spring Boot 应用"
|
||||
mvn -q -U -DskipTests clean package
|
||||
|
||||
JAR_FILE="$(find "${ROOT_DIR}/target" -maxdepth 1 -type f -name "*.jar" ! -name "*-sources.jar" ! -name "*-javadoc.jar" | head -n 1)"
|
||||
[[ -n "${JAR_FILE}" ]] || abort "未找到可用的 JAR 文件,请检查 Maven 构建结果。"
|
||||
print_info "找到 JAR:${JAR_FILE}"
|
||||
|
||||
print_step "构建应用镜像 ${APP_IMAGE}"
|
||||
docker build \
|
||||
--build-arg VERSION="${VERSION}" \
|
||||
--build-arg BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
-t "${APP_IMAGE}" \
|
||||
"${ROOT_DIR}"
|
||||
|
||||
print_step "拉取依赖镜像"
|
||||
docker pull "${PGVECTOR_IMAGE}"
|
||||
docker pull "${REDIS_IMAGE}"
|
||||
|
||||
print_step "生成离线交付包目录 ${OUTPUT_DIR}"
|
||||
ensure_clean_dir "${OUTPUT_DIR}"
|
||||
mkdir -p "${IMAGES_DIR}" "${CONFIG_DIR}" "${INIT_DIR}"
|
||||
|
||||
if [[ -f "${ROOT_DIR}/src/main/resources/application.yml" ]]; then
|
||||
cp "${ROOT_DIR}/src/main/resources/application.yml" "${CONFIG_DIR}/application.yml"
|
||||
print_info "已复制默认 application.yml 到 ${CONFIG_DIR}/application.yml"
|
||||
else
|
||||
print_info "未找到 src/main/resources/application.yml,可手动放置到 ${CONFIG_DIR}/application.yml"
|
||||
fi
|
||||
|
||||
cat > "${OUTPUT_DIR}/.env" <<EOF
|
||||
# PostgreSQL (pgvector)
|
||||
POSTGRES_DB=${POSTGRES_DB}
|
||||
POSTGRES_USER=${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_PORT=${POSTGRES_PORT}
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
REDIS_PORT=${REDIS_PORT}
|
||||
|
||||
# 应用
|
||||
APP_PORT=${APP_PORT}
|
||||
EOF
|
||||
|
||||
cat > "${INIT_DIR}/001-create-vector-extension.sql" <<'EOF'
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
EOF
|
||||
|
||||
cat > "${OUTPUT_DIR}/docker-compose.yml" <<EOF
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
pgvector:
|
||||
image: ${PGVECTOR_IMAGE}
|
||||
container_name: ${APP_NAME}-pgvector
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: \${POSTGRES_DB}
|
||||
POSTGRES_USER: \${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- "\${POSTGRES_PORT}:5432"
|
||||
volumes:
|
||||
- pgvector_data:/var/lib/postgresql/data
|
||||
- ./pgvector-init:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER} -d \${POSTGRES_DB}"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: ${REDIS_IMAGE}
|
||||
container_name: ${APP_NAME}-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "\${REDIS_PASSWORD}"]
|
||||
ports:
|
||||
- "\${REDIS_PORT}:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "\${REDIS_PASSWORD}", "ping"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
image: ${APP_IMAGE}
|
||||
container_name: ${APP_NAME}-app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pgvector:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "\${APP_PORT}:8000"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://pgvector:5432/\${POSTGRES_DB}
|
||||
SPRING_DATASOURCE_USERNAME: \${POSTGRES_USER}
|
||||
SPRING_DATASOURCE_PASSWORD: \${POSTGRES_PASSWORD}
|
||||
SPRING_DATA_REDIS_HOST: redis
|
||||
SPRING_DATA_REDIS_PORT: 6379
|
||||
SPRING_DATA_REDIS_PASSWORD: \${REDIS_PASSWORD}
|
||||
SERVER_PORT: 8000
|
||||
volumes:
|
||||
- ./config/application.yml:/app/config/application.yml:ro
|
||||
- app_uploads:/app/uploads
|
||||
- app_extracts:/app/extracts
|
||||
- app_logs:/app/logs
|
||||
|
||||
volumes:
|
||||
pgvector_data:
|
||||
redis_data:
|
||||
app_uploads:
|
||||
app_extracts:
|
||||
app_logs:
|
||||
EOF
|
||||
|
||||
cat > "${OUTPUT_DIR}/README.md" <<EOF
|
||||
# Docker 离线部署包(${APP_NAME} ${VERSION})
|
||||
|
||||
## 包含内容
|
||||
- 应用镜像:${APP_IMAGE}
|
||||
- pgvector 数据库镜像:${PGVECTOR_IMAGE}
|
||||
- Redis 缓存镜像:${REDIS_IMAGE}
|
||||
- docker-compose.yml / .env / config / pgvector-init 目录
|
||||
|
||||
## 使用步骤
|
||||
1. 将整个目录拷贝到目标服务器并进入该目录:
|
||||
\`\`\`
|
||||
cd $(basename "${OUTPUT_DIR}")
|
||||
\`\`\`
|
||||
2. 导入离线镜像:
|
||||
\`\`\`
|
||||
docker load -i images/app-${VERSION}.tar
|
||||
docker load -i images/pgvector.tar
|
||||
docker load -i images/redis.tar
|
||||
\`\`\`
|
||||
3. 根据需要修改 \`.env\`、\`config/application.yml\`。
|
||||
4. 启动:
|
||||
\`\`\`
|
||||
docker compose up -d
|
||||
\`\`\`
|
||||
5. 查看状态:
|
||||
\`\`\`
|
||||
docker compose ps
|
||||
docker compose logs -f app
|
||||
\`\`\`
|
||||
|
||||
缺省账号密码可通过执行脚本前设置环境变量覆盖,如:
|
||||
\`\`\`
|
||||
POSTGRES_PASSWORD=StrongPass REDIS_PASSWORD=Secret ./build-docker-bundle.sh
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
print_step "保存离线镜像到 ${IMAGES_DIR}"
|
||||
docker save "${APP_IMAGE}" -o "${IMAGES_DIR}/app-${VERSION}.tar"
|
||||
docker save "${PGVECTOR_IMAGE}" -o "${IMAGES_DIR}/pgvector.tar"
|
||||
docker save "${REDIS_IMAGE}" -o "${IMAGES_DIR}/redis.tar"
|
||||
|
||||
print_step "打包完成"
|
||||
print_info "输出目录:${OUTPUT_DIR}"
|
||||
print_info "镜像文件:${IMAGES_DIR}"
|
||||
print_info "可根据需要编辑 ${OUTPUT_DIR}/.env 与 ${CONFIG_DIR}/application.yml 后,执行 docker compose up -d 启动。"
|
||||
|
||||
145
config/application.yml.template
Normal file
@@ -0,0 +1,145 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: gdyd_zhpb_zgf
|
||||
|
||||
# PostgreSQL 单数据源配置
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://127.0.0.1:5432/gdyd_zhpb_zgf
|
||||
username: postgres
|
||||
password: 123456
|
||||
|
||||
# JPA 配置
|
||||
jpa:
|
||||
database: postgresql
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
format_sql: true
|
||||
jdbc.time_zone: Asia/Shanghai
|
||||
|
||||
# Redis 配置
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 6000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件最大大小 (500MB)
|
||||
max-file-size: 500MB
|
||||
# 整个请求最大大小 (500MB)
|
||||
max-request-size: 500MB
|
||||
# 文件写入磁盘的阈值 (10MB,超过此大小会写入临时文件)
|
||||
file-size-threshold: 10MB
|
||||
# 启用multipart上传
|
||||
enabled: true
|
||||
|
||||
# SSO配置
|
||||
sso:
|
||||
# ES接口基础地址
|
||||
es-base-url: http://es-integration.es-uat-paas:8890
|
||||
# 获取用户信息接口路径
|
||||
getUserInfoPath: /bi/getUserInfoByTicket
|
||||
|
||||
# 文件管理配置
|
||||
file:
|
||||
# 文件上传存储路径
|
||||
upload-path: ./uploads
|
||||
# ZIP文件解压路径
|
||||
extract-path: ./extracts
|
||||
# 支持的文件类型
|
||||
allowed-types: pdf,doc,docx,txt
|
||||
# 是否只返回技术文件类型(PDF、DOC、DOCX、PPT、PPTX、TXT、JSON)
|
||||
filter:
|
||||
technical-only: true
|
||||
|
||||
# 向量化服务配置
|
||||
vectorize:
|
||||
# 向量化接口基础地址
|
||||
base-url: http://localhost:8001
|
||||
# 向量化接口路径
|
||||
api-path: /vectorize
|
||||
# 向量维度(从接口返回的dimension字段获取,默认896)
|
||||
dimension: 896
|
||||
|
||||
# 向量化模型阈值配置(按模型区分)
|
||||
vector-limits:
|
||||
default:
|
||||
max-chars: 300
|
||||
max-tokens: 512
|
||||
text-embedding-ada-002:
|
||||
max-chars: 300
|
||||
max-tokens: 512
|
||||
embed-bge-large-zh-v1.5:
|
||||
max-chars: 300
|
||||
max-tokens: 512
|
||||
|
||||
# Swagger/OpenAPI 配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
enabled: true
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
enabled: true
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
try-it-out-enabled: true
|
||||
display-request-duration: true
|
||||
show-actuator: false
|
||||
# 排除异常处理器包,避免兼容性问题
|
||||
packages-to-exclude: com.zhpb.gdyd_zhpb_zgf.exception
|
||||
# 不扫描ControllerAdvice类
|
||||
use-management-port: false
|
||||
|
||||
# Actuator 配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
health:
|
||||
indicators:
|
||||
enabled: true
|
||||
|
||||
# AI模型配置
|
||||
ai:
|
||||
model-type: public # public: 公网模型(DeepSeek), private: 私有模型
|
||||
|
||||
# DeepSeek AI 配置 (公网模型)
|
||||
deepseek:
|
||||
api-key: sk-62f7d771d03046fa8cf77a4adfb9048f # 建议通过环境变量设置
|
||||
api-url: https://api.deepseek.com/v1/chat/completions
|
||||
model: deepseek-chat
|
||||
temperature: 0.7
|
||||
max-tokens: 4096
|
||||
|
||||
# 私有大模型配置
|
||||
private-model:
|
||||
url: http://IP:PORT/bigmodel_infer_gateway/v1/service # 私有模型API地址
|
||||
appid: "" # 私有模型AppID
|
||||
appkey: "" # 私有模型AppKey
|
||||
capabilityname: semantic0000000000000000 # 能力名称
|
||||
model: JiuTian-75B-8K # 私有模型名称
|
||||
temperature: 0.1 # 温度参数
|
||||
max-tokens: 1024 # 最大token数
|
||||
135
docker-compose.yml
Normal file
@@ -0,0 +1,135 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL数据库
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: smart-qa-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_DATABASE: smart_qa
|
||||
MYSQL_USER: appuser
|
||||
MYSQL_PASSWORD: apppassword
|
||||
MYSQL_CHARSET: utf8mb4
|
||||
MYSQL_COLLATION: utf8mb4_unicode_ci
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./docker/mysql/init:/docker-entrypoint-initdb.d
|
||||
- ./docker/mysql/conf.d:/etc/mysql/conf.d
|
||||
networks:
|
||||
- smart-qa-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
# PostgreSQL数据库 (用于向量存储)
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: smart-qa-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: smart_qa_vector
|
||||
POSTGRES_USER: vectoruser
|
||||
POSTGRES_PASSWORD: vectorpassword
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./docker/postgres/init:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- smart-qa-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U vectoruser -d smart_qa_vector"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# Redis缓存
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: smart-qa-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./docker/redis/redis.conf:/etc/redis/redis.conf
|
||||
command: redis-server /etc/redis/redis.conf
|
||||
networks:
|
||||
- smart-qa-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# 应用服务
|
||||
app:
|
||||
image: smart-qa-app:v202511191435
|
||||
container_name: smart-qa-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
# 数据库配置
|
||||
- SPRING_DATASOURCE_MYSQL_URL=jdbc:mysql://mysql:3306/smart_qa?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
- SPRING_DATASOURCE_MYSQL_USERNAME=appuser
|
||||
- SPRING_DATASOURCE_MYSQL_PASSWORD=apppassword
|
||||
|
||||
- SPRING_DATASOURCE_PGSQL_URL=jdbc:postgresql://postgres:5432/smart_qa_vector
|
||||
- SPRING_DATASOURCE_PGSQL_USERNAME=vectoruser
|
||||
- SPRING_DATASOURCE_PGSQL_PASSWORD=vectorpassword
|
||||
|
||||
# Redis配置
|
||||
- SPRING_DATA_REDIS_HOST=redis
|
||||
- SPRING_DATA_REDIS_PORT=6379
|
||||
|
||||
# 应用配置
|
||||
- SERVER_PORT=8080
|
||||
- LOGGING_LEVEL_COM_ZHPB=INFO
|
||||
- FILE_UPLOAD_MAX_SIZE=100MB
|
||||
- FILE_UPLOAD_MAX_REQUEST_SIZE=100MB
|
||||
|
||||
# 外部配置文件路径
|
||||
- SPRING_CONFIG_LOCATION=file:/app/config/application.yml
|
||||
volumes:
|
||||
# 挂载配置文件
|
||||
- ./config/application.yml:/app/config/application.yml:ro
|
||||
# 挂载上传文件目录
|
||||
- uploads_data:/app/uploads
|
||||
# 挂载解压文件目录
|
||||
- extracts_data:/app/extracts
|
||||
# 挂载日志目录
|
||||
- logs_data:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- smart-qa-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
uploads_data:
|
||||
driver: local
|
||||
extracts_data:
|
||||
driver: local
|
||||
logs_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
smart-qa-network:
|
||||
driver: bridge
|
||||
29
docker/mysql/conf.d/mysql.cnf
Normal file
@@ -0,0 +1,29 @@
|
||||
[mysql]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysqld]
|
||||
# 基本设置
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
init_connect='SET NAMES utf8mb4'
|
||||
|
||||
# 性能优化
|
||||
innodb_buffer_pool_size=256M
|
||||
innodb_log_file_size=64M
|
||||
innodb_flush_log_at_trx_commit=1
|
||||
innodb_flush_method=O_DIRECT
|
||||
|
||||
# 连接设置
|
||||
max_connections=200
|
||||
wait_timeout=28800
|
||||
interactive_timeout=28800
|
||||
|
||||
# 日志设置
|
||||
general_log=0
|
||||
slow_query_log=1
|
||||
slow_query_log_file=/var/lib/mysql/mysql-slow.log
|
||||
long_query_time=2
|
||||
|
||||
# 其他设置
|
||||
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
|
||||
lower_case_table_names=1
|
||||
11
docker/postgres/init/01-init-db.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- PostgreSQL 初始化脚本
|
||||
-- 用于向量数据库的初始化
|
||||
|
||||
-- 创建向量扩展(如果需要的话)
|
||||
-- CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- 设置默认编码
|
||||
SET client_encoding = 'UTF8';
|
||||
|
||||
-- 创建必要的模式和权限
|
||||
-- 这里可以添加更多的初始化SQL
|
||||
37
docker/redis/redis.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
# Redis配置文件 - 适用于Docker环境
|
||||
|
||||
# 网络配置
|
||||
bind 0.0.0.0
|
||||
port 6379
|
||||
timeout 0
|
||||
tcp-keepalive 300
|
||||
|
||||
# 内存管理
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# 持久化
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
|
||||
# 日志
|
||||
loglevel notice
|
||||
logfile ""
|
||||
|
||||
# 安全
|
||||
protected-mode no
|
||||
requirepass ""
|
||||
|
||||
# 其他优化
|
||||
tcp-backlog 511
|
||||
databases 16
|
||||
|
||||
# AOF持久化
|
||||
appendonly yes
|
||||
appendfilename "appendonly.aof"
|
||||
appendfsync everysec
|
||||
|
||||
# 慢日志
|
||||
slowlog-log-slower-than 10000
|
||||
slowlog-max-len 128
|
||||
BIN
images/.DS_Store
vendored
Normal file
BIN
images/主页.jpeg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
images/智能问答对话页面.jpeg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
images/智能问答选择文件.jpeg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
images/智能问答页面.jpeg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
images/概括总结.jpeg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
images/概括总结答案.jpeg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
images/概括总结选择文件.jpeg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
images/横向对比结果.jpeg
Normal file
|
After Width: | Height: | Size: 357 KiB |
BIN
images/横向对比页面.jpeg
Normal file
|
After Width: | Height: | Size: 90 KiB |
282
main(2).py
Normal file
@@ -0,0 +1,282 @@
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class UmiOcrClient:
|
||||
"""
|
||||
封装了 Umi-OCR HTTP API 的客户端。
|
||||
提供了文档识别的完整异步流程,以及图片和二维码识别接口。
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str = "http://154.219.106.93:1224"):
|
||||
"""
|
||||
初始化客户端。
|
||||
:param base_url: Umi-OCR 服务的基地址。
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.headers_json = {"Content-Type": "application/json"}
|
||||
print(f"UmiOcrClient 初始化。目标地址: {self.base_url}")
|
||||
|
||||
def _send_request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None,
|
||||
files: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
发送 HTTP 请求的通用内部方法。
|
||||
"""
|
||||
url = self.base_url + endpoint
|
||||
|
||||
try:
|
||||
if method.upper() == 'GET':
|
||||
response = requests.get(url)
|
||||
elif method.upper() == 'POST':
|
||||
if files:
|
||||
# 使用 multipart/form-data for file upload (Step 1)
|
||||
response = requests.post(url, data=data, files=files, timeout=300)
|
||||
else:
|
||||
# 使用 application/json for other POST requests
|
||||
data_str = json.dumps(data)
|
||||
response = requests.post(url, data=data_str, headers=self.headers_json, timeout=300)
|
||||
else:
|
||||
raise ValueError(f"不支持的 HTTP 方法: {method}")
|
||||
|
||||
# 检查 HTTP 状态码
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析响应 JSON
|
||||
return response.json()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"请求 {url} 失败。")
|
||||
raise Exception(f"请求错误: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
# 某些接口(如 /api/doc/clear)可能返回空响应体,这里需要容错
|
||||
if response.status_code == 200 and not response.text:
|
||||
return {"code": 100, "data": "Success (No content)"}
|
||||
print(f"响应内容不是有效的 JSON。原始内容:\n{response.text}")
|
||||
raise Exception(f"JSON 解析错误: {e}")
|
||||
|
||||
# ==========================================================================
|
||||
# 📚 文档识别(PDF等文件)接口封装
|
||||
# ==========================================================================
|
||||
|
||||
def doc_get_options(self) -> Dict[str, Any]:
|
||||
""" 0. 准备工作:参数查询 (/api/doc/get_options) """
|
||||
endpoint = "/api/doc/get_options"
|
||||
return self._send_request("GET", endpoint)
|
||||
|
||||
def doc_upload(self, file_path: str, options: Optional[Dict[str, Any]] = None) -> str:
|
||||
""" 1. 上传待识别文档 (/api/doc/upload),返回任务ID """
|
||||
print(f"🚀 上传文件: {file_path}")
|
||||
endpoint = "/api/doc/upload"
|
||||
|
||||
# 准备 form-data 的 'json' 部分
|
||||
json_data = json.dumps(options if options is not None else {})
|
||||
|
||||
# 准备 form-data 的 'file' 部分
|
||||
files = {'file': open(file_path, 'rb')}
|
||||
data = {'json': json_data}
|
||||
|
||||
try:
|
||||
response = self._send_request("POST", endpoint, data=data, files=files)
|
||||
|
||||
if response.get('code') == 100:
|
||||
return response.get('data') # 任务ID
|
||||
else:
|
||||
raise Exception(f"文件上传失败: {response.get('data')}")
|
||||
finally:
|
||||
# 确保文件流关闭
|
||||
if 'file' in files and files['file'].closed == False:
|
||||
files['file'].close()
|
||||
|
||||
def doc_query_result(self, task_id: str, is_data: bool = False, is_unread: bool = True,
|
||||
data_format: str = "dict") -> Dict[str, Any]:
|
||||
""" 2. 查询任务状态 (/api/doc/result) """
|
||||
endpoint = "/api/doc/result"
|
||||
request_data = {
|
||||
"id": task_id,
|
||||
"is_data": is_data,
|
||||
"is_unread": is_unread,
|
||||
"format": data_format,
|
||||
}
|
||||
return self._send_request("POST", endpoint, request_data)
|
||||
|
||||
def doc_get_download_link(self, task_id: str, file_types: List[str] = ["pdfLayered"], ignore_blank: bool = True) -> \
|
||||
Dict[str, str]:
|
||||
""" 3. 获取结果下载链接 (/api/doc/download) """
|
||||
endpoint = "/api/doc/download"
|
||||
request_data = {
|
||||
"id": task_id,
|
||||
"file_types": file_types,
|
||||
"ignore_blank": ignore_blank,
|
||||
}
|
||||
response = self._send_request("POST", endpoint, request_data)
|
||||
|
||||
if response.get('code') == 100:
|
||||
return {
|
||||
"download_url": self.base_url + response.get('data'), # 完整的下载链接
|
||||
"file_name": response.get('name')
|
||||
}
|
||||
else:
|
||||
raise Exception(f"生成下载链接失败: {response.get('data')}")
|
||||
|
||||
def doc_clear_task(self, task_id: str) -> bool:
|
||||
""" 5. 任务清理 (/api/doc/clear/<id>) """
|
||||
endpoint = f"/api/doc/clear/{task_id}"
|
||||
response = self._send_request("GET", endpoint)
|
||||
|
||||
if response.get('code') == 100:
|
||||
print(f"🧹 任务ID {task_id} 清理成功。")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 任务ID {task_id} 清理失败或不存在: {response.get('data')}")
|
||||
return False
|
||||
|
||||
# --- 高级同步流程函数 ---
|
||||
|
||||
def run_doc_ocr_sync(self, file_path: str, options: Optional[Dict[str, Any]] = None,
|
||||
download_file_types: List[str] = ["pdfLayered"], interval: int = 5) -> Dict[str, Any]:
|
||||
"""
|
||||
同步执行整个文档OCR流程 (上传 -> 轮询 -> 下载链接 -> 清理)。
|
||||
"""
|
||||
task_id = None
|
||||
try:
|
||||
# 1. 上传文件,获取任务ID
|
||||
task_id = self.doc_upload(file_path, options)
|
||||
print(f"📝 任务启动成功,任务ID: {task_id}")
|
||||
|
||||
# 2. 轮询任务状态
|
||||
print("⏳ 正在轮询任务状态...")
|
||||
while True:
|
||||
status_res = self.doc_query_result(task_id, is_data=False)
|
||||
|
||||
is_done = status_res.get('is_done', False)
|
||||
state = status_res.get('state', 'unknown')
|
||||
processed = status_res.get('processed_count', 0)
|
||||
total = status_res.get('pages_count', '?')
|
||||
|
||||
print(f" - 状态: {state.upper()} ({processed}/{total} 页已处理)")
|
||||
|
||||
if is_done:
|
||||
if state == "success":
|
||||
print("✅ 任务完成!")
|
||||
break
|
||||
else:
|
||||
message = status_res.get('message', '未知错误')
|
||||
raise Exception(f"任务执行失败 (State: {state}): {message}")
|
||||
|
||||
time.sleep(interval) # 休息指定间隔时间后继续查询
|
||||
|
||||
# 3. 获取下载链接
|
||||
download_info = self.doc_get_download_link(task_id, download_file_types)
|
||||
print(f"🔗 下载链接生成成功。文件: {download_info['file_name']}")
|
||||
|
||||
# 4. 获取识别文本(可选步骤,通过查询获取)
|
||||
text_result = self.doc_query_result(task_id, is_data=True, data_format="text").get('data')
|
||||
|
||||
return {
|
||||
"code": 100,
|
||||
"task_id": task_id,
|
||||
"download_info": download_info,
|
||||
"recognized_text": text_result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 捕捉所有错误并报告
|
||||
raise Exception(f"文档识别流程失败: {e}")
|
||||
|
||||
finally:
|
||||
# 5. 任务清理 (无论成功失败都清理)
|
||||
if task_id:
|
||||
self.doc_clear_task(task_id)
|
||||
|
||||
# ==========================================================================
|
||||
# 📸 其他接口(保留自上次封装)
|
||||
# ==========================================================================
|
||||
|
||||
def ocr_base64_image(self, base64_image: str, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
""" 图片OCR:Base64 识别接口 (/api/ocr) """
|
||||
print("\n--- 调用图片OCR Base64 识别接口 (/api/ocr) ---")
|
||||
endpoint = "/api/ocr"
|
||||
request_data = {
|
||||
"base64": base64_image,
|
||||
"options": options if options is not None else {}
|
||||
}
|
||||
return self._send_request("POST", endpoint, request_data)
|
||||
|
||||
def recognize_qrcode_base64(self, base64_image: str) -> Dict[str, Any]:
|
||||
""" 【推测接口】二维码:Base64 识别 (/api/qrcode/recognize) """
|
||||
print("\n--- 调用二维码 Base64 识别接口(推测: /api/qrcode/recognize)---")
|
||||
endpoint = "/api/qrcode/recognize"
|
||||
request_data = {"base64": base64_image}
|
||||
return self._send_request("POST", endpoint, request_data)
|
||||
|
||||
def generate_qrcode(self, text: str, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
""" 【推测接口】二维码:从文本生成图片 (/api/qrcode/generate) """
|
||||
print("\n--- 调用二维码生成接口(推测: /api/qrcode/generate)---")
|
||||
endpoint = "/api/qrcode/generate"
|
||||
request_data = {
|
||||
"text": text,
|
||||
"options": options if options is not None else {}
|
||||
}
|
||||
return self._send_request("POST", endpoint, request_data)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 示例用法
|
||||
# ==============================================================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 请根据您的实际情况修改以下参数
|
||||
TEST_PDF_PATH = r"D:\daima\java\gdyd_zhpb_zgf\第2章 方案建议书.pdf"
|
||||
|
||||
# 实例化客户端
|
||||
client = UmiOcrClient()
|
||||
|
||||
# --- 演示文档识别流程 ---
|
||||
print("\n" + "=" * 50)
|
||||
print(" 演示:同步文档识别流程 (Step 1-5)")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 尝试使用 run_doc_ocr_sync 接口
|
||||
# 假设我们想要中文识别,并生成双层PDF和纯文本txt
|
||||
doc_options = {
|
||||
"ocr.language": "models/config_chinese.txt",
|
||||
"pageRangeEnd": 1 # 限制只识别前5页
|
||||
}
|
||||
|
||||
# ⚠️ 注意: 运行此段代码前,请确保您的目录下存在 TEST_PDF_PATH 所指的文件
|
||||
final_result = client.run_doc_ocr_sync(
|
||||
file_path=TEST_PDF_PATH,
|
||||
options=doc_options,
|
||||
download_file_types=["pdfLayered", "txtPlain"],
|
||||
interval=3 # 轮询间隔3秒
|
||||
)
|
||||
|
||||
print("\n🎉 整个文档识别任务执行完毕。")
|
||||
print("--- 总结结果 ---")
|
||||
print(f"任务ID: {final_result['task_id']}")
|
||||
print(f"下载文件名: {final_result['download_info']['file_name']}")
|
||||
print(f"下载链接: {final_result['download_info']['download_url']}")
|
||||
print(f"部分识别文本 (前100字): \n{final_result['recognized_text']}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 文档识别流程中断/失败。错误信息: {e}")
|
||||
print("提示:请确认 Umi-OCR 是否运行在目标 IP/端口,并且文件路径正确。")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(" 演示:图片 Base64 识别")
|
||||
print("=" * 50)
|
||||
|
||||
# 演示图片识别 (与上次封装相同,确认接口可用)
|
||||
EXAMPLE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAC4AAAAXCAIAAAD7ruoFAAAACXBIWXMAABnWAAAZ1gEY0crtAAAAEXRFWHRTb2Z0d2FyZQBTbmlwYXN0ZV0Xzt0AAAHjSURBVEiJ7ZYrcsMwEEBXnR7FLuj0BPIJHJOi0DAZ2qSsMCxEgjYrDQqJdALrBJ2ASndRgeNI8ledutOCLrLl1e7T/mRkjIG/IXe/DWBldRTNEoQSpgNURe5puiiaJehrMuJSXSTgbaby0A1WzLrCCQCmyn0FwoN0V06QONWAt1nUxfnjHYA8p65GjhDKxcjedVH6JOejBPwYh21eE0Wzfe0tqIsEkGXcVcpoMH4CRZ+P0lsQp/pWJ4ripf1XFDFe8GHSHlYcSo9Es31t60RdFlN1RUmrma5oTzTVB8ZUaeeYEC9GmL6kNkDw9BANAQYo3xTNdqUkvHq+rYhDKW0Bj3RSEIpmyWyBaZaMTCrCK+tJ5Jsa07fs3E7esE66HzralRLgJKp0/BD6fJRSxvmDsb6joqkcFXGqMVVFFEHDL2gTxwCAaTabnkFUWhDCHTd9iYrGcAL1ZnqIp5Vpiqh7bCfua7FA4qN0INMcN1+cgCzj+UFxtbmvwdZvGIrI41JiqhZBWhhF8WxorkYPpQwJiWYJeA3rXE4hzcwJ+B96F9zCFHC0FcVegghvFul7oeEE8PvHeJqC0w0AUbbFIT8JnEwGbPKcS2OxU3HMTqD0r4wgEIuiKJ7i4MS16+og8/+bPZRPLa+6Ld2DSzcAAAAASUVORK5CYII="
|
||||
try:
|
||||
ocr_result = client.ocr_base64_image(
|
||||
base64_image=EXAMPLE_BASE64,
|
||||
options={"data.format": "text"}
|
||||
)
|
||||
print("✅ Base64 识别成功。识别结果:", ocr_result.get('data'))
|
||||
except Exception as e:
|
||||
print(f"❌ Base64 识别失败。")
|
||||
295
mvnw
vendored
Executable file
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
171
pom.xml
Normal file
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.7</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.zhpb</groupId>
|
||||
<artifactId>gdyd_zhpb_zgf</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>gdyd_zhpb_zgf</name>
|
||||
<description>gdyd_zhpb_zgf</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL 数据源 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL 数据源 -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Fastjson2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.43</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache HttpComponents -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger/OpenAPI 3 文档 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator 健康检查 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache PDFBox for PDF reading -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Compress -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.20</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Tabula-Java for PDF table extraction -->
|
||||
<dependency>
|
||||
<groupId>technology.tabula</groupId>
|
||||
<artifactId>tabula</artifactId>
|
||||
<version>1.0.5</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
653
product_manual.html
Normal file
@@ -0,0 +1,653 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>智慧评标-主观分助手 | 产品手册</title>
|
||||
<!-- 引入 Inter 字体,提升阅读体验 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
--primary-dark: #1d4ed8;
|
||||
--secondary-color: #4f46e5;
|
||||
--text-color: #334155;
|
||||
--heading-color: #0f172a;
|
||||
--bg-color: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 64px;
|
||||
--border-color: #e2e8f0;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: var(--header-height);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
|
||||
color: white;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 50;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 24px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #fff;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: #fff;
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Nav Group Title */
|
||||
.nav-group-title {
|
||||
padding: 8px 24px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Main Content Styles */
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
flex: 1;
|
||||
background-color: var(--bg-color);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header h3 {
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 40px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
padding: 48px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--heading-color);
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--heading-color);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.35rem;
|
||||
margin: 2rem 0 1rem;
|
||||
color: var(--heading-color);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--heading-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--text-color);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
.img-container {
|
||||
margin: 24px 0 32px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.img-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.img-caption {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f1f5f9;
|
||||
font-weight: 600;
|
||||
color: var(--heading-color);
|
||||
}
|
||||
|
||||
td {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Feature Cards */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.feature-card h4 {
|
||||
margin-top: 0;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Step List */
|
||||
.step-list {
|
||||
counter-reset: step;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.step-list li {
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.step-list li::before {
|
||||
counter-increment: step;
|
||||
content: counter(step);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
background-color: #dbeafe;
|
||||
color: var(--primary-color);
|
||||
vertical-align: middle;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#3b82f6" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M2 17L12 22L22 17" stroke="#3b82f6" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M2 12L12 17L22 12" stroke="#3b82f6" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
智慧评标助手
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-item"><a href="#intro" class="nav-link active">产品概述</a></li>
|
||||
|
||||
<div class="nav-group-title">核心功能</div>
|
||||
<li class="nav-item"><a href="#start" class="nav-link">快速入门</a></li>
|
||||
<li class="nav-item"><a href="#summary" class="nav-link">概括总结</a></li>
|
||||
<li class="nav-item"><a href="#qa" class="nav-link">智能问答</a></li>
|
||||
<li class="nav-item"><a href="#comparison" class="nav-link">横向对比</a></li>
|
||||
|
||||
<div class="nav-group-title">其他</div>
|
||||
<li class="nav-item"><a href="#review" class="nav-link">评审与评分</a></li>
|
||||
<li class="nav-item"><a href="#faq" class="nav-link">常见问题</a></li>
|
||||
<li class="nav-item"><a href="#support" class="nav-link">联系方式</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="header">
|
||||
<h3>文档中心 / 用户手册 v1.0</h3>
|
||||
<div style="font-size: 0.9em; color: #64748b;">最后更新: 2025-08-15</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- 产品概述 -->
|
||||
<section id="intro">
|
||||
<h1>主观分评审助手 <span class="badge">v1.0</span></h1>
|
||||
|
||||
<!-- 产品介绍视频 -->
|
||||
<div class="img-container" style="border:none; box-shadow:none; background:transparent;">
|
||||
<video controls width="100%"
|
||||
style="border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);">
|
||||
<source src="./主观分助手.mp4" type="video/mp4">
|
||||
您的浏览器不支持视频播放,请下载视频观看。
|
||||
</video>
|
||||
<div class="img-caption">产品功能演示视频</div>
|
||||
</div>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/主页.jpeg" alt="产品主页展示">
|
||||
<div class="img-caption">产品主界面:清晰的卡片式布局与专业配色</div>
|
||||
</div>
|
||||
|
||||
<h3>1.2 产品简介</h3>
|
||||
<p>主观分评审助手v1.0是深度集成于智慧评审系统的AI辅助工具,基于LLM(大型语言模型)和RAG(检索增强生成)技术,提供<strong>智能问答</strong>、<strong>概括总结</strong>、<strong>横向对比</strong>三大核心功能。产品旨在通过智能化手段,帮助用户快速定位投标文件关键信息、建立方案整体认知、实现多供应商方案标准化对比,最终提升评审效率、保障评审公平性。
|
||||
</p>
|
||||
|
||||
<h3>1.3 适用范围</h3>
|
||||
<p>本手册适用于使用智慧评审系统进行招投标项目评审的专家,涵盖技术、财务、服务等各类主观分评审场景。</p>
|
||||
|
||||
<h3>1.4 环境要求</h3>
|
||||
<ul>
|
||||
<li><strong>运行环境</strong>:需在智慧评审系统内使用,支持主流浏览器(Chrome 90+、Edge 90+、Firefox 88+);</li>
|
||||
<li><strong>权限要求</strong>:需具备对应招投标项目的评审权限(由系统管理员分配);</li>
|
||||
<li><strong>文档要求</strong>:仅支持可正常读取的PDF、Word格式投标文件,暂不支持加密、手写版、图片密集型文档。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 快速入门 -->
|
||||
<section id="start">
|
||||
<h2>二、核心功能快速入门</h2>
|
||||
<p>产品全程嵌入智慧评审系统,以“评审助手”按钮为核心入口,操作流程遵循“准备-概览-质询-对比-结论”逻辑。</p>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h4>Step 1: 启动</h4>
|
||||
<p>登录系统,在主观分评审项(如服务方案)旁点击【评审助手】按钮。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h4>Step 2: 概览</h4>
|
||||
<p>使用【概括总结】功能,快速了解各供应商方案框架与核心内容。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h4>Step 3: 质询</h4>
|
||||
<p>通过【智能问答】功能,精准检索细节,支持跨文档对比提问。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h4>Step 4: 对比</h4>
|
||||
<p>使用【横向对比】生成多维度对比矩阵,辅助打分。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 概括总结 -->
|
||||
<section id="summary">
|
||||
<h2>3.2 概括总结功能操作</h2>
|
||||
<p>自动提取投标文件的层级目录,生成各章节核心摘要,帮助用户快速建立对方案的整体认知,替代传统逐页阅读。</p>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/概括总结.jpeg" alt="概括总结界面">
|
||||
<div class="img-caption">目录提取与章节智能摘要</div>
|
||||
</div>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/概括总结答案.jpeg" alt="概括总结详细内容">
|
||||
<div class="img-caption">生成的详细章节总结,支持点击原文</div>
|
||||
</div>
|
||||
|
||||
<h3>操作步骤</h3>
|
||||
<ul class="step-list">
|
||||
<li>在评审助手主界面,点击【概括总结】按钮,进入功能界面;</li>
|
||||
<li>系统自动加载当前评审项对应的所有供应商投标文件,左侧显示“供应商列表”;</li>
|
||||
<li>选择目标供应商,系统自动提取该供应商方案的树状目录(含章节标题及页码);</li>
|
||||
<li>点击目录任意章节,右侧“摘要展示区”实时显示该章节AI摘要;</li>
|
||||
<li>点击摘要中“查看原文”链接,可直达投标文件对应页码预览。</li>
|
||||
</ul>
|
||||
|
||||
<div class="feature-card" style="background:#fff7ed; border-color:#fed7aa; margin-top:20px;">
|
||||
<h4 style="color:#c2410c">⚠️ 注意事项</h4>
|
||||
<p style="color:#9a3412; margin-bottom:0">摘要通常30秒内完成。若投标文件有更新,请点击【刷新】按钮重新提取。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 智能问答 -->
|
||||
<section id="qa">
|
||||
<h2>3.3 智能问答功能操作</h2>
|
||||
<p>支持自然语言提问,基于检索增强生成(RAG)技术,精准检索相关信息并生成答案,附带原文溯源。</p>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/智能问答对话页面.jpeg" alt="智能问答对话界面">
|
||||
<div class="img-caption">支持多轮对话与原文溯源</div>
|
||||
</div>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/智能问答选择文件.jpeg" alt="多文档选择">
|
||||
<div class="img-caption">支持同时勾选多个文档进行对比提问</div>
|
||||
</div>
|
||||
|
||||
<h3>操作步骤</h3>
|
||||
<ul class="step-list">
|
||||
<li>点击【文档问答】按钮,进入功能界面;</li>
|
||||
<li>通过“文件选择器”勾选需要提问的投标文件(单选深入挖掘,多选横向对比);</li>
|
||||
<li>输入自然语言问题(如“A公司自研时序数据库的性能指标是什么?”);</li>
|
||||
<li>点击发送,系统生成答案并标注来源(如 <code>来源:P28,3.1章节</code>);</li>
|
||||
<li>点击来源标注可直接跳转原文高亮查看。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 横向对比 -->
|
||||
<section id="comparison">
|
||||
<h2>3.4 横向对比功能操作</h2>
|
||||
<p>支持选择多供应商、多维度进行方案对比,系统生成标准化对比矩阵,直观呈现优劣差异。</p>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/横向对比页面.jpeg" alt="横向对比配置">
|
||||
<div class="img-caption">Step 1: 配置对比维度与厂商</div>
|
||||
</div>
|
||||
|
||||
<div class="img-container">
|
||||
<img src="./images/横向对比结果.jpeg" alt="横向对比结果矩阵">
|
||||
<div class="img-caption">Step 2: 查看多维度对比矩阵结果</div>
|
||||
</div>
|
||||
|
||||
<h3>核心流程</h3>
|
||||
<div class="feature-grid">
|
||||
<div>
|
||||
<h4>1. 配置 (P4页面)</h4>
|
||||
<ul>
|
||||
<li>选择预设评审模块或添加“自定义维度”</li>
|
||||
<li>勾选需对比的2家及以上供应商</li>
|
||||
<li>点击【开始解析对比】启动异步任务</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>2. 查看 (P5页面)</h4>
|
||||
<ul>
|
||||
<li>任务完成后自动展示对比矩阵</li>
|
||||
<li>每行代表一个维度,每列代表一家厂商</li>
|
||||
<li>若结果不满意,支持单维度【重新生成】</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 评审与评分 -->
|
||||
<section id="review">
|
||||
<h2>3.5 评审意见填写与评分</h2>
|
||||
<p>完成概览、质询、对比后,可直接在评审助手侧边栏下方的“评审意见”输入区填写评审结论。填写完成后,点击输入区下方【保存】按钮,意见会同步至智慧评审系统的对应区域。</p>
|
||||
</section>
|
||||
|
||||
<!-- 常见问题 -->
|
||||
<section id="faq">
|
||||
<h2>四、常见问题与解决方法</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="25%">常见问题</th>
|
||||
<th width="35%">可能原因</th>
|
||||
<th width="40%">解决方法</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>点击“评审助手”无响应</td>
|
||||
<td>浏览器缓存异常;无权限</td>
|
||||
<td>刷新页面或清除缓存;联系管理员确认权限</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>无法生成摘要</td>
|
||||
<td>文档加密;文档过大</td>
|
||||
<td>联系招标方获取非加密文档;耐心等待处理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>问答不准确或无来源</td>
|
||||
<td>问题模糊;文档无相关信息</td>
|
||||
<td>优化问题表述;确认文档相关性</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>横向对比时间过长</td>
|
||||
<td>对比厂商/维度过多</td>
|
||||
<td>建议每次对比厂商≤5家,维度≤8个</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<section id="support">
|
||||
<h2>五、联系方式与支持</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h4>📞 技术支持</h4>
|
||||
<p>电话:400-XXXX-XXXX<br><span style="font-size:0.9em; color:#64748b">(工作日 9:00-18:00)</span></p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h4>📧 邮箱支持</h4>
|
||||
<p>support@xxx.com<br><span style="font-size:0.9em; color:#64748b">(24小时内回复)</span></p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h4>💬 系统反馈</h4>
|
||||
<p>登录系统 -> 【帮助中心】 -> 【问题反馈】提交工单。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px; font-size: 0.9em; color: #94a3b8; text-align: center;">
|
||||
注:文档部分辅助内容可能由AI生成,请以实际评审要求为准。
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 平滑滚动与导航高亮
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
targetSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
history.pushState(null, null, targetId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 滚动监听 (Scroll Spy)
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-20% 0px -60% 0px',
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute('id');
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + id) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('section').forEach(section => {
|
||||
observer.observe(section);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
30
sql/migration_add_catalog_extraction_status.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 添加目录摘取状态相关字段到 t_file 表
|
||||
-- 执行时间: 2025-12-02
|
||||
|
||||
-- 添加目录摘取状态字段
|
||||
ALTER TABLE t_file ADD COLUMN IF NOT EXISTS catalog_extraction_status VARCHAR(32) DEFAULT 'not_started';
|
||||
|
||||
-- 添加目录摘取重试次数字段
|
||||
ALTER TABLE t_file ADD COLUMN IF NOT EXISTS catalog_extraction_retry_count INT DEFAULT 0 NOT NULL;
|
||||
|
||||
-- 添加目录摘取完成时间字段
|
||||
ALTER TABLE t_file ADD COLUMN IF NOT EXISTS catalog_extraction_time TIMESTAMP;
|
||||
|
||||
-- 添加字段注释
|
||||
COMMENT ON COLUMN t_file.catalog_extraction_status IS '目录摘取状态:not_started-未开始, processing-处理中, completed-已完成';
|
||||
COMMENT ON COLUMN t_file.catalog_extraction_retry_count IS '目录摘取重试次数(最多重试3次)';
|
||||
COMMENT ON COLUMN t_file.catalog_extraction_time IS '目录摘取完成时间';
|
||||
|
||||
-- 为已有数据设置初始状态
|
||||
-- 如果已经有目录记录,设置为 completed
|
||||
UPDATE t_file f
|
||||
SET catalog_extraction_status = 'completed',
|
||||
catalog_extraction_time = f.update_time
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM t_file_directory fd
|
||||
WHERE fd.file_id = f.file_id
|
||||
)
|
||||
AND (catalog_extraction_status IS NULL OR catalog_extraction_status = 'not_started');
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_file_catalog_status ON t_file(catalog_extraction_status);
|
||||
BIN
src/main/java/com/zhpb/.DS_Store
vendored
Normal file
BIN
src/main/java/com/zhpb/gdyd_zhpb_zgf/.DS_Store
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf
|
||||
* @ClassName: GdydZhpbZgfApplication
|
||||
* @Description: Spring Boot应用主启动类,启用异步支持
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class GdydZhpbZgfApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("http://127.0.0.1:8080");
|
||||
SpringApplication.run(GdydZhpbZgfApplication.class, args);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: AIModelConfig
|
||||
* @Description: AI模型配置类
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-05
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.AIModelService;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.impl.DeepSeekServiceImpl;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.impl.PrivateModelServiceImpl;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* AI模型配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class AIModelConfig {
|
||||
|
||||
@Value("${ai.model-type:public}")
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 根据配置选择使用的AI模型服务
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public AIModelService aiModelService(DeepSeekServiceImpl deepSeekService,
|
||||
PrivateModelServiceImpl privateModelService) {
|
||||
if ("private".equalsIgnoreCase(modelType)) {
|
||||
return privateModelService;
|
||||
} else {
|
||||
return deepSeekService;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/zhpb/gdyd_zhpb_zgf/config/AsyncConfig.java
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: AsyncConfig
|
||||
* @Description: 异步任务线程池配置
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-12-11
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 异步任务线程池配置
|
||||
* 为AI模型调用和异步处理任务提供专用的线程池
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AsyncConfig.class);
|
||||
|
||||
/**
|
||||
* AI任务专用线程池
|
||||
* 用于章节总结、横向对比等AI相关的异步任务
|
||||
*/
|
||||
@Bean(name = "aiTaskExecutor")
|
||||
public Executor aiTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(3); // 核心线程数
|
||||
executor.setMaxPoolSize(6); // 最大线程数
|
||||
executor.setQueueCapacity(50); // 队列容量
|
||||
executor.setThreadNamePrefix("ai-task-");
|
||||
executor.setKeepAliveSeconds(60);
|
||||
|
||||
// 设置拒绝策略:由调用者线程执行(确保任务不会丢失)
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
executor.initialize();
|
||||
logger.info("AI任务线程池初始化完成: coreSize={}, maxSize={}, queueCapacity={}",
|
||||
executor.getCorePoolSize(), executor.getMaxPoolSize(), 50);
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录抽取专用线程池
|
||||
* 用于文件目录抽取等IO密集型任务
|
||||
*/
|
||||
@Bean(name = "directoryTaskExecutor")
|
||||
public Executor directoryTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2); // 核心线程数
|
||||
executor.setMaxPoolSize(4); // 最大线程数
|
||||
executor.setQueueCapacity(100); // 队列容量
|
||||
executor.setThreadNamePrefix("dir-task-");
|
||||
executor.setKeepAliveSeconds(60);
|
||||
|
||||
// 设置拒绝策略:由调用者线程执行
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
executor.initialize();
|
||||
logger.info("目录抽取线程池初始化完成: coreSize={}, maxSize={}, queueCapacity={}",
|
||||
executor.getCorePoolSize(), executor.getMaxPoolSize(), 100);
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比分析专用线程池
|
||||
* 用于供应商横向对比等分析任务
|
||||
*/
|
||||
@Bean(name = "comparisonTaskExecutor")
|
||||
public Executor comparisonTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2); // 核心线程数
|
||||
executor.setMaxPoolSize(4); // 最大线程数
|
||||
executor.setQueueCapacity(30); // 队列容量
|
||||
executor.setThreadNamePrefix("comp-task-");
|
||||
executor.setKeepAliveSeconds(60);
|
||||
|
||||
// 设置拒绝策略:由调用者线程执行
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
executor.initialize();
|
||||
logger.info("对比分析线程池初始化完成: coreSize={}, maxSize={}, queueCapacity={}",
|
||||
executor.getCorePoolSize(), executor.getMaxPoolSize(), 30);
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
225
src/main/java/com/zhpb/gdyd_zhpb_zgf/config/DataInitializer.java
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: DataInitializer
|
||||
* @Description: 应用启动时初始化数据
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.File;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.PromptTemplate;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileRepository;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.PromptTemplateRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据初始化器
|
||||
*/
|
||||
@Component
|
||||
public class DataInitializer implements CommandLineRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataInitializer.class);
|
||||
|
||||
@Autowired
|
||||
private PromptTemplateRepository promptTemplateRepository;
|
||||
|
||||
@Autowired
|
||||
private FileRepository fileRepository;
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
logger.info("开始初始化系统数据...");
|
||||
|
||||
// 初始化提示词模板
|
||||
initializePromptTemplates();
|
||||
|
||||
logger.info("系统数据初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化提示词模板
|
||||
*/
|
||||
private void initializePromptTemplates() {
|
||||
logger.info("检查是否需要初始化提示词模板...");
|
||||
|
||||
// 检查是否已有各种类型的模板
|
||||
long qaCount = promptTemplateRepository.countByTemplateType("qa");
|
||||
long summaryCount = promptTemplateRepository.countByTemplateType("summary");
|
||||
long comparisonCount = promptTemplateRepository.countByTemplateType("comparison");
|
||||
|
||||
if (qaCount > 0 && summaryCount > 0 && comparisonCount > 0) {
|
||||
logger.info("所有提示词模板已存在 (QA: {}, Summary: {}, Comparison: {}), 跳过初始化", qaCount, summaryCount, comparisonCount);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("开始初始化提示词模板数据 (QA: {}, Summary: {}, Comparison: {})...", qaCount, summaryCount, comparisonCount);
|
||||
|
||||
List<PromptTemplate> templatesToAdd = new ArrayList<>();
|
||||
|
||||
// 如果没有QA模板,添加QA模板
|
||||
if (qaCount == 0) {
|
||||
templatesToAdd.addAll(Arrays.asList(
|
||||
createTemplate("tender-qa-general-001", "通用评标问答", 0,
|
||||
"你是一个专业的招标评审专家助手。请基于以下提供的招标文件内容,准确、客观地回答评审专家的问题。\n\n招标文件内容:\n{document_content}\n\n评审问题:{query}\n\n请遵循以下原则:\n1. 严格基于招标文件内容回答,不添加外部知识或个人观点\n2. 引用具体条款、页码或章节,便于核实\n3. 对于技术参数、商务条件要准确表述\n4. 如有多个相关条款,应进行归纳整理\n5. 对于文件未明确的内容,应明确说明\"招标文件中未明确规定\"\n6. 保持专业、客观、中立的态度\n\n评审意见:",
|
||||
"通用评标问答提示词,适用于各类招标项目的评审问答"),
|
||||
|
||||
createTemplate("tender-qa-technical-002", "技术评审问答", 1,
|
||||
"你是一个专业的技术评审专家。请基于以下技术规格书和相关技术文档,回答技术评审相关问题。\n\n技术文档内容:\n{document_content}\n\n技术问题:{query}\n\n技术评审要点:\n1. 严格按照技术规格书要求进行评审\n2. 重点关注技术参数、性能指标、质量标准\n3. 识别技术风险和潜在问题\n4. 评估技术方案的可行性和先进性\n5. 检查技术指标是否满足最低要求\n6. 对于技术偏离应明确指出影响程度\n\n技术评审意见:\n- 符合性分析:\n- 技术风险评估:\n- 建议意见:",
|
||||
"技术评审专用提示词,重点关注技术参数、性能指标等技术评审内容"),
|
||||
|
||||
createTemplate("tender-qa-commercial-003", "商务评审问答", 2,
|
||||
"你是一个专业的商务评审专家。请基于以下商务文件和报价文件,回答商务评审相关问题。\n\n商务文件内容:\n{document_content}\n\n商务问题:{query}\n\n商务评审要点:\n1. 严格按照招标文件商务要求进行评审\n2. 重点关注报价合理性、商务条件、付款方式\n3. 检查商务偏差及其影响\n4. 评估供应商的商务实力和信誉\n5. 审核商务条款的合规性和可行性\n6. 计算商务得分并给出评分理由\n\n商务评审意见:\n- 报价分析:\n- 商务条件评估:\n- 合规性检查:\n- 评分建议:",
|
||||
"商务评审专用提示词,重点关注报价、商务条件、付款方式等商务评审内容"),
|
||||
|
||||
createTemplate("tender-qa-compliance-004", "合规性检查问答", 3,
|
||||
"你是一个专业的招标合规性审查专家。请基于招标文件和相关法律法规,回答合规性相关问题。\n\n招标文件及法律法规内容:\n{document_content}\n\n合规性问题:{query}\n\n合规性审查要点:\n1. 检查投标文件是否符合招标文件要求\n2. 审核供应商资质和相关证书\n3. 验证投标保证金、履约保证金等保证措施\n4. 审查投标文件完整性和有效性\n5. 识别潜在的违规行为和风险\n6. 确认投标有效期和投标截止时间\n\n合规性审查意见:\n- 资格审查:\n- 文件完整性:\n- 保证措施:\n- 违规风险:\n- 审查结论:",
|
||||
"合规性检查专用提示词,重点关注投标文件的合规性和有效性审查"),
|
||||
|
||||
createTemplate("tender-qa-comparison-005", "供应商对比问答", 4,
|
||||
"你是一个专业的供应商对比分析专家。请基于多个供应商的投标文件,进行对比分析并回答相关问题。\n\n供应商投标文件内容:\n{document_content}\n\n对比分析问题:{query}\n\n供应商对比要点:\n1. 对比各供应商的技术方案差异\n2. 分析报价合理性和竞争力\n3. 评估供应商实力和服务能力\n4. 识别各供应商的优势和劣势\n5. 综合评价供应商的综合竞争力\n6. 提出供应商选择建议\n\n供应商对比分析:\n- 技术方案对比:\n- 商务条件对比:\n- 供应商实力评估:\n- 综合评分建议:\n- 推荐意见:",
|
||||
"供应商对比分析专用提示词,用于多个供应商投标文件的对比评审")
|
||||
));
|
||||
}
|
||||
|
||||
// 如果没有Summary模板,添加Summary模板
|
||||
if (summaryCount == 0) {
|
||||
templatesToAdd.add(
|
||||
createSummaryTemplate("default-summary-template-001", "默认文档摘要提示词", 10,
|
||||
"章节标题:{title}\n\n请直接返回该章节的总结内容,不要任何开场白、结尾语或其他多余文字。不使用markdown格式。不说'好的,这是...'之类的话。只返回纯文本总结内容,300字以内,可以换行。总结要准确、客观、简洁,突出核心要点。",
|
||||
"默认的文档摘要提示词模板,用于指导AI如何对文档章节进行结构化摘要")
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有Comparison模板,添加Comparison模板
|
||||
if (comparisonCount == 0) {
|
||||
templatesToAdd.add(
|
||||
createComparisonTemplate("default-comparison-template-001", "默认供应商对比提示词", 11,
|
||||
"请对以下供应商在{comparison_dimension}维度的表现进行对比分析。\n\n供应商信息:\n{supplier_info}\n\n请直接返回对比分析结果,不要任何开场白、结尾语或其他多余文字。不使用markdown格式。只返回纯文本分析内容,500字以内,可以换行。分析要求:客观、准确、突出差异和特点。",
|
||||
"默认的供应商对比分析提示词模板,用于指导AI如何进行供应商间的横向对比")
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!templatesToAdd.isEmpty()) {
|
||||
promptTemplateRepository.saveAll(templatesToAdd);
|
||||
logger.info("成功初始化 {} 个提示词模板", templatesToAdd.size());
|
||||
} else {
|
||||
logger.info("没有需要初始化的提示词模板");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("初始化提示词模板失败", e);
|
||||
}
|
||||
|
||||
// 初始化测试文件数据
|
||||
initializeTestFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化测试文件数据
|
||||
*/
|
||||
private void initializeTestFiles() {
|
||||
logger.info("开始初始化测试文件数据...");
|
||||
|
||||
List<File> testFiles = Arrays.asList(
|
||||
createTestFile("file-001", "技术方案.pdf", "project-001", "supplier-001", 2048576L),
|
||||
createTestFile("file-002", "商务文件.pdf", "project-001", "supplier-002", 1536000L),
|
||||
createTestFile("file-003", "报价清单.xlsx", "project-001", "supplier-001", 512000L),
|
||||
createTestFile("file-004", "技术参数表.doc", "project-001", "supplier-002", 1024000L),
|
||||
createTestFile("file-005", "合规性承诺书.pdf", "project-001", "supplier-003", 768000L)
|
||||
);
|
||||
|
||||
try {
|
||||
fileRepository.saveAll(testFiles);
|
||||
logger.info("成功初始化 {} 个测试文件", testFiles.size());
|
||||
} catch (Exception e) {
|
||||
logger.error("初始化测试文件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建QA提示词模板对象
|
||||
*/
|
||||
private PromptTemplate createTemplate(String templateId, String templateName, int sortOrder,
|
||||
String promptContent, String description) {
|
||||
PromptTemplate template = new PromptTemplate();
|
||||
template.setTemplateId(templateId);
|
||||
template.setTemplateName(templateName);
|
||||
template.setTemplateType("qa");
|
||||
template.setPromptContent(promptContent);
|
||||
template.setDescription(description);
|
||||
template.setIsActive(true);
|
||||
template.setSortOrder(sortOrder);
|
||||
template.setCreatedBy("system");
|
||||
template.setUpdatedBy("system");
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文档摘要提示词模板对象
|
||||
*/
|
||||
private PromptTemplate createSummaryTemplate(String templateId, String templateName, int sortOrder,
|
||||
String promptContent, String description) {
|
||||
PromptTemplate template = new PromptTemplate();
|
||||
template.setTemplateId(templateId);
|
||||
template.setTemplateName(templateName);
|
||||
template.setTemplateType("summary");
|
||||
template.setPromptContent(promptContent);
|
||||
template.setDescription(description);
|
||||
template.setIsActive(true);
|
||||
template.setSortOrder(sortOrder);
|
||||
template.setCreatedBy("system");
|
||||
template.setUpdatedBy("system");
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建供应商对比分析提示词模板对象
|
||||
*/
|
||||
private PromptTemplate createComparisonTemplate(String templateId, String templateName, int sortOrder,
|
||||
String promptContent, String description) {
|
||||
PromptTemplate template = new PromptTemplate();
|
||||
template.setTemplateId(templateId);
|
||||
template.setTemplateName(templateName);
|
||||
template.setTemplateType("comparison");
|
||||
template.setPromptContent(promptContent);
|
||||
template.setDescription(description);
|
||||
template.setIsActive(true);
|
||||
template.setSortOrder(sortOrder);
|
||||
template.setCreatedBy("system");
|
||||
template.setUpdatedBy("system");
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试文件对象
|
||||
*/
|
||||
private File createTestFile(String fileId, String fileName, String projectId, String supplierId, Long fileSize) {
|
||||
File file = new File();
|
||||
file.setFileId(fileId);
|
||||
file.setFileName(fileName);
|
||||
file.setProjectId(projectId);
|
||||
file.setSupplierId(supplierId);
|
||||
file.setFileSize(fileSize);
|
||||
file.setFileExtension(fileName.substring(fileName.lastIndexOf('.') + 1));
|
||||
file.setFilePath("/test/" + fileName); // 模拟路径
|
||||
file.setFileMd5("test-md5-" + fileId); // 模拟MD5
|
||||
file.setStatus("uploaded");
|
||||
file.setIsVectorized(true); // 设为已向量化,方便测试
|
||||
file.setVectorizationStatus("completed");
|
||||
file.setVectorizationCount(1);
|
||||
file.setDescription("测试文件 - " + fileName);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: DataSourceConfig
|
||||
* @Description: 数据源配置类,配置MySQL和PostgreSQL双数据源,支持数据库自动创建
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Properties;
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableJpaRepositories(basePackages = "com.zhpb.gdyd_zhpb_zgf.repository")
|
||||
public class DataSourceConfig {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceConfig.class);
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public DataSourceConfig(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public DataSource dataSource() {
|
||||
ensurePostgreSQLDatabase();
|
||||
|
||||
DataSource dataSource = DataSourceBuilder.create()
|
||||
.driverClassName(environment.getProperty("spring.datasource.driver-class-name"))
|
||||
.url(environment.getProperty("spring.datasource.url"))
|
||||
.username(environment.getProperty("spring.datasource.username"))
|
||||
.password(environment.getProperty("spring.datasource.password"))
|
||||
.build();
|
||||
|
||||
ensurePostgreSQLVectorExtension();
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
|
||||
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
|
||||
em.setDataSource(dataSource);
|
||||
em.setPackagesToScan("com.zhpb.gdyd_zhpb_zgf.entity");
|
||||
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setShowSql(Boolean.parseBoolean(environment.getProperty("spring.jpa.show-sql", "false")));
|
||||
em.setJpaVendorAdapter(vendorAdapter);
|
||||
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("hibernate.hbm2ddl.auto",
|
||||
environment.getProperty("spring.jpa.hibernate.ddl-auto", "update"));
|
||||
properties.setProperty("hibernate.dialect",
|
||||
environment.getProperty("spring.jpa.properties.hibernate.dialect",
|
||||
"org.hibernate.dialect.PostgreSQLDialect"));
|
||||
properties.setProperty("hibernate.format_sql",
|
||||
environment.getProperty("spring.jpa.properties.hibernate.format_sql", "false"));
|
||||
properties.setProperty("hibernate.jdbc.time_zone",
|
||||
environment.getProperty("spring.jpa.properties.hibernate.jdbc.time_zone", "Asia/Shanghai"));
|
||||
em.setJpaProperties(properties);
|
||||
|
||||
return em;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public PlatformTransactionManager transactionManager(
|
||||
LocalContainerEntityManagerFactoryBean entityManagerFactory) {
|
||||
return new JpaTransactionManager(entityManagerFactory.getObject());
|
||||
}
|
||||
|
||||
private void ensurePostgreSQLVectorExtension() {
|
||||
try {
|
||||
String url = environment.getProperty("spring.datasource.url");
|
||||
String username = environment.getProperty("spring.datasource.username", "postgres");
|
||||
String password = environment.getProperty("spring.datasource.password", "123456");
|
||||
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (java.sql.Connection conn = java.sql.DriverManager.getConnection(url, username, password);
|
||||
java.sql.Statement stmt = conn.createStatement()) {
|
||||
|
||||
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
|
||||
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery(
|
||||
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 't_pdf_vector')")) {
|
||||
if (rs.next() && !rs.getBoolean(1)) {
|
||||
stmt.execute("CREATE TABLE t_pdf_vector (" +
|
||||
"id BIGSERIAL PRIMARY KEY, " +
|
||||
"file_path VARCHAR(1000) NOT NULL, " +
|
||||
"file_name VARCHAR(500) NOT NULL, " +
|
||||
"content TEXT, " +
|
||||
"vector vector, " +
|
||||
"dimension INTEGER NOT NULL, " +
|
||||
"file_id VARCHAR(64), " +
|
||||
"project_id VARCHAR(100), " +
|
||||
"supplier_id VARCHAR(64), " +
|
||||
"file_type_id BIGINT, " +
|
||||
"create_time TIMESTAMP(6) NOT NULL DEFAULT NOW(), " +
|
||||
"update_time TIMESTAMP(6) DEFAULT NOW()" +
|
||||
")");
|
||||
LOGGER.info("PostgreSQL t_pdf_vector表已创建");
|
||||
} else {
|
||||
addMissingColumnsIfNeeded(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery(
|
||||
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 't_product')")) {
|
||||
if (rs.next() && !rs.getBoolean(1)) {
|
||||
stmt.execute("CREATE TABLE t_product (" +
|
||||
"id BIGSERIAL PRIMARY KEY, " +
|
||||
"name VARCHAR(255), " +
|
||||
"price DOUBLE PRECISION" +
|
||||
")");
|
||||
LOGGER.info("PostgreSQL t_product表已创建");
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("PostgreSQL pgvector扩展和表初始化完成");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("创建PostgreSQL pgvector扩展或表失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void addMissingColumnsIfNeeded(java.sql.Statement stmt) {
|
||||
try {
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery(
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'public' AND table_name = 't_pdf_vector' AND column_name = 'file_id')")) {
|
||||
if (rs.next() && !rs.getBoolean(1)) {
|
||||
stmt.execute("ALTER TABLE t_pdf_vector ADD COLUMN file_id VARCHAR(64)");
|
||||
LOGGER.info("已添加file_id列到t_pdf_vector表");
|
||||
}
|
||||
}
|
||||
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery(
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'public' AND table_name = 't_pdf_vector' AND column_name = 'supplier_id')")) {
|
||||
if (rs.next() && !rs.getBoolean(1)) {
|
||||
stmt.execute("ALTER TABLE t_pdf_vector ADD COLUMN supplier_id VARCHAR(64)");
|
||||
LOGGER.info("已添加supplier_id列到t_pdf_vector表");
|
||||
}
|
||||
}
|
||||
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery(
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'public' AND table_name = 't_pdf_vector' AND column_name = 'file_type_id')")) {
|
||||
if (rs.next() && !rs.getBoolean(1)) {
|
||||
stmt.execute("ALTER TABLE t_pdf_vector ADD COLUMN file_type_id BIGINT");
|
||||
LOGGER.info("已添加file_type_id列到t_pdf_vector表");
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("t_pdf_vector表字段检查完成");
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("添加缺失列失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePostgreSQLDatabase() {
|
||||
try {
|
||||
String url = environment.getProperty("spring.datasource.url");
|
||||
String username = environment.getProperty("spring.datasource.username", "postgres");
|
||||
String password = environment.getProperty("spring.datasource.password");
|
||||
|
||||
if (url == null || password == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String databaseName = extractDatabaseName(url);
|
||||
if (databaseName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String defaultUrl = url.replace("/" + databaseName, "/postgres");
|
||||
|
||||
try (java.sql.Connection conn = java.sql.DriverManager.getConnection(defaultUrl, username, password)) {
|
||||
String checkSql = "SELECT 1 FROM pg_database WHERE datname = ?";
|
||||
try (java.sql.PreparedStatement stmt = conn.prepareStatement(checkSql)) {
|
||||
stmt.setString(1, databaseName);
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
conn.setAutoCommit(true);
|
||||
String createSql = "CREATE DATABASE \"" + databaseName + "\"";
|
||||
try (java.sql.Statement createStmt = conn.createStatement()) {
|
||||
createStmt.execute(createSql);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("自动创建PostgreSQL数据库失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String extractDatabaseName(String url) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int lastSlash = url.lastIndexOf('/');
|
||||
if (lastSlash > 0 && lastSlash < url.length() - 1) {
|
||||
String dbPart = url.substring(lastSlash + 1);
|
||||
int questionMark = dbPart.indexOf('?');
|
||||
if (questionMark > 0) {
|
||||
return dbPart.substring(0, questionMark);
|
||||
}
|
||||
return dbPart;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: DatabaseInitializer
|
||||
* @Description: 数据库初始化器,用于在应用启动时创建数据库(已迁移到DataSourceConfig)
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* 数据库初始化配置类
|
||||
* 用于自动创建不存在的数据库
|
||||
* 在数据源Bean创建之前执行初始化
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "spring.datasource.url")
|
||||
public class DatabaseInitializer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseInitializer.class);
|
||||
|
||||
@Value("${spring.datasource.url}")
|
||||
private String pgsqlUrl;
|
||||
|
||||
@Value("${spring.datasource.username:postgres}")
|
||||
private String pgsqlUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String pgsqlPassword;
|
||||
|
||||
/**
|
||||
* 应用启动后自动初始化数据库
|
||||
* 使用@PostConstruct确保在Bean初始化时立即执行
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initializeDatabase() {
|
||||
try {
|
||||
// 解析数据库名称
|
||||
String databaseName = extractDatabaseName(pgsqlUrl);
|
||||
if (databaseName == null) {
|
||||
logger.warn("无法从PostgreSQL URL中解析数据库名称,跳过自动创建");
|
||||
return;
|
||||
}
|
||||
|
||||
// 连接到默认的postgres数据库
|
||||
String defaultUrl = pgsqlUrl.replace("/" + databaseName, "/postgres");
|
||||
|
||||
logger.info("正在检查PostgreSQL数据库 '{}' 是否存在...", databaseName);
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(defaultUrl, pgsqlUsername, pgsqlPassword)) {
|
||||
// 检查数据库是否存在
|
||||
String checkSql = "SELECT 1 FROM pg_database WHERE datname = ?";
|
||||
try (PreparedStatement stmt = conn.prepareStatement(checkSql)) {
|
||||
stmt.setString(1, databaseName);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
// 数据库不存在,创建它
|
||||
logger.info("数据库 '{}' 不存在,正在创建...", databaseName);
|
||||
|
||||
// PostgreSQL不允许在事务中执行CREATE DATABASE
|
||||
// CREATE DATABASE不能使用PreparedStatement,需要使用Statement
|
||||
conn.setAutoCommit(true);
|
||||
String createSql = "CREATE DATABASE \"" + databaseName + "\"";
|
||||
try (Statement createStmt = conn.createStatement()) {
|
||||
createStmt.execute(createSql);
|
||||
logger.info("数据库 '{}' 创建成功!", databaseName);
|
||||
}
|
||||
} else {
|
||||
logger.info("数据库 '{}' 已存在,跳过创建", databaseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("自动创建PostgreSQL数据库失败: {}", e.getMessage());
|
||||
logger.debug("详细信息", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JDBC URL中提取数据库名称
|
||||
*
|
||||
* @param url JDBC URL,例如:jdbc:postgresql://localhost:5432/database_name
|
||||
* @return 数据库名称
|
||||
*/
|
||||
private String extractDatabaseName(String url) {
|
||||
try {
|
||||
// 格式:jdbc:postgresql://host:port/database_name
|
||||
int lastSlash = url.lastIndexOf('/');
|
||||
if (lastSlash > 0 && lastSlash < url.length() - 1) {
|
||||
String dbPart = url.substring(lastSlash + 1);
|
||||
// 移除可能的查询参数
|
||||
int questionMark = dbPart.indexOf('?');
|
||||
if (questionMark > 0) {
|
||||
return dbPart.substring(0, questionMark);
|
||||
}
|
||||
return dbPart;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("解析数据库名称失败", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: DatabaseLogAppender
|
||||
* @Description: Logback自定义Appender,将所有日志输出保存到数据库
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.UnsynchronizedAppenderBase;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.SysLog;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.SysLogRepository;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 数据库日志Appender
|
||||
*/
|
||||
public class DatabaseLogAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
|
||||
|
||||
private static ApplicationContext applicationContext;
|
||||
private static SysLogRepository sysLogRepository;
|
||||
|
||||
public static void setApplicationContext(ApplicationContext context) {
|
||||
DatabaseLogAppender.applicationContext = context;
|
||||
if (context != null) {
|
||||
try {
|
||||
sysLogRepository = context.getBean(SysLogRepository.class);
|
||||
} catch (Exception e) {
|
||||
// Bean未准备好,稍后重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void append(ILoggingEvent event) {
|
||||
// 过滤掉不需要记录的日志
|
||||
if (shouldSkip(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sysLogRepository == null && applicationContext != null) {
|
||||
try {
|
||||
sysLogRepository = applicationContext.getBean(SysLogRepository.class);
|
||||
} catch (Exception e) {
|
||||
// Bean未准备好,忽略
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sysLogRepository == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SysLog sysLog = new SysLog();
|
||||
sysLog.setLogLevel(event.getLevel().toString());
|
||||
sysLog.setLoggerName(event.getLoggerName());
|
||||
|
||||
// 格式化日志消息
|
||||
String formattedMessage = event.getFormattedMessage();
|
||||
// 将多行内容合并为一行
|
||||
String singleLineContent = formattedMessage.replace("\n", " ").replace("\r", " ").trim();
|
||||
sysLog.setLogContent(singleLineContent);
|
||||
sysLog.setFullMessage(getFullMessage(event).replace("\n", " ").replace("\r", " "));
|
||||
|
||||
// 异常堆栈(保留多行格式)
|
||||
if (event.getThrowableProxy() != null) {
|
||||
sysLog.setExceptionStack(getStackTrace(event));
|
||||
}
|
||||
|
||||
sysLog.setThreadName(event.getThreadName());
|
||||
sysLog.setCreateTime(LocalDateTime.now());
|
||||
sysLog.setApplicationName("gdyd_zhpb_zgf");
|
||||
|
||||
// 获取调用位置信息
|
||||
if (event.getCallerData() != null && event.getCallerData().length > 0) {
|
||||
StackTraceElement caller = event.getCallerData()[0];
|
||||
sysLog.setClassName(caller.getClassName());
|
||||
sysLog.setMethodName(caller.getMethodName());
|
||||
sysLog.setLineNumber(caller.getLineNumber());
|
||||
}
|
||||
|
||||
// 异步保存(使用新线程,避免阻塞)
|
||||
new Thread(() -> {
|
||||
try {
|
||||
sysLogRepository.save(sysLog);
|
||||
} catch (Exception e) {
|
||||
// 忽略保存失败,避免循环日志
|
||||
}
|
||||
}).start();
|
||||
} catch (Exception e) {
|
||||
// 忽略异常,避免影响正常日志输出
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该跳过该日志
|
||||
*/
|
||||
private boolean shouldSkip(ILoggingEvent event) {
|
||||
String loggerName = event.getLoggerName().toLowerCase();
|
||||
String message = event.getFormattedMessage().toLowerCase();
|
||||
|
||||
// 跳过SQL相关日志
|
||||
if (loggerName.contains("org.hibernate.sql") ||
|
||||
loggerName.contains("org.hibernate.type") ||
|
||||
message.contains("hibernate:") ||
|
||||
message.contains("select ") ||
|
||||
message.contains("insert ") ||
|
||||
message.contains("update ") ||
|
||||
message.contains("delete ") ||
|
||||
message.contains("create table") ||
|
||||
message.contains("alter table")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 跳过Spring Boot启动相关日志
|
||||
if (loggerName.contains("org.springframework.boot") ||
|
||||
loggerName.contains("org.springframework.context") ||
|
||||
loggerName.contains("org.springframework.beans") ||
|
||||
loggerName.contains("org.hibernate.boot") ||
|
||||
loggerName.contains("org.hibernate.jpa") ||
|
||||
loggerName.contains("org.springframework.orm") ||
|
||||
message.contains("started application") ||
|
||||
message.contains("beanfactory") ||
|
||||
message.contains("obtaining singleton") ||
|
||||
message.contains("autowired annotation") ||
|
||||
message.contains("initialized jpa")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 跳过Hibernate初始化日志
|
||||
if ((loggerName.contains("hibernate") || loggerName.contains("org.hibernate")) &&
|
||||
(message.contains("dialect") ||
|
||||
message.contains("sessionfactory") ||
|
||||
message.contains("entitymanagerfactory") ||
|
||||
message.contains("hibernate is in classpath"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getFullMessage(ILoggingEvent event) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[").append(event.getLevel()).append("] ");
|
||||
sb.append("[").append(event.getThreadName()).append("] ");
|
||||
sb.append(event.getLoggerName()).append(" - ");
|
||||
sb.append(event.getFormattedMessage());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String getStackTrace(ILoggingEvent event) {
|
||||
if (event.getThrowableProxy() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(event.getThrowableProxy().getClassName()).append(": ")
|
||||
.append(event.getThrowableProxy().getMessage()).append("\n");
|
||||
|
||||
if (event.getThrowableProxy().getStackTraceElementProxyArray() != null) {
|
||||
for (int i = 0; i < event.getThrowableProxy().getStackTraceElementProxyArray().length; i++) {
|
||||
sb.append(" at ").append(event.getThrowableProxy().getStackTraceElementProxyArray()[i])
|
||||
.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: DatabaseMigration
|
||||
* @Description: 数据库迁移配置 - 启动时自动添加缺失的字段
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-12-02
|
||||
* @Version: 1.0
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 数据库迁移 - 自动添加缺失的字段
|
||||
*/
|
||||
@Component
|
||||
public class DatabaseMigration implements ApplicationRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
logger.info("开始执行数据库迁移检查...");
|
||||
|
||||
try {
|
||||
// 添加目录摘取相关字段
|
||||
addCatalogExtractionFields();
|
||||
|
||||
logger.info("数据库迁移检查完成");
|
||||
} catch (Exception e) {
|
||||
logger.error("数据库迁移失败", e);
|
||||
// 不抛出异常,避免影响应用启动
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加目录摘取相关字段
|
||||
*/
|
||||
private void addCatalogExtractionFields() {
|
||||
try {
|
||||
// 检查并添加 catalog_extraction_status 字段
|
||||
if (!columnExists("t_file", "catalog_extraction_status")) {
|
||||
logger.info("添加字段: t_file.catalog_extraction_status");
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE t_file ADD COLUMN catalog_extraction_status VARCHAR(32) DEFAULT 'processing'"
|
||||
);
|
||||
logger.info("字段添加成功: catalog_extraction_status");
|
||||
} else {
|
||||
logger.info("字段已存在: catalog_extraction_status");
|
||||
}
|
||||
|
||||
// 检查并添加 catalog_extraction_retry_count 字段
|
||||
if (!columnExists("t_file", "catalog_extraction_retry_count")) {
|
||||
logger.info("添加字段: t_file.catalog_extraction_retry_count");
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE t_file ADD COLUMN catalog_extraction_retry_count INT DEFAULT 0 NOT NULL"
|
||||
);
|
||||
logger.info("字段添加成功: catalog_extraction_retry_count");
|
||||
} else {
|
||||
logger.info("字段已存在: catalog_extraction_retry_count");
|
||||
}
|
||||
|
||||
// 检查并添加 catalog_extraction_time 字段
|
||||
if (!columnExists("t_file", "catalog_extraction_time")) {
|
||||
logger.info("添加字段: t_file.catalog_extraction_time");
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE t_file ADD COLUMN catalog_extraction_time TIMESTAMP"
|
||||
);
|
||||
logger.info("字段添加成功: catalog_extraction_time");
|
||||
} else {
|
||||
logger.info("字段已存在: catalog_extraction_time");
|
||||
}
|
||||
|
||||
// 添加字段注释
|
||||
try {
|
||||
jdbcTemplate.execute(
|
||||
"COMMENT ON COLUMN t_file.catalog_extraction_status IS '目录摘取状态:not_started-未开始, processing-处理中, completed-已完成'"
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"COMMENT ON COLUMN t_file.catalog_extraction_retry_count IS '目录摘取重试次数(最多重试3次)'"
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"COMMENT ON COLUMN t_file.catalog_extraction_time IS '目录摘取完成时间'"
|
||||
);
|
||||
logger.info("字段注释添加成功");
|
||||
} catch (Exception e) {
|
||||
logger.warn("添加字段注释失败(可忽略): {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 为已有数据设置初始状态
|
||||
try {
|
||||
int updatedRows = jdbcTemplate.update(
|
||||
"UPDATE t_file f SET catalog_extraction_status = 'completed', " +
|
||||
"catalog_extraction_time = f.update_time " +
|
||||
"WHERE EXISTS (SELECT 1 FROM t_file_directory fd WHERE fd.file_id = f.file_id) " +
|
||||
"AND (catalog_extraction_status IS NULL OR catalog_extraction_status = 'not_started')"
|
||||
);
|
||||
logger.info("更新已有数据状态,影响行数: {}", updatedRows);
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新已有数据状态失败(可忽略): {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
try {
|
||||
jdbcTemplate.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_file_catalog_status ON t_file(catalog_extraction_status)"
|
||||
);
|
||||
logger.info("索引创建成功: idx_file_catalog_status");
|
||||
} catch (Exception e) {
|
||||
logger.warn("创建索引失败(可忽略): {}", e.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("添加目录摘取字段失败", e);
|
||||
throw new RuntimeException("数据库迁移失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字段是否存在
|
||||
*/
|
||||
private boolean columnExists(String tableName, String columnName) {
|
||||
try {
|
||||
String sql = "SELECT COUNT(*) FROM information_schema.columns " +
|
||||
"WHERE table_name = ? AND column_name = ?";
|
||||
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName, columnName);
|
||||
return count != null && count > 0;
|
||||
} catch (Exception e) {
|
||||
logger.warn("检查字段是否存在时出错: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/main/java/com/zhpb/gdyd_zhpb_zgf/config/LogConfig.java
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: LogConfig
|
||||
* @Description: 日志配置类,初始化数据库日志Appender
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 日志配置类
|
||||
*/
|
||||
@Component
|
||||
public class LogConfig implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
// 初始化数据库日志Appender(通过logback-spring.xml配置的Appender会自动调用setApplicationContext)
|
||||
DatabaseLogAppender.setApplicationContext(applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: OpenApiConfig
|
||||
* @Description: Swagger/OpenAPI配置类,配置API文档的标题、描述和分组
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("智慧评标-主观分助手 API 文档")
|
||||
.version("1.0.0")
|
||||
.description("智慧评标-主观分助手接口文档,支持单点登录(SSO)通过ES接口验证ticket并生成JWT Token")
|
||||
.contact(new Contact()
|
||||
.name("开发团队")
|
||||
.email("dev@example.com"))
|
||||
.license(new License()
|
||||
.name("Apache 2.0")
|
||||
.url("https://www.apache.org/licenses/LICENSE-2.0.html")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi publicApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("default")
|
||||
.pathsToMatch("/api/**")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: PostgreSQLHealthIndicator
|
||||
* @Description: PostgreSQL健康检查指示器,用于Actuator健康检查
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* PostgreSQL 数据库健康检查指示器
|
||||
*/
|
||||
@Component("pgsqlHealthIndicator")
|
||||
public class PostgreSQLHealthIndicator implements HealthIndicator {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
if (connection.isValid(1)) {
|
||||
// 可以执行一个简单的查询来验证连接
|
||||
try (java.sql.Statement stmt = connection.createStatement();
|
||||
java.sql.ResultSet rs = stmt.executeQuery("SELECT version()")) {
|
||||
if (rs.next()) {
|
||||
String version = rs.getString(1);
|
||||
return Health.up()
|
||||
.withDetail("database", "PostgreSQL")
|
||||
.withDetail("status", "连接正常")
|
||||
.withDetail("version", version)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
return Health.up()
|
||||
.withDetail("database", "PostgreSQL")
|
||||
.withDetail("status", "连接正常")
|
||||
.build();
|
||||
} else {
|
||||
return Health.down()
|
||||
.withDetail("database", "PostgreSQL")
|
||||
.withDetail("status", "连接无效")
|
||||
.build();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return Health.down()
|
||||
.withDetail("database", "PostgreSQL")
|
||||
.withDetail("status", "连接失败")
|
||||
.withDetail("error", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: PostgreSQLVectorInitializer
|
||||
* @Description: PostgreSQL向量扩展初始化器,确保pgvector扩展已安装
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* PostgreSQL向量扩展初始化器
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "vectorize.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class PostgreSQLVectorInitializer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostgreSQLVectorInitializer.class);
|
||||
|
||||
@Value("${spring.datasource.url:jdbc:postgresql://127.0.0.1:5432/gdyd_zhpb_zgf}")
|
||||
private String pgsqlUrl;
|
||||
|
||||
@Value("${spring.datasource.username:postgres}")
|
||||
private String pgsqlUsername;
|
||||
|
||||
@Value("${spring.datasource.password:123456}")
|
||||
private String pgsqlPassword;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
// 连接到目标数据库并创建扩展
|
||||
try (Connection conn = java.sql.DriverManager.getConnection(pgsqlUrl, pgsqlUsername, pgsqlPassword)) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
// 创建pgvector扩展
|
||||
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
|
||||
logger.info("PostgreSQL pgvector扩展初始化成功");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("PostgreSQL pgvector扩展初始化失败,可能未安装pgvector扩展: {}", e.getMessage());
|
||||
logger.warn("请手动在PostgreSQL中执行: CREATE EXTENSION IF NOT EXISTS vector;");
|
||||
logger.warn("如果pgvector未安装,请参考: https://github.com/pgvector/pgvector");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/main/java/com/zhpb/gdyd_zhpb_zgf/config/RedisConfig.java
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: RedisConfig
|
||||
* @Description: Redis配置类,配置Redis连接和序列化方式
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* Redis Template 配置
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 设置 Key 的序列化方式
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// 设置 Value 的序列化方式
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: RequestResponseWrapperConfig
|
||||
* @Description: 请求响应包装器配置类,包装HttpServletRequest和HttpServletResponse以支持内容缓存
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求和响应包装器配置
|
||||
* 用于在拦截器中读取请求体和响应体
|
||||
*/
|
||||
@Component
|
||||
public class RequestResponseWrapperConfig implements Filter {
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
// 不需要初始化
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
|
||||
String uri = httpRequest.getRequestURI();
|
||||
if (uri.contains("/api/v1/files/stream/pdf") || uri.contains("/api/v1/files/preview/pdf")) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 包装请求和响应,以便可以多次读取内容
|
||||
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(httpRequest);
|
||||
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(httpResponse);
|
||||
|
||||
try {
|
||||
chain.doFilter(wrappedRequest, wrappedResponse);
|
||||
} finally {
|
||||
// 确保响应内容被写入
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// 不需要清理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: SystemOutInterceptor
|
||||
* @Description: 拦截System.out.println输出,保存到数据库
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.SysLog;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.SysLogRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* System.out拦截器
|
||||
*/
|
||||
@Component
|
||||
public class SystemOutInterceptor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SystemOutInterceptor.class);
|
||||
|
||||
@Autowired
|
||||
private SysLogRepository sysLogRepository;
|
||||
|
||||
@Value("${spring.application.name:gdyd_zhpb_zgf}")
|
||||
private String applicationName;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 创建拦截输出流
|
||||
try {
|
||||
PrintStream originalOut = System.out;
|
||||
PrintStream originalErr = System.err;
|
||||
|
||||
System.setOut(new PrintStream(new DatabasePrintStream(originalOut, "OUT", false), true, "UTF-8"));
|
||||
System.setErr(new PrintStream(new DatabasePrintStream(originalErr, "ERR", true), true, "UTF-8"));
|
||||
logger.info("System.out和System.err拦截器已启动");
|
||||
} catch (Exception e) {
|
||||
logger.error("启动System.out拦截器失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义打印流,同时输出到原流和数据库
|
||||
*/
|
||||
private class DatabasePrintStream extends ByteArrayOutputStream {
|
||||
private final PrintStream original;
|
||||
private final String type;
|
||||
private final boolean isError;
|
||||
|
||||
public DatabasePrintStream(PrintStream original, String type, boolean isError) {
|
||||
this.original = original;
|
||||
this.type = type;
|
||||
this.isError = isError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) {
|
||||
super.write(b, off, len);
|
||||
original.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) {
|
||||
super.write(b);
|
||||
original.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
try {
|
||||
super.flush();
|
||||
original.flush();
|
||||
saveToDatabase();
|
||||
} catch (Exception e) {
|
||||
// 忽略异常,确保不影响正常输出
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToDatabase() {
|
||||
try {
|
||||
String content = new String(toByteArray(), Charset.forName("UTF-8"));
|
||||
if (content != null && !content.trim().isEmpty()) {
|
||||
// 清除缓冲区
|
||||
reset();
|
||||
|
||||
// 过滤掉不需要记录的日志
|
||||
if (shouldSkip(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 将多行内容合并为一行(替换换行符为空格)
|
||||
String singleLine = content.replace("\n", " ").replace("\r", " ").trim();
|
||||
|
||||
if (singleLine.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SysLog sysLog = new SysLog();
|
||||
sysLog.setLogLevel(isError ? "ERROR" : "OUT");
|
||||
sysLog.setLoggerName("SYSTEM_" + type);
|
||||
sysLog.setLogContent(singleLine);
|
||||
sysLog.setFullMessage("[" + type + "] " + singleLine);
|
||||
sysLog.setThreadName(Thread.currentThread().getName());
|
||||
sysLog.setCreateTime(LocalDateTime.now());
|
||||
sysLog.setApplicationName(applicationName);
|
||||
|
||||
// 异步保存
|
||||
final SysLog finalLog = sysLog;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
sysLogRepository.save(finalLog);
|
||||
} catch (Exception e) {
|
||||
// 忽略保存失败,避免循环
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略异常,确保不影响正常输出
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该跳过该日志
|
||||
*/
|
||||
private boolean shouldSkip(String content) {
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String lowerContent = content.toLowerCase();
|
||||
|
||||
// 跳过SQL语句
|
||||
if (lowerContent.contains("hibernate:") ||
|
||||
lowerContent.contains("select ") ||
|
||||
lowerContent.contains("insert ") ||
|
||||
lowerContent.contains("update ") ||
|
||||
lowerContent.contains("delete ") ||
|
||||
lowerContent.contains("create table") ||
|
||||
lowerContent.contains("alter table") ||
|
||||
lowerContent.contains("drop table")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 跳过Spring Boot启动相关日志
|
||||
if (lowerContent.contains("started gdydzhpbzgfapplication") ||
|
||||
lowerContent.contains("spring application startup") ||
|
||||
lowerContent.contains("beanfactory") ||
|
||||
lowerContent.contains("obtaining singleton bean") ||
|
||||
lowerContent.contains("autowired annotation") ||
|
||||
lowerContent.contains("initialized jpa entitymanagerfactory") ||
|
||||
lowerContent.contains("hibernate is in classpath") ||
|
||||
lowerContent.contains("creating shared instance") ||
|
||||
lowerContent.startsWith("http://127.0.0.1:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 跳过Hibernate相关日志
|
||||
if (lowerContent.contains("hibernate") &&
|
||||
(lowerContent.contains("dialect") ||
|
||||
lowerContent.contains("sessionfactory") ||
|
||||
lowerContent.contains("entitymanagerfactory"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
flush();
|
||||
super.close();
|
||||
} catch (Exception e) {
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.UmiOcrClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* Umi-OCR 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class UmiOcrConfig {
|
||||
|
||||
@Value("${umi-ocr.base-url:http://127.0.0.1:1224}")
|
||||
private String baseUrl;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UmiOcrClient umiOcrClient(RestTemplate restTemplate, ObjectMapper objectMapper) {
|
||||
return new UmiOcrClient(baseUrl, restTemplate, objectMapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: VectorizationHealthIndicator
|
||||
* @Description: 向量化服务健康检查指示器,用于Actuator健康检查
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.File;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 向量化服务健康检查指示器
|
||||
*/
|
||||
@Component
|
||||
public class VectorizationHealthIndicator implements HealthIndicator {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VectorizationHealthIndicator.class);
|
||||
|
||||
@Autowired
|
||||
private FileRepository fileRepository;
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
logger.debug("开始执行向量化服务健康检查");
|
||||
try {
|
||||
// 检查数据库连接和文件表
|
||||
logger.debug("查询所有文件记录");
|
||||
List<File> files = fileRepository.findAll();
|
||||
logger.debug("查询到 {} 个文件记录", files.size());
|
||||
|
||||
// 统计向量化状态
|
||||
int totalFiles = files.size();
|
||||
int completedFiles = 0;
|
||||
int processingFiles = 0;
|
||||
int failedFiles = 0;
|
||||
int notStartedFiles = 0;
|
||||
|
||||
LocalDateTime lastVectorizedTime = null;
|
||||
|
||||
for (File file : files) {
|
||||
String status = file.getVectorizationStatus();
|
||||
if ("completed".equals(status)) {
|
||||
completedFiles++;
|
||||
if (lastVectorizedTime == null ||
|
||||
(file.getVectorizedTime() != null && file.getVectorizedTime().isAfter(lastVectorizedTime))) {
|
||||
lastVectorizedTime = file.getVectorizedTime();
|
||||
}
|
||||
} else if ("processing".equals(status)) {
|
||||
processingFiles++;
|
||||
} else if ("failed".equals(status)) {
|
||||
failedFiles++;
|
||||
} else {
|
||||
notStartedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算健康状态
|
||||
// 如果有太多失败的文件,认为服务不健康
|
||||
double failureRate = totalFiles > 0 ? (double) failedFiles / totalFiles : 0.0;
|
||||
|
||||
if (failureRate > 0.5) {
|
||||
// 失败率超过50%,认为服务不健康
|
||||
logger.warn("向量化服务健康检查失败:失败率过高 ({})", String.format("%.2f%%", failureRate * 100));
|
||||
return Health.down()
|
||||
.withDetail("service", "向量化服务")
|
||||
.withDetail("status", "健康状况异常")
|
||||
.withDetail("total_files", totalFiles)
|
||||
.withDetail("completed_files", completedFiles)
|
||||
.withDetail("processing_files", processingFiles)
|
||||
.withDetail("failed_files", failedFiles)
|
||||
.withDetail("not_started_files", notStartedFiles)
|
||||
.withDetail("failure_rate", String.format("%.2f%%", failureRate * 100))
|
||||
.withDetail("error", "向量化失败率过高")
|
||||
.build();
|
||||
} else if (processingFiles > 0) {
|
||||
// 有文件正在处理,认为是正常但有警告
|
||||
logger.info("向量化服务健康检查通过:有文件正在处理");
|
||||
return Health.up()
|
||||
.withDetail("service", "向量化服务")
|
||||
.withDetail("status", "正常(有文件正在处理)")
|
||||
.withDetail("total_files", totalFiles)
|
||||
.withDetail("completed_files", completedFiles)
|
||||
.withDetail("processing_files", processingFiles)
|
||||
.withDetail("failed_files", failedFiles)
|
||||
.withDetail("not_started_files", notStartedFiles)
|
||||
.withDetail("completion_rate", String.format("%.2f%%", totalFiles > 0 ? (double) completedFiles / totalFiles * 100 : 0))
|
||||
.withDetail("last_vectorized", lastVectorizedTime != null ? lastVectorizedTime.toString() : "无")
|
||||
.build();
|
||||
} else {
|
||||
// 正常状态
|
||||
logger.info("向量化服务健康检查通过:运行正常");
|
||||
return Health.up()
|
||||
.withDetail("service", "向量化服务")
|
||||
.withDetail("status", "运行正常")
|
||||
.withDetail("total_files", totalFiles)
|
||||
.withDetail("completed_files", completedFiles)
|
||||
.withDetail("processing_files", processingFiles)
|
||||
.withDetail("failed_files", failedFiles)
|
||||
.withDetail("not_started_files", notStartedFiles)
|
||||
.withDetail("completion_rate", String.format("%.2f%%", totalFiles > 0 ? (double) completedFiles / totalFiles * 100 : 0))
|
||||
.withDetail("last_vectorized", lastVectorizedTime != null ? lastVectorizedTime.toString() : "无")
|
||||
.build();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return Health.down()
|
||||
.withDetail("service", "向量化服务")
|
||||
.withDetail("status", "检查失败")
|
||||
.withDetail("error", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/main/java/com/zhpb/gdyd_zhpb_zgf/config/WebConfig.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.config
|
||||
* @ClassName: WebConfig
|
||||
* @Description: Web配置类,注册API日志拦截器和静态资源处理器
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.config;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.interceptor.ApiLogInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private ApiLogInterceptor apiLogInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(apiLogInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/static/**",
|
||||
"/webjars/**"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 确保静态资源正常访问
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
|
||||
// 添加 /view/** 路径映射到 static/view/ 目录
|
||||
registry.addResourceHandler("/view/**")
|
||||
.addResourceLocations("classpath:/static/view/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.zhpb.gdyd_zhpb_zgf.context;
|
||||
|
||||
/**
|
||||
* 线程级模型请求上下文,用于在调用模型/向量化前标记业务来源。
|
||||
*/
|
||||
public final class ModelRequestContextHolder {
|
||||
|
||||
private static final ThreadLocal<String> REQUEST_SOURCE = new ThreadLocal<>();
|
||||
|
||||
private ModelRequestContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前线程的模型请求来源。
|
||||
*
|
||||
* @param requestSource 业务来源标识,例如 QA_CHAT、DOC_SUMMARY
|
||||
*/
|
||||
public static void setRequestSource(String requestSource) {
|
||||
if (requestSource == null || requestSource.isBlank()) {
|
||||
REQUEST_SOURCE.remove();
|
||||
} else {
|
||||
REQUEST_SOURCE.set(requestSource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前线程的模型请求来源,若为空则返回默认值。
|
||||
*
|
||||
* @param defaultValue 默认业务来源
|
||||
* @return 请求来源
|
||||
*/
|
||||
public static String getRequestSourceOrDefault(String defaultValue) {
|
||||
String source = REQUEST_SOURCE.get();
|
||||
if (source == null || source.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前线程上下文,防止线程复用导致交叉污染。
|
||||
*/
|
||||
public static void clear() {
|
||||
REQUEST_SOURCE.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: ComparisonController
|
||||
* @Description: 供应商对比分析控制器
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-04
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.ComparisonRequest;
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.ComparisonResponse;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.ComparisonService;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.UserLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 供应商对比分析控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/comparison")
|
||||
@Tag(name = "供应商对比分析", description = "供应商横向对比分析相关接口")
|
||||
public class ComparisonController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ComparisonController.class);
|
||||
|
||||
@Autowired
|
||||
private ComparisonService comparisonService;
|
||||
|
||||
@Autowired
|
||||
private UserLogService userLogService;
|
||||
|
||||
/**
|
||||
* 获取指定项目的供应商数据
|
||||
*/
|
||||
@GetMapping("/vendors")
|
||||
@Operation(
|
||||
summary = "获取项目供应商数据",
|
||||
description = "获取指定项目下的所有供应商及其关联文件,用于在对比时选择。"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "获取成功",
|
||||
content = @io.swagger.v3.oas.annotations.media.Content(
|
||||
mediaType = "application/json",
|
||||
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ComparisonResponse.VendorsData.class)
|
||||
)
|
||||
),
|
||||
@ApiResponse(responseCode = "422", description = "参数验证错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getProjectVendors(
|
||||
@Parameter(description = "项目ID", required = true, example = "project-001")
|
||||
@RequestParam("project_id") String projectId,
|
||||
@Parameter(description = "用户ID", required = false, example = "user-001")
|
||||
@RequestParam(value = "userId", required = false) String userId,
|
||||
@Parameter(description = "用户ID(兼容字段)", required = false, example = "user-001")
|
||||
@RequestParam(value = "userld", required = false) String legacyUserId,
|
||||
@Parameter(description = "用户名", required = false, example = "张三")
|
||||
@RequestParam(value = "userName", required = false) String userName,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
String resolvedUserId = userId != null ? userId : legacyUserId;
|
||||
|
||||
try {
|
||||
ComparisonResponse.VendorsData data = comparisonService.getProjectVendors(projectId);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("message", "获取供应商数据成功");
|
||||
response.put("data", data);
|
||||
|
||||
userLogService.logUserOperation(resolvedUserId, userName, projectId,
|
||||
"VIEW_VENDORS", "获取项目供应商数据", null, request);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取项目供应商数据失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("message", "获取供应商数据失败: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动供应商对比分析任务
|
||||
*/
|
||||
@PostMapping("/start")
|
||||
@Operation(
|
||||
summary = "启动对比分析任务",
|
||||
description = "提交一个异步的横向对比任务。"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "任务启动成功",
|
||||
content = @io.swagger.v3.oas.annotations.media.Content(
|
||||
mediaType = "application/json",
|
||||
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ComparisonResponse.StartTaskData.class)
|
||||
)
|
||||
),
|
||||
@ApiResponse(responseCode = "422", description = "参数验证错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> startComparison(
|
||||
@Parameter(description = "对比分析请求参数", required = true)
|
||||
@Valid @RequestBody ComparisonRequest request) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
ComparisonResponse.StartTaskData data = comparisonService.startComparison(request);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("message", "对比分析任务已启动");
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.status(201).body(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("启动对比分析任务失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("message", "启动对比分析任务失败: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对比任务历史
|
||||
*/
|
||||
@GetMapping("/history/tasks")
|
||||
@Operation(
|
||||
summary = "获取对比任务历史",
|
||||
description = "获取指定用户在某个项目下的所有历史对比任务列表。"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "获取成功",
|
||||
content = @io.swagger.v3.oas.annotations.media.Content(
|
||||
mediaType = "application/json",
|
||||
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ComparisonResponse.TaskHistoryData.class)
|
||||
)
|
||||
),
|
||||
@ApiResponse(responseCode = "422", description = "参数验证错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getComparisonHistory(
|
||||
@Parameter(description = "项目ID", required = true, example = "project-001")
|
||||
@RequestParam("project_id") String projectId,
|
||||
@Parameter(description = "用户ID", required = true, example = "user-001")
|
||||
@RequestParam("user_id") String userId,
|
||||
@Parameter(description = "用户名", required = false, example = "张三")
|
||||
@RequestParam(value = "userName", required = false) String userName,
|
||||
@Parameter(description = "用户名(兼容字段)", required = false, example = "张三")
|
||||
@RequestParam(value = "user_name", required = false) String legacyUserName,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
ComparisonResponse.TaskHistoryData data = comparisonService.getComparisonHistory(projectId, userId);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("message", "获取对比任务历史成功");
|
||||
response.put("data", data);
|
||||
|
||||
String resolvedUserName = userName;
|
||||
if (resolvedUserName == null || resolvedUserName.isBlank()) {
|
||||
resolvedUserName = legacyUserName;
|
||||
}
|
||||
if ((resolvedUserName == null || resolvedUserName.isBlank()) && request != null) {
|
||||
resolvedUserName = request.getHeader("User-Name");
|
||||
if (resolvedUserName == null || resolvedUserName.isBlank()) {
|
||||
resolvedUserName = request.getHeader("userName");
|
||||
}
|
||||
}
|
||||
|
||||
userLogService.logUserOperation(userId, resolvedUserName, projectId,
|
||||
"VIEW_COMPARISON_HISTORY", "获取对比任务历史", null, request);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取对比任务历史失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("message", "获取对比任务历史失败: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对比任务结果
|
||||
*/
|
||||
@GetMapping("/result")
|
||||
@Operation(
|
||||
summary = "获取对比任务结果",
|
||||
description = "根据任务ID,获取对比任务的状态和结果。甲方可通过轮询此接口来获取最终结果。"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "获取成功",
|
||||
content = @io.swagger.v3.oas.annotations.media.Content(
|
||||
mediaType = "application/json",
|
||||
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ComparisonResponse.ComparisonResultData.class)
|
||||
)
|
||||
),
|
||||
@ApiResponse(responseCode = "422", description = "参数验证错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getComparisonResult(
|
||||
@Parameter(description = "任务ID", required = true, example = "task-001")
|
||||
@RequestParam("task_id") String taskId) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
ComparisonResponse.ComparisonResultData data = comparisonService.getComparisonResult(taskId);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("message", "获取对比任务结果成功");
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取对比任务结果失败", e);
|
||||
response.put("code", 500);
|
||||
response.put("message", "获取对比任务结果失败: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.File;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 简单文件浏览与下载接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/view/api/v1/files")
|
||||
@Tag(name = "文件浏览", description = "列出已存储文件并提供下载")
|
||||
public class FileBrowseController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FileBrowseController.class);
|
||||
|
||||
private final FileRepository fileRepository;
|
||||
|
||||
public FileBrowseController(FileRepository fileRepository) {
|
||||
this.fileRepository = fileRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出文件,可按项目/供应商筛选
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "列出已存储文件", description = "可按项目ID、供应商ID过滤")
|
||||
public List<Map<String, Object>> listFiles(
|
||||
@Parameter(description = "项目ID") @RequestParam(required = false) String projectId,
|
||||
@Parameter(description = "供应商ID") @RequestParam(required = false) String supplierId) {
|
||||
|
||||
List<File> files;
|
||||
if (projectId != null && supplierId != null) {
|
||||
files = fileRepository.findByProjectIdAndSupplierId(projectId, supplierId);
|
||||
} else if (projectId != null) {
|
||||
files = fileRepository.findByProjectId(projectId);
|
||||
} else if (supplierId != null) {
|
||||
files = fileRepository.findBySupplierId(supplierId);
|
||||
} else {
|
||||
files = fileRepository.findAll();
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(files)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return files.stream().map(f -> Map.<String, Object>ofEntries(
|
||||
Map.entry("id", f.getId()),
|
||||
Map.entry("fileId", f.getFileId()),
|
||||
Map.entry("projectId", f.getProjectId()),
|
||||
Map.entry("supplierId", f.getSupplierId()),
|
||||
Map.entry("fileName", f.getFileName()),
|
||||
Map.entry("filePath", f.getFilePath()),
|
||||
Map.entry("fileSize", f.getFileSize()),
|
||||
Map.entry("fileExtension", f.getFileExtension()),
|
||||
Map.entry("fileTypeId", f.getFileTypeId()),
|
||||
Map.entry("status", f.getStatus()),
|
||||
Map.entry("downloadUrl", "/api/v1/files/download/" + f.getId())
|
||||
)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按ID下载文件
|
||||
*/
|
||||
@GetMapping("/download/{id}")
|
||||
@Operation(summary = "下载文件", description = "根据文件记录ID下载实际文件")
|
||||
public ResponseEntity<Resource> download(@PathVariable Long id) {
|
||||
return fileRepository.findById(id)
|
||||
.map(file -> {
|
||||
Path path = Paths.get(file.getFilePath());
|
||||
if (!Files.exists(path)) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).<Resource>build();
|
||||
}
|
||||
Resource resource = new FileSystemResource(path);
|
||||
String encodedName = URLEncoder.encode(file.getFileName(), StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedName)
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(resource);
|
||||
})
|
||||
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).<Resource>build());
|
||||
}
|
||||
}
|
||||
|
||||
1009
src/main/java/com/zhpb/gdyd_zhpb_zgf/controller/FileController.java
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: IndexController
|
||||
* @Description: 首页控制器,提供欢迎页面和健康检查页面的路由
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 首页控制器
|
||||
*/
|
||||
@Controller
|
||||
public class IndexController {
|
||||
|
||||
/**
|
||||
* 访问根路径时显示欢迎页面
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
// 重定向到静态页面
|
||||
return "redirect:/view/index.html";
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查页面
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public String health() {
|
||||
// 返回健康检查HTML页面
|
||||
return "redirect:/view/health.html";
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息接口
|
||||
*/
|
||||
@GetMapping("/version")
|
||||
public ResponseEntity<Map<String, Object>> version() {
|
||||
Map<String, Object> versionInfo = new HashMap<>();
|
||||
versionInfo.put("project", "智慧评标-主观分助手");
|
||||
versionInfo.put("version", System.getProperty("APP_VERSION", "1.0.0"));
|
||||
versionInfo.put("build_time", System.getProperty("BUILD_TIME", "unknown"));
|
||||
versionInfo.put("java_version", System.getProperty("java.version"));
|
||||
versionInfo.put("spring_boot_version", "3.2.0");
|
||||
|
||||
return ResponseEntity.ok(versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: OcrController
|
||||
* @Description: OCR控制器,提供OCR结果查询和管理接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-12-10
|
||||
* @Version: 1.0
|
||||
* @Copyright: 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.OcrResult;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.OcrService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* OCR控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/files/ocr")
|
||||
@Tag(name = "OCR管理", description = "OCR识别结果查询和管理接口")
|
||||
public class OcrController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OcrController.class);
|
||||
|
||||
@Autowired
|
||||
private OcrService ocrService;
|
||||
|
||||
/**
|
||||
* 根据文件ID获取OCR结果
|
||||
*/
|
||||
@GetMapping("/file/{fileId}")
|
||||
@Operation(summary = "获取文件OCR结果", description = "根据文件ID获取OCR识别结果")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "204", description = "OCR结果不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getOcrResultByFileId(
|
||||
@Parameter(description = "文件ID") @PathVariable Long fileId) {
|
||||
|
||||
try {
|
||||
Optional<OcrResult> ocrResult = ocrService.getOcrResultByFileId(fileId);
|
||||
|
||||
if (ocrResult.isPresent()) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "查询成功");
|
||||
result.put("data", ocrResult.get());
|
||||
return ResponseEntity.ok(result);
|
||||
} else {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 404);
|
||||
result.put("message", "OCR结果不存在");
|
||||
return ResponseEntity.status(404).body(result);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询OCR结果失败: fileId={}", fileId, e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据项目ID获取OCR结果列表
|
||||
*/
|
||||
@GetMapping("/project/{projectId}")
|
||||
@Operation(summary = "获取项目OCR结果列表", description = "根据项目ID获取所有OCR识别结果")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getOcrResultsByProject(
|
||||
@Parameter(description = "项目ID") @PathVariable String projectId,
|
||||
@Parameter(description = "供应商ID(可选)") @RequestParam(required = false) String supplierId) {
|
||||
|
||||
try {
|
||||
List<OcrResult> ocrResults;
|
||||
if (supplierId != null && !supplierId.trim().isEmpty()) {
|
||||
ocrResults = ocrService.getOcrResultsByProjectAndSupplier(projectId, supplierId);
|
||||
} else {
|
||||
ocrResults = ocrService.getOcrResultsByProject(projectId);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "查询成功");
|
||||
result.put("data", ocrResults);
|
||||
result.put("total", ocrResults.size());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询项目OCR结果失败: projectId={}, supplierId={}", projectId, supplierId, e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目OCR统计信息
|
||||
*/
|
||||
@GetMapping("/project/{projectId}/stats")
|
||||
@Operation(summary = "获取项目OCR统计", description = "获取项目的OCR处理统计信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getProjectOcrStatistics(
|
||||
@Parameter(description = "项目ID") @PathVariable String projectId) {
|
||||
|
||||
try {
|
||||
Map<String, Object> stats = ocrService.getProjectOcrStatistics(projectId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "查询成功");
|
||||
result.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询项目OCR统计失败: projectId={}", projectId, e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试失败的OCR任务
|
||||
*/
|
||||
@PostMapping("/retry/{ocrResultId}")
|
||||
@Operation(summary = "重试OCR任务", description = "重试失败的OCR识别任务")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "重试任务已启动"),
|
||||
@ApiResponse(responseCode = "404", description = "OCR结果不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> retryOcrTask(
|
||||
@Parameter(description = "OCR结果ID") @PathVariable Long ocrResultId) {
|
||||
|
||||
try {
|
||||
// 这里可以先检查OCR结果是否存在和状态
|
||||
// 但为了简单,直接调用重试方法
|
||||
ocrService.retryFailedOcrTask(ocrResultId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "重试任务已启动");
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("重试OCR任务失败: ocrResultId={}", ocrResultId, e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取失败的OCR任务列表
|
||||
*/
|
||||
@GetMapping("/failed")
|
||||
@Operation(summary = "获取失败的OCR任务", description = "获取所有失败的OCR任务列表")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getFailedOcrTasks() {
|
||||
|
||||
try {
|
||||
List<OcrResult> failedTasks = ocrService.getFailedOcrTasks();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "查询成功");
|
||||
result.put("data", failedTasks);
|
||||
result.put("total", failedTasks.size());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询失败的OCR任务失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: PromptTemplateController
|
||||
* @Description: 提示词模板控制器
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.PromptTemplateDto;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.PromptTemplateService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 提示词模板控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/prompt-templates")
|
||||
@Tag(name = "提示词模板", description = "提示词模板管理接口")
|
||||
public class PromptTemplateController {
|
||||
|
||||
@Autowired
|
||||
private PromptTemplateService promptTemplateService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取所有提示词模板", description = "获取所有提示词模板,可按类型过滤")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "获取成功")
|
||||
})
|
||||
public ResponseEntity<List<PromptTemplateDto>> getAllTemplates(
|
||||
@Parameter(description = "模板类型,可为空")
|
||||
@RequestParam(required = false) String templateType) {
|
||||
|
||||
List<PromptTemplateDto> templates = promptTemplateService.getAllTemplates(templateType);
|
||||
return ResponseEntity.ok(templates);
|
||||
}
|
||||
|
||||
@GetMapping("/{templateId}")
|
||||
@Operation(summary = "获取指定模板", description = "根据模板ID获取模板详情")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "获取成功"),
|
||||
@ApiResponse(responseCode = "404", description = "模板不存在")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getTemplateById(
|
||||
@Parameter(description = "模板ID", required = true)
|
||||
@PathVariable String templateId) {
|
||||
|
||||
PromptTemplateDto template = promptTemplateService.getTemplateById(templateId);
|
||||
if (template == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 200);
|
||||
response.put("message", "模板不存在");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("message", "Success");
|
||||
response.put("data", template);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建提示词模板", description = "创建新的提示词模板")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "创建成功"),
|
||||
@ApiResponse(responseCode = "400", description = "参数错误")
|
||||
})
|
||||
public ResponseEntity<PromptTemplateDto> createTemplate(
|
||||
@Parameter(description = "模板信息", required = true)
|
||||
@RequestBody @Validated PromptTemplateDto dto,
|
||||
@Parameter(description = "操作用户ID")
|
||||
@RequestParam(defaultValue = "admin") String userId) {
|
||||
|
||||
try {
|
||||
PromptTemplateDto createdTemplate = promptTemplateService.createTemplate(dto, userId);
|
||||
return ResponseEntity.ok(createdTemplate);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{templateId}")
|
||||
@Operation(summary = "更新提示词模板", description = "更新指定模板的信息")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "更新成功"),
|
||||
@ApiResponse(responseCode = "404", description = "模板不存在"),
|
||||
@ApiResponse(responseCode = "400", description = "参数错误")
|
||||
})
|
||||
public ResponseEntity<PromptTemplateDto> updateTemplate(
|
||||
@Parameter(description = "模板ID", required = true)
|
||||
@PathVariable String templateId,
|
||||
@Parameter(description = "模板信息", required = true)
|
||||
@RequestBody @Validated PromptTemplateDto dto,
|
||||
@Parameter(description = "操作用户ID")
|
||||
@RequestParam(defaultValue = "admin") String userId) {
|
||||
|
||||
try {
|
||||
PromptTemplateDto updatedTemplate = promptTemplateService.updateTemplate(templateId, dto, userId);
|
||||
return ResponseEntity.ok(updatedTemplate);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{templateId}")
|
||||
@Operation(summary = "删除提示词模板", description = "删除指定的提示词模板")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "删除成功"),
|
||||
@ApiResponse(responseCode = "404", description = "模板不存在")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> deleteTemplate(
|
||||
@Parameter(description = "模板ID", required = true)
|
||||
@PathVariable String templateId) {
|
||||
|
||||
try {
|
||||
promptTemplateService.deleteTemplate(templateId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("message", "删除成功");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 200);
|
||||
response.put("message", "模板不存在");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{templateId}/status")
|
||||
@Operation(summary = "切换模板状态", description = "启用或禁用指定的提示词模板")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "操作成功"),
|
||||
@ApiResponse(responseCode = "404", description = "模板不存在")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> toggleTemplateStatus(
|
||||
@Parameter(description = "模板ID", required = true)
|
||||
@PathVariable String templateId,
|
||||
@Parameter(description = "是否启用", required = true)
|
||||
@RequestParam Boolean isActive,
|
||||
@Parameter(description = "操作用户ID")
|
||||
@RequestParam(defaultValue = "admin") String userId) {
|
||||
|
||||
try {
|
||||
promptTemplateService.toggleTemplateStatus(templateId, isActive, userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("message", "操作成功");
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 200);
|
||||
response.put("message", "模板不存在");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: QaController
|
||||
* @Description: 智能问答控制器
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.QaHistoryResponse;
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.QaRequest;
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.QaResponse;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.QaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.UserLogService;
|
||||
|
||||
/**
|
||||
* 智能问答控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/qa")
|
||||
@Tag(name = "智能问答", description = "基于文档的智能问答接口")
|
||||
public class QaController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(QaController.class);
|
||||
|
||||
@Autowired
|
||||
private QaService qaService;
|
||||
|
||||
@Autowired
|
||||
private UserLogService userLogService;
|
||||
|
||||
@PostMapping("/sendMessage")
|
||||
@Operation(summary = "发送对话消息", description = "用户输入问题,针对一个或多个文件进行提问")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Successful Response"),
|
||||
@ApiResponse(responseCode = "422", description = "Validation Error")
|
||||
})
|
||||
public ResponseEntity<QaResponse> sendMessage(
|
||||
@Parameter(description = "智能问答请求")
|
||||
@RequestBody @Validated QaRequest request) {
|
||||
|
||||
logger.info("收到智能问答请求: projectId={}, userId={}, query={}",
|
||||
request.getProjectId(), request.getUserId(), request.getQuery());
|
||||
|
||||
try {
|
||||
QaResponse.QaData data = qaService.sendMessage(request);
|
||||
|
||||
QaResponse response = new QaResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("查询成功");
|
||||
response.setData(data);
|
||||
|
||||
// 记录用户操作日志
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
|
||||
HttpServletRequest httpRequest = attributes.getRequest();
|
||||
userLogService.logUserOperation(request.getUserId(), null, request.getProjectId(),
|
||||
"QA_QUESTION", "发送问答消息: " + request.getQuery(), null, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("智能问答处理失败", e);
|
||||
QaResponse response = new QaResponse();
|
||||
response.setCode(500);
|
||||
response.setMessage("系统错误: " + e.getMessage());
|
||||
response.setData(new QaResponse.QaData());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/history/sessions")
|
||||
@Operation(summary = "查询历史会话", description = "根据项目ID和用户ID查询历史会话列表")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<QaHistoryResponse> getSessionHistory(
|
||||
@Parameter(description = "项目ID", required = true)
|
||||
@RequestParam String projectId,
|
||||
@Parameter(description = "用户ID", required = true)
|
||||
@RequestParam String userId,
|
||||
@Parameter(description = "用户名", required = false)
|
||||
@RequestParam(required = false) String userName,
|
||||
@Parameter(description = "用户名(兼容字段)", required = false)
|
||||
@RequestParam(value = "user_name", required = false) String legacyUserName) {
|
||||
|
||||
logger.info("查询历史会话: projectId={}, userId={}", projectId, userId);
|
||||
|
||||
try {
|
||||
QaHistoryResponse.SessionsData data = qaService.getSessionHistory(projectId, userId);
|
||||
|
||||
QaHistoryResponse response = new QaHistoryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
// 记录用户操作日志
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
|
||||
HttpServletRequest httpRequest = attributes.getRequest();
|
||||
String resolvedUserName = StringUtils.hasText(userName) ? userName : legacyUserName;
|
||||
if (!StringUtils.hasText(resolvedUserName) && httpRequest != null) {
|
||||
resolvedUserName = httpRequest.getHeader("User-Name");
|
||||
if (!StringUtils.hasText(resolvedUserName)) {
|
||||
resolvedUserName = httpRequest.getHeader("userName");
|
||||
}
|
||||
}
|
||||
userLogService.logUserOperation(userId, resolvedUserName, projectId,
|
||||
"VIEW_QA_SESSIONS", "查询历史会话", null, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询历史会话失败", e);
|
||||
QaHistoryResponse response = new QaHistoryResponse();
|
||||
response.setCode(500);
|
||||
response.setMessage("系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/history/messages")
|
||||
@Operation(summary = "查询会话消息历史", description = "根据会话ID查询消息历史")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "404", description = "会话不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<QaHistoryResponse> getMessageHistory(
|
||||
@Parameter(description = "会话ID", required = true)
|
||||
@RequestParam String sessionId) {
|
||||
|
||||
logger.info("查询会话消息历史: sessionId={}", sessionId);
|
||||
|
||||
try {
|
||||
QaHistoryResponse.MessagesData data = qaService.getMessageHistory(sessionId);
|
||||
|
||||
if (data.getMessages().isEmpty()) {
|
||||
QaHistoryResponse response = new QaHistoryResponse();
|
||||
response.setCode(200);
|
||||
response.setMessage("会话不存在或无消息记录");
|
||||
return ResponseEntity.ok().body(response);
|
||||
}
|
||||
|
||||
QaHistoryResponse response = new QaHistoryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
// 记录用户操作日志
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
|
||||
HttpServletRequest httpRequest = attributes.getRequest();
|
||||
userLogService.logUserOperation(null, null, null,
|
||||
"VIEW_QA_MESSAGES", "查看问答历史消息", sessionId, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询会话消息历史失败", e);
|
||||
QaHistoryResponse response = new QaHistoryResponse();
|
||||
response.setCode(500);
|
||||
response.setMessage("系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: SsoController
|
||||
* @Description: SSO单点登录控制器,通过ES接口验证ticket并生成JWT Token
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.SsoLoginResponse;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.SsoLog;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.User;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.SsoLogRepository;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.UserService;
|
||||
import com.zhpb.gdyd_zhpb_zgf.utils.HttpClientUtils;
|
||||
import com.zhpb.gdyd_zhpb_zgf.utils.JwtUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SSO单点登录控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sso")
|
||||
@Tag(name = "单点登录", description = "SSO单点登录相关接口")
|
||||
public class SsoController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private SsoLogRepository ssoLogRepository;
|
||||
|
||||
@Value("${sso.es-base-url:http://es-integration.es-uat-paas:8890}")
|
||||
private String esBaseUrl;
|
||||
|
||||
@Value("${sso.getUserInfoPath:/bi/getUserInfoByTicket}")
|
||||
private String getUserInfoPath;
|
||||
|
||||
/**
|
||||
* SSO登录接口
|
||||
*
|
||||
* @param ticket SSO票据
|
||||
* @param request HTTP请求对象
|
||||
* @param response HTTP响应对象
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
@Operation(
|
||||
summary = "SSO单点登录",
|
||||
description = "通过ES接口验证ticket,获取用户信息并生成JWT Token返回给前端。用户不存在则自动创建。"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "登录成功",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = SsoLoginResponse.class),
|
||||
examples = @ExampleObject(
|
||||
name = "登录成功示例",
|
||||
value = "{\n" +
|
||||
" \"code\": 200,\n" +
|
||||
" \"message\": \"登录成功\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n" +
|
||||
" \"userId\": \"n001\",\n" +
|
||||
" \"userName\": \"张三\",\n" +
|
||||
" \"projectId\": \"0001\"\n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
)
|
||||
)
|
||||
),
|
||||
@ApiResponse(
|
||||
responseCode = "302",
|
||||
description = "Ticket验证失败,重定向到登录页",
|
||||
content = @Content
|
||||
)
|
||||
})
|
||||
public void ssoLogin(
|
||||
@Parameter(
|
||||
description = "SSO票据,从ES系统获取",
|
||||
required = true,
|
||||
example = "ticket_abc123xyz456"
|
||||
)
|
||||
@RequestParam String ticket,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws IOException {
|
||||
|
||||
// 创建日志对象
|
||||
SsoLog ssoLog = new SsoLog();
|
||||
ssoLog.setTicket(ticket);
|
||||
ssoLog.setLoginIp(getClientIp(request));
|
||||
ssoLog.setLoginTime(LocalDateTime.now());
|
||||
ssoLog.setCreateTime(LocalDateTime.now());
|
||||
|
||||
try {
|
||||
// 1. 调用ES接口验证ticket
|
||||
String time = String.valueOf(System.currentTimeMillis());
|
||||
String esUrl = esBaseUrl + getUserInfoPath + "?ticket=" + ticket + "&time=" + time;
|
||||
ssoLog.setRequestUrl(esUrl);
|
||||
|
||||
String result = HttpClientUtils.doGet(esUrl);
|
||||
ssoLog.setResponseData(result);
|
||||
|
||||
JSONObject res = JSONObject.parseObject(result);
|
||||
|
||||
if (res.getInteger("ret") != 200) {
|
||||
String errorMsg = res.getString("msg");
|
||||
ssoLog.setLoginStatus("FAILED");
|
||||
ssoLog.setErrorMsg("Ticket验证失败: " + errorMsg);
|
||||
ssoLogRepository.save(ssoLog);
|
||||
|
||||
// URL编码中文参数
|
||||
String encodedError = URLEncoder.encode("ticket无效", StandardCharsets.UTF_8);
|
||||
response.sendRedirect("/login?error=" + encodedError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 解析用户信息
|
||||
JSONObject data = res.getJSONObject("data");
|
||||
String userId = data.getString("id");
|
||||
String name = data.getString("name");
|
||||
String projectId = data.getString("projectid");
|
||||
|
||||
ssoLog.setUserId(userId);
|
||||
ssoLog.setUserName(name);
|
||||
ssoLog.setProjectId(projectId);
|
||||
|
||||
// 3. 检查用户是否存在,不存在则创建
|
||||
User user = userService.findByUserId(userId);
|
||||
if (user == null) {
|
||||
user = userService.createUser(userId, name, projectId);
|
||||
} else {
|
||||
// 更新项目ID(如果需要)
|
||||
if (projectId != null && !projectId.equals(user.getProjectId())) {
|
||||
user.setProjectId(projectId);
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userService.updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 生成JWT Token
|
||||
String token = JwtUtils.generateToken(userId, name);
|
||||
ssoLog.setToken(token);
|
||||
ssoLog.setLoginStatus("SUCCESS");
|
||||
|
||||
// 5. 保存登录日志
|
||||
ssoLogRepository.save(ssoLog);
|
||||
|
||||
// 6. 将Token和用户信息返回给前端
|
||||
JSONObject responseData = new JSONObject();
|
||||
responseData.put("code", 200);
|
||||
responseData.put("message", "登录成功");
|
||||
JSONObject userData = new JSONObject();
|
||||
userData.put("token", token);
|
||||
userData.put("userId", userId);
|
||||
userData.put("userName", name);
|
||||
userData.put("projectId", projectId);
|
||||
responseData.put("data", userData);
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(responseData.toJSONString());
|
||||
|
||||
} catch (Exception e) {
|
||||
// 记录错误日志
|
||||
ssoLog.setLoginStatus("ERROR");
|
||||
ssoLog.setErrorMsg("登录异常: " + e.getMessage());
|
||||
ssoLogRepository.save(ssoLog);
|
||||
|
||||
// URL编码中文参数
|
||||
String encodedError = URLEncoder.encode("登录失败", StandardCharsets.UTF_8);
|
||||
response.sendRedirect("/login?error=" + encodedError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: StatisticsController
|
||||
* @Description: API访问统计控制器,提供API调用次数统计查询接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.ApiStatisticsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 统计数据控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/statistics")
|
||||
@Tag(name = "统计信息", description = "API访问统计相关接口")
|
||||
public class StatisticsController {
|
||||
|
||||
@Autowired
|
||||
private ApiStatisticsService apiStatisticsService;
|
||||
|
||||
/**
|
||||
* 获取API访问总次数
|
||||
*
|
||||
* @return 总访问次数
|
||||
*/
|
||||
@GetMapping("/total")
|
||||
@Operation(summary = "获取API总访问次数", description = "返回系统中所有API接口的总访问次数")
|
||||
public Map<String, Object> getTotalCount() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "获取成功");
|
||||
result.put("data", apiStatisticsService.getTotalApiCount());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定接口的访问次数
|
||||
*
|
||||
* @param path 接口路径
|
||||
* @return 访问次数
|
||||
*/
|
||||
@GetMapping("/count")
|
||||
@Operation(summary = "获取特定接口访问次数", description = "根据接口路径返回该接口的访问次数")
|
||||
public Map<String, Object> getApiCount(String path) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", 200);
|
||||
result.put("message", "获取成功");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("path", path);
|
||||
data.put("count", apiStatisticsService.getApiCount(path));
|
||||
result.put("data", data);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,673 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: SummaryController
|
||||
* @Description: 总结相关控制器
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.context.ModelRequestContextHolder;
|
||||
import com.zhpb.gdyd_zhpb_zgf.dto.SummaryResponse;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileDirectory;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileDirectoryRepository;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.AIModelService;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.ChapterContentResolver;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.FilePathResolver;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.PromptTemplateService;
|
||||
import com.zhpb.gdyd_zhpb_zgf.utils.HttpClientUtils;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import com.zhpb.gdyd_zhpb_zgf.service.UserLogService;
|
||||
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 总结相关控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/summary")
|
||||
@Tag(name = "总结管理", description = "文件目录和章节总结相关接口")
|
||||
public class SummaryController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SummaryController.class);
|
||||
|
||||
@Autowired
|
||||
private FileDirectoryRepository fileDirectoryRepository;
|
||||
|
||||
@Autowired
|
||||
private com.zhpb.gdyd_zhpb_zgf.repository.mysql.FileRepository fileRepository;
|
||||
|
||||
@Autowired
|
||||
private com.zhpb.gdyd_zhpb_zgf.service.FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private PromptTemplateService promptTemplateService;
|
||||
|
||||
@Autowired
|
||||
private AIModelService aiModelService;
|
||||
|
||||
@Autowired
|
||||
private ChapterContentResolver chapterContentResolver;
|
||||
|
||||
@Autowired
|
||||
private org.springframework.core.env.Environment environment;
|
||||
|
||||
@Autowired
|
||||
private FilePathResolver filePathResolver;
|
||||
|
||||
@Autowired
|
||||
private UserLogService userLogService;
|
||||
|
||||
@GetMapping("/directory/count")
|
||||
@Operation(summary = "查询t_file_directory表记录数")
|
||||
public ResponseEntity<?> getDirectoryCount() {
|
||||
logger.info("查询t_file_directory表记录数");
|
||||
try {
|
||||
long count = fileDirectoryRepository.count();
|
||||
logger.info("t_file_directory表总记录数: {}", count);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("table", "t_file_directory");
|
||||
response.put("totalRecords", count);
|
||||
response.put("timestamp", java.time.LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("查询表记录数失败", e);
|
||||
Map<String, Object> errorResponse = new HashMap<>();
|
||||
errorResponse.put("status", "error");
|
||||
errorResponse.put("message", e.getMessage());
|
||||
return ResponseEntity.status(500).body(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/directory/status")
|
||||
@Operation(summary = "检查文件目录状态", description = "检查文件是否存在以及是否有目录数据")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getDirectoryStatus(
|
||||
@Parameter(description = "文件ID", required = true)
|
||||
@RequestParam String file_id) {
|
||||
|
||||
logger.info("检查文件目录状态: fileId={}", file_id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 检查文件是否在t_file表中存在
|
||||
java.util.Optional<com.zhpb.gdyd_zhpb_zgf.entity.mysql.File> fileOptional = fileRepository.findByFileId(file_id);
|
||||
boolean fileExists = fileOptional.isPresent();
|
||||
|
||||
// 检查目录数据是否存在
|
||||
long directoryCount = fileDirectoryRepository.countByFileId(file_id);
|
||||
|
||||
result.put("fileId", file_id);
|
||||
result.put("fileExists", fileExists);
|
||||
result.put("directoryExists", directoryCount > 0);
|
||||
result.put("directoryCount", directoryCount);
|
||||
|
||||
if (fileExists) {
|
||||
com.zhpb.gdyd_zhpb_zgf.entity.mysql.File file = fileOptional.get();
|
||||
result.put("fileName", file.getFileName());
|
||||
result.put("fileSize", file.getFileSize());
|
||||
result.put("uploadTime", file.getCreateTime());
|
||||
result.put("vectorizationStatus", file.getVectorizationStatus());
|
||||
}
|
||||
|
||||
result.put("code", 0);
|
||||
result.put("message", "查询成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("检查文件目录状态失败", e);
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/directory")
|
||||
@Operation(summary = "获取文件目录结构", description = "根据文件ID获取文件的目录结构")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "404", description = "文件不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<SummaryResponse.DirectoryResponse> getDirectory(
|
||||
@Parameter(description = "文件ID", required = true)
|
||||
@RequestParam String file_id) {
|
||||
|
||||
logger.info("查询文件目录: fileId={}", file_id);
|
||||
|
||||
try {
|
||||
List<FileDirectory> directories = fileDirectoryRepository.findByFileIdOrderBySortOrderAsc(file_id);
|
||||
|
||||
if (directories.isEmpty()) {
|
||||
SummaryResponse.DirectoryResponse response = new SummaryResponse.DirectoryResponse();
|
||||
response.setCode(200);
|
||||
response.setMessage("文件目录不存在,可能文件还没有进行目录抽取处理");
|
||||
return ResponseEntity.ok().body(response);
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
List<SummaryResponse.DirectoryItem> directoryItems = buildDirectoryTree(directories);
|
||||
|
||||
SummaryResponse.DirectoryData data = new SummaryResponse.DirectoryData();
|
||||
data.setFileId(file_id);
|
||||
data.setDirectory(directoryItems);
|
||||
|
||||
SummaryResponse.DirectoryResponse response = new SummaryResponse.DirectoryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
// 记录用户操作日志
|
||||
HttpServletRequest httpRequest = ((org.springframework.web.context.request.ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes())
|
||||
.getRequest();
|
||||
userLogService.logUserOperation(null, null, null,
|
||||
"VIEW_CATALOG", "查看文件目录结构", file_id, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询文件目录失败", e);
|
||||
SummaryResponse.DirectoryResponse response = new SummaryResponse.DirectoryResponse();
|
||||
response.setCode(500);
|
||||
response.setMessage("系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/chapter")
|
||||
@Operation(summary = "获取章节总结", description = "根据文件ID和章节ID获取章节总结")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功"),
|
||||
@ApiResponse(responseCode = "404", description = "章节不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<SummaryResponse.ChapterSummaryResponse> getChapterSummary(
|
||||
@Parameter(description = "文件ID", required = true)
|
||||
@RequestParam String file_id,
|
||||
@Parameter(description = "章节ID", required = true)
|
||||
@RequestParam String chapter_id) {
|
||||
|
||||
logger.info("查询章节总结: fileId={}, chapterId={}", file_id, chapter_id);
|
||||
|
||||
try {
|
||||
FileDirectory directory = fileDirectoryRepository.findByFileIdAndChapterId(file_id, chapter_id);
|
||||
|
||||
if (directory == null) {
|
||||
SummaryResponse.ChapterSummaryResponse response = new SummaryResponse.ChapterSummaryResponse();
|
||||
response.setCode(200);
|
||||
response.setMessage("章节不存在");
|
||||
return ResponseEntity.ok().body(response);
|
||||
}
|
||||
|
||||
// 如果数据库中有总结内容,直接返回
|
||||
if (directory.getSummary() != null && !directory.getSummary().isEmpty()) {
|
||||
SummaryResponse.ChapterSummaryData data = new SummaryResponse.ChapterSummaryData();
|
||||
data.setFileId(file_id);
|
||||
data.setChapterId(chapter_id);
|
||||
data.setSummary(directory.getSummary());
|
||||
|
||||
// 构建sources信息(如果需要的话)
|
||||
data.setSources(new ArrayList<>());
|
||||
|
||||
SummaryResponse.ChapterSummaryResponse response = new SummaryResponse.ChapterSummaryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
// 记录用户操作日志
|
||||
HttpServletRequest httpRequest = ((org.springframework.web.context.request.ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes())
|
||||
.getRequest();
|
||||
userLogService.logUserOperation(null, null, null,
|
||||
"CATALOG_SUMMARY", "查看章节总结", chapter_id, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
// 如果数据库中没有总结内容,实时调用模型生成(3分钟超时)
|
||||
try {
|
||||
logger.info("数据库中无总结内容,实时调用模型生成: fileId={}, chapterId={}", file_id, chapter_id);
|
||||
|
||||
// 使用CompletableFuture实现3分钟超时控制
|
||||
final FileDirectory finalDirectory = directory;
|
||||
java.util.concurrent.CompletableFuture<String> summaryFuture = java.util.concurrent.CompletableFuture.supplyAsync(() -> {
|
||||
return generateChapterSummaryWithDeepSeek(finalDirectory);
|
||||
});
|
||||
|
||||
// 等待最多3分钟
|
||||
String summaryContent = null;
|
||||
try {
|
||||
summaryContent = summaryFuture.get(3, java.util.concurrent.TimeUnit.MINUTES);
|
||||
} catch (java.util.concurrent.TimeoutException te) {
|
||||
logger.warn("实时生成摘要超时(3分钟): fileId={}, chapterId={}", file_id, chapter_id);
|
||||
summaryFuture.cancel(true);
|
||||
}
|
||||
|
||||
if (summaryContent != null && !summaryContent.isEmpty()) {
|
||||
// 更新数据库
|
||||
directory.setSummary(summaryContent);
|
||||
fileDirectoryRepository.save(directory);
|
||||
logger.info("实时生成摘要成功并保存: fileId={}, chapterId={}, 长度={}",
|
||||
file_id, chapter_id, summaryContent.length());
|
||||
|
||||
SummaryResponse.ChapterSummaryData data = new SummaryResponse.ChapterSummaryData();
|
||||
data.setFileId(file_id);
|
||||
data.setChapterId(chapter_id);
|
||||
data.setSummary(summaryContent);
|
||||
data.setSources(new ArrayList<>());
|
||||
|
||||
SummaryResponse.ChapterSummaryResponse response = new SummaryResponse.ChapterSummaryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("实时生成章节总结失败: fileId={}, chapterId={}, error={}", file_id, chapter_id, e.getMessage());
|
||||
}
|
||||
|
||||
// 如果都获取不到,返回默认消息
|
||||
SummaryResponse.ChapterSummaryData data = new SummaryResponse.ChapterSummaryData();
|
||||
data.setFileId(file_id);
|
||||
data.setChapterId(chapter_id);
|
||||
data.setSummary("暂无总结内容");
|
||||
data.setSources(new ArrayList<>());
|
||||
|
||||
SummaryResponse.ChapterSummaryResponse response = new SummaryResponse.ChapterSummaryResponse();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询章节总结失败", e);
|
||||
SummaryResponse.ChapterSummaryResponse response = new SummaryResponse.ChapterSummaryResponse();
|
||||
response.setCode(500);
|
||||
response.setMessage("系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从外部API获取章节总结(这里使用DeepSeek API)
|
||||
*/
|
||||
private String fetchChapterSummaryFromExternalAPI(String fileId, String chapterId) {
|
||||
try {
|
||||
// 获取章节信息
|
||||
com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileDirectory directory = fileDirectoryRepository.findByFileIdAndChapterId(fileId, chapterId);
|
||||
if (directory == null) {
|
||||
logger.warn("未找到章节信息: fileId={}, chapterId={}", fileId, chapterId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用DeepSeek API生成章节总结
|
||||
return generateChapterSummaryWithDeepSeek(directory);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取章节总结失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用AI模型生成章节总结
|
||||
*/
|
||||
private String generateChapterSummaryWithDeepSeek(com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileDirectory directory) {
|
||||
try {
|
||||
// 获取文档摘要提示词模板
|
||||
com.zhpb.gdyd_zhpb_zgf.entity.mysql.PromptTemplate summaryTemplate = promptTemplateService.getActiveTemplate("summary");
|
||||
if (summaryTemplate == null) {
|
||||
throw new RuntimeException("未找到激活的文档摘要提示词模板,请在提示词配置页面启用或创建summary类型的提示词模板");
|
||||
}
|
||||
|
||||
String promptTemplate = summaryTemplate.getPromptContent();
|
||||
logger.info("使用数据库提示词模板: {} (ID: {}), 内容长度: {}", summaryTemplate.getTemplateName(), summaryTemplate.getTemplateId(), promptTemplate.length());
|
||||
logger.debug("提示词内容: {}", promptTemplate);
|
||||
|
||||
// 使用模板替换变量
|
||||
String prompt = promptTemplate.replace("{title}", directory.getTitle());
|
||||
logger.debug("最终使用的提示词: {}", prompt);
|
||||
|
||||
logger.info("调用AI模型生成章节总结: chapterId={}", directory.getChapterId());
|
||||
|
||||
String userMessage = chapterContentResolver.buildUserMessage(directory);
|
||||
|
||||
ModelRequestContextHolder.setRequestSource("DOC_SUMMARY_MANUAL");
|
||||
String summary;
|
||||
try {
|
||||
summary = aiModelService.generateResponse(prompt, userMessage);
|
||||
} finally {
|
||||
ModelRequestContextHolder.clear();
|
||||
}
|
||||
|
||||
if (summary != null && !summary.trim().isEmpty()) {
|
||||
logger.info("AI模型返回章节总结,长度: {}", summary.length());
|
||||
return summary.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("调用AI模型生成章节总结失败: chapterId={}", directory.getChapterId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置文件的值
|
||||
*/
|
||||
private String getConfigValue(String key, String defaultValue) {
|
||||
try {
|
||||
String value = environment.getProperty(key);
|
||||
return value != null ? value : defaultValue;
|
||||
} catch (Exception e) {
|
||||
logger.warn("获取配置值失败: key={}, 使用默认值: {}", key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/directory/process")
|
||||
@Operation(summary = "手动触发目录抽取", description = "为指定文件手动触发目录抽取和总结处理")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "处理开始"),
|
||||
@ApiResponse(responseCode = "404", description = "文件不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> processDirectory(
|
||||
@Parameter(description = "文件ID", required = true)
|
||||
@RequestParam String file_id) {
|
||||
|
||||
logger.info("手动触发目录抽取: fileId={}", file_id);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
java.util.Optional<com.zhpb.gdyd_zhpb_zgf.entity.mysql.File> fileOptional = fileRepository.findByFileId(file_id);
|
||||
if (!fileOptional.isPresent()) {
|
||||
result.put("code", 200);
|
||||
result.put("message", "文件不存在");
|
||||
return ResponseEntity.ok().body(result);
|
||||
}
|
||||
|
||||
com.zhpb.gdyd_zhpb_zgf.entity.mysql.File file = fileOptional.get();
|
||||
|
||||
// 获取文件的完整路径
|
||||
String filePath = constructFilePath(file);
|
||||
if (filePath == null) {
|
||||
result.put("code", 200);
|
||||
result.put("message", "文件路径不存在,无法找到文件:" + file.getFileName() +
|
||||
"(fileId: " + file.getFileId() + ")");
|
||||
result.put("fileId", file_id);
|
||||
result.put("fileName", file.getFileName());
|
||||
result.put("storedPath", file.getFilePath());
|
||||
return ResponseEntity.ok().body(result);
|
||||
}
|
||||
|
||||
// 异步处理文件目录
|
||||
try {
|
||||
java.lang.reflect.Method method = fileService.getClass().getDeclaredMethod("processFileDirectoriesAsync",
|
||||
java.util.List.class, String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
java.util.List<String> filePaths = java.util.Arrays.asList(filePath);
|
||||
method.invoke(fileService, filePaths, file.getProjectId());
|
||||
|
||||
result.put("code", 0);
|
||||
result.put("message", "目录抽取任务已启动,请稍后查询");
|
||||
result.put("fileId", file_id);
|
||||
result.put("fileName", file.getFileName());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("启动目录抽取任务失败", e);
|
||||
result.put("code", 500);
|
||||
result.put("message", "启动任务失败: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("手动触发目录抽取失败", e);
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件信息构造文件路径
|
||||
*/
|
||||
private String constructFilePath(com.zhpb.gdyd_zhpb_zgf.entity.mysql.File file) {
|
||||
try {
|
||||
java.util.Optional<java.nio.file.Path> resolvedOpt = filePathResolver.resolveExistingPath(
|
||||
file.getFilePath(), file.getProjectId(), file.getFileName());
|
||||
if (resolvedOpt.isPresent()) {
|
||||
java.nio.file.Path resolvedPath = resolvedOpt.get();
|
||||
String normalized = filePathResolver.normalizeForStorage(resolvedPath);
|
||||
if (normalized != null && !normalized.equals(file.getFilePath())) {
|
||||
file.setFilePath(normalized);
|
||||
fileRepository.save(file);
|
||||
logger.info("已自动纠正文件路径: {} -> {}", file.getFileId(), normalized);
|
||||
}
|
||||
return resolvedPath.toString();
|
||||
}
|
||||
logger.warn("无法解析文件路径: fileId={}, storedPath={}", file.getFileId(), file.getFilePath());
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("构造文件路径失败: fileId={}", file.getFileId(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null) return "";
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
return lastDotIndex > 0 ? fileName.substring(lastDotIndex) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建目录树形结构
|
||||
*/
|
||||
private List<SummaryResponse.DirectoryItem> buildDirectoryTree(List<FileDirectory> directories) {
|
||||
List<SummaryResponse.DirectoryItem> result = new ArrayList<>();
|
||||
|
||||
// 获取顶级目录(没有父章节的)
|
||||
List<FileDirectory> rootDirectories = directories.stream()
|
||||
.filter(dir -> dir.getParentChapterId() == null || dir.getParentChapterId().isEmpty())
|
||||
.toList();
|
||||
|
||||
for (FileDirectory rootDir : rootDirectories) {
|
||||
SummaryResponse.DirectoryItem item = convertToDirectoryItem(rootDir);
|
||||
item.setChildren(buildChildren(directories, rootDir.getChapterId()));
|
||||
result.add(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建子目录
|
||||
*/
|
||||
private List<SummaryResponse.DirectoryItem> buildChildren(List<FileDirectory> allDirectories, String parentChapterId) {
|
||||
List<SummaryResponse.DirectoryItem> children = new ArrayList<>();
|
||||
|
||||
List<FileDirectory> childDirectories = allDirectories.stream()
|
||||
.filter(dir -> parentChapterId.equals(dir.getParentChapterId()))
|
||||
.toList();
|
||||
|
||||
for (FileDirectory childDir : childDirectories) {
|
||||
SummaryResponse.DirectoryItem item = convertToDirectoryItem(childDir);
|
||||
item.setChildren(buildChildren(allDirectories, childDir.getChapterId()));
|
||||
children.add(item);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换FileDirectory为DirectoryItem
|
||||
*/
|
||||
private SummaryResponse.DirectoryItem convertToDirectoryItem(FileDirectory directory) {
|
||||
SummaryResponse.DirectoryItem item = new SummaryResponse.DirectoryItem();
|
||||
item.setChapterId(directory.getChapterId());
|
||||
item.setTitle(directory.getTitle());
|
||||
item.setPage(directory.getPage());
|
||||
item.setStartPage(directory.getStartPage());
|
||||
item.setEndPage(directory.getEndPage());
|
||||
item.setChildren(new ArrayList<>());
|
||||
return item;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private com.zhpb.gdyd_zhpb_zgf.service.CatalogExtractionRetryService catalogExtractionRetryService;
|
||||
|
||||
@PostMapping("/retry/failed")
|
||||
@Operation(summary = "重试所有失败任务", description = "重新处理所有目录抽取失败和向量化失败的文件")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "重试任务已启动"),
|
||||
@ApiResponse(responseCode = "500", description = "系统错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> retryAllFailedTasks(
|
||||
@Parameter(description = "项目ID(可选,不传则处理所有项目)", required = false)
|
||||
@RequestParam(value = "project_id", required = false) String projectId) {
|
||||
|
||||
logger.info("重试所有失败任务: projectId={}", projectId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
int catalogRetryCount = 0;
|
||||
int vectorRetryCount = 0;
|
||||
|
||||
// 1. 重试目录抽取失败的文件
|
||||
java.util.List<com.zhpb.gdyd_zhpb_zgf.entity.mysql.File> catalogFailedFiles;
|
||||
if (projectId != null && !projectId.isEmpty()) {
|
||||
catalogFailedFiles = fileRepository.findByProjectIdAndCatalogExtractionStatus(projectId, "failed");
|
||||
} else {
|
||||
catalogFailedFiles = fileRepository.findByCatalogExtractionStatus("failed");
|
||||
}
|
||||
|
||||
for (com.zhpb.gdyd_zhpb_zgf.entity.mysql.File file : catalogFailedFiles) {
|
||||
file.setCatalogExtractionStatus("not_started");
|
||||
file.setCatalogExtractionRetryCount(0);
|
||||
fileRepository.save(file);
|
||||
catalogRetryCount++;
|
||||
}
|
||||
|
||||
// 2. 重试向量化失败的文件
|
||||
java.util.List<com.zhpb.gdyd_zhpb_zgf.entity.mysql.File> vectorFailedFiles;
|
||||
if (projectId != null && !projectId.isEmpty()) {
|
||||
vectorFailedFiles = fileRepository.findByProjectIdAndVectorizationStatus(projectId, "failed");
|
||||
} else {
|
||||
vectorFailedFiles = fileRepository.findByVectorizationStatus("failed");
|
||||
}
|
||||
|
||||
for (com.zhpb.gdyd_zhpb_zgf.entity.mysql.File file : vectorFailedFiles) {
|
||||
file.setVectorizationStatus("not_started");
|
||||
file.setIsVectorized(false);
|
||||
fileRepository.save(file);
|
||||
vectorRetryCount++;
|
||||
}
|
||||
|
||||
// 3. 处理卡住的任务(processing 状态超过30分钟的)
|
||||
catalogExtractionRetryService.scanAndRetryStuckCatalogExtraction();
|
||||
|
||||
result.put("code", 0);
|
||||
result.put("message", "重试任务已启动");
|
||||
result.put("catalogRetryCount", catalogRetryCount);
|
||||
result.put("vectorRetryCount", vectorRetryCount);
|
||||
result.put("projectId", projectId);
|
||||
|
||||
logger.info("重试失败任务完成: 目录抽取重置{}个, 向量化重置{}个", catalogRetryCount, vectorRetryCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("重试失败任务出错", e);
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/status/failed")
|
||||
@Operation(summary = "查询失败任务数量", description = "查询目录抽取失败和向量化失败的文件数量")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "查询成功")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> getFailedTasksStatus(
|
||||
@Parameter(description = "项目ID(可选,不传则查询所有项目)", required = false)
|
||||
@RequestParam(value = "project_id", required = false) String projectId) {
|
||||
|
||||
logger.info("查询失败任务状态: projectId={}", projectId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
long catalogFailedCount;
|
||||
long catalogProcessingCount;
|
||||
long vectorFailedCount;
|
||||
long vectorProcessingCount;
|
||||
|
||||
if (projectId != null && !projectId.isEmpty()) {
|
||||
catalogFailedCount = fileRepository.countByProjectIdAndCatalogExtractionStatus(projectId, "failed");
|
||||
catalogProcessingCount = fileRepository.countByProjectIdAndCatalogExtractionStatus(projectId, "processing");
|
||||
vectorFailedCount = fileRepository.countByProjectIdAndVectorizationStatus(projectId, "failed");
|
||||
vectorProcessingCount = fileRepository.countByProjectIdAndVectorizationStatus(projectId, "processing");
|
||||
} else {
|
||||
catalogFailedCount = fileRepository.countByCatalogExtractionStatus("failed");
|
||||
catalogProcessingCount = fileRepository.countByCatalogExtractionStatus("processing");
|
||||
vectorFailedCount = fileRepository.countByVectorizationStatus("failed");
|
||||
vectorProcessingCount = fileRepository.countByVectorizationStatus("processing");
|
||||
}
|
||||
|
||||
result.put("code", 0);
|
||||
result.put("message", "查询成功");
|
||||
result.put("projectId", projectId);
|
||||
result.put("catalogExtractionFailed", catalogFailedCount);
|
||||
result.put("catalogExtractionProcessing", catalogProcessingCount);
|
||||
result.put("vectorizationFailed", vectorFailedCount);
|
||||
result.put("vectorizationProcessing", vectorProcessingCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查询失败任务状态出错", e);
|
||||
result.put("code", 500);
|
||||
result.put("message", "系统错误: " + e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.controller
|
||||
* @ClassName: SysLogController
|
||||
* @Description: 系统日志查询接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-24
|
||||
* @Version: 1.0
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.controller;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.SysLog;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.SysLogRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 系统日志查询接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/logs")
|
||||
@Tag(name = "系统日志", description = "系统日志查询接口")
|
||||
public class SysLogController {
|
||||
|
||||
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
|
||||
private static final DateTimeFormatter NORMAL_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final List<String> KNOWN_LEVELS = Arrays.asList("ERROR", "WARN", "INFO", "DEBUG", "TRACE", "OUT");
|
||||
|
||||
@Autowired
|
||||
private SysLogRepository sysLogRepository;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询日志", description = "支持按级别、关键字、时间范围筛选日志")
|
||||
public Map<String, Object> queryLogs(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String level,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime) {
|
||||
|
||||
int safePage = Math.max(page, 0);
|
||||
int safeSize = Math.min(Math.max(size, 1), 200);
|
||||
|
||||
Pageable pageable = PageRequest.of(safePage, safeSize, Sort.by(Sort.Direction.DESC, "createTime"));
|
||||
Specification<SysLog> specification = buildSpecification(level, keyword, startTime, endTime);
|
||||
|
||||
Page<SysLog> pageData = sysLogRepository.findAll(specification, pageable);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", pageData.getContent());
|
||||
response.put("totalElements", pageData.getTotalElements());
|
||||
response.put("totalPages", pageData.getTotalPages());
|
||||
response.put("page", pageData.getNumber());
|
||||
response.put("size", pageData.getSize());
|
||||
return response;
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@Operation(summary = "日志统计信息", description = "返回日志数量、各级别分布等统计数据")
|
||||
public Map<String, Object> logStats() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
long total = sysLogRepository.count();
|
||||
result.put("total", total);
|
||||
|
||||
LocalDateTime last24h = LocalDateTime.now().minusHours(24);
|
||||
long last24hCount = sysLogRepository.count((root, query, cb) ->
|
||||
cb.greaterThanOrEqualTo(root.get("createTime"), last24h));
|
||||
result.put("last24Hours", last24hCount);
|
||||
|
||||
Map<String, Long> levelStats = new LinkedHashMap<>();
|
||||
for (String lvl : KNOWN_LEVELS) {
|
||||
long count = sysLogRepository.count((root, query, cb) ->
|
||||
cb.equal(cb.upper(root.get("logLevel")), lvl));
|
||||
levelStats.put(lvl, count);
|
||||
}
|
||||
result.put("levelStats", levelStats);
|
||||
|
||||
SysLog latest = sysLogRepository.findTopByOrderByCreateTimeDesc();
|
||||
result.put("latest", latest);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Specification<SysLog> buildSpecification(String level, String keyword,
|
||||
String startTime, String endTime) {
|
||||
return (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (StringUtils.hasText(level) && !"ALL".equalsIgnoreCase(level)) {
|
||||
predicates.add(cb.equal(cb.upper(root.get("logLevel")), level.trim().toUpperCase()));
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
String likeValue = "%" + keyword.trim().toLowerCase() + "%";
|
||||
predicates.add(cb.or(
|
||||
cb.like(cb.lower(root.get("logContent")), likeValue),
|
||||
cb.like(cb.lower(root.get("fullMessage")), likeValue),
|
||||
cb.like(cb.lower(root.get("loggerName")), likeValue),
|
||||
cb.like(cb.lower(root.get("threadName")), likeValue)
|
||||
));
|
||||
}
|
||||
|
||||
LocalDateTime start = parseDateTime(startTime);
|
||||
LocalDateTime end = parseDateTime(endTime);
|
||||
if (start != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("createTime"), start));
|
||||
}
|
||||
if (end != null) {
|
||||
predicates.add(cb.lessThanOrEqualTo(root.get("createTime"), end));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
}
|
||||
|
||||
private LocalDateTime parseDateTime(String raw) {
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
return null;
|
||||
}
|
||||
String value = raw.trim();
|
||||
// datetime-local 会带 T
|
||||
if (value.contains("T") && !value.contains("Z")) {
|
||||
value = value.replace("T", " ");
|
||||
}
|
||||
List<DateTimeFormatter> formatters = Arrays.asList(ISO_FORMATTER, NORMAL_FORMATTER,
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
for (DateTimeFormatter formatter : formatters) {
|
||||
try {
|
||||
return LocalDateTime.parse(value, formatter);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: ComparisonRequest
|
||||
* @Description: 对比分析请求DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-04
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对比分析请求DTO
|
||||
*/
|
||||
@Schema(description = "对比分析请求")
|
||||
public class ComparisonRequest {
|
||||
|
||||
@Schema(description = "项目ID", example = "project-001", required = true)
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "用户ID", example = "user-001", required = true)
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "供应商ID列表", required = true)
|
||||
private List<String> vendorIds;
|
||||
|
||||
@Schema(description = "对比维度", required = true)
|
||||
private List<ComparisonDimension> dimensions;
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setProjectId(String projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public List<String> getVendorIds() {
|
||||
return vendorIds;
|
||||
}
|
||||
|
||||
public void setVendorIds(List<String> vendorIds) {
|
||||
this.vendorIds = vendorIds;
|
||||
}
|
||||
|
||||
public List<ComparisonDimension> getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
public void setDimensions(List<ComparisonDimension> dimensions) {
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比维度
|
||||
*/
|
||||
public static class ComparisonDimension {
|
||||
@Schema(description = "维度类型", example = "price", allowableValues = {"price", "quality", "delivery", "service", "technology"})
|
||||
private String type;
|
||||
|
||||
@Schema(description = "维度值", example = "价格")
|
||||
private String value;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
471
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/ComparisonResponse.java
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: ComparisonResponse
|
||||
* @Description: 对比分析响应DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-04
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对比分析响应DTO
|
||||
*/
|
||||
public class ComparisonResponse {
|
||||
|
||||
/**
|
||||
* 获取供应商数据响应
|
||||
*/
|
||||
@Schema(description = "获取供应商数据响应")
|
||||
public static class VendorsData {
|
||||
@Schema(description = "供应商列表")
|
||||
private List<VendorInfo> vendors;
|
||||
|
||||
public List<VendorInfo> getVendors() {
|
||||
return vendors;
|
||||
}
|
||||
|
||||
public void setVendors(List<VendorInfo> vendors) {
|
||||
this.vendors = vendors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 供应商信息
|
||||
*/
|
||||
@Schema(description = "供应商信息")
|
||||
public static class VendorInfo {
|
||||
@Schema(description = "供应商ID", example = "supplier-001")
|
||||
private String vendorId;
|
||||
|
||||
@Schema(description = "供应商名称", example = "彩讯科技股份有限公司")
|
||||
private String vendorName;
|
||||
|
||||
@Schema(description = "供应商类型", example = "技术服务商")
|
||||
private String vendorType;
|
||||
|
||||
@Schema(description = "文件列表")
|
||||
private List<FileInfo> files;
|
||||
|
||||
public String getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public void setVendorId(String vendorId) {
|
||||
this.vendorId = vendorId;
|
||||
}
|
||||
|
||||
public String getVendorName() {
|
||||
return vendorName;
|
||||
}
|
||||
|
||||
public void setVendorName(String vendorName) {
|
||||
this.vendorName = vendorName;
|
||||
}
|
||||
|
||||
public String getVendorType() {
|
||||
return vendorType;
|
||||
}
|
||||
|
||||
public void setVendorType(String vendorType) {
|
||||
this.vendorType = vendorType;
|
||||
}
|
||||
|
||||
public List<FileInfo> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<FileInfo> files) {
|
||||
this.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件信息
|
||||
*/
|
||||
@Schema(description = "文件信息")
|
||||
public static class FileInfo {
|
||||
@Schema(description = "文件ID", example = "file-001")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "文件名称", example = "技术方案.pdf")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "文件类型", example = "技术文件")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "文件大小(字节)", example = "2048576")
|
||||
private Long fileSize;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public Long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(Long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动对比任务响应
|
||||
*/
|
||||
@Schema(description = "启动对比任务响应")
|
||||
public static class StartTaskData {
|
||||
@Schema(description = "任务ID", example = "task-001")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "任务状态", example = "pending:待处理")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建时间", example = "2025-11-04T12:00:00Z")
|
||||
private String createdTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
public void setCreatedTime(String createdTime) {
|
||||
this.createdTime = createdTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比任务历史响应
|
||||
*/
|
||||
@Schema(description = "对比任务历史响应")
|
||||
public static class TaskHistoryData {
|
||||
@Schema(description = "任务列表")
|
||||
private List<TaskInfo> tasks;
|
||||
|
||||
public List<TaskInfo> getTasks() {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public void setTasks(List<TaskInfo> tasks) {
|
||||
this.tasks = tasks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务信息
|
||||
*/
|
||||
@Schema(description = "任务信息")
|
||||
public static class TaskInfo {
|
||||
@Schema(description = "任务ID", example = "task-001")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "任务名称", example = "对比任务1")
|
||||
private String taskName;
|
||||
|
||||
@Schema(description = "项目ID", example = "project-001")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "用户ID", example = "user-001")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "任务状态", example = "completed:已完成", allowableValues = {"pending:待处理", "processing:处理中", "completed:已完成", "failed:失败"})
|
||||
private String status;
|
||||
|
||||
@Schema(description = "供应商数量", example = "3")
|
||||
private Integer vendorCount;
|
||||
|
||||
@Schema(description = "对比维度数量", example = "5")
|
||||
private Integer dimensionCount;
|
||||
|
||||
@Schema(description = "创建时间", example = "2025-11-04T12:00:00Z")
|
||||
private String createdTime;
|
||||
|
||||
@Schema(description = "完成时间", example = "2025-11-04T12:05:00Z")
|
||||
private String completedTime;
|
||||
|
||||
@Schema(description = "对比维度列表", example = "[\"技术能力\", \"商务条款\", \"报价分析\"]")
|
||||
private List<String> dimensions;
|
||||
|
||||
@Schema(description = "供应商列表", example = "[\"供应商A\", \"供应商B\", \"供应商C\"]")
|
||||
private List<String> vendors;
|
||||
|
||||
@Schema(description = "任务耗时(秒)", example = "120")
|
||||
private Long time;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setProjectId(String projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Integer getVendorCount() {
|
||||
return vendorCount;
|
||||
}
|
||||
|
||||
public void setVendorCount(Integer vendorCount) {
|
||||
this.vendorCount = vendorCount;
|
||||
}
|
||||
|
||||
public Integer getDimensionCount() {
|
||||
return dimensionCount;
|
||||
}
|
||||
|
||||
public void setDimensionCount(Integer dimensionCount) {
|
||||
this.dimensionCount = dimensionCount;
|
||||
}
|
||||
|
||||
public String getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
public void setCreatedTime(String createdTime) {
|
||||
this.createdTime = createdTime;
|
||||
}
|
||||
|
||||
public String getCompletedTime() {
|
||||
return completedTime;
|
||||
}
|
||||
|
||||
public void setCompletedTime(String completedTime) {
|
||||
this.completedTime = completedTime;
|
||||
}
|
||||
|
||||
public List<String> getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
public void setDimensions(List<String> dimensions) {
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
public List<String> getVendors() {
|
||||
return vendors;
|
||||
}
|
||||
|
||||
public void setVendors(List<String> vendors) {
|
||||
this.vendors = vendors;
|
||||
}
|
||||
|
||||
public Long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(Long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getTaskName() {
|
||||
return taskName;
|
||||
}
|
||||
|
||||
public void setTaskName(String taskName) {
|
||||
this.taskName = taskName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比结果响应
|
||||
*/
|
||||
@Schema(description = "对比结果响应")
|
||||
public static class ComparisonResultData {
|
||||
@Schema(description = "任务ID", example = "task-001")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "任务状态", example = "completed:已完成")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "对比结果矩阵")
|
||||
private List<ResultMatrixItem> resultMatrix;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<ResultMatrixItem> getResultMatrix() {
|
||||
return resultMatrix;
|
||||
}
|
||||
|
||||
public void setResultMatrix(List<ResultMatrixItem> resultMatrix) {
|
||||
this.resultMatrix = resultMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果矩阵项
|
||||
*/
|
||||
@Schema(description = "结果矩阵项")
|
||||
public static class ResultMatrixItem {
|
||||
@Schema(description = "对比维度", example = "价格")
|
||||
private String dimension;
|
||||
|
||||
@Schema(description = "维度类型", example = "price")
|
||||
private String dimensionType;
|
||||
|
||||
@Schema(description = "供应商对比结果")
|
||||
private List<VendorComparison> comparisons;
|
||||
|
||||
public String getDimension() {
|
||||
return dimension;
|
||||
}
|
||||
|
||||
public void setDimension(String dimension) {
|
||||
this.dimension = dimension;
|
||||
}
|
||||
|
||||
public String getDimensionType() {
|
||||
return dimensionType;
|
||||
}
|
||||
|
||||
public void setDimensionType(String dimensionType) {
|
||||
this.dimensionType = dimensionType;
|
||||
}
|
||||
|
||||
public List<VendorComparison> getComparisons() {
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
public void setComparisons(List<VendorComparison> comparisons) {
|
||||
this.comparisons = comparisons;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 供应商对比结果
|
||||
*/
|
||||
@Schema(description = "供应商对比结果")
|
||||
public static class VendorComparison {
|
||||
@Schema(description = "供应商ID", example = "supplier-001")
|
||||
private String vendorId;
|
||||
|
||||
@Schema(description = "供应商名称", example = "彩讯科技股份有限公司")
|
||||
private String vendorName;
|
||||
|
||||
@Schema(description = "对比总结", example = "该供应商在价格方面具有竞争力...")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "相似度分数列表")
|
||||
private List<Double> similarityScores;
|
||||
|
||||
@Schema(description = "相关文件信息")
|
||||
private List<FileInfo> fileInfo;
|
||||
|
||||
public String getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public void setVendorId(String vendorId) {
|
||||
this.vendorId = vendorId;
|
||||
}
|
||||
|
||||
public String getVendorName() {
|
||||
return vendorName;
|
||||
}
|
||||
|
||||
public void setVendorName(String vendorName) {
|
||||
this.vendorName = vendorName;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public List<Double> getSimilarityScores() {
|
||||
return similarityScores;
|
||||
}
|
||||
|
||||
public void setSimilarityScores(List<Double> similarityScores) {
|
||||
this.similarityScores = similarityScores;
|
||||
}
|
||||
|
||||
public List<FileInfo> getFileInfo() {
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
public void setFileInfo(List<FileInfo> fileInfo) {
|
||||
this.fileInfo = fileInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/FileUploadRequest.java
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: FileUploadRequest
|
||||
* @Description: 文件上传请求DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-05
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ES平台文件上传请求DTO
|
||||
*/
|
||||
public class FileUploadRequest {
|
||||
|
||||
/**
|
||||
* 采购文件上传请求
|
||||
*/
|
||||
@Schema(description = "采购文件上传请求")
|
||||
public static class ProcurementFileRequest {
|
||||
@Schema(description = "项目ID", example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "包ID", example = "550e8400-e29b-41d4-a716-446655440001")
|
||||
private String packageId;
|
||||
|
||||
@Schema(description = "采购文件JSON的URL列表")
|
||||
private List<String> fileIds;
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setProjectId(String projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
}
|
||||
|
||||
public void setPackageId(String packageId) {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public List<String> getFileIds() {
|
||||
return fileIds;
|
||||
}
|
||||
|
||||
public void setFileIds(List<String> fileIds) {
|
||||
this.fileIds = fileIds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应答文件上传请求
|
||||
*/
|
||||
@Schema(description = "应答文件上传请求")
|
||||
public static class ResponseFileRequest {
|
||||
@Schema(description = "项目ID", example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "包ID", example = "550e8400-e29b-41d4-a716-446655440001")
|
||||
private String packageId;
|
||||
|
||||
@Schema(description = "供应商ID", example = "supplier-001")
|
||||
private String supplierId;
|
||||
|
||||
@Schema(description = "供应商应答文件URL列表")
|
||||
private List<String> fileIds;
|
||||
|
||||
@Schema(description = "文件类型:1商务、2技术", example = "1")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "采购文件JSON的URL地址")
|
||||
private String jsonFileId;
|
||||
|
||||
@Schema(description = "单位ID", example = "00030000000000000000")
|
||||
private String unitId;
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setProjectId(String projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
}
|
||||
|
||||
public void setPackageId(String packageId) {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getSupplierId() {
|
||||
return supplierId;
|
||||
}
|
||||
|
||||
public void setSupplierId(String supplierId) {
|
||||
this.supplierId = supplierId;
|
||||
}
|
||||
|
||||
public List<String> getFileIds() {
|
||||
return fileIds;
|
||||
}
|
||||
|
||||
public void setFileIds(List<String> fileIds) {
|
||||
this.fileIds = fileIds;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public String getJsonFileId() {
|
||||
return jsonFileId;
|
||||
}
|
||||
|
||||
public void setJsonFileId(String jsonFileId) {
|
||||
this.jsonFileId = jsonFileId;
|
||||
}
|
||||
|
||||
public String getUnitId() {
|
||||
return unitId;
|
||||
}
|
||||
|
||||
public void setUnitId(String unitId) {
|
||||
this.unitId = unitId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用响应
|
||||
*/
|
||||
@Schema(description = "API响应")
|
||||
public static class ApiResponse {
|
||||
@Schema(description = "响应编码:0成功,其他失败", example = "0")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "响应消息", example = "成功")
|
||||
private String msg;
|
||||
|
||||
public ApiResponse() {}
|
||||
|
||||
public ApiResponse(String code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/FileUploadResponse.java
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: FileUploadResponse
|
||||
* @Description: 文件上传响应DTO,包含上传结果、文件路径列表和按公司、文件类型整理后的文件列表
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 文件上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "文件上传响应")
|
||||
public class FileUploadResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "上传成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "响应数据")
|
||||
private FileUploadData data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "文件上传响应数据")
|
||||
public static class FileUploadData {
|
||||
@Schema(description = "项目ID", example = "project-001")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "原始ZIP文件路径", example = "./uploads/project-001/file.zip")
|
||||
private String zipFilePath;
|
||||
|
||||
@Schema(description = "解压路径", example = "./extracts/project-001/")
|
||||
private String extractPath;
|
||||
|
||||
@Schema(description = "解压后的文件路径列表", example = "[\"./extracts/project-001/file1.pdf\", \"./extracts/project-001/file2.doc\"]")
|
||||
private List<String> extractedFilePaths;
|
||||
|
||||
@Schema(description = "文件数量", example = "5")
|
||||
private Integer fileCount;
|
||||
|
||||
@Schema(description = "按公司和文件类型整理后的文件列表", example = "{\"公司1\": {\"技术文件\": [\"...\"], \"商务文件\": [...]}}")
|
||||
private Map<String, Map<String, List<String>>> organizedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件向量化状态
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "文件向量化状态信息")
|
||||
public static class VectorizationStatus {
|
||||
@Schema(description = "文件ID", example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "文件名", example = "技术方案.pdf")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "项目ID", example = "project-001")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "向量化状态", example = "completed", allowableValues = {"not_started", "processing", "completed", "failed"})
|
||||
private String vectorizationStatus;
|
||||
|
||||
@Schema(description = "是否已向量化", example = "true")
|
||||
private Boolean isVectorized;
|
||||
|
||||
@Schema(description = "向量化数据条数", example = "150")
|
||||
private Integer vectorizationCount;
|
||||
|
||||
@Schema(description = "向量化完成时间", example = "2025-11-03T15:30:00")
|
||||
private LocalDateTime vectorizedTime;
|
||||
|
||||
@Schema(description = "文件扩展名", example = "pdf")
|
||||
private String fileExtension;
|
||||
|
||||
@Schema(description = "文件大小(字节)", example = "2048576")
|
||||
private Long fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量化状态统计
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "向量化状态统计信息")
|
||||
public static class VectorizationStats {
|
||||
@Schema(description = "总文件数", example = "25")
|
||||
private int totalFiles;
|
||||
|
||||
@Schema(description = "已完成向量化文件数", example = "20")
|
||||
private int completedFiles;
|
||||
|
||||
@Schema(description = "正在处理的文件数", example = "2")
|
||||
private int processingFiles;
|
||||
|
||||
@Schema(description = "向量化失败的文件数", example = "1")
|
||||
private int failedFiles;
|
||||
|
||||
@Schema(description = "未开始向化的文件数", example = "2")
|
||||
private int notStartedFiles;
|
||||
|
||||
@Schema(description = "向量化完成率", example = "0.8")
|
||||
private double completionRate;
|
||||
|
||||
@Schema(description = "最近的文件列表")
|
||||
private List<VectorizationStatus> recentFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 供应商信息
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "供应商基本信息")
|
||||
public static class SupplierInfo {
|
||||
@Schema(description = "供应商ID", example = "supplier-001")
|
||||
private String supplierId;
|
||||
|
||||
@Schema(description = "供应商名称", example = "彩讯科技股份有限公司")
|
||||
private String supplierName;
|
||||
|
||||
@Schema(description = "供应商代码", example = "CXKJ")
|
||||
private String supplierCode;
|
||||
|
||||
@Schema(description = "供应商类型", example = "企业")
|
||||
private String supplierType;
|
||||
|
||||
@Schema(description = "联系人", example = "张三")
|
||||
private String contactPerson;
|
||||
|
||||
@Schema(description = "联系电话", example = "13800138000")
|
||||
private String contactPhone;
|
||||
|
||||
@Schema(description = "联系邮箱", example = "contact@company.com")
|
||||
private String contactEmail;
|
||||
|
||||
@Schema(description = "供应商地址", example = "北京市朝阳区")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "供应商状态", example = "active")
|
||||
private String status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目供应商和文件信息(简化格式)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "项目供应商和文件信息(供应商为键,文件列表为值)")
|
||||
public static class ProjectSuppliersAndFilesSimple {
|
||||
@Schema(description = "供应商列表")
|
||||
private List<SupplierInfo> 供应商;
|
||||
|
||||
// 动态字段:供应商名称 -> 文件列表
|
||||
// 使用Map来存储供应商名称和对应的文件列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目供应商和文件信息(完整格式)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "项目供应商和文件信息")
|
||||
public static class ProjectSuppliersAndFiles {
|
||||
@Schema(description = "项目ID", example = "project-001")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "项目名称", example = "智慧评标系统项目")
|
||||
private String projectName;
|
||||
|
||||
@Schema(description = "供应商列表")
|
||||
private List<SupplierInfo> suppliers;
|
||||
|
||||
@Schema(description = "文件列表")
|
||||
private List<VectorizationStatus> files;
|
||||
|
||||
@Schema(description = "供应商文件分组")
|
||||
private Map<String, List<VectorizationStatus>> supplierFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: ProjectFilesResponse
|
||||
* @Description: 项目文件列表查询响应DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目文件列表查询响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "项目文件列表查询响应")
|
||||
public class ProjectFilesResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "查询成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "响应数据")
|
||||
private ProjectFilesData data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "项目文件数据")
|
||||
public static class ProjectFilesData {
|
||||
|
||||
@Schema(description = "项目ID", example = "7654")
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "文件总数", example = "6")
|
||||
private Integer totalFiles;
|
||||
|
||||
@Schema(description = "文件列表")
|
||||
private List<FileInfo> files;
|
||||
|
||||
@Schema(description = "按公司分组的文件列表")
|
||||
private Map<String, List<String>> companyFiles;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "文件信息")
|
||||
public static class FileInfo {
|
||||
|
||||
@Schema(description = "文件ID", example = "1a0a5bdd-b073-4249-be61-e925be42e87f")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "公司名称", example = "xx科技有限公司")
|
||||
private String companyName;
|
||||
|
||||
@Schema(description = "文件名", example = "1 技术规范文件应答.docx")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "文件大小(字节)", example = "58680")
|
||||
private Long fileSize;
|
||||
|
||||
@Schema(description = "文件类型", example = "docx")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "相对路径", example = "test_files\\xx科技有限公司\\技术文件-2502071344\\1 技术规范文件应答.docx")
|
||||
private String relativePath;
|
||||
|
||||
@Schema(description = "上传时间", example = "2025-11-03T15:42:35.372798")
|
||||
private LocalDateTime uploadTime;
|
||||
|
||||
@Schema(description = "向量化状态", example = "processing", allowableValues = {"not_started", "processing", "completed", "failed"})
|
||||
private String vectorizationStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: PromptTemplateDto
|
||||
* @Description: 提示词模板DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 提示词模板DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "提示词模板")
|
||||
public class PromptTemplateDto {
|
||||
|
||||
@Schema(description = "模板ID", example = "template-001")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "模板名称", example = "智能问答提示词")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "模板类型", example = "qa", allowableValues = {"qa", "summary"})
|
||||
private String templateType;
|
||||
|
||||
@Schema(description = "提示词内容", example = "请基于以下文档内容回答用户的问题...")
|
||||
private String promptContent;
|
||||
|
||||
@Schema(description = "模板描述", example = "用于智能问答的提示词模板")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否启用", example = "true")
|
||||
private Boolean isActive;
|
||||
|
||||
@Schema(description = "排序号", example = "1")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "创建人", example = "admin")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "更新人", example = "admin")
|
||||
private String updatedBy;
|
||||
|
||||
@Schema(description = "创建时间", example = "2025-11-03T10:00:00")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", example = "2025-11-03T10:00:00")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
101
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/QaHistoryResponse.java
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: QaHistoryResponse
|
||||
* @Description: QA历史记录响应DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* QA历史记录响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "QA历史记录响应")
|
||||
public class QaHistoryResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "Success")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "响应数据")
|
||||
private Object data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "会话历史数据")
|
||||
public static class SessionsData {
|
||||
|
||||
@Schema(description = "会话列表")
|
||||
private List<SessionInfo> sessions;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "消息历史数据")
|
||||
public static class MessagesData {
|
||||
|
||||
@Schema(description = "会话ID", example = "session-5e205312")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "消息列表")
|
||||
private List<MessageInfo> messages;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "会话信息")
|
||||
public static class SessionInfo {
|
||||
|
||||
@Schema(description = "会话ID", example = "session-5e205312")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "开始时间", example = "2025-11-03T16:50:00")
|
||||
private String startTime;
|
||||
|
||||
@Schema(description = "首次问题内容", example = "这个项目的预算有多少?")
|
||||
private String firstQuery;
|
||||
|
||||
@Schema(description = "当前会话涉及的文件ID列表")
|
||||
private List<String> fileIds;
|
||||
|
||||
@Schema(description = "当前会话涉及的文件名列表")
|
||||
private List<String> fileNames;
|
||||
|
||||
@Schema(description = "当前会话涉及的供应商名称列表")
|
||||
private List<String> supplierNames;
|
||||
|
||||
@Schema(description = "当前会话涉及的供应商ID列表")
|
||||
private List<String> supplierIds;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "消息信息")
|
||||
public static class MessageInfo {
|
||||
|
||||
@Schema(description = "消息ID", example = "msg-4e529b65")
|
||||
private String messageId;
|
||||
|
||||
@Schema(description = "发送者", example = "user", allowableValues = {"user", "ai"})
|
||||
private String sender;
|
||||
|
||||
@Schema(description = "消息内容", example = "这个项目的预算有多少?")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "来源信息")
|
||||
private Object sources;
|
||||
|
||||
@Schema(description = "时间戳", example = "2025-11-03T16:50:00")
|
||||
private String timestamp;
|
||||
|
||||
@Schema(description = "引用文件ID列表")
|
||||
private List<String> fileIds;
|
||||
}
|
||||
}
|
||||
39
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/QaRequest.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: QaRequest
|
||||
* @Description: 智能问答请求DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智能问答请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能问答请求")
|
||||
public class QaRequest {
|
||||
|
||||
@Schema(description = "项目ID", example = "project-001", required = true)
|
||||
private String projectId;
|
||||
|
||||
@Schema(description = "用户ID", example = "user-001", required = true)
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "会话ID", example = "session-001")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "用户查询问题", example = "这个项目的预算有多少?", required = true)
|
||||
private String query;
|
||||
|
||||
@Schema(description = "相关文件ID列表", example = "[\"file-001\", \"file-002\"]", required = true)
|
||||
private List<String> fileIds;
|
||||
}
|
||||
76
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/QaResponse.java
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: QaResponse
|
||||
* @Description: 智能问答响应DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智能问答响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能问答响应")
|
||||
public class QaResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "查询成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "响应数据")
|
||||
private QaData data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能问答响应数据")
|
||||
public static class QaData {
|
||||
|
||||
@Schema(description = "会话ID", example = "session-001")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "消息ID", example = "msg-001")
|
||||
private String messageId;
|
||||
|
||||
@Schema(description = "AI回答内容", example = "根据项目文档显示,这个项目的总预算为500万元人民币。")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "答案来源信息")
|
||||
private List<SourceInfo> sources;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "答案来源信息")
|
||||
public static class SourceInfo {
|
||||
|
||||
@Schema(description = "文件ID", example = "file-001")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "文件名", example = "技术方案.pdf")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "供应商名称", example = "彩讯科技股份有限公司")
|
||||
private String vendorName;
|
||||
|
||||
@Schema(description = "页码", example = "5")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "章节", example = "第3章 项目预算")
|
||||
private String chapter;
|
||||
|
||||
@Schema(description = "内容片段", example = "项目总预算为500万元,其中包含...")
|
||||
private String snippet;
|
||||
|
||||
@Schema(description = "相似度", example = "0.85")
|
||||
private Double similarity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: SsoLoginResponse
|
||||
* @Description: SSO登录响应DTO,用于Swagger API文档说明
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* SSO登录响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SSO登录响应")
|
||||
public class SsoLoginResponse {
|
||||
|
||||
@Schema(description = "响应码", example = "200")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "登录成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "用户数据")
|
||||
private UserData data;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "用户数据")
|
||||
public static class UserData {
|
||||
|
||||
@Schema(description = "JWT Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "用户ID", example = "n001")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "用户名称", example = "张三")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "项目ID", example = "0001")
|
||||
private String projectId;
|
||||
}
|
||||
}
|
||||
|
||||
124
src/main/java/com/zhpb/gdyd_zhpb_zgf/dto/SummaryResponse.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.dto
|
||||
* @ClassName: SummaryResponse
|
||||
* @Description: 总结相关响应DTO
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 总结相关响应DTO
|
||||
*/
|
||||
public class SummaryResponse {
|
||||
|
||||
@Data
|
||||
@Schema(description = "目录响应")
|
||||
public static class DirectoryResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "Success")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "目录数据")
|
||||
private DirectoryData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "目录数据")
|
||||
public static class DirectoryData {
|
||||
|
||||
@Schema(description = "文件ID", example = "9e607a37-ab87-4e02-8b42-125270d6bf58")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "目录结构")
|
||||
private List<DirectoryItem> directory;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "目录项")
|
||||
public static class DirectoryItem {
|
||||
|
||||
@Schema(description = "章节ID", example = "chap-0")
|
||||
private String chapterId;
|
||||
|
||||
@Schema(description = "章节标题", example = "1、技术规范文件应答")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "页码信息", example = "1")
|
||||
private String page;
|
||||
|
||||
@Schema(description = "起始页码", example = "1")
|
||||
private Integer startPage;
|
||||
|
||||
@Schema(description = "结束页码", example = "1")
|
||||
private Integer endPage;
|
||||
|
||||
@Schema(description = "子章节列表")
|
||||
private List<DirectoryItem> children;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "章节总结响应")
|
||||
public static class ChapterSummaryResponse {
|
||||
|
||||
@Schema(description = "响应状态码,0表示成功", example = "0")
|
||||
private Integer code;
|
||||
|
||||
@Schema(description = "响应消息", example = "Success")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "章节总结数据")
|
||||
private ChapterSummaryData data;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "章节总结数据")
|
||||
public static class ChapterSummaryData {
|
||||
|
||||
@Schema(description = "文件ID", example = "9e607a37-ab87-4e02-8b42-125270d6bf58")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "章节ID", example = "chap-0")
|
||||
private String chapterId;
|
||||
|
||||
@Schema(description = "章节总结", example = "本章节主要介绍了...")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "来源信息")
|
||||
private List<SourceInfo> sources;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Schema(description = "来源信息")
|
||||
public static class SourceInfo {
|
||||
|
||||
@Schema(description = "页码", example = "1")
|
||||
private String page;
|
||||
|
||||
@Schema(description = "文件ID", example = "9e607a37-ab87-4e02-8b42-125270d6bf58")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "内容片段", example = "具体内容...")
|
||||
private String snippet;
|
||||
|
||||
@Schema(description = "章节ID", example = "chap-0")
|
||||
private String chapterId;
|
||||
|
||||
@Schema(description = "起始页码", example = "1")
|
||||
private Integer startPage;
|
||||
|
||||
@Schema(description = "结束页码", example = "1")
|
||||
private Integer endPage;
|
||||
}
|
||||
}
|
||||
107
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/ApiLog.java
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: ApiLog
|
||||
* @Description: API调用日志实体类,映射t_api_log表,记录所有API调用的详细信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API调用日志实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_api_log")
|
||||
@Data
|
||||
public class ApiLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 调用的接口路径
|
||||
*/
|
||||
@Column(name = "api_path", nullable = false, length = 512)
|
||||
private String apiPath;
|
||||
|
||||
/**
|
||||
* 接口中文名称
|
||||
*/
|
||||
@Column(name = "api_name", length = 128)
|
||||
private String apiName;
|
||||
|
||||
/**
|
||||
* HTTP方法(GET, POST, PUT, DELETE等)
|
||||
*/
|
||||
@Column(name = "http_method", length = 16)
|
||||
private String httpMethod;
|
||||
|
||||
/**
|
||||
* 请求参数(JSON格式)
|
||||
*/
|
||||
@Column(name = "request_params", columnDefinition = "TEXT")
|
||||
private String requestParams;
|
||||
|
||||
/**
|
||||
* 请求体内容(JSON格式)
|
||||
*/
|
||||
@Column(name = "request_body", columnDefinition = "TEXT")
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 返回参数(JSON格式)
|
||||
*/
|
||||
@Column(name = "response_body", columnDefinition = "TEXT")
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 响应状态码(200, 404, 500等)
|
||||
*/
|
||||
@Column(name = "status_code")
|
||||
private Integer statusCode;
|
||||
|
||||
/**
|
||||
* 耗费时长(毫秒)
|
||||
*/
|
||||
@Column(name = "duration")
|
||||
private Long duration;
|
||||
|
||||
/**
|
||||
* 客户端IP地址
|
||||
*/
|
||||
@Column(name = "client_ip", length = 64)
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户ID(如果有)
|
||||
*/
|
||||
@Column(name = "user_id", length = 64)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户名(如果有)
|
||||
*/
|
||||
@Column(name = "user_name", length = 128)
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 错误信息(如果有)
|
||||
*/
|
||||
@Column(name = "error_msg", columnDefinition = "TEXT")
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: ComparisonTask
|
||||
* @Description: 对比分析任务实体
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-04
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 对比分析任务实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_comparison_task")
|
||||
public class ComparisonTask {
|
||||
|
||||
@Id
|
||||
@Column(name = "task_id", length = 64)
|
||||
private String taskId;
|
||||
|
||||
@Column(name = "project_id", length = 64, nullable = false)
|
||||
private String projectId;
|
||||
|
||||
@Column(name = "user_id", length = 64, nullable = false)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "vendor_ids", columnDefinition = "TEXT")
|
||||
private String vendorIds; // JSON格式存储供应商ID列表
|
||||
|
||||
@Column(name = "dimensions", columnDefinition = "TEXT")
|
||||
private String dimensions; // JSON格式存储对比维度
|
||||
|
||||
@Column(name = "status", length = 32, nullable = false)
|
||||
private String status; // pending, processing, completed, failed
|
||||
|
||||
@Lob
|
||||
@Column(name = "result", columnDefinition = "TEXT")
|
||||
private String result; // JSON格式存储对比结果
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
@Column(name = "progress", nullable = false)
|
||||
private Integer progress = 0; // 进度百分比 0-100
|
||||
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Column(name = "completed_time")
|
||||
private LocalDateTime completedTime;
|
||||
|
||||
// Getters and Setters
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setProjectId(String projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getVendorIds() {
|
||||
return vendorIds;
|
||||
}
|
||||
|
||||
public void setVendorIds(String vendorIds) {
|
||||
this.vendorIds = vendorIds;
|
||||
}
|
||||
|
||||
public String getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
public void setDimensions(String dimensions) {
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public Integer getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(Integer progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(LocalDateTime updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getCompletedTime() {
|
||||
return completedTime;
|
||||
}
|
||||
|
||||
public void setCompletedTime(LocalDateTime completedTime) {
|
||||
this.completedTime = completedTime;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
updateTime = LocalDateTime.now();
|
||||
if (progress == null) {
|
||||
progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
171
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/File.java
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: File
|
||||
* @Description: 文件实体类,映射t_file表,存储文件信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-01
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 文件实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_file")
|
||||
@Data
|
||||
public class File {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文件ID(业务ID,唯一)
|
||||
*/
|
||||
@Column(name = "file_id", unique = true, nullable = false, length = 64)
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* 项目ID(关联t_project表)
|
||||
*/
|
||||
@Column(name = "project_id", nullable = false, length = 64)
|
||||
private String projectId;
|
||||
|
||||
/**
|
||||
* 供应商ID(关联t_supplier表)
|
||||
*/
|
||||
@Column(name = "supplier_id", length = 64)
|
||||
private String supplierId;
|
||||
|
||||
/**
|
||||
* 文件名称
|
||||
*/
|
||||
@Column(name = "file_name", nullable = false, length = 512)
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件路径(完整路径)
|
||||
*/
|
||||
@Column(name = "file_path", nullable = false, columnDefinition = "TEXT")
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件扩展名(pdf, doc, xls等)
|
||||
*/
|
||||
@Column(name = "file_extension", length = 32)
|
||||
private String fileExtension;
|
||||
|
||||
/**
|
||||
* 文件类型ID(关联t_file_type表)
|
||||
*/
|
||||
@Column(name = "file_type_id")
|
||||
private Long fileTypeId;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@Column(name = "file_size")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件MD5值
|
||||
*/
|
||||
@Column(name = "file_md5", length = 64)
|
||||
private String fileMd5;
|
||||
|
||||
/**
|
||||
* 文件状态(uploaded, processed, vectorized, failed等)
|
||||
*/
|
||||
@Column(name = "status", length = 32)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 向量化状态(not_started-未开始, processing-处理中, completed-已完成, failed-失败)
|
||||
*/
|
||||
@Column(name = "vectorization_status", length = 32)
|
||||
private String vectorizationStatus = "not_started";
|
||||
|
||||
/**
|
||||
* 是否已向量化(兼容字段,根据vectorization_status自动设置)
|
||||
*/
|
||||
@Column(name = "is_vectorized", nullable = false)
|
||||
private Boolean isVectorized = false;
|
||||
|
||||
/**
|
||||
* 向量化时间
|
||||
*/
|
||||
@Column(name = "vectorized_time")
|
||||
private LocalDateTime vectorizedTime;
|
||||
|
||||
/**
|
||||
* 向量化数据条数(该文件对应的向量化记录数量)
|
||||
*/
|
||||
@Column(name = "vectorization_count", nullable = false)
|
||||
private Integer vectorizationCount = 0;
|
||||
|
||||
/**
|
||||
* 目录摘取状态(not_started-未开始, processing-处理中, completed-已完成, failed-失败)
|
||||
* 注意:失败且重试次数<3时会重置为 not_started,重试次数>=3时为 failed
|
||||
*/
|
||||
@Column(name = "catalog_extraction_status", length = 32, columnDefinition = "varchar(32) default 'not_started'")
|
||||
private String catalogExtractionStatus = "not_started";
|
||||
|
||||
/**
|
||||
* 目录摘取重试次数(最多重试3次)
|
||||
*/
|
||||
@Column(name = "catalog_extraction_retry_count", nullable = false, columnDefinition = "integer default 0")
|
||||
private Integer catalogExtractionRetryCount = 0;
|
||||
|
||||
/**
|
||||
* 目录摘取完成时间
|
||||
*/
|
||||
@Column(name = "catalog_extraction_time")
|
||||
private LocalDateTime catalogExtractionTime;
|
||||
|
||||
/**
|
||||
* 文件描述
|
||||
*/
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 预览接口URL(用于在线预览,如:/api/v1/files/preview/pdf/{file_id})
|
||||
*/
|
||||
@Column(name = "preview_path", columnDefinition = "TEXT")
|
||||
private String previewPath;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
if (this.fileId == null) {
|
||||
// 如果没有设置fileId,自动生成UUID
|
||||
this.fileId = java.util.UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: FileDirectory
|
||||
* @Description: 文件目录实体
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件目录实体
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_file_directory")
|
||||
public class FileDirectory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "file_id", nullable = false, length = 100)
|
||||
private String fileId;
|
||||
|
||||
@Column(name = "chapter_id", nullable = false, length = 100)
|
||||
private String chapterId;
|
||||
|
||||
@Column(name = "title", nullable = false, columnDefinition = "TEXT")
|
||||
private String title;
|
||||
|
||||
@Column(name = "page", length = 50)
|
||||
private String page;
|
||||
|
||||
@Column(name = "start_page")
|
||||
private Integer startPage;
|
||||
|
||||
@Column(name = "end_page")
|
||||
private Integer endPage;
|
||||
|
||||
@Column(name = "parent_chapter_id", length = 100)
|
||||
private String parentChapterId;
|
||||
|
||||
@Column(name = "level", columnDefinition = "int default 1")
|
||||
private Integer level = 1;
|
||||
|
||||
@Column(name = "sort_order", columnDefinition = "int default 0")
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
@Column(name = "summary", columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Transient
|
||||
private List<FileDirectory> children;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
112
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/FileType.java
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: FileType
|
||||
* @Description: 文件类型实体类,映射t_file_type表,存储文件类型信息(如:投标文件、采购文件、结果文件等)
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-01
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 文件类型实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_file_type")
|
||||
@Data
|
||||
public class FileType {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文件类型代码(唯一,如:tender, purchase, result等)
|
||||
*/
|
||||
@Column(name = "type_code", unique = true, nullable = false, length = 64)
|
||||
private String typeCode;
|
||||
|
||||
/**
|
||||
* 文件类型名称(如:投标文件、采购文件、结果文件等)
|
||||
*/
|
||||
@Column(name = "type_name", nullable = false, length = 128)
|
||||
private String typeName;
|
||||
|
||||
/**
|
||||
* 文件类型描述
|
||||
*/
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 识别关键词(JSON格式,用于自动识别文件类型)
|
||||
* 例如:["投标", "标书", "tender"] 或 ["采购", "purchase", "采购文件"]
|
||||
*/
|
||||
@Column(name = "keywords", columnDefinition = "TEXT")
|
||||
private String keywords;
|
||||
|
||||
/**
|
||||
* 文件扩展名(JSON格式,可选,如:["pdf", "doc", "docx"])
|
||||
*/
|
||||
@Column(name = "file_extensions", columnDefinition = "TEXT")
|
||||
private String fileExtensions;
|
||||
|
||||
/**
|
||||
* 优先级(数字越大优先级越高,用于匹配时优先使用)
|
||||
*/
|
||||
@Column(name = "priority")
|
||||
private Integer priority = 0;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
if (this.typeCode == null || this.typeCode.isEmpty()) {
|
||||
// 如果没有提供typeCode,从typeName生成
|
||||
this.typeCode = generateTypeCode(this.typeName);
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从类型名称生成类型代码
|
||||
*/
|
||||
private String generateTypeCode(String typeName) {
|
||||
if (typeName == null || typeName.isEmpty()) {
|
||||
return "unknown";
|
||||
}
|
||||
// 转换为小写,替换空格和下划线为连字符
|
||||
return typeName.toLowerCase()
|
||||
.replaceAll("\\s+", "_")
|
||||
.replaceAll("[^a-z0-9_]", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: ModelRequestLog
|
||||
* @Description: 模型请求日志实体,记录所有模型/向量化调用信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-25
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_model_request_log")
|
||||
@Data
|
||||
public class ModelRequestLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 请求发起时间
|
||||
*/
|
||||
@Column(name = "request_time", nullable = false)
|
||||
private LocalDateTime requestTime;
|
||||
|
||||
/**
|
||||
* 请求来源(QA、SUMMARY、VECTORIZE等)
|
||||
*/
|
||||
@Column(name = "request_source", length = 64)
|
||||
private String requestSource;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
@Column(name = "request_url", length = 512)
|
||||
private String requestUrl;
|
||||
|
||||
/**
|
||||
* HTTP方法
|
||||
*/
|
||||
@Column(name = "request_method", length = 16)
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@Column(name = "model_name", length = 128)
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 请求体
|
||||
*/
|
||||
@Column(name = "request_payload", columnDefinition = "TEXT")
|
||||
private String requestPayload;
|
||||
|
||||
/**
|
||||
* 响应体
|
||||
*/
|
||||
@Column(name = "response_payload", columnDefinition = "TEXT")
|
||||
private String responsePayload;
|
||||
|
||||
/**
|
||||
* 请求状态(SUCCESS/FAILED)
|
||||
*/
|
||||
@Column(name = "status", length = 32)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 耗时(毫秒)
|
||||
*/
|
||||
@Column(name = "duration_ms")
|
||||
private Long durationMs;
|
||||
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
@Column(name = "api_key", length = 256)
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* AppKey(私有/办公模型用)
|
||||
*/
|
||||
@Column(name = "app_key", length = 256)
|
||||
private String appKey;
|
||||
|
||||
/**
|
||||
* AppSecret/AppSecret
|
||||
*/
|
||||
@Column(name = "app_secret", length = 256)
|
||||
private String appSecret;
|
||||
|
||||
/**
|
||||
* Prompt Token数量
|
||||
*/
|
||||
@Column(name = "prompt_tokens")
|
||||
private Integer promptTokens;
|
||||
|
||||
/**
|
||||
* Completion Token数量
|
||||
*/
|
||||
@Column(name = "completion_tokens")
|
||||
private Integer completionTokens;
|
||||
|
||||
/**
|
||||
* 总Token数量
|
||||
*/
|
||||
@Column(name = "total_tokens")
|
||||
private Integer totalTokens;
|
||||
|
||||
/**
|
||||
* TraceId
|
||||
*/
|
||||
@Column(name = "trace_id", length = 128)
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 额外信息(用于存放header、扩展字段等)
|
||||
*/
|
||||
@Column(name = "extra_info", columnDefinition = "TEXT")
|
||||
private String extraInfo;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
if (this.requestTime == null) {
|
||||
this.requestTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
118
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/OcrResult.java
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: OcrResult
|
||||
* @Description: OCR识别结果实体类,映射t_ocr_result表
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-12-09
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* OCR识别结果实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_ocr_result")
|
||||
@Data
|
||||
public class OcrResult {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文件ID(关联t_file表)
|
||||
*/
|
||||
@Column(name = "file_id", nullable = false)
|
||||
private Long fileId;
|
||||
|
||||
/**
|
||||
* 项目ID
|
||||
*/
|
||||
@Column(name = "project_id", nullable = false, length = 64)
|
||||
private String projectId;
|
||||
|
||||
/**
|
||||
* 供应商ID
|
||||
*/
|
||||
@Column(name = "supplier_id", length = 64)
|
||||
private String supplierId;
|
||||
|
||||
/**
|
||||
* OCR任务ID
|
||||
*/
|
||||
@Column(name = "ocr_task_id", length = 100)
|
||||
private String ocrTaskId;
|
||||
|
||||
/**
|
||||
* 识别状态(processing-处理中, success-成功, failed-失败)
|
||||
*/
|
||||
@Column(name = "status", length = 32, columnDefinition = "varchar(32) default 'processing'")
|
||||
private String status = "processing";
|
||||
|
||||
/**
|
||||
* 识别文本内容
|
||||
*/
|
||||
@Column(name = "recognized_text", columnDefinition = "TEXT")
|
||||
private String recognizedText;
|
||||
|
||||
/**
|
||||
* 下载链接(OCR结果文件下载链接)
|
||||
*/
|
||||
@Column(name = "download_url", columnDefinition = "TEXT")
|
||||
private String downloadUrl;
|
||||
|
||||
/**
|
||||
* 下载文件名
|
||||
*/
|
||||
@Column(name = "download_file_name", length = 512)
|
||||
private String downloadFileName;
|
||||
|
||||
/**
|
||||
* 处理页数
|
||||
*/
|
||||
@Column(name = "processed_pages")
|
||||
private Integer processedPages;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
@Column(name = "total_pages")
|
||||
private Integer totalPages;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@Column(name = "start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 完成时间
|
||||
*/
|
||||
@Column(name = "end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private LocalDateTime updateTime = LocalDateTime.now();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: Project
|
||||
* @Description: 项目实体类,映射t_project表,存储项目信息和文件路径列表
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 项目实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_project")
|
||||
@Data
|
||||
public class Project {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 项目ID(UUID或业务ID)
|
||||
*/
|
||||
@Column(name = "project_id", unique = true, nullable = false, length = 64)
|
||||
private String projectId;
|
||||
|
||||
/**
|
||||
* 项目名称
|
||||
*/
|
||||
@Column(name = "project_name", length = 256)
|
||||
private String projectName;
|
||||
|
||||
/**
|
||||
* 项目描述
|
||||
*/
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 项目状态
|
||||
*/
|
||||
@Column(name = "status", length = 32)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 文件路径(上传或解压的文件路径,JSON格式存储多个路径)
|
||||
*/
|
||||
@Column(name = "file_paths", columnDefinition = "TEXT")
|
||||
private String filePaths;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: PromptTemplate
|
||||
* @Description: 提示词模板实体类,映射t_prompt_template表,存储智能问答的提示词配置
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 提示词模板实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_prompt_template")
|
||||
@Data
|
||||
public class PromptTemplate {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板ID(业务ID,唯一)
|
||||
*/
|
||||
@Column(name = "template_id", unique = true, nullable = false, length = 64)
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
@Column(name = "template_name", nullable = false, length = 256)
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 模板类型(qa-智能问答, summary-摘要, etc)
|
||||
*/
|
||||
@Column(name = "template_type", nullable = false, length = 32)
|
||||
private String templateType = "qa";
|
||||
|
||||
/**
|
||||
* 提示词内容
|
||||
*/
|
||||
@Column(name = "prompt_content", nullable = false, columnDefinition = "TEXT")
|
||||
private String promptContent;
|
||||
|
||||
/**
|
||||
* 模板描述
|
||||
*/
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive = true;
|
||||
|
||||
/**
|
||||
* 排序号
|
||||
*/
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Column(name = "created_by", length = 64)
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Column(name = "updated_by", length = 64)
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
if (this.templateId == null) {
|
||||
this.templateId = java.util.UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: QaMessage
|
||||
* @Description: QA消息实体
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* QA消息实体
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_qa_message")
|
||||
public class QaMessage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "message_id", nullable = false, length = 100)
|
||||
private String messageId;
|
||||
|
||||
@Column(name = "session_id", nullable = false, length = 100)
|
||||
private String sessionId;
|
||||
|
||||
@Column(name = "sender", nullable = false, length = 20)
|
||||
private String sender; // "user" or "ai"
|
||||
|
||||
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "sources", columnDefinition = "TEXT")
|
||||
private String sources; // JSON string
|
||||
|
||||
@Column(name = "file_ids", columnDefinition = "TEXT")
|
||||
private String fileIds; // JSON string
|
||||
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: QaSession
|
||||
* @Description: QA会话实体
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* QA会话实体
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_qa_session")
|
||||
public class QaSession {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "session_id", nullable = false, length = 100)
|
||||
private String sessionId;
|
||||
|
||||
@Column(name = "project_id", nullable = false, length = 50)
|
||||
private String projectId;
|
||||
|
||||
@Column(name = "user_id", nullable = false, length = 50)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "first_query", columnDefinition = "TEXT")
|
||||
private String firstQuery;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column(name = "last_message_time")
|
||||
private LocalDateTime lastMessageTime;
|
||||
|
||||
@Column(name = "message_count", columnDefinition = "int default 0")
|
||||
private Integer messageCount = 0;
|
||||
|
||||
@Column(name = "status", length = 20, columnDefinition = "varchar(20) default 'active'")
|
||||
private String status = "active";
|
||||
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: SsoLog
|
||||
* @Description: SSO登录日志实体类,映射t_sso_log表,记录SSO登录的详细信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* SSO登录日志实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_sso_log")
|
||||
@Data
|
||||
public class SsoLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "ticket", length = 256)
|
||||
private String ticket;
|
||||
|
||||
@Column(name = "user_id", length = 64)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "user_name", length = 128)
|
||||
private String userName;
|
||||
|
||||
@Column(name = "project_id", length = 64)
|
||||
private String projectId;
|
||||
|
||||
@Column(name = "token", length = 512)
|
||||
private String token;
|
||||
|
||||
@Column(name = "login_ip", length = 64)
|
||||
private String loginIp;
|
||||
|
||||
@Column(name = "login_status", length = 32)
|
||||
private String loginStatus;
|
||||
|
||||
@Column(name = "error_msg", length = 512)
|
||||
private String errorMsg;
|
||||
|
||||
@Column(name = "request_url", length = 512)
|
||||
private String requestUrl;
|
||||
|
||||
@Column(name = "response_data", columnDefinition = "TEXT")
|
||||
private String responseData;
|
||||
|
||||
@Column(name = "login_time")
|
||||
private LocalDateTime loginTime;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
112
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/Supplier.java
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: Supplier
|
||||
* @Description: 供应商实体类,映射t_supplier表,存储供应商基本信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-01
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 供应商实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_supplier")
|
||||
@Data
|
||||
public class Supplier {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 供应商ID(业务ID,唯一)
|
||||
*/
|
||||
@Column(name = "supplier_id", unique = true, nullable = false, length = 64)
|
||||
private String supplierId;
|
||||
|
||||
/**
|
||||
* 供应商名称
|
||||
*/
|
||||
@Column(name = "supplier_name", nullable = false, length = 256)
|
||||
private String supplierName;
|
||||
|
||||
/**
|
||||
* 供应商代码
|
||||
*/
|
||||
@Column(name = "supplier_code", length = 128)
|
||||
private String supplierCode;
|
||||
|
||||
/**
|
||||
* 供应商类型
|
||||
*/
|
||||
@Column(name = "supplier_type", length = 64)
|
||||
private String supplierType;
|
||||
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
@Column(name = "contact_person", length = 128)
|
||||
private String contactPerson;
|
||||
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
@Column(name = "contact_phone", length = 64)
|
||||
private String contactPhone;
|
||||
|
||||
/**
|
||||
* 联系邮箱
|
||||
*/
|
||||
@Column(name = "contact_email", length = 128)
|
||||
private String contactEmail;
|
||||
|
||||
/**
|
||||
* 供应商地址
|
||||
*/
|
||||
@Column(name = "address", columnDefinition = "TEXT")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 供应商状态(active, inactive等)
|
||||
*/
|
||||
@Column(name = "status", length = 32)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@Column(name = "remarks", columnDefinition = "TEXT")
|
||||
private String remarks;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
119
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/SysLog.java
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: SysLog
|
||||
* @Description: 系统日志实体类,映射t_sys_log表,记录所有控制台输出和系统日志
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 系统日志实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_sys_log", indexes = {
|
||||
@Index(name = "idx_log_level", columnList = "log_level"),
|
||||
@Index(name = "idx_create_time", columnList = "create_time"),
|
||||
@Index(name = "idx_thread_name", columnList = "thread_name")
|
||||
})
|
||||
@Data
|
||||
public class SysLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 日志级别(INFO, DEBUG, WARN, ERROR, OUT等)
|
||||
*/
|
||||
@Column(name = "log_level", length = 20, nullable = false)
|
||||
private String logLevel;
|
||||
|
||||
/**
|
||||
* 日志来源(logger名称或SYSTEM_OUT)
|
||||
*/
|
||||
@Column(name = "logger_name", length = 255)
|
||||
private String loggerName;
|
||||
|
||||
/**
|
||||
* 线程名称
|
||||
*/
|
||||
@Column(name = "thread_name", length = 255)
|
||||
private String threadName;
|
||||
|
||||
/**
|
||||
* 日志内容
|
||||
*/
|
||||
@Column(name = "log_content", columnDefinition = "TEXT", nullable = false)
|
||||
private String logContent;
|
||||
|
||||
/**
|
||||
* 完整的日志消息(包含格式化信息)
|
||||
*/
|
||||
@Column(name = "full_message", columnDefinition = "TEXT")
|
||||
private String fullMessage;
|
||||
|
||||
/**
|
||||
* 异常堆栈信息(如果有)
|
||||
*/
|
||||
@Column(name = "exception_stack", columnDefinition = "TEXT")
|
||||
private String exceptionStack;
|
||||
|
||||
/**
|
||||
* 类名
|
||||
*/
|
||||
@Column(name = "class_name", length = 500)
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 方法名
|
||||
*/
|
||||
@Column(name = "method_name", length = 255)
|
||||
private String methodName;
|
||||
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
@Column(name = "line_number")
|
||||
private Integer lineNumber;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 应用名称
|
||||
*/
|
||||
@Column(name = "application_name", length = 100)
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 用户ID(如果有)
|
||||
*/
|
||||
@Column(name = "user_id", length = 64)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 请求ID或跟踪ID(如果有)
|
||||
*/
|
||||
@Column(name = "trace_id", length = 64)
|
||||
private String traceId;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
if (this.createTime == null) {
|
||||
this.createTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/mysql/User.java
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: User
|
||||
* @Description: MySQL用户实体类,映射t_user表,存储用户基本信息
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MySQL 用户实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_user")
|
||||
@Data
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", unique = true, nullable = false, length = 64)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 128)
|
||||
private String name;
|
||||
|
||||
@Column(name = "project_id", length = 64)
|
||||
private String projectId;
|
||||
|
||||
@Column(name = "status", length = 16)
|
||||
private String status;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.mysql
|
||||
* @ClassName: UserLog
|
||||
* @Description: 用户操作日志实体类,映射t_user_log表,记录用户的关键操作信息
|
||||
* @Author:
|
||||
* @Date: 2025-11-28
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.mysql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户操作日志实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_user_log")
|
||||
@Data
|
||||
public class UserLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@Column(name = "user_id", nullable = false, length = 64)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Column(name = "user_name", length = 128)
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 项目ID
|
||||
*/
|
||||
@Column(name = "project_id", length = 64)
|
||||
private String projectId;
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
* UPLOAD_FILE-上传文件
|
||||
* QA_QUESTION-智能问答
|
||||
* QA_MESSAGE-问答消息
|
||||
* VIEW_CATALOG-查看目录
|
||||
* CATALOG_SUMMARY-目录总结
|
||||
* MODEL_REPLY-大模型回复
|
||||
*/
|
||||
@Column(name = "operation_type", nullable = false, length = 64)
|
||||
private String operationType;
|
||||
|
||||
/**
|
||||
* 操作详情
|
||||
*/
|
||||
@Column(name = "operation_detail", columnDefinition = "TEXT")
|
||||
private String operationDetail;
|
||||
|
||||
/**
|
||||
* 关联ID(如文件ID、会话ID等)
|
||||
*/
|
||||
@Column(name = "related_id", length = 64)
|
||||
private String relatedId;
|
||||
|
||||
/**
|
||||
* 操作IP地址
|
||||
*/
|
||||
@Column(name = "ip_address", length = 64)
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 用户代理
|
||||
*/
|
||||
@Column(name = "user_agent", length = 512)
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime = LocalDateTime.now();
|
||||
}
|
||||
175
src/main/java/com/zhpb/gdyd_zhpb_zgf/entity/pgsql/PdfVector.java
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.pgsql
|
||||
* @ClassName: PdfVector
|
||||
* @Description: PDF向量化实体类,映射t_pdf_vector表,存储PDF文件的向量化结果(使用PostgreSQL的pgvector扩展)
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.pgsql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* PDF向量化实体类
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_pdf_vector")
|
||||
@Data
|
||||
public class PdfVector {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* PDF文件路径
|
||||
*/
|
||||
@Column(name = "file_path", nullable = false, length = 1000)
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* PDF文件名
|
||||
*/
|
||||
@Column(name = "file_name", nullable = false, length = 500)
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* PDF原始文本内容(可存储部分内容或摘要)
|
||||
*/
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 向量化后的向量(使用pgvector类型存储)
|
||||
* 注意:Hibernate无法自动创建vector类型,表通过原生SQL手动创建
|
||||
* 此字段不参与Hibernate的DDL,完全通过原生SQL操作
|
||||
*/
|
||||
@Transient
|
||||
private String vectorText;
|
||||
|
||||
/**
|
||||
* 向量数组(临时字段,不直接存储到数据库,通过vectorText间接存储)
|
||||
*/
|
||||
@Transient
|
||||
private float[] vector;
|
||||
|
||||
/**
|
||||
* 设置向量数组(自动转换为字符串)
|
||||
*/
|
||||
public void setVector(float[] vector) {
|
||||
this.vector = vector;
|
||||
if (vector != null && vector.length > 0) {
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < vector.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(vector[i]);
|
||||
}
|
||||
sb.append("]");
|
||||
this.vectorText = sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取向量数组(从字符串解析)
|
||||
*/
|
||||
public float[] getVector() {
|
||||
if (vector == null && vectorText != null && !vectorText.isEmpty()) {
|
||||
try {
|
||||
String content = vectorText.trim();
|
||||
if (content.startsWith("[") && content.endsWith("]")) {
|
||||
content = content.substring(1, content.length() - 1);
|
||||
String[] parts = content.split(",");
|
||||
vector = new float[parts.length];
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
vector[i] = Float.parseFloat(parts[i].trim());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败
|
||||
}
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量字符串格式(用于原生SQL查询)
|
||||
* 格式:[0.1,0.2,0.3,...]
|
||||
*/
|
||||
@Transient
|
||||
public String getVectorString() {
|
||||
if (vector == null || vector.length == 0) {
|
||||
return "[]";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < vector.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append(vector[i]);
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量维度
|
||||
*/
|
||||
@Column(name = "dimension", nullable = false)
|
||||
private Integer dimension;
|
||||
|
||||
/**
|
||||
* 文件ID(关联t_file表的file_id)
|
||||
*/
|
||||
@Column(name = "file_id", length = 64)
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* 项目ID(关联t_project表的project_id)
|
||||
*/
|
||||
@Column(name = "project_id", length = 100)
|
||||
private String projectId;
|
||||
|
||||
/**
|
||||
* 供应商ID(关联t_supplier表的supplier_id)
|
||||
*/
|
||||
@Column(name = "supplier_id", length = 64)
|
||||
private String supplierId;
|
||||
|
||||
/**
|
||||
* 文件类型ID(关联t_file_type表的id)
|
||||
*/
|
||||
@Column(name = "file_type_id")
|
||||
private Long fileTypeId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createTime = LocalDateTime.now();
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.entity.pgsql
|
||||
* @ClassName: Product
|
||||
* @Description: PostgreSQL产品实体类示例,映射t_product表
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.entity.pgsql;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* PostgreSQL 产品实体类示例
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "t_product")
|
||||
@Data
|
||||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "price")
|
||||
private Double price;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.exception
|
||||
* @ClassName: GlobalExceptionHandler
|
||||
* @Description: 全局异常处理器(暂时禁用,避免SpringDoc兼容性问题)
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.exception;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* 注意:为了避免SpringDoc兼容性问题,暂时禁用此处理器
|
||||
* 文件上传大小限制异常已在FileController中直接处理
|
||||
*
|
||||
* 如果未来SpringDoc版本修复了兼容性问题,可以重新启用:
|
||||
* - 取消注释 @RestControllerAdvice
|
||||
* - 取消注释 @ExceptionHandler 方法
|
||||
*/
|
||||
// 暂时禁用,避免SpringDoc 2.6.0与Spring Boot 3.5.7的兼容性问题
|
||||
/*
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(
|
||||
MaxUploadSizeExceededException e) {
|
||||
logger.warn("文件上传大小超限: {}", e.getMessage());
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 413);
|
||||
response.put("message", "文件大小超过限制,最大允许上传500MB");
|
||||
response.put("error", "MaxUploadSizeExceeded");
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(response);
|
||||
}
|
||||
}
|
||||
*/
|
||||
public class GlobalExceptionHandler {
|
||||
// 暂时为空类,保留以备将来使用
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.filter
|
||||
* @ClassName: ApiStatisticsFilter
|
||||
* @Description: API统计过滤器(已废弃,现在由ApiLogInterceptor处理日志记录)
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* API访问统计过滤器(已废弃)
|
||||
* 统计功能已由 ApiLogInterceptor 接管,日志保存在数据库表中
|
||||
* 保留此类是为了避免删除后的兼容性问题
|
||||
*
|
||||
* @deprecated 使用 {@link com.zhpb.gdyd_zhpb_zgf.interceptor.ApiLogInterceptor} 代替
|
||||
*/
|
||||
@Component
|
||||
@Deprecated
|
||||
public class ApiStatisticsFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 不再进行任何统计操作,直接放行
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.interceptor
|
||||
* @ClassName: ApiLogInterceptor
|
||||
* @Description: API日志拦截器,拦截所有API请求并记录详细的调用信息到数据库
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.interceptor;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.ApiLog;
|
||||
import com.zhpb.gdyd_zhpb_zgf.repository.mysql.ApiLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* API调用日志拦截器
|
||||
*/
|
||||
@Component
|
||||
public class ApiLogInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private ApiLogRepository apiLogRepository;
|
||||
|
||||
// 接口名称映射
|
||||
private static final Map<String, String> API_NAME_MAP = new HashMap<String, String>() {{
|
||||
put("/api/v1/sso/login", "SSO登录接口");
|
||||
put("/api/v1/statistics/total", "获取API总访问次数");
|
||||
put("/api/v1/statistics/count", "获取特定接口访问次数");
|
||||
put("/api/v1/files/uploadzip", "上传ZIP压缩包接口");
|
||||
put("/health", "健康检查");
|
||||
put("/actuator/health", "健康检查");
|
||||
put("/", "首页");
|
||||
put("/swagger-ui.html", "Swagger文档");
|
||||
put("/api-docs", "API文档JSON");
|
||||
}};
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
long startTime = System.currentTimeMillis();
|
||||
request.setAttribute("startTime", startTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
||||
// 不做处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
long startTime = (Long) request.getAttribute("startTime");
|
||||
long endTime = System.currentTimeMillis();
|
||||
long duration = endTime - startTime;
|
||||
|
||||
String path = request.getRequestURI();
|
||||
String method = request.getMethod();
|
||||
|
||||
// 排除健康检查页面发起的请求(避免产生大量无意义的日志)
|
||||
// 健康检查页面通常使用HEAD/OPTIONS请求来检查接口状态,不应该记录日志
|
||||
if ("HEAD".equalsIgnoreCase(method) || "OPTIONS".equalsIgnoreCase(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只记录API接口(以/api/开头的路径)和健康检查接口,排除静态资源
|
||||
if (!path.startsWith("/api/") &&
|
||||
!path.equals("/health") &&
|
||||
!path.equals("/actuator/health") &&
|
||||
!path.startsWith("/swagger") &&
|
||||
!path.startsWith("/webjars") &&
|
||||
!path.startsWith("/v3/api-docs")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 在异步执行前,同步获取所有需要的信息(避免响应对象被回收)
|
||||
int statusCode = response.getStatus();
|
||||
String clientIp = getClientIp(request);
|
||||
String apiName = getApiName(path);
|
||||
String requestParams = getRequestParams(request);
|
||||
|
||||
// 获取请求体和响应体的内容(需要在响应对象被回收前获取)
|
||||
String requestBody = null;
|
||||
String responseBody = null;
|
||||
|
||||
if (request instanceof ContentCachingRequestWrapper) {
|
||||
ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request;
|
||||
requestBody = getRequestBody(wrappedRequest);
|
||||
}
|
||||
|
||||
if (response instanceof ContentCachingResponseWrapper) {
|
||||
ContentCachingResponseWrapper wrappedResponse = (ContentCachingResponseWrapper) response;
|
||||
try {
|
||||
responseBody = getResponseBody(wrappedResponse);
|
||||
} catch (Exception e) {
|
||||
// 如果读取响应体失败,只记录警告,不阻塞
|
||||
System.err.println("读取响应体失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取异常信息
|
||||
String errorMsg = ex != null ? ex.getMessage() : null;
|
||||
|
||||
// 使用获取到的值创建最终变量(避免在异步线程中访问响应对象)
|
||||
final int finalStatusCode = statusCode;
|
||||
final String finalClientIp = clientIp;
|
||||
final String finalApiName = apiName;
|
||||
final String finalRequestParams = requestParams;
|
||||
final String finalRequestBody = requestBody;
|
||||
final String finalResponseBody = responseBody;
|
||||
final String finalErrorMsg = errorMsg;
|
||||
final String finalPath = path;
|
||||
final String finalMethod = method;
|
||||
final long finalDuration = duration;
|
||||
|
||||
// 异步保存日志,避免阻塞请求
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
ApiLog apiLog = new ApiLog();
|
||||
// 从请求参数中提取userId和userName(处理不同接口的参数命名差异)
|
||||
String userId = null;
|
||||
String userName = null;
|
||||
if (finalRequestParams != null) {
|
||||
try {
|
||||
Map<String, Object> params = JSON.parseObject(finalRequestParams, Map.class);
|
||||
// 尝试多种参数命名方式
|
||||
userId = (String) params.get("userId");
|
||||
if (userId == null) {
|
||||
userId = (String) params.get("user_id");
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = (String) params.get("userld");
|
||||
}
|
||||
|
||||
userName = (String) params.get("userName");
|
||||
if (userName == null) {
|
||||
userName = (String) params.get("user_name");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败忽略
|
||||
}
|
||||
}
|
||||
|
||||
apiLog.setApiPath(finalPath);
|
||||
apiLog.setApiName(finalApiName);
|
||||
apiLog.setHttpMethod(finalMethod);
|
||||
apiLog.setStatusCode(finalStatusCode);
|
||||
apiLog.setDuration(finalDuration);
|
||||
apiLog.setClientIp(finalClientIp);
|
||||
apiLog.setUserId(userId);
|
||||
apiLog.setUserName(userName);
|
||||
apiLog.setCreateTime(LocalDateTime.now());
|
||||
apiLog.setRequestParams(finalRequestParams);
|
||||
apiLog.setRequestBody(finalRequestBody);
|
||||
apiLog.setResponseBody(finalResponseBody);
|
||||
apiLog.setErrorMsg(finalErrorMsg);
|
||||
|
||||
// 保存到数据库
|
||||
apiLogRepository.save(apiLog);
|
||||
} catch (Exception e) {
|
||||
// 记录日志失败不影响主流程,只打印错误
|
||||
System.err.println("保存API日志失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取接口中文名称
|
||||
*/
|
||||
private String getApiName(String path) {
|
||||
return API_NAME_MAP.getOrDefault(path, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*/
|
||||
private String getRequestParams(HttpServletRequest request) {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if (parameterMap.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
if (values.length == 1) {
|
||||
params.put(entry.getKey(), values[0]);
|
||||
} else {
|
||||
params.put(entry.getKey(), values);
|
||||
}
|
||||
}
|
||||
return params.isEmpty() ? null : JSON.toJSONString(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求体
|
||||
*/
|
||||
private String getRequestBody(ContentCachingRequestWrapper request) {
|
||||
byte[] content = request.getContentAsByteArray();
|
||||
if (content.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String body = new String(content, StandardCharsets.UTF_8);
|
||||
// 限制请求体长度,避免存储过大的数据
|
||||
return body.length() > 10000 ? body.substring(0, 10000) + "...(truncated)" : body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应体
|
||||
*/
|
||||
private String getResponseBody(ContentCachingResponseWrapper response) {
|
||||
byte[] content = response.getContentAsByteArray();
|
||||
if (content.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String body = new String(content, StandardCharsets.UTF_8);
|
||||
// 限制响应体长度,避免存储过大的数据
|
||||
return body.length() > 10000 ? body.substring(0, 10000) + "...(truncated)" : body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.repository.mysql
|
||||
* @ClassName: ApiLogRepository
|
||||
* @Description: API调用日志Repository接口,提供API日志数据访问和统计方法
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-10-31
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.ApiLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* API调用日志 Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface ApiLogRepository extends JpaRepository<ApiLog, Long> {
|
||||
|
||||
/**
|
||||
* 获取总访问次数
|
||||
*
|
||||
* @return 总访问次数
|
||||
*/
|
||||
@Query("SELECT COUNT(*) FROM ApiLog")
|
||||
Long countTotal();
|
||||
|
||||
/**
|
||||
* 获取特定接口的访问次数
|
||||
*
|
||||
* @param apiPath 接口路径
|
||||
* @return 访问次数
|
||||
*/
|
||||
@Query("SELECT COUNT(*) FROM ApiLog WHERE apiPath = :apiPath")
|
||||
Long countByApiPath(@Param("apiPath") String apiPath);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.repository.mysql
|
||||
* @ClassName: ComparisonTaskRepository
|
||||
* @Description: 对比分析任务Repository接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-04
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.ComparisonTask;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对比分析任务 Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface ComparisonTaskRepository extends JpaRepository<ComparisonTask, Long> {
|
||||
|
||||
/**
|
||||
* 根据任务ID查找任务
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @return 对比分析任务
|
||||
*/
|
||||
ComparisonTask findByTaskId(String taskId);
|
||||
|
||||
/**
|
||||
* 根据项目ID和用户ID查找任务列表
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param userId 用户ID
|
||||
* @return 任务列表
|
||||
*/
|
||||
@Query("SELECT t FROM ComparisonTask t WHERE t.projectId = :projectId AND t.userId = :userId ORDER BY t.createTime DESC")
|
||||
List<ComparisonTask> findByProjectIdAndUserIdOrderByCreateTimeDesc(@Param("projectId") String projectId, @Param("userId") String userId);
|
||||
|
||||
/**
|
||||
* 根据项目ID查找任务列表
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<ComparisonTask> findByProjectIdOrderByCreateTimeDesc(String projectId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找任务列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<ComparisonTask> findByUserIdOrderByCreateTimeDesc(String userId);
|
||||
|
||||
/**
|
||||
* 根据状态查找任务列表
|
||||
*
|
||||
* @param status 任务状态
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<ComparisonTask> findByStatusOrderByCreateTimeDesc(String status);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.repository.mysql
|
||||
* @ClassName: FileDirectoryRepository
|
||||
* @Description: 文件目录Repository
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-03
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileDirectory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件目录Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface FileDirectoryRepository extends JpaRepository<FileDirectory, Long> {
|
||||
|
||||
/**
|
||||
* 根据文件ID查找目录结构
|
||||
*/
|
||||
List<FileDirectory> findByFileIdOrderBySortOrderAsc(String fileId);
|
||||
|
||||
/**
|
||||
* 根据文件ID和父章节ID查找子章节
|
||||
*/
|
||||
List<FileDirectory> findByFileIdAndParentChapterIdOrderBySortOrderAsc(String fileId, String parentChapterId);
|
||||
|
||||
/**
|
||||
* 根据文件ID和章节ID查找章节
|
||||
*/
|
||||
FileDirectory findByFileIdAndChapterId(String fileId, String chapterId);
|
||||
|
||||
/**
|
||||
* 根据文件ID删除所有目录
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM FileDirectory fd WHERE fd.fileId = :fileId")
|
||||
void deleteByFileId(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* 根据文件ID统计章节数量
|
||||
*/
|
||||
long countByFileId(String fileId);
|
||||
|
||||
/**
|
||||
* 根据文件ID查找顶级章节(无父章节)
|
||||
*/
|
||||
List<FileDirectory> findByFileIdAndParentChapterIdIsNullOrderBySortOrderAsc(String fileId);
|
||||
|
||||
/**
|
||||
* 查询摘要为空或为null的目录记录
|
||||
* 用于定时任务扫描需要生成摘要的章节
|
||||
*/
|
||||
@Query("SELECT fd FROM FileDirectory fd WHERE fd.summary IS NULL OR fd.summary = '' ORDER BY fd.createTime ASC")
|
||||
List<FileDirectory> findBySummaryIsNullOrEmpty();
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.repository.mysql
|
||||
* @ClassName: FileRepository
|
||||
* @Description: 文件Repository接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-01
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.File;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 文件 Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface FileRepository extends JpaRepository<File, Long> {
|
||||
|
||||
/**
|
||||
* 根据文件ID查找文件
|
||||
*
|
||||
* @param fileId 文件ID
|
||||
* @return 文件对象
|
||||
*/
|
||||
Optional<File> findByFileId(String fileId);
|
||||
|
||||
/**
|
||||
* 根据项目ID查找所有文件
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findByProjectId(String projectId);
|
||||
|
||||
/**
|
||||
* 根据供应商ID查找所有文件
|
||||
*
|
||||
* @param supplierId 供应商ID
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findBySupplierId(String supplierId);
|
||||
|
||||
/**
|
||||
* 根据文件路径查找文件
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @return 文件对象
|
||||
*/
|
||||
Optional<File> findByFilePath(String filePath);
|
||||
|
||||
/**
|
||||
* 根据项目ID和供应商ID查找所有文件
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param supplierId 供应商ID
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findByProjectIdAndSupplierId(String projectId, String supplierId);
|
||||
|
||||
/**
|
||||
* 根据项目ID和是否已向量化查找文件
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param isVectorized 是否已向量化
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findByProjectIdAndIsVectorized(String projectId, Boolean isVectorized);
|
||||
|
||||
/**
|
||||
* 根据项目ID和文件名查找文件
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param fileName 文件名
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findByProjectIdAndFileName(String projectId, String fileName);
|
||||
|
||||
/**
|
||||
* 根据目录摘取状态和更新时间查找文件(用于查找卡住的任务)
|
||||
*
|
||||
* @param status 目录摘取状态
|
||||
* @param updateTime 更新时间截止点
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<File> findByCatalogExtractionStatusAndUpdateTimeBefore(String status, java.time.LocalDateTime updateTime);
|
||||
|
||||
/**
|
||||
* 根据目录摘取状态查找文件
|
||||
*/
|
||||
List<File> findByCatalogExtractionStatus(String status);
|
||||
|
||||
/**
|
||||
* 根据项目ID和目录摘取状态查找文件
|
||||
*/
|
||||
List<File> findByProjectIdAndCatalogExtractionStatus(String projectId, String status);
|
||||
|
||||
/**
|
||||
* 根据向量化状态查找文件
|
||||
*/
|
||||
List<File> findByVectorizationStatus(String status);
|
||||
|
||||
/**
|
||||
* 根据项目ID和向量化状态查找文件
|
||||
*/
|
||||
List<File> findByProjectIdAndVectorizationStatus(String projectId, String status);
|
||||
|
||||
/**
|
||||
* 统计目录摘取状态的文件数量
|
||||
*/
|
||||
long countByCatalogExtractionStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计项目下目录摘取状态的文件数量
|
||||
*/
|
||||
long countByProjectIdAndCatalogExtractionStatus(String projectId, String status);
|
||||
|
||||
/**
|
||||
* 统计向量化状态的文件数量
|
||||
*/
|
||||
long countByVectorizationStatus(String status);
|
||||
|
||||
/**
|
||||
* 统计项目下向量化状态的文件数量
|
||||
*/
|
||||
long countByProjectIdAndVectorizationStatus(String projectId, String status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @ProjectName: 智慧评标-主观分助手
|
||||
* @Package: com.zhpb.gdyd_zhpb_zgf.repository.mysql
|
||||
* @ClassName: FileTypeRepository
|
||||
* @Description: 文件类型Repository接口
|
||||
* @Author: 张志锋
|
||||
* @Date: 2025-11-01
|
||||
* @Version: 1.0
|
||||
* @Copyright: © 2025 智慧评标-主观分助手
|
||||
*/
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.FileType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 文件类型 Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface FileTypeRepository extends JpaRepository<FileType, Long> {
|
||||
|
||||
/**
|
||||
* 根据类型代码查找文件类型
|
||||
*
|
||||
* @param typeCode 类型代码
|
||||
* @return 文件类型对象
|
||||
*/
|
||||
Optional<FileType> findByTypeCode(String typeCode);
|
||||
|
||||
/**
|
||||
* 根据类型名称查找文件类型
|
||||
*
|
||||
* @param typeName 类型名称
|
||||
* @return 文件类型对象
|
||||
*/
|
||||
Optional<FileType> findByTypeName(String typeName);
|
||||
|
||||
/**
|
||||
* 查找所有启用的文件类型,按优先级降序排列
|
||||
*
|
||||
* @param enabled 是否启用
|
||||
* @return 文件类型列表
|
||||
*/
|
||||
List<FileType> findByEnabledOrderByPriorityDesc(Boolean enabled);
|
||||
|
||||
/**
|
||||
* 查找所有启用的文件类型
|
||||
*
|
||||
* @return 文件类型列表
|
||||
*/
|
||||
List<FileType> findByEnabledTrue();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.zhpb.gdyd_zhpb_zgf.repository.mysql;
|
||||
|
||||
import com.zhpb.gdyd_zhpb_zgf.entity.mysql.ModelRequestLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface ModelRequestLogRepository extends JpaRepository<ModelRequestLog, Long> {
|
||||
}
|
||||
|
||||
|
||||