Skip to content

构建与部署 ​

适用端:后端 + Web | 类型:操作

两条完全独立的构建链:后端 mvn clean package 出一个 325 MB 的胖 jar,前端 pnpm build:prod 出一个 32 MB 的静态目录。本页讲清产物里到底装了什么、三个 profile 差在哪、前端五个 .env 文件的覆盖顺序(这里有一个会让生产包关掉验证码的陷阱),以及仓库里那套 Docker / Jenkins / shell 脚本为什么现在照抄跑不通。

前置阅读:本地环境搭建、新增一个菜单功能端到端。

它解决什么问题 ​

三条最容易翻车的边:

位置翻车表现详见
B2 → B3jar 里 spring.profiles.active 硬编码 local,忘传参数就连 127.0.0.1 的库核心机制一
F1 → F2.env.local 会参与 --mode prod 的构建,并把验证码开关压成 false核心机制二
F3 ⇢ B3.env.prod 里 VITE_BASE_URL='',前端走同源相对路径,没有反代就全 404第 5 步

代码在哪 ​

关注点路径
版本、Java 版本、Boot 版本、镜像源ainb/pom.xml(155 行;revision 在 :29、java.version 在 :31、spring.boot.version 在 :39)
jar 名与 repackageainb/enlev-server/pom.xml:177(finalName)、:180-191(spring-boot-maven-plugin)
三个 profileainb/enlev-server/src/main/resources/application-{dev,local,prod}.yaml + 公共 application.yaml
初始化 SQLainb/sql/mysql/(6 个文件,共 ≈72 MB)
前端构建脚本ainb-ui-admin-vue3/package.json:9-24(dev / build:* / test:*)、:27-34(lint*)
前端环境变量ainb-ui-admin-vue3/.env、.env.local、.env.{dev,test,stage,prod}
Vite 配置ainb-ui-admin-vue3/vite.config.ts(:21-28 环境加载分支、:31 base、:123-153 build 块)
Vite 插件编排ainb-ui-admin-vue3/build/vite/index.ts(自动导入、dts: !isBuild)
代码生成模板ainb/enlev-module-infra/src/main/resources/codegen/(106 个 .vm,打进 jar)
仓库自带的部署脚本ainb/script/{docker,jenkins,shell}/、ainb/enlev-server/Dockerfile

核心机制一:后端 repackage ​

enlev-server/pom.xml:175-193 只有两件事:把 jar 定名,和把依赖塞进去。

xml
<build>
    <!-- 设置构建的 jar 包名 -->
    <finalName>${project.artifactId}</finalName>
    <plugins>
        <!-- 打包 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring.boot.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

finalName 是 ${project.artifactId},所以产物永远叫 enlev-server.jar,不带版本号。版本号由根 pom 的 <revision>2026.07-SNAPSHOT</revision> 统一管,靠 flatten-maven-plugin(flattenMode=oss,绑定在 process-resources 阶段)把 ${revision} 展开写进各模块的实际 pom。想改版本号只改根 pom 一处。

实测的产物内容 ​

bash
cd ainb
mvn clean package -DskipTests

实测 BUILD SUCCESS,Total time: 01:40 min(本机、离线仓库已就绪的情况下)。产物:

文件大小是什么
enlev-server/target/enlev-server.jar340,904,023 B(≈325 MB)repackage 后的可执行胖 jar
enlev-server/target/enlev-server.jar.original36,346 Brepackage 之前的瘦 jar,只含本模块 class,留着做对照用,不要部署它

胖在哪:BOOT-INF/lib/ 下 531 个 jar,最大的五个是

26,174,568  js-language-24.1.2.jar        ← GraalVM,AI 模块跑 JS 工具用
17,696,710  icu4j-24.1.2.jar              ← 随 GraalVM 一起来的
14,246,039  poi-ooxml-full-5.5.1.jar      ← Excel 导入导出
12,855,659  truffle-api-24.1.2.jar        ← 又是 GraalVM
 9,754,835  grpc-netty-shaded-1.65.1.jar  ← 向量库 / Milvus 客户端

GraalVM 三件套(js-language + icu4j + truffle-api)合计 ≈54 MB,占了 jar 的六分之一。这是 AI 模块的既有依赖,不要为了瘦身去 exclude——enlev-module-agent 在自己 pom 里本地钉住 spring-ai-bom 版本也是同一类刻意为之的取舍。

代码生成模板在 jar 里面,改模板必须重新打包

