Эх сурвалжийг харах

feat(config): 后端配置支持环境变量覆盖,新增 Linux 环境变量脚本

- application*.yaml 中除 mybatis-plus、sa-token 外的配置项统一写成 ${ENV_NAME:默认值}:未设置环境变量时沿用原值,dev/pro 各自保留原默认值
- 服务端口、context-path、应用名保持字面值(固定配置),不参与参数化
- 新增 deploy/env.sh:Linux 下发环境变量用,与 yaml 占位符 1:1 对齐(65 项),支持 source / systemd EnvironmentFile / docker --env-file;向量模型目录默认指向 /opt/zsjz-ai/bge-m3
- 新增 .gitattributes:*.sh 强制 LF,避免 CRLF 带到 Linux
cc 2 өдөр өмнө
parent
commit
40b9d70f24

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+*.sh text eol=lf

+ 22 - 18
ai-server/src/main/resources/application-dev.yaml

@@ -1,3 +1,6 @@
+# 配置约定同 application.yaml:所有配置项都写成 ${ENV_NAME:默认值},
+# 未设置环境变量时使用下面的默认值(dev 环境的现有取值)。
+# 例外:sa-token 段按约定保持字面值,不参数化;mybatis-plus 段在 application.yaml 里同样保持字面值。
 spring:
   # ========================================
   # PostgreSQL 数据库配置
@@ -8,26 +11,26 @@ spring:
   datasource:
     dynamic:
       # 主数据源:pgsql,永不改变
-      primary: master
+      primary: "${SPRING_DATASOURCE_DYNAMIC_PRIMARY:master}"
       # 严格模式:@DS 写错 key 时直接报错,而不是静默回退主库。案件库走 CaseRoutingDataSource
       # 的 slave 分支,不受影响;这能防止未来新增 mapper 时把案件 SQL 误发到 PostgreSQL。
-      strict: true
+      strict: "${SPRING_DATASOURCE_DYNAMIC_STRICT:true}"
       datasource:
         master:
-          url: jdbc:postgresql://192.168.0.106:5432/zsjz-ai
-          username: postgres
-          password: postgres
-          driver-class-name: org.postgresql.Driver
+          url: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL:jdbc:postgresql://192.168.0.106:5432/zsjz-ai}"
+          username: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME:postgres}"
+          password: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD:postgres}"
+          driver-class-name: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVER_CLASS_NAME:org.postgresql.Driver}"
   data:
     redis:
-      host: 192.168.0.106
-      port: 6379
+      host: "${SPRING_DATA_REDIS_HOST:192.168.0.106}"
+      port: "${SPRING_DATA_REDIS_PORT:6379}"
 
   elasticsearch:
     # 全文检索(替代原嵌入式 Lucene)。本机单节点默认地址,独立服务器时改 uris 即可
-    uris: http://192.168.0.106:9200
-    connection-timeout: 5s
-    socket-timeout: 60s
+    uris: "${SPRING_ELASTICSEARCH_URIS:http://192.168.0.106:9200}"
+    connection-timeout: "${SPRING_ELASTICSEARCH_CONNECTION_TIMEOUT:5s}"
+    socket-timeout: "${SPRING_ELASTICSEARCH_SOCKET_TIMEOUT:60s}"
     #username: elastic
     #password: a*qayNZRcIpE2S4sRac=
 
@@ -35,11 +38,11 @@ spring:
 storage:
   local:
     # 本地文件系统存储根目录
-    root-path: ./data/files
+    root-path: "${STORAGE_LOCAL_ROOT_PATH:./data/files}"
     # 文件对外访问基础地址:须与 server.port(8980) 和 context-path(/js/a) 一致。
     # 注意:/files/** 与 /file-download/** 这两个对外端点当前尚未实现,
     # 该地址目前只被 LocalFileStorageUtil 用于拼接返回给前端的 URL。
-    base-url: http://localhost:8980/js/a
+    base-url: "${STORAGE_LOCAL_BASE_URL:http://localhost:8980/js/a}"
 
 # ========================================
 # Sa-Token 认证配置
@@ -68,16 +71,17 @@ sa-token:
   auto-renew: true
 
 neo4j:
