构建与部署
适用端:后端 + Web | 类型:操作
两条完全独立的构建链:后端 mvn clean package 出一个 325 MB 的胖 jar,前端 pnpm build:prod 出一个 32 MB 的静态目录。本页讲清产物里到底装了什么、三个 profile 差在哪、前端五个 .env 文件的覆盖顺序(这里有一个会让生产包关掉验证码的陷阱),以及仓库里那套 Docker / Jenkins / shell 脚本为什么现在照抄跑不通。
前置阅读:本地环境搭建、新增一个菜单功能端到端。
它解决什么问题
三条最容易翻车的边:
| 位置 | 翻车表现 | 详见 |
|---|---|---|
B2 → B3 | jar 里 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 名与 repackage | ainb/enlev-server/pom.xml:177(finalName)、:180-191(spring-boot-maven-plugin) |
| 三个 profile | ainb/enlev-server/src/main/resources/application-{dev,local,prod}.yaml + 公共 application.yaml |
| 初始化 SQL | ainb/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 定名,和把依赖塞进去。
<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 一处。
实测的产物内容
cd ainb
mvn clean package -DskipTests实测 BUILD SUCCESS,Total time: 01:40 min(本机、离线仓库已就绪的情况下)。产物:
| 文件 | 大小 | 是什么 |
|---|---|---|
enlev-server/target/enlev-server.jar | 340,904,023 B(≈325 MB) | repackage 后的可执行胖 jar |
enlev-server/target/enlev-server.jar.original | 36,346 B | repackage 之前的瘦 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:
spring:
application:
name: enlev-server
profiles:
active: local这意味着 jar 默认跑 local profile。local 连的是 127.0.0.1:3306/ainb-professional-init,口令也是本地那一套。部署到服务器必须显式覆盖,两种方式任选:
java -jar enlev-server.jar --spring.profiles.active=prod
# 或
SPRING_PROFILES_ACTIVE=prod java -jar enlev-server.jarscript/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:
"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 文件:
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}.localpnpm 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)后去产物里搜:
$ 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:
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 的关键差异
| 配置项 | dev | local | prod |
|---|---|---|---|
server.port | 48080 | 48080 | 48080 |
Druid initial-size | 5 | 1 | 1 |
Druid min-idle | 10 | 1 | 1 |
Druid max-active | 20 | 20 | 20 |
Druid max-wait | 60000 | 60000 | 60000 |
Druid remove-abandoned | 未设置 | 未设置 | 未设置(三个 profile 都没有,即默认关闭) |
enlev.captcha.enable | — | false | false(注释还写着「本地环境,暂时关闭」) |
enlev.security.mock-enable | false(: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"。于是任何人在生产环境发一个
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.sql | 40,937,926 B(≈39 MB) | 211 | 表结构 + 演示数据,唯一能把平台跑起来的快照(Source Schema: ainb-professional-init) |
ainb-init.sql | 13,579,207 B(≈13 MB) | 95 | 开源版快照(Source Schema: ainb-public-init)。缺 64 张代码声明的表,见下面的警告 |
ruoyi-vue-pro.sql | 1,951,942 B | 48 | 上游基线,对照用,不要导 |
quartz.sql | 41,969 B | 11 | Quartz 表。专业版快照里已含 qrtz_ 11 张,不需要再导 |
update/*.sql | 6 个 | — | 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。
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-*.js | 6,915.63 kB |
monaco-editor-*.js | 4,362.21 kB |
index-*.js | 2,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
cd ainb
mvn clean package -DskipTests打包前先确认没有进程占着 jar
Windows 上如果有 java -jar enlev-server/target/enlev-server.jar 正在跑,mvn clean 删不掉被占用的文件,报的是 Failed to delete。先找到并结束它:
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 或建库时对齐,别让两边各说各话。
CREATE DATABASE `ruoyi-vue-pro` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;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 步:起后端并验证
java -jar enlev-server/target/enlev-server.jar --spring.profiles.active=prod三个验证点,实测都可用:
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 步:打前端
cd ainb-ui-admin-vue3
pnpm install # 只用 pnpm,npm i / yarn 会破坏 lockfile
pnpm build:prod打之前先过一遍本地门禁,别把红灯带进产物:
pnpm lint # eslint + stylelint + prettier 三合一的 check 版
pnpm ts:check # 增量,缓存在 node_modules/.cache/vue-tsc/
pnpm test:unit # vitest run --config vitest.config.tspnpm 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,那个目录整个不存在),下面是最小可用骨架,需要自己写:
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.json | 404(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-path | application-{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
三处致命:
stage('检出') {
steps {
git url: "https://gitee.com/will-we/ruoyi-vue-pro.git",
branch: "devops"
}
}- 检出的是上游
ruoyi-vue-pro,不是本仓(同文件:24的GITHUB_ACCOUNT已经改成了https://gitee.com/enlev/ainb,但git url没跟着改)。跑通了也是部署上游的代码。 stage('部署')里cp -f bin/deploy.sh ...——仓库里没有bin/目录,脚本在script/shell/deploy.sh。- 它假定的是上游的多模块布局(
${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 默认跑
localprofile(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 里,导入了也没有服务在跑。
→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引,移动端构建那半见移动端坑位。
