I第一次提交

This commit is contained in:
zhangzf1119
2025-12-13 23:00:09 +08:00
commit ac08a0b6ff
180 changed files with 28023 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

41
.dockerignore Normal file
View 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
View File

@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
.gitignore vendored Normal file
View 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
View 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

Binary file not shown.

View 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
View 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
View 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 启动。"

View 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
View 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

View 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

View File

@@ -0,0 +1,11 @@
-- PostgreSQL 初始化脚本
-- 用于向量数据库的初始化
-- 创建向量扩展(如果需要的话)
-- CREATE EXTENSION IF NOT EXISTS vector;
-- 设置默认编码
SET client_encoding = 'UTF8';
-- 创建必要的模式和权限
-- 这里可以添加更多的初始化SQL

37
docker/redis/redis.conf Normal file
View 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

Binary file not shown.

BIN
images/主页.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
images/概括总结.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

282
main(2).py Normal file
View 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]:
""" 图片OCRBase64 识别接口 (/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
View 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
View 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
View 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
View 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>来源P283.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>

View 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

Binary file not shown.

Binary file not shown.

View 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);
}
}

View File

@@ -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;
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}
}

View 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;
}
}

View File

@@ -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() {
// 不需要清理
}
}

View File

@@ -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) {
// 忽略异常
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View 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/");
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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;
}
}

View 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;
}

View 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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}

View File

@@ -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();
}
}

View 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();
}
}

View File

@@ -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();
}
}

View 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_]", "");
}
}

View File

@@ -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();
}
}
}

View 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();
}

View File

@@ -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;
/**
* 项目IDUUID或业务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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View 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();
}
}

View 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();
}
}
}

View 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;
}

View File

@@ -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();
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -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 {
// 暂时为空类,保留以备将来使用
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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> {
}

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