-  uri: 'bolt://localhost:7687'
-  username: 'neo4j'
-  password: 'neo4j123'
+  uri: "${NEO4J_URI:bolt://localhost:7687}"
+  username: "${NEO4J_USERNAME:neo4j}"
+  password: "${NEO4J_PASSWORD:neo4j123}"
 
 # ======================================================================
 # 嵌入模型(开发机覆盖):在线 DashScope embedding 免费额度耗尽(403 FreeTierOnly),
 # 切回本地 bge-m3 ONNX。模型目录在仓库根(E:/workspace/zsjz-ai/bge-m3),
 # 相对路径在 mvn spring-boot:run(user.dir=ai-server)下解析不到,这里用绝对路径。
+# 环境变量覆盖规则同上:未设置 ZSJZ_EMBEDDING_* 时用下面的开发机默认值。
 # ======================================================================
 zsjz:
   embedding:
-    enabled: true
-    model-path: E:/workspace/zsjz-ai/bge-m3
+    enabled: "${ZSJZ_EMBEDDING_ENABLED:true}"
+    model-path: "${ZSJZ_EMBEDDING_MODEL_PATH:E:/workspace/zsjz-ai/bge-m3}"

+ 30 - 16
ai-server/src/main/resources/application-pro.yaml

@@ -1,3 +1,6 @@
+# 配置约定同 application.yaml:所有配置项都写成 ${ENV_NAME:默认值},
+# 未设置环境变量时使用下面的默认值(pro 环境的现有取值)。
+# 例外:sa-token 段按约定保持字面值,不参数化;mybatis-plus 段在 application.yaml 里同样保持字面值。
 spring:
   # ========================================
   # PostgreSQL 数据库配置
@@ -8,26 +11,26 @@ spring:
   datasource:
     dynamic:
       # 主数据源:pgsql,永不改变
-      primary: master
+      primary: "${SPRING_DATASOURCE_DYNAMIC_PRIMARY:master}"
       # 严格模式:@DS 写错 key 时直接报错,而不是静默回退主库。案件库走 CaseRoutingDataSource
       # 的 slave 分支,不受影响;这能防止未来新增 mapper 时把案件 SQL 误发到 PostgreSQL。
-      strict: true
+      strict: "${SPRING_DATASOURCE_DYNAMIC_STRICT:true}"
       datasource:
         master:
-          url: jdbc:postgresql://localhost:5432/zsjz-ai
-          username: postgres
-          password: postgres
-          driver-class-name: org.postgresql.Driver
+          url: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL:jdbc:postgresql://localhost:5432/zsjz-ai}"
+          username: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME:postgres}"
+          password: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD:postgres}"
+          driver-class-name: "${SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVER_CLASS_NAME:org.postgresql.Driver}"
   data:
     redis:
-      host: localhost
-      port: 6379
+      host: "${SPRING_DATA_REDIS_HOST:localhost}"
+      port: "${SPRING_DATA_REDIS_PORT:6379}"
 
   elasticsearch:
     # 全文检索(替代原嵌入式 Lucene)。本机单节点默认地址,独立服务器时改 uris 即可
-    uris: http://localhost:9200
-    connection-timeout: 5s
-    socket-timeout: 60s
+    uris: "${SPRING_ELASTICSEARCH_URIS:http://localhost:9200}"
+    connection-timeout: "${SPRING_ELASTICSEARCH_CONNECTION_TIMEOUT:5s}"
+    socket-timeout: "${SPRING_ELASTICSEARCH_SOCKET_TIMEOUT:60s}"
     #username: elastic
     #password: a*qayNZRcIpE2S4sRac=
 
@@ -35,11 +38,22 @@ spring:
 storage:
   local:
     # 本地文件系统存储根目录
-    root-path: ./data/files
+    root-path: "${STORAGE_LOCAL_ROOT_PATH:./data/files}"
     # 文件对外访问基础地址:须与 server.port(8980) 和 context-path(/js/a) 一致。
     # 注意:/files/** 与 /file-download/** 这两个对外端点当前尚未实现,
     # 该地址目前只被 LocalFileStorageUtil 用于拼接返回给前端的 URL。