codegen/*.vm 不在外层 jar 的条目列表里(unzip -l enlev-server.jar | grep codegen/ 返回 0),它嵌在 BOOT-INF/lib/enlev-module-infra-2026.07-SNAPSHOT.jar 内,实测 106 个 .vm。运行时由 CodegenEngine 通过 Hutool 的 cn.hutool.extra.template.engine.velocity.VelocityEngine 从 classpath 读(CodegenEngine.java:9、:333、:433)。

所以:改了 enlev-module-infra/src/main/resources/codegen/ 下的模板,必须重新 mvn package 并重启,改完刷新页面是不生效的。只改了 infra 模块时可以 mvn package -pl enlev-module-infra,enlev-server -am -DskipTests 省点时间。

profile 是硬编码的 local ​

application.yaml:1-7:

yaml
spring:
  application:
    name: enlev-server

  profiles:
    active: local

这意味着 jar 默认跑 local profile。local 连的是 127.0.0.1:3306/ainb-professional-init,口令也是本地那一套。部署到服务器必须显式覆盖,两种方式任选:

bash
java -jar enlev-server.jar --spring.profiles.active=prod
# 或
SPRING_PROFILES_ACTIVE=prod java -jar enlev-server.jar

script/docker/docker-compose.yml 用的就是第二种(SPRING_PROFILES_ACTIVE: local)。

仓库里只有 dev / local / prod 三个 profile 文件。script/shell/deploy.sh 写的 PROFILES_ACTIVE=development 在这三者里找不到对应文件——详见 仓库自带的三套部署脚本。

核心机制二:前端 mode → env 文件 → outDir ​

package.json:13-17 是五个构建脚本,差别只在 --mode:

json
"build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode env.local",
"build:dev":   "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test":  "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod":  "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",

--max_old_space_size=8192 已经写在脚本里,直接 pnpm build:prod 即可,不需要自己 export NODE_OPTIONS。

vite.config.ts:22-28 决定读哪些 env 文件:

ts
const isBuild = command === 'build'
if (!isBuild) {
  env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
} else {
  env = loadEnv(mode, root)
}

pnpm dev = vite --mode env.local,此时 command 是 serve,走上面那个分支从 process.argv 里抠出 env.local 当 mode 名。名字叫 env.local 只是为了不和 dev 撞(pnpm dev-server 用的是 --mode dev,指向上游芋道演示后端),并不代表去读 .env.env.local 这个文件。

env 文件的加载顺序,与一个生产陷阱 ​

Vite(本仓实测 8.1.4)的 loadEnv 按固定顺序拼四个文件名,然后用 Object.fromEntries 合并——后面的覆盖前面的:

.env  →  .env.local  →  .env.${mode}  →  .env.${mode}.local

pnpm build:prod 时 mode=prod,于是 .env + .env.local + .env.prod 三个文件全部参与。而本仓的 .env.local 是纳入 git 跟踪的(.gitignore 里没有它,这是刻意保留的),克隆下来就存在,所以它一定会掺进生产构建。

.env.prod 里定义的变量(VITE_BASE_URL、VITE_OUT_DIR、VITE_DROP_CONSOLE……)会盖掉 .env.local,这没问题。问题出在**.env.prod 没定义、但 .env.local 定义了**的变量——此时 .env.local 盖掉的是 .env:

pnpm build:prod 打出来的包,登录页验证码是关的

  • .env:12 → VITE_APP_CAPTCHA_ENABLE=true
  • .env.local:31 → VITE_APP_CAPTCHA_ENABLE=false
  • .env.prod → 没有这一项

合并结果:false。消费点是 src/views/Login/components/LoginForm.vue:178(还有 ForgetPasswordForm.vue:170、RegisterForm.vue:156、SocialLogin.vue:204),判断写法是 loginData.captchaEnable === 'true',于是滑块验证码不渲染。

想真正打开验证码,要么在 .env.prod 里显式写 VITE_APP_CAPTCHA_ENABLE=true,要么构建前临时移走 .env.local。别只改 .env——它优先级最低,改了没用。

这不是推断。实跑一次 pnpm build:prod(✓ built in 49.56s,exit 0)后去产物里搜:

bash
$ grep -ohE 'captchaEnable:[^,}]{1,20}' dist-prod/assets/LoginForm-*.js
captchaEnable:`false`

三个登录相关 chunk(LoginForm-*.js、ForgetPasswordForm-*.js、RegisterForm-*.js)里都是字面量 `false`;SocialLogin-*.js 里是 captchaEnable:!1(因为它写的是 !== 'false',构建期被折成了布尔常量)。于是 v-if="loginData.captchaEnable === 'true'" 恒假,滑块不渲染;getCode() 走 === 'false' 分支,直接 handleLogin({})。

同一台后端也是关着的(application-prod.yaml:242-243 的 enlev.captcha.enable: false),所以前后端目前是一致的「都关」。要开就两边一起开,只开一边会出现「前端弹了滑块、后端不认」或反过来。

五个 env 文件的实际分工:

文件VITE_BASE_URL用途
.env无所有模式共享:标题、端口、租户开关、API 加解密密钥、默认登录账号
.env.local'http://localhost:48080'本地开发(pnpm dev),也被所有 build:* 掺一脚
.env.dev'http://api-dashboard.enlev.cn'上游演示后端,别用它验本地改动
.env.test'http://localhost:48080'测试环境
.env.stage'http://api-dashboard.enlev.cn'预发
.env.prod''(空)生产:走同源相对路径,必须有反向代理

.env 里 VITE_PORT=80,所以 pnpm dev 起在 80 端口(Windows 上需要管理员权限,被占用时 Vite 会自动 +1,看终端输出的实际端口)。

build 块逐条 ​

vite.config.ts:123-153:

ts
build: {
  chunkSizeWarningLimit: 2000,
  minify: 'oxc',
  outDir: env.VITE_OUT_DIR || 'dist',
  reportCompressedSize: false,
  // 用独立的 .map 文件,不要内联。
  // 内联时 vite 的 build-import-analysis 会走
  // chunk.code.replace(convertSourceMap.mapFileCommentRegex, ''),
  // 而这个正则在本项目这种体量的 chunk 上会回溯到爆栈:
  // RangeError: Maximum call stack size exceeded,构建直接失败。
  sourcemap: env.VITE_SOURCEMAP === 'true',
  rollupOptions: {
    output: {
      minify: {
        compress: {
          dropDebugger: env.VITE_DROP_DEBUGGER === 'true',
          dropConsole: env.VITE_DROP_CONSOLE === 'true'
        }
      },
      codeSplitting: {
        groups: [
          { name: 'echarts', test: /node_modules[\\/]echarts[\\/]/ },
          // form-create 运行时已 vendored 到 src/lib,不再在 node_modules 下,
          // 这条 test 必须跟着改,否则它会散进主 chunk。
          { name: 'form-create', test: /src[\\/]lib[\\/]form-create[\\/]/ },
          { name: 'form-designer', test: /src[\\/]lib[\\/]form-designer[\\/]/ },
          { name: 'monaco-editor', test: /node_modules[\\/]monaco-editor[\\/]/ }
        ]
      }
    }
  }
}

四个点值得单独记住:

  • minify: 'oxc' 不是 'terser' 也不是 'esbuild',压缩器换过;dropConsole / dropDebugger 走的是 rollupOptions.output.minify.compress,不是老的 terserOptions。
  • 想开 sourcemap 只能出独立 .map。注释里那段 RangeError: Maximum call stack size exceeded 是真实踩过的:内联 sourcemap 会让构建直接失败,不是「产物变大」而已。
  • codeSplitting.groups 里 form-create / form-designer 的 test 匹配的是 src/lib/...,因为这两个库是 vendored 源码副本而非 npm 依赖。谁把它们改回 npm 依赖,这条 test 就失效,产物会突然多出一个巨大的主 chunk。
  • chunkSizeWarningLimit: 2000 是放宽到 2 MB 才警告。看到 chunk 体积警告说明已经超了 2 MB,不是「构建正常」。

base: env.VITE_BASE_PATH(:31)取自 env,.env.prod 里是 /。要放到子路径下(比如 https://x.com/admin/),改 VITE_BASE_PATH 而不是改 vite.config.ts。

public/ 下的东西会被原样拷进 outDir,不经过打包。本仓 public/ 有 THIRD-PARTY-NOTICES.md、favicon.ico、imgs/,所以 dist-prod/ 里也能看到这三样——dist-prod/THIRD-PARTY-NOTICES.md 不是构建生成的,是拷贝来的。

产物与环境矩阵 ​

三个后端 profile 的关键差异 ​

配置项devlocalprod
server.port480804808048080
Druid initial-size511
Druid min-idle1011
Druid max-active202020
Druid max-wait600006000060000
Druid remove-abandoned未设置未设置未设置(三个 profile 都没有,即默认关闭)
enlev.captcha.enable—falsefalse(注释还写着「本地环境,暂时关闭」)
enlev.security.mock-enablefalse(:247)false(:246)false(:248)
Druid 控制台 /druid/*开、allow 默认 127.0.0.1开、allow 默认 127.0.0.1默认关(DRUID_STAT_VIEW_ENABLED:false)
数据库口令演示环境本地口令明文写在 yaml 里,与 local 不同

prod 的 initial-size / min-idle 都是 1(和 local 一样,只有 dev 是 5 / 10)。生产上并发一进来就要现建连接,配合 max-active: 20 和 remove-abandoned 未开,连接池是这个部署配置里最脆弱的一环——详见坑位索引。

免登录 mock token 与 Druid 控制台:都已收口,但都靠配置、都能被改回去

这两处曾经都是上线即暴露的形状,现已修(mock token 476b4b2637,Druid 3315ae157d),留在这一节是因为它们都只由 yaml 决定,新增 profile 或照着邻居抄配置时会一起被抄回来。

mock token:application-prod.yaml 曾写着 enlev.security.mock-enable: true,而 mock-secret 全项目从未覆盖,用的是 SecurityProperties.java:40 的默认值 "test"。于是任何人在生产环境发一个

bash
curl http://<prod>/admin-api/system/user/profile/get \
  -H "Authorization: Bearer test1" -H "tenant-id: 1"

就能拿到用户编号 1(admin / 超级管理员)的身份,并且因为超管走权限短路,所有 @PreAuthorize 全部放行。TokenAuthenticationFilter.java:98 的方法注释自己写着「注意,在线上环境下,一定要关闭该功能!!!」。代码路径一行没改,mockLoginUser 还在,开关一打开就立刻复活。

Druid 控制台:三个 profile 曾全是 stat-view-servlet.enabled: true + allow 空 + 无口令,实测无鉴权返回 JDBC URL(含库名与连接参数)、UserName: root、194 KB 已执行 SQL 原文与参数、全部接口路径。现在 prod 默认 enabled: ${DRUID_STAT_VIEW_ENABLED:false}(@ConditionalOnProperty 无 matchIfMissing,所以是根本不注册 servlet),local/dev 的 allow 默认收成 127.0.0.1。

🔴 两处都必须重启后端才生效;改完 yaml 不重启,正在跑的进程仍是旧的。上线前的两条验收命令与「跨机器看 MySQL 监控页会 403」这个连带后果见坑位索引 A1 / A2。

application-prod.yaml 里数据库口令是明文,而且和本地那套不一样。别把它抄进文档、截图、commit message 或者聊天工具。

初始化 SQL ​

ainb/sql/mysql/,实测 2026-10-25 只剩 4 个文件(ai-2026-04-16.sql / bpm-2026-04-18.sql / im-2026-06-20.sql 已被 12347efcd1 refactor(系统):清除未用上的模块 删掉,它们的内容并进了专业版快照),外加一个 update/ 目录:

文件大小建表数内容
ainb-professional-init.sql40,937,926 B(≈39 MB)211表结构 + 演示数据,唯一能把平台跑起来的快照(Source Schema: ainb-professional-init)
ainb-init.sql13,579,207 B(≈13 MB)95开源版快照(Source Schema: ainb-public-init)。缺 64 张代码声明的表,见下面的警告
ruoyi-vue-pro.sql1,951,942 B48上游基线,对照用,不要导
quartz.sql41,969 B11Quartz 表。专业版快照里已含 qrtz_ 11 张,不需要再导
update/*.sql6 个—nav-node 特性的增量迁移,必须按依赖顺序人工跑,没有 Flyway

两份快照各缺一半,都不能单独用

ainb-init.sql(95 张)有 system_nav_node 建表与 9 行「App菜单」菜单,但没有 infra_form_design.app_nav_node_id,且缺 64 张 @TableName 声明过的表——包括 infra_codegen_table / infra_codegen_column,连代码生成器自己都不能用,AI / Agent / 工作流 / 低代码全部起不来。 ainb-professional-init.sql(211 张)平台表齐全,但 nav-node 相关的东西一样都没有:没有建表、没有那 9 行菜单,只有一处手工加的 app_nav_node_id 列(:14270),而它的注释还引用着那张不存在的表。

导入顺序:先建库(ainb-professional-init,注意库名带连字符,连接串里原样写着),导专业版快照,再按依赖顺序跑 update/ 那 6 个脚本,最后重启后端(GrantDecisionService#resourceChainCache 是进程级缓存且无 TTL,菜单与 sys_resource 的迁移不重启不生效)。39 MB 导入慢是正常的,别中途 kill。

bash
U=ainb/sql/mysql/update
# 🔴 不能写 for f in $U/*.sql —— 通配按字母序展开,会让
#    menu-manage-split-web-app.sql 跑在它自己声明的前置之前、
#    nav-node-prune-undelivered.sql 跑在它要裁的 seed 之前。
for f in $U/2026-10-24-system-nav-node.sql \
         $U/2026-10-24-system-nav-node-seed.sql \
         $U/2026-10-24-nav-node-prune-undelivered.sql \
         $U/2026-10-24-system-nav-node-menu.sql \
         $U/2026-10-24-menu-manage-split-web-app.sql \
         $U/2026-10-24-form-design-app-nav.sql; do
  mysql --default-character-set=utf8mb4 -h127.0.0.1 -uroot -p123456 ainb-professional-init < "$f"
done

这 6 个脚本都是幂等的(按语义键先删后插,或 CREATE TABLE IF NOT EXISTS),可以重复执行。

前端产物 ​

pnpm build:prod 实测 ✓ built in 49.56s(exit 0,依赖已装好的情况下)。dist-prod/:1013 个 assets 文件、共 32 MB。四个 codeSplitting.groups 分组都生效了,产物里能一一对上:

echarts-WIu35uif.js        echarts-data-V625yfCH.js
form-create-BxS_2Skg.js    form-create-CYwqDfg_.css
form-designer-Dse3uKMT.js  form-designer-TXU_jg55.css
monaco-editor-C_38bjoX.js  ts.worker-XonqDHUu.js

目录顶层另有 index.html(154,257 B)、favicon.ico、imgs/、THIRD-PARTY-NOTICES.md。dist* 在 .gitignore:5,产物不入库。

构建器是 Rolldown,超体积警告现在就是红的

本仓 vite 锁在 8.1.4(package.json:152),底层已经是 Rolldown——构建日志里出现 builtin:vite-reporter、[PLUGIN_TIMINGS] 和 rolldown.rs 的链接。vite.config.ts 里写的 build.rollupOptions 仍然被接受(上面四组 chunk 就是证据),但 Vite 自己的提示语已经改口叫 build.rolldownOptions,将来迁移时按新名字来。

当前 prod 构建确实会打超体积警告,三个 chunk 越过了 chunkSizeWarningLimit: 2000:

chunk体积
ts.worker-*.js6,915.63 kB
monaco-editor-*.js4,362.21 kB
index-*.js2,084.61 kB

前两个是 Monaco 的 TypeScript worker 与编辑器本体,属于按需加载的独立 chunk,不影响首屏;index-*.js 刚过线 84 kB,是真正值得盯的那个。看到这条警告不要当成「构建正常」忽略掉,也不要顺手把 chunkSizeWarningLimit 往上调——那只是把红线挪走。

日志里另有一条 Use of direct eval 的告警来自 lottie-web@5.13.0(build/player/lottie.js:14422),是第三方库自己的写法,改不了也不影响产物,认得它就行,别当成项目代码的问题去查。

怎么部署 ​

第 1 步:打后端 jar ​

bash
cd ainb
mvn clean package -DskipTests

打包前先确认没有进程占着 jar

Windows 上如果有 java -jar enlev-server/target/enlev-server.jar 正在跑,mvn clean 删不掉被占用的文件,报的是 Failed to delete。先找到并结束它:

bash
netstat -ano | grep ':48080' | grep LISTENING   # 拿到 PID
taskkill //f //pid <PID>

跑测试请单独来:mvn test(全量)或 mvn test -pl enlev-module-system -Dtest=PostServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false(单个,详见测试规范)。不要用 -DskipTests 打包后就认为测试是绿的——它们是两件事。

enlev-module-im 已随 12347efcd1 连目录一起删除(不是「注释出 reactor」,ls -d enlev-module-* 只剩 5 个),所以 jar 里没有它,-pl enlev-module-im 也必然失败。详见后端架构与模块依赖。

第 2 步:初始化数据库 ​

库名三个 profile 不一致

application-local.yaml:53 与 application-dev.yaml:53,74 都指向 ainb-professional-init,而 application-prod.yaml:54,75 仍是 ruoyi-vue-pro(local 由 5b955cb8a7 改、dev 由 c3dccc1a82 改,prod 从未跟随)。下面按 prod 的实际配置写;如果你的目标库叫别的名字,改 yaml 或建库时对齐,别让两边各说各话。

sql
CREATE DATABASE `ruoyi-vue-pro` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
bash
mysql -uroot -p ruoyi-vue-pro < ainb/sql/mysql/ainb-professional-init.sql
# 再按依赖顺序补 nav-node 那 6 个增量脚本,顺序与原因见上文「初始化 SQL」

quartz.sql 不用再导——专业版快照里已含 qrtz_ 11 张表。导完重启后端(resourceChainCache 无 TTL)。

Redis 起在 127.0.0.1:6379(或按 profile 改)。

第 3 步:起后端并验证 ​

bash
java -jar enlev-server/target/enlev-server.jar --spring.profiles.active=prod

三个验证点,实测都可用:

bash
curl -s http://127.0.0.1:48080/actuator/health
# {"status":"UP"}

curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:48080/doc.html
# 200   ← knife4j 接口文档

curl -s http://127.0.0.1:48080/admin-api/system/user/profile/get
# {"code":401,"msg":"账号未登录","data":null}

未登录返回的是 HTTP 200 + 业务码 401

最后一条 curl 的 HTTP 状态码是 200,code: 401 在响应体里。这是全局响应包装的既有行为,所以运维层用 curl -I 或负载均衡的健康探测判断不出鉴权是否正常,必须解析 body。script/shell/deploy.sh 的健康检查用的正是 curl -I -w %{http_code}——它能证明进程活着,证明不了服务可用。

/actuator/health 无需鉴权即可访问(application.yaml 里没有 management: 段,走 Boot 3 默认暴露),这正好被 deploy.sh 当健康检查用。

第 4 步:打前端 ​

bash
cd ainb-ui-admin-vue3
pnpm install          # 只用 pnpm,npm i / yarn 会破坏 lockfile
pnpm build:prod

打之前先过一遍本地门禁,别把红灯带进产物:

bash
pnpm lint          # eslint + stylelint + prettier 三合一的 check 版
pnpm ts:check      # 增量,缓存在 node_modules/.cache/vue-tsc/
pnpm test:unit     # vitest run --config vitest.config.ts

pnpm ts:check 前必须先跑过一次 pnpm dev

src/types/auto-*.d.ts 被 gitignore,只由 dev server 生成(build/vite/index.ts 里 dts: !isBuild)。刚克隆下来直接 pnpm ts:check 会报一堆「找不到组件」,那是缺声明文件,不是代码坏了。跑一次 pnpm dev 让它生成,再 ts:check。

ts:check 走增量缓存,改了配置或依赖后结果可疑时用 pnpm ts:check:clean(先 rimraf 掉 tsconfig.tsbuildinfo 再跑)。

第 5 步:配反向代理 ​

.env.prod 里 VITE_BASE_URL='' + VITE_API_URL=/admin-api,所以生产包发出的请求是同源相对路径 /admin-api/...。仓库里没有任何 nginx 配置(script/docker/Docker-HOWTO.md 声称有 ainb-ui-admin/nginx.conf,那个目录整个不存在),下面是最小可用骨架,需要自己写:

nginx
server {
    listen 80;
    root /var/www/dist-prod;
    index index.html;

    location /admin-api/ {
        proxy_pass http://127.0.0.1:48080/admin-api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # AI 流式接口(SSE)必须关掉缓冲,否则前端一直转圈等不到 token
        proxy_buffering off;
        proxy_read_timeout 600s;
    }

    location / {
        try_files $uri $uri/ /index.html;   # history 路由必须回落 index.html
    }
}

proxy_buffering off 那两行不是可选优化:AI 对话和工作流执行走 SSE,Nginx 默认缓冲会让流式输出攒到结束才吐出来,用户看到的是「转圈很久然后一次性刷出一大段」。

前端 .env 里 VITE_APP_API_ENCRYPT_ENABLE = true,请求体会用 AES 加密并带 X-Api-Encrypt 头。反代不要改写或剥掉这个头,也不要试图在网关层解析 body。

第 6 步:验收清单 ​

检查命令 / 操作期望
进程活着curl -s http://127.0.0.1:48080/actuator/health{"status":"UP"}
鉴权生效不带 token 请求任一 /admin-api/**body 里 code: 401
mock token 已关curl -H "Authorization: Bearer test1" -H "tenant-id: 1" .../system/user/profile/get必须是 401;返回 admin 资料说明 mock-enable 还开着
Druid 已收口curl -s -o /dev/null -w '%{http_code}' http://<host>:48080/druid/sql.json404(enabled 默认 false,servlet 没注册);200 = 有人给了 DRUID_STAT_VIEW_ENABLED=true 却没同时给 allow 与口令
前端能登打开站点,用 .env 里的默认账号 admin / admin123进首页;生产环境请立即改密
路由不白屏刷新任意二级菜单页不白屏、Console 无报错(白屏且零报错 = component 路径没匹配上,见新增菜单功能第 8 步)
静态资源Network 面板无 404;/admin-api 与页面同源
手册引用可点文档仓 pnpm check:live(见下面第 7 步)全部落到正确的页;不要用状态码判定

第 7 步:手册站与 AI 知识库的引用链接 ​

AI 助手回答「怎么配」时会附上手册页链接。那条链接能不能点开被引用的那一页,由三个各自独立的环节决定,缺一个就不行:

环节事实来源坏了长什么样
① 后端 enlev.agent.doc.root-pathapplication-{local,dev,prod}.yaml,走 ${AGENT_DOC_ROOT_PATH:...}没配 → 第一次调用同步接口就抛 AGENT_DOC_ROOT_PATH_MISSING(这是对的);配成 / 则静默扫出 0 篇、接口照样返回 200
② 手册站托管端的 cleanUrls 回落文档仓 README.md §4.3 的 try_files $uri $uri.html $uri/ =404;写成 SPA 式 /index.html 之后,每个 clean url 都「成功地」返回首页,且状态码全是 200
③ 线上产物与语料同版pnpm docs:build + 上传 dist/产物比语料旧 → 改名/新增的页如实 404

base-url(三个 profile 都是 https://doc.enlev.cn)决定链接前缀,它与前端 ainb-ui-admin-vue3/src/utils/url.ts 的 MANUAL_DOC_HOSTS 白名单是同一个事实的两份拷贝:只有过了白名单的地址才渲染成 <a>,其余降级成纯文本(死链比没链更坏,故宁可少一个可点的东西)。那道白名单刻意不从后端读配置——否则「后端或库被污染时前端不跟着放开」这道防线就没了。改一边必须改另一边。

🔴 实测现状(2026-10-25):② 与 ③ 都是坏的。 托管端对任意路径返回同一份首页——/guide/01-system/user 与一个根本不存在的路径拿到的字节完全一样,而 /guide/01-system/user.html 能拿到真页面(71,888 字符 vs 首页 32,291 字符)。知识库要引用的 125 条 url 里 122 条落在首页,只有 /guide/、/develop/ 这类目录索引页因命中 $uri/ 而幸免。同时线上产物仍有 05-practice、没有 06-practice 与 05-mobile(.html 形式直接 404)。

验证不能用状态码。 curl -I 一圈全 200,check:render --base= 也只断言 status === 200(tools/check-render.mjs:79),两者都发现不了「拿回来的是首页」。判据必须是内容:文档仓的 pnpm check:live 按 kb-url-map.json 的 corpus 逐条比对「渲染出的 <h1>」与「本地 md 的首个一级标题」,对不上即 exit 1。这条 H1 规则与后端两份 extractTitle(AiRagApiImpl / AgentDocMarkdownSanitizer)同源,所以它顺带也验了「库里存的标题」与「线上显示的标题」是同一个东西。

①②③ 都到位之后才轮到灌库:先 doc-sync-plan 核对 addedCount / removedCount(口径与「为什么 plan 不能省」见架构总览 · 坑与注意第 1 条),再 doc-sync,最后 doc-sync-verify 用固定问句抽测实召回。

仓库自带的三套部署脚本:现在照抄跑不通 ​

ainb/script/ 下的东西大部分是从上游 ruoyi-vue-pro 带过来的,改名时只替换了字符串,没有跟着仓库的真实结构调整。要用的话每一套都得先修。

script/docker/ ​

docker compose --env-file docker.env up -d 直接失败。docker-compose.yml 引用的四个构建上下文/挂载路径,在仓库里一个都不存在:

compose 里的路径实际情况
server.build.context: ./enlev-server/script/docker/enlev-server/ 不存在(真的 Dockerfile 在仓库根下的 enlev-server/Dockerfile)
admin.build.context: ./ainb-ui-admin整个目录不存在,前端仓库是平级的另一个 git 仓库 ainb-ui-admin-vue3/
./sql/mysql/ruoyi-vue-pro.sql 挂载script/docker/sql/ 不存在
Docker-HOWTO.md 列的 ainb-ui-admin/{.dockerignore,Dockerfile,nginx.conf}三个都不存在;全仓搜不到任何 nginx.conf

admin 服务传的还是 Vue2 时代的构建参数:VUE_APP_TITLE、VUE_APP_BASE_API=/prod-api、VUE_APP_TENANT_ENABLE、VUE_APP_CAPTCHA_ENABLE、VUE_APP_DOC_ENABLE、VUE_APP_BAIDU_CODE(docker-compose.yml:66-77 与 docker.env:19-27 两处都写着)。真实前端是 Vite,读的是 VITE_*;接口前缀也是 /admin-api 而不是 /prod-api。就算把目录补齐,这套参数一个都传不进去。

docker-compose.yml:1 的 version: "3.4" 在 compose v2 下会打 deprecation 警告(HOWTO 里也提到了这点,但给的建议是「删掉 name:、降版本」——方向反了,v2 认 name:,不认 version:)。

script/jenkins/Jenkinsfile ​

三处致命:

groovy
stage('检出') {
    steps {
        git url: "https://gitee.com/will-we/ruoyi-vue-pro.git",
                branch: "devops"
    }
}
  1. 检出的是上游 ruoyi-vue-pro,不是本仓(同文件 :24 的 GITHUB_ACCOUNT 已经改成了 https://gitee.com/enlev/ainb,但 git url 没跟着改)。跑通了也是部署上游的代码。
  2. stage('部署') 里 cp -f bin/deploy.sh ...——仓库里没有 bin/ 目录,脚本在 script/shell/deploy.sh。
  3. 它假定的是上游的多模块布局(${APP_NAME}/src/main/resources、${APP_NAME}/target/*.jar,APP_NAME=enlev-server),本仓 enlev-server 确实在,所以这两条路径恰好对得上;但 mvn clean package 是在检出根目录跑的,而 archiveArtifacts 只收 enlev-server/target/*.jar——这一步是对的。真正卡住的还是前两条。

script/shell/deploy.sh ​

脚本本身写得挺完整(backup → stop(kill -15,最多等 120 秒,超时 kill -9)→ transfer → start → healthCheck),但有两处会让它跑不出正确结果:

  • PROFILES_ACTIVE=development(:12)。仓库里只有 application-{dev,local,prod}.yaml,没有 development。Spring Boot 找不到对应 profile 文件时不会报错,只会用 application.yaml 的默认值——也就是 local 那套数据库连接。表现为「部署成功了,但连的是错误的库」,或者干脆启动失败在数据源初始化。
  • BASE_PATH=/work/projects/enlev-server(:6)与 Jenkinsfile 里的 APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/'(:26)不是同一个路径。Jenkins 把 jar 拷到后者,脚本去前者的 build/ 目录找,两边对不上。

另外它把日志 tail 的是 nohup.out(:139、:143),而 start() 里的 nohup java ... & 没有重定向(:118),输出确实会落到当前目录的 nohup.out——这一点是自洽的,但要求 deploy() 先 cd $BASE_PATH(:150 确实这么做了)。

enlev-server/Dockerfile 单独看是能用的,只有一处不一致:基础镜像是 eclipse-temurin:21-jre,而项目 java.version 是 17(根 pom.xml:31)。用 21 跑 17 编译的字节码通常没问题,但 GraalVM 的 js-language / truffle-api 对 JDK 版本敏感,换 JDK 大版本前先在测试环境验一遍 AI 模块的工具执行。镜像里 ENV JAVA_OPTS="-Xms512m -Xmx512m"——325 MB 的胖 jar 加上 Spring AI 的模型/向量缓存,512 MB 堆很容易 OOM,按实际负载往上调。

坑与注意 ​

  • jar 默认跑 local profile(application.yaml:5-6 硬编码)。部署忘传 --spring.profiles.active=prod,会安静地去连 127.0.0.1:3306,报错信息是数据源连不上,看不出是 profile 没切。
  • mock-enable 与 Druid 控制台都已收口,但都只由 yaml 决定:mock-enable 三个 profile 全是 false(代码路径没删,开关一开就复活);Druid 在 prod 默认 enabled: false、local/dev 的 allow 默认 127.0.0.1。🔴 两者改完都要重启才生效,详见上面的 warning 块与坑位索引 A1 / A2。
  • prod 关掉 Druid 控制台会让 Web 端「MySQL 监控」页白屏——那个菜单是 iframe 内嵌 /druid/index.html。要在生产用它,DRUID_STAT_VIEW_ENABLED=true + DRUID_STAT_VIEW_ALLOW=<运维网段> + DRUID_STAT_VIEW_PASSWORD 三个一起给,只给第一个等于把无口令公开的坑原地复活。
  • pnpm build:prod 会掺进 .env.local,把 VITE_APP_CAPTCHA_ENABLE 压成 false。.env.prod 没定义的变量都会中招,不只这一个。
  • .env.prod 里 VITE_BASE_URL='',生产包走同源相对路径,没有 /admin-api 反代就全站 404。
  • public/ 是原样拷贝,改 public/THIRD-PARTY-NOTICES.md 不用重新构建配置,但必须重新构建才会进 dist-prod/。
  • enlev-server.jar.original 只有 36 KB,别把它当成产物拷走——那是 repackage 之前的瘦 jar,跑不起来。
  • 改代码生成模板要重新打包。106 个 .vm 嵌在 BOOT-INF/lib/enlev-module-infra-*.jar 里,从 classpath 读。
  • mvn clean 会被正在运行的 jar 卡住(Windows 文件锁)。先 taskkill。
  • ts:check 依赖 pnpm dev 生成的 src/types/auto-*.d.ts,那些文件被 gitignore;克隆后直接 check 会误报找不到组件。
  • script/docker/、script/jenkins/、script/shell/ 三套脚本都是上游遗留,路径和参数与本仓真实结构不匹配,照抄必失败。要用先按上面的清单修。
  • 构建器已经是 Rolldown(vite 锁 8.1.4)。日志里的选项名是 build.rolldownOptions,配置里写的 build.rollupOptions 目前仍生效;prod 构建当前会打 3 条 chunk 超体积警告(ts.worker / monaco-editor / index),这是既有状态,别靠调大 chunkSizeWarningLimit 把它消掉。
  • deploy.sh 的 PROFILES_ACTIVE=development 不存在对应 profile 文件,Spring Boot 不报错、静默回落到默认值,排查方向会被带偏。
  • Dockerfile 用 JRE 21,项目是 Java 17;-Xmx512m 对 325 MB 的胖 jar 偏小。
  • im-2026-06-20.sql 对应的模块不在 reactor 里,导入了也没有服务在跑。

→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引,移动端构建那半见移动端坑位。

相关页面 ​