-    base-url: http://10.66.66.66:8980/js/a
+    base-url: "${STORAGE_LOCAL_BASE_URL:http://10.66.66.66:8980/js/a}"
+
+# ======================================================================
+# 嵌入模型(开发机覆盖):在线 DashScope embedding 免费额度耗尽(403 FreeTierOnly),
+# 切回本地 bge-m3 ONNX。模型目录在仓库根(E:/workspace/zsjz-ai/bge-m3),
+# 相对路径在 mvn spring-boot:run(user.dir=ai-server)下解析不到,这里用绝对路径。
+# 环境变量覆盖规则同上:未设置 ZSJZ_EMBEDDING_* 时用下面的默认值。
+# ======================================================================
+zsjz:
+  embedding:
+    enabled: "${ZSJZ_EMBEDDING_ENABLED:true}"
+    model-path: "${ZSJZ_EMBEDDING_MODEL_PATH:E:/workspace/zsjz-ai/bge-m3}"
 
 # ========================================
 # Sa-Token 认证配置
@@ -68,6 +82,6 @@ sa-token:
   auto-renew: true
 
 neo4j:
-  uri: 'bolt://localhost:7687'
-  username: 'neo4j'
-  password: 'neo4j123'
+  uri: "${NEO4J_URI:bolt://localhost:7687}"
+  username: "${NEO4J_USERNAME:neo4j}"
+  password: "${NEO4J_PASSWORD:neo4j123}"

+ 61 - 52
ai-server/src/main/resources/application.yaml

@@ -1,3 +1,7 @@
+# 配置约定:所有配置项都写成 ${ENV_NAME:默认值},可用环境变量覆盖;
+# 未设置环境变量时取冒号后的默认值(与改造前一致),设置了则以环境变量为准。
+# 例外:mybatis-plus 段是框架内部装配(别名包 / Mapper 位置 / 日志实现),不参与参数化;
+#       spring.profiles.active 用 Spring 原生的 SPRING_PROFILES_ACTIVE 控制。
 server:
   port: 8980
   servlet:
@@ -8,13 +12,14 @@ server:
 # 有固定域名时把 allowed-origin-patterns 收敛成具体来源(逗号分隔,支持通配)。
 app:
   cors:
-    enabled: true
-    allowed-origin-patterns: "*"
+    enabled: "${APP_CORS_ENABLED:true}"
+    allowed-origin-patterns: "${APP_CORS_ALLOWED_ORIGIN_PATTERNS:*}"
 
 spring:
   application:
     name: ai-server
   profiles:
+    # 由环境变量 SPRING_PROFILES_ACTIVE 覆盖,不写占位符(profile 决定加载哪些文件,需在配置数据阶段确定)
     active: dev
   # 调度线程池扩容(P1-10):默认单线程调度器上跑着 SSE 心跳(5s,遍历所有连接)、
   # 数据源回收(5min)、AI 清洗看门狗(60s)、附件孤儿清理(1h)——任一批量任务变慢
@@ -22,35 +27,33 @@ spring:
   task:
     scheduling:
       pool:
-        size: 4
+        size: "${SPRING_TASK_SCHEDULING_POOL_SIZE:4}"
   http:
     codecs:
-      max-in-memory-size: 10MB
+      max-in-memory-size: "${SPRING_HTTP_CODECS_MAX_IN_MEMORY_SIZE:10MB}"
   servlet:
     multipart:
-      max-file-size: 200MB
-      max-request-size: 500MB
-
-
+      max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:200MB}"
+      max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:500MB}"
 
 
 # AI 研判(对象关系分析)配置 —— 见 continuous-insight-plan.md §4.1.7
 zsjz:
   insight:
     # 单案会话数上限,超出拒绝新建并提示清理历史
-    max-sessions-per-case: 500
+    max-sessions-per-case: "${ZSJZ_INSIGHT_MAX_SESSIONS_PER_CASE:500}"
     # 留存天数;0 = 不自动清理
-    retention-days: 0
+    retention-days: "${ZSJZ_INSIGHT_RETENTION_DAYS:0}"
     # 消息分页 limit 默认值(与 Query.limit 默认 20 一致)
-    message-page-size: 20
+    message-page-size: "${ZSJZ_INSIGHT_MESSAGE_PAGE_SIZE:20}"
     # 消息分页 limit 上限,超出直接截断(不报错)
-    message-page-size-max: 100
+    message-page-size-max: "${ZSJZ_INSIGHT_MESSAGE_PAGE_SIZE_MAX:100}"
     # 会话锁等待超时(毫秒)
-    session-lock-timeout-ms: 3000
+    session-lock-timeout-ms: "${ZSJZ_INSIGHT_SESSION_LOCK_TIMEOUT_MS:3000}"
     # 图谱快照默认取前 N 个节点 / N 条关系(写进提示词,避免误以为全量)
-    default-top-n: 100
+    default-top-n: "${ZSJZ_INSIGHT_DEFAULT_TOP_N:100}"
     # 图谱快照硬上限,防止提示词爆 token
-    max-top-n: 200
+    max-top-n: "${ZSJZ_INSIGHT_MAX_TOP_N:200}"
 
   # DuckDB 案件库资源预算。
   # 注意:DuckdbUnpooledDataSource 下发的是 SET GLOBAL(database instance 级),
@@ -58,15 +61,15 @@ zsjz:
   # 这里改为按 max-open-cases 分摊总预算,见 DuckdbBudgetProperties。
   duckdb:
     # 同时打开的案件库数量上限,超出直接拒绝开案
-    max-open-cases: 8
+    max-open-cases: "${ZSJZ_DUCKDB_MAX_OPEN_CASES:8}"
     # 所有案件实例合计可占用的物理内存比例(0~1)
-    memory-ratio: 0.6
+    memory-ratio: "${ZSJZ_DUCKDB_MEMORY_RATIO:0.6}"
     # 每个案件实例的 DuckDB 线程数
-    threads-per-instance: 4
+    threads-per-instance: "${ZSJZ_DUCKDB_THREADS_PER_INSTANCE:4}"
     # 兜底回收器:启动后延迟多久开始第一次扫描(毫秒)
-    reaper-initial-delay-ms: 60000
+    reaper-initial-delay-ms: "${ZSJZ_DUCKDB_REAPER_INITIAL_DELAY_MS:60000}"
     # 兜底回收器:两次扫描的间隔(毫秒)
-    reaper-interval-ms: 300000
+    reaper-interval-ms: "${ZSJZ_DUCKDB_REAPER_INTERVAL_MS:300000}"
 
   # 非表格文件(txt / doc / docx / pdf / ppt / 压缩包 …)的 AI 识别调度。
   # 这类文件解析不了表格数据(fileStatus 恒为 FILE_UNSUPPORTED_FAIL),但往往有情报价值:
@@ -74,11 +77,11 @@ zsjz:
   # 结果落案件库的 file_ai_profile 表,供前端展示与后续数据分析工具按类别取数。
   file-ai:
     # 同时调模型的任务数上限,其余排队等待(需求:同时执行 3 个文件,其余排队处理)
-    max-concurrent: 3
+    max-concurrent: "${ZSJZ_FILE_AI_MAX_CONCURRENT:3}"
     # 排队积压上限,超过直接标 SKIP —— 没有这一层,一次拖入几千个文件会把队列和内存堆爆
-    max-pending: 200
+    max-pending: "${ZSJZ_FILE_AI_MAX_PENDING:200}"
     # 单个文件识别的模型调用超时(秒)
-    timeout-seconds: 120
+    timeout-seconds: "${ZSJZ_FILE_AI_TIMEOUT_SECONDS:120}"
 
   # 向量模型(进程内 ONNX 本地模型 / Web 界面配置的在线 embedding 模型)。
   # 开关语义(zsjz.embedding.enabled):
@@ -91,22 +94,22 @@ zsjz:
   # 可通过 JVM 参数 --enable-native-access=ALL-UNNAMED 消除。
   embedding:
     # 向量模型开关:true = 本地模型;false = 在线模型
-    enabled: false
+    enabled: "${ZSJZ_EMBEDDING_ENABLED:false}"
     # 模型目录:含 tokenizer.json 与 onnx/model.onnx(+ model.onnx_data 外部权重)。
     # 相对路径优先按工作目录(user.dir)解析,其次按进程当前目录解析
-    model-path: bge-m3
+    model-path: "${ZSJZ_EMBEDDING_MODEL_PATH:bge-m3}"
     # 模型声明输出维度(bge-m3 = 1024):建向量表/状态展示用,不触发权重加载
-    dimensions: 1024
+    dimensions: "${ZSJZ_EMBEDDING_DIMENSIONS:1024}"
     # 单文本最大 token 数(超出截断;bge-m3 上限 8192,长文本 CPU 推理耗时显著上升)
-    max-seq-len: 8192
+    max-seq-len: "${ZSJZ_EMBEDDING_MAX_SEQ_LEN:8192}"
     # 批量推理每批文本条数(批内按最长文本 padding)
-    batch-size: 8
+    batch-size: "${ZSJZ_EMBEDDING_BATCH_SIZE:8}"
     # ORT intra-op 并行线程数;0 = 交给 ONNX Runtime 默认策略
-    intra-op-threads: 0
+    intra-op-threads: "${ZSJZ_EMBEDDING_INTRA_OP_THREADS:0}"
     # ONNX 图优化级别:NO_OPT / BASIC_OPT / EXTENDED_OPT / ALL_OPT(ALL 首次加载更慢)
-    opt-level: BASIC_OPT
+    opt-level: "${ZSJZ_EMBEDDING_OPT_LEVEL:BASIC_OPT}"
     # 启动时后台预热加载模型(false = 懒加载,首次业务调用时才载入)
-    eager-load: false
+    eager-load: "${ZSJZ_EMBEDDING_EAGER_LOAD:false}"
 
   # 智能清洗(AI 兜底链路,module/aiclean)。
   # max-concurrent:AI 判定并发闸门(未配置走代码默认 3);模型调用重试由 LlmRetry 固定 3 次封顶。
@@ -115,8 +118,8 @@ zsjz:
   # stale-minutes:批次心跳超时分钟数 —— 每完成一个 sheet 都会刷新 update_time,
   # 超时无推进的批次由看门狗标 FAILED,用户重新点「智能」即可恢复(进程重启同理,启动时自动清理)。
   ai-clean:
-    batch-token-budget: 0
-    stale-minutes: 30
+    batch-token-budget: "${ZSJZ_AI_CLEAN_BATCH_TOKEN_BUDGET:0}"
+    stale-minutes: "${ZSJZ_AI_CLEAN_STALE_MINUTES:30}"
     # 模板元数据向量索引(module/aiclean/index):
     # 只读两套模板元数据(table_info/table_field + ai_clean_template/ai_clean_field,不扫描 DuckDB),
     # 写进独立向量表 rag_tpl{tag}_d{dims}(本地模型带模型名标签,如 rag_tpl_bge_m3_d1024)。
@@ -126,17 +129,17 @@ zsjz:
     # + PG 已装 pgvector;未满足时全链路 fail-open。
     vector:
       # 总开关;关闭行为与改造前完全一致(无索引、无召回)
-      enabled: true
+      enabled: "${ZSJZ_AI_CLEAN_VECTOR_ENABLED:true}"
       # 模板新增/更新/删除后自动增量更新索引(关闭只能手工调 /aiClean/templateVector/rebuild)
-      auto-index-on-template-change: true
+      auto-index-on-template-change: "${ZSJZ_AI_CLEAN_VECTOR_AUTO_INDEX_ON_TEMPLATE_CHANGE:true}"
       # 启动时若索引表不存在/为空则后台自举重建(存量模板不会补发变更事件)
-      auto-rebuild-on-startup: true
+      auto-rebuild-on-startup: "${ZSJZ_AI_CLEAN_VECTOR_AUTO_REBUILD_ON_STARTUP:true}"
       # 召回条数上限:只补充送给 LLM 的候选,不是最终匹配结果
-      top-k: 12
+      top-k: "${ZSJZ_AI_CLEAN_VECTOR_TOP_K:12}"
       # 召回相似度阈值(cosine 转 score),太低会塞无关模板,太高召不回同义不同词的模板
-      score-threshold: 0.30
+      score-threshold: "${ZSJZ_AI_CLEAN_VECTOR_SCORE_THRESHOLD:0.30}"
       # 全量重建的文档数上限,防止模板库异常膨胀时一次 rebuild 打爆内存/嵌入额度
-      max-documents-per-rebuild: 5000
+      max-documents-per-rebuild: "${ZSJZ_AI_CLEAN_VECTOR_MAX_DOCUMENTS_PER_REBUILD:5000}"
 
   # 对话附件(输入框附件问答,module/agent/attachment)。
   # 三路:小内容内联进消息;大表格只给文件路径,Agent 用 execute_python 全量计算;
@@ -145,33 +148,40 @@ zsjz:
   # 附件按 userId/caseId/sessionId 三层隔离;24h 未发消息的孤儿附件自动清理。
   chat-attachment:
     # 文档大小上限(MB,Tika 解析对象)
-    max-doc-mb: 50
+    max-doc-mb: "${ZSJZ_CHAT_ATTACHMENT_MAX_DOC_MB:50}"
     # 表格大小上限(MB,流式扫描对象,100MB csv 也没问题)
-    max-sheet-mb: 200
+    max-sheet-mb: "${ZSJZ_CHAT_ATTACHMENT_MAX_SHEET_MB:200}"
     # 文档内联/RAG 的提取字符上限(超过截断并标注;也是 sidecar 写入上限)
-    inline-max-chars: 30000
+    inline-max-chars: "${ZSJZ_CHAT_ATTACHMENT_INLINE_MAX_CHARS:30000}"
     # 表格内联行数上限(超过走 Python 直读)
-    inline-max-rows: 200
+    inline-max-rows: "${ZSJZ_CHAT_ATTACHMENT_INLINE_MAX_ROWS:200}"
     # RAG 分块/重叠/块数上限/检索条数
-    chunk-size: 1000
-    chunk-overlap: 100
-    max-chunks: 500
-    top-k: 8
+    chunk-size: "${ZSJZ_CHAT_ATTACHMENT_CHUNK_SIZE:1000}"
+    chunk-overlap: "${ZSJZ_CHAT_ATTACHMENT_CHUNK_OVERLAP:100}"
+    max-chunks: "${ZSJZ_CHAT_ATTACHMENT_MAX_CHUNKS:500}"
+    top-k: "${ZSJZ_CHAT_ATTACHMENT_TOP_K:8}"
+
+  # 登录安全(module/plat)。
+  auth:
+    # 「账号正在使用」判定窗口(分钟):账号有有效会话且该窗口内活跃过,
+    # 再次登录需要用户确认后才能顶掉旧会话
+    in-use-window-minutes: "${ZSJZ_AUTH_IN_USE_WINDOW_MINUTES:30}"
 
 # Agent 表结构语义检索(RAG):向量索引的事实来源只有两套模板元数据
-# (table_info/table_field + ai_clean_template/ai_clean_field),不扫描案件 DuckDB。
+# (table_info/table_field + ai_clean_template/ai_clean_field,不扫描案件 DuckDB)。
 # 索引同时服务:① search_table_schema(Agent 生成 SQL 时选表)② AI 模板匹配召回。
 # 依赖向量模型可用(本地开关打开,或 agent_model 表有活跃 type=embedding 模型)+ PG pgvector;
 # 缺失时工具返回可读错误,不影响清洗。
 etl:
   rag:
     # 是否注册 search_table_schema 工具
-    enabled: true
+    enabled: "${ETL_RAG_ENABLED:true}"
     # 检索默认返回条数
-    top-k: 5
+    top-k: "${ETL_RAG_TOP_K:5}"
     # 相似度阈值(cosine 转 score)
-    score-threshold: 0.3
+    score-threshold: "${ETL_RAG_SCORE_THRESHOLD:0.3}"
 
+# 按约定不参数化:别名包 / Mapper 位置 / 日志实现都是框架内部装配,随代码发布
 mybatis-plus:
   # 实体别名包(各业务域实体)。已核实 10 个域的实体类简单名无重复,可安全启用;
   # 若后续新增实体出现重名,MyBatis 会因别名冲突启动失败,此时应删除本配置(XML 全用全限定名)。
@@ -181,4 +191,3 @@ mybatis-plus:
     cacheEnabled: false
     default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
     log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
-

+ 172 - 0
deploy/env.sh

@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+# =============================================================================
+#  清鉴(zsjz-ai)后端环境变量
+#
+#  ai-server 的 application*.yaml 里配置项都写成 ${ENV_NAME:默认值},
+#  本文件把这些变量显式列出来,方便在 Linux 上统一下发/逐项修改。
+#  一个变量都不设置时,后端行为与本文件里的取值一致(即 yaml 中的默认值)。
+#
+#  服务端口(8980)、context-path(/js/a)、应用名(ai-server)固定在
+#  application.yaml 里,不参与参数化,因此不在本文件中。
+#
+#  用法(任选其一):
+#    1) 启动前导入当前 shell:
+#         set -a; source deploy/env.sh; set +a
+#         java -jar ai-server.jar
+#    2) systemd 单元:
+#         EnvironmentFile=/opt/zsjz-ai/deploy/env.sh
+#
+#  用法(任选其一):
+#    1) 启动前导入当前 shell:
+#         set -a; source deploy/env.sh; set +a
+#         java -jar ai-server.jar
+#    2) systemd 单元:
+#         EnvironmentFile=/opt/zsjz-ai/deploy/env.sh
+#
+#  注意:
+#  · 文件内含数据库口令,部署时请 chmod 600,并按实际环境修改地址与口令。
+#  · 下面各项默认值取自 application-dev.yaml(当前开发环境);
+#    【按环境修改】标注的项,生产部署务必逐项确认。
+#  · 生效 profile 由 Spring 原生的 SPRING_PROFILES_ACTIVE 控制,默认 dev;
+#    生产可取消下面这行的注释。
+#  · sa-token、mybatis-plus 不参与参数化,不在此文件范围。
+# =============================================================================
+
+# SPRING_PROFILES_ACTIVE="pro"
+
+# ---------------------------------------------------------------------------
+# 跨域(桌面客户端用 file:// 加载前端,必须允许跨域)
+# ---------------------------------------------------------------------------
+APP_CORS_ENABLED="true"
+APP_CORS_ALLOWED_ORIGIN_PATTERNS="*"
+
+# ---------------------------------------------------------------------------
+# Spring 基础
+# ---------------------------------------------------------------------------
+SPRING_TASK_SCHEDULING_POOL_SIZE="4"
+SPRING_HTTP_CODECS_MAX_IN_MEMORY_SIZE="10MB"
+SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE="200MB"
+SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE="500MB"
+
+# ---------------------------------------------------------------------------
+# PostgreSQL(平台库,必须装 pgvector 扩展)
+# 【按环境修改】dev 默认 192.168.0.106,pro 默认 localhost
+# ---------------------------------------------------------------------------
+SPRING_DATASOURCE_DYNAMIC_PRIMARY="master"
+SPRING_DATASOURCE_DYNAMIC_STRICT="true"
+SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL="jdbc:postgresql://192.168.0.106:5432/zsjz-ai"
+SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME="postgres"
+SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD="postgres"
+SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVER_CLASS_NAME="org.postgresql.Driver"
+
+# ---------------------------------------------------------------------------
+# Redis(sa-token 登录态硬依赖,不可用则登录相关接口全部异常)
+# 【按环境修改】
+# ---------------------------------------------------------------------------
+SPRING_DATA_REDIS_HOST="192.168.0.106"
+SPRING_DATA_REDIS_PORT="6379"
+
+# ---------------------------------------------------------------------------
+# Elasticsearch(全文检索,索引 zsjz_search)
+# 【按环境修改】需要鉴权时再打开下面两行
+# ---------------------------------------------------------------------------
+SPRING_ELASTICSEARCH_URIS="http://192.168.0.106:9200"
+SPRING_ELASTICSEARCH_CONNECTION_TIMEOUT="5s"
+SPRING_ELASTICSEARCH_SOCKET_TIMEOUT="60s"
+# SPRING_ELASTICSEARCH_USERNAME=""
+# SPRING_ELASTICSEARCH_PASSWORD=""
+
+# ---------------------------------------------------------------------------
+# 本地文件存储
+# 【按环境修改】root-path 相对 JVM 工作目录(须为仓库/部署根目录);
+#              base-url 必须与 SERVER_PORT、SERVER_SERVLET_CONTEXT_PATH 一致
+# ---------------------------------------------------------------------------
+STORAGE_LOCAL_ROOT_PATH="./data/files"
+STORAGE_LOCAL_BASE_URL="http://localhost:8980/js/a"
+
+# ---------------------------------------------------------------------------
+# Neo4j(可选,仅图谱能力需要)
+# ---------------------------------------------------------------------------
+NEO4J_URI="bolt://localhost:7687"
+NEO4J_USERNAME="neo4j"
+NEO4J_PASSWORD="neo4j123"
+
+# ---------------------------------------------------------------------------
+# Agent 表结构语义检索(RAG)
+# ---------------------------------------------------------------------------
+ETL_RAG_ENABLED="true"
+ETL_RAG_TOP_K="5"
+ETL_RAG_SCORE_THRESHOLD="0.3"
+
+# ---------------------------------------------------------------------------
+# zsjz.insight(AI 研判 / 会话)
+# ---------------------------------------------------------------------------
+ZSJZ_INSIGHT_MAX_SESSIONS_PER_CASE="500"
+ZSJZ_INSIGHT_RETENTION_DAYS="0"
+ZSJZ_INSIGHT_MESSAGE_PAGE_SIZE="20"
+ZSJZ_INSIGHT_MESSAGE_PAGE_SIZE_MAX="100"
+ZSJZ_INSIGHT_SESSION_LOCK_TIMEOUT_MS="3000"
+ZSJZ_INSIGHT_DEFAULT_TOP_N="100"
+ZSJZ_INSIGHT_MAX_TOP_N="200"
+
+# ---------------------------------------------------------------------------
+# zsjz.duckdb(案件库资源预算与兜底回收)
+# 【按环境修改】max-open-cases 按机器内存/CPU 调整
+# ---------------------------------------------------------------------------
+ZSJZ_DUCKDB_MAX_OPEN_CASES="8"
+ZSJZ_DUCKDB_MEMORY_RATIO="0.6"
+ZSJZ_DUCKDB_THREADS_PER_INSTANCE="4"
+ZSJZ_DUCKDB_REAPER_INITIAL_DELAY_MS="60000"
+ZSJZ_DUCKDB_REAPER_INTERVAL_MS="300000"
+
+# ---------------------------------------------------------------------------
+# zsjz.file-ai(非表格文件 AI 识别调度)
+# ---------------------------------------------------------------------------
+ZSJZ_FILE_AI_MAX_CONCURRENT="3"
+ZSJZ_FILE_AI_MAX_PENDING="200"
+ZSJZ_FILE_AI_TIMEOUT_SECONDS="120"
+
+# ---------------------------------------------------------------------------
+# zsjz.embedding(向量模型)
+# 【按环境修改】model-path 指向部署机上的模型目录(含 tokenizer.json 与
+#              onnx/model.onnx)。dev 的 yaml 里是 Windows 绝对路径,
+#              Linux 上必须改成实际路径,例如 /opt/zsjz-ai/bge-m3。
+# ---------------------------------------------------------------------------
+ZSJZ_EMBEDDING_ENABLED="true"
+ZSJZ_EMBEDDING_MODEL_PATH="/opt/zsjz-ai/bge-m3"
+ZSJZ_EMBEDDING_DIMENSIONS="1024"
+ZSJZ_EMBEDDING_MAX_SEQ_LEN="8192"
+ZSJZ_EMBEDDING_BATCH_SIZE="8"
+ZSJZ_EMBEDDING_INTRA_OP_THREADS="0"
+ZSJZ_EMBEDDING_OPT_LEVEL="BASIC_OPT"
+ZSJZ_EMBEDDING_EAGER_LOAD="false"
+
+# ---------------------------------------------------------------------------
+# zsjz.ai-clean(AI 清洗与模板向量索引)
+# ---------------------------------------------------------------------------
+ZSJZ_AI_CLEAN_BATCH_TOKEN_BUDGET="0"
+ZSJZ_AI_CLEAN_STALE_MINUTES="30"
+ZSJZ_AI_CLEAN_VECTOR_ENABLED="true"
+ZSJZ_AI_CLEAN_VECTOR_AUTO_INDEX_ON_TEMPLATE_CHANGE="true"
+ZSJZ_AI_CLEAN_VECTOR_AUTO_REBUILD_ON_STARTUP="true"
+ZSJZ_AI_CLEAN_VECTOR_TOP_K="12"
+ZSJZ_AI_CLEAN_VECTOR_SCORE_THRESHOLD="0.30"
+ZSJZ_AI_CLEAN_VECTOR_MAX_DOCUMENTS_PER_REBUILD="5000"
+
+# ---------------------------------------------------------------------------
+# zsjz.chat-attachment(对话附件解析与 RAG)
+# ---------------------------------------------------------------------------
+ZSJZ_CHAT_ATTACHMENT_MAX_DOC_MB="50"
+ZSJZ_CHAT_ATTACHMENT_MAX_SHEET_MB="200"
+ZSJZ_CHAT_ATTACHMENT_INLINE_MAX_CHARS="30000"
+ZSJZ_CHAT_ATTACHMENT_INLINE_MAX_ROWS="200"
+ZSJZ_CHAT_ATTACHMENT_CHUNK_SIZE="1000"
+ZSJZ_CHAT_ATTACHMENT_CHUNK_OVERLAP="100"
+ZSJZ_CHAT_ATTACHMENT_MAX_CHUNKS="500"
+ZSJZ_CHAT_ATTACHMENT_TOP_K="8"
+
+# ---------------------------------------------------------------------------
+# zsjz.auth(登录安全)
+# 「账号正在使用」判定窗口(分钟):窗口内有活跃会话时,再次登录需确认后顶号
+# ---------------------------------------------------------------------------
+ZSJZ_AUTH_IN_USE_WINDOW_MINUTES="30"