Files
ERP/diagrams/techdoc.html
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

219 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html><html><head><meta charset='utf-8'><style>
@page { size:A4; margin:14mm 15mm; }
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#1d2433;font-size:12px;line-height:1.65;}
.page{page-break-after:always;}
.page:last-child{page-break-after:auto;}
.cover{height:250mm;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;}
.cover .badge{display:inline-block;background:#1d2433;color:#fff;font-size:12px;padding:5px 16px;border-radius:20px;margin-bottom:22px;}
.cover h1{font-size:40px;color:#1947aa;margin-bottom:12px;}
.cover .csub{font-size:16px;color:#667085;}
.cover .meta{margin-top:34px;font-size:13px;color:#8a94a6;}
.chap{display:inline-block;background:#eef3ff;color:#2764e7;font-weight:700;font-size:12px;padding:3px 11px;border-radius:5px;margin-bottom:7px;}
h2{font-size:19px;color:#1d2433;margin-bottom:10px;border-bottom:2px solid #2764e7;padding-bottom:6px;}
h3{font-size:14px;color:#1947aa;margin:14px 0 6px;}
p{margin-bottom:8px;}
ul{margin:0 0 8px 18px;} li{margin-bottom:3px;}
code{font-family:"SF Mono",Menlo,Consolas,monospace;background:#f3f5f7;color:#c7254e;padding:1px 5px;border-radius:4px;font-size:11px;}
pre{background:#1d2433;color:#e8ebf1;font-family:"SF Mono",Menlo,Consolas,monospace;font-size:10.5px;line-height:1.55;padding:12px 14px;border-radius:8px;margin:8px 0;white-space:pre-wrap;overflow-wrap:anywhere;}
pre .c{color:#8a94a6;} pre .k{color:#7ec3ff;}
table{width:100%;border-collapse:collapse;margin:8px 0;font-size:11px;}
th,td{border:1px solid #e8ebf1;padding:6px 9px;text-align:left;vertical-align:top;}
th{background:#f7f8fb;color:#1947aa;font-weight:700;}
.warn{background:#fdecec;border-left:3px solid #c73535;padding:8px 12px;border-radius:0 6px 6px 0;margin:8px 0;font-size:11.5px;color:#9a2a2a;}
.tip{background:#e9f6ef;border-left:3px solid #16845b;padding:8px 12px;border-radius:0 6px 6px 0;margin:8px 0;font-size:11.5px;color:#16845b;}
.flow{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:10px 0;}
.fb{background:#fff;border:1.5px solid #2764e7;border-radius:8px;padding:8px 12px;font-size:11px;font-weight:600;color:#1947aa;}
.fb.db{border-color:#16845b;color:#16845b;}
.fb.ext{border-color:#b86b00;color:#9a5b00;}
.ar{color:#2764e7;font-weight:700;font-size:16px;}
.toc div{padding:6px 0;border-bottom:1px solid #eef1f5;font-size:13px;}
.toc b{color:#2764e7;margin-right:10px;}
.ent{display:inline-block;border:1.5px solid #2764e7;border-radius:8px;padding:8px 12px;margin:5px;font-size:11px;vertical-align:top;background:#fff;}
.ent b{color:#1947aa;display:block;margin-bottom:3px;}
.ent small{color:#667085;display:block;font-size:10px;}
</style></head><body><div class="page"><div class="cover">
<span class="badge">内部技术文档 · 开发 / 运维 / 维护</span>
<h1>凯迪 ERP 系统技术文档</h1>
<div class="csub">架构 · 代码结构 · 数据模型 · 自动联动引擎 · 构建部署 · 运维手册 · 踩坑清单</div>
<div class="meta">Spring Boot 3.2.5 + Vue 3 + SQLite · 单 jar 部署 · 端口 8090<br>凯迪科技 · 2026</div></div></div><div class="page"><h2>目录</h2><div class="toc">
<div><b></b>技术架构总览</div>
<div><b></b>代码结构(前端 + 后端)</div>
<div><b></b>后端核心约定(ApiResp / 认证 / CORS / 异常)</div>
<div><b></b>数据模型(核心实体与互链)</div>
<div><b></b>自动联动引擎(TriggerRuleEngine</div>
<div><b></b>通用台账机制(BizRecord</div>
<div><b></b>前端机制(导航契约 / 路由 / 组件 / 设计规范)</div>
<div><b></b>构建与部署(精确命令)</div>
<div><b></b>运维手册(重启 / 日志 / 数据库 / 隧道)</div>
<div><b></b>坑与硬约束(必读)</div>
</div></div><div class="page"><span class="chap">第一章</span><h2>技术架构总览</h2>
<p>一套 <b>ERP + OA 一体化</b>平台。前后端打包成<b>单个 jar</b>,同一端口(8090)既提供 SPA 静态资源、又提供 REST API;通过 ngrok 隧道对公网演示。</p>
<div class="flow">
<span class="fb">浏览器 SPA<br><small>Vue3+Element</small></span><span class="ar"></span>
<span class="fb ext">ngrok 隧道<br><small>公网演示</small></span><span class="ar"></span>
<span class="fb">Spring Boot jar :8090<br><small>SPA 静态 + /api/oa/* REST</small></span><span class="ar"></span>
<span class="fb db">SQLite<br><small>data/oa.db</small></span>
</div>
<h3>技术栈</h3>
<table>
<tr><th></th><th>技术</th><th>说明</th></tr>
<tr><td>前端</td><td>Vue 3 + TypeScript + Vite + Element Plus 2.14.1</td><td>vue-router createWebHistorybase '/',干净路径无 #);图标只用 @element-plus/icons-vue</td></tr>
<tr><td>后端</td><td>Spring Boot 3.2.5 + Spring Data JPA</td><td>REST 接口前缀 /api/oa/*;统一返回 ApiResp 信封</td></tr>
<tr><td>数据库</td><td>SQLiteorg.hibernate.community.dialect.SQLiteDialect</td><td>jdbc:sqlite:./data/oa.dbddl-auto=update 自动建表加列</td></tr>
<tr><td>实时</td><td>WebSocket + yjs CRDT</td><td>文档协同实时编辑(doccollab</td></tr>
<tr><td>部署</td><td>单 jargradlew bootJar</td><td>SPA 打进 classpath:/static,与 API 同端口</td></tr>
</table>
<div class="tip">关键点:前端 <code>npm run build</code> 的产物直接输出到 <code>oa-backend/src/main/resources/static</code>emptyOutDir=true),所以打 jar 时 SPA 被一起封进去——一个 jar 跑全栈。</div></div><div class="page"><span class="chap">第二章</span><h2>代码结构</h2>
<h3>后端 oa-backendcom.kaidi.oa</h3>
<table>
<tr><th></th><th>数量</th><th>职责</th></tr>
<tr><td><code>domain/</code></td><td>90</td><td>JPA 实体(@Entity)。Contract/Payment/SealUse/Invoice/Project/Bid/FormInstance/AutomationLog/BizRecord …</td></tr>
<tr><td><code>repository/</code></td><td>90</td><td>Spring Data JpaRepositoryfindByXxx 派生查询)</td></tr>
<tr><td><code>web/</code></td><td>98</td><td>@RestControllerREST CRUD + 业务动作</td></tr>
<tr><td><code>service/</code></td><td>8</td><td>WorkflowService(审批引擎)、TriggerRuleEngine(自动联动)、PaymentService、AuthService、NodeAssigneeResolver、NotificationService、FullTextSearchService、MentionService</td></tr>
<tr><td><code>common/</code></td><td>5</td><td>ApiResp、ApiException、GlobalExceptionHandler、NotFoundException、PasswordUtilPBKDF2</td></tr>
<tr><td><code>config/</code></td><td>5</td><td>AuthInterceptor、CorsConfig、JacksonConfig、SpaWebConfigSPA fallback)、WebSocketConfig</td></tr>
</table>
<h3>前端 ofbiz-framework/plugins/modern-ui/app/src</h3>
<table>
<tr><td><code>data/oaModules.ts</code></td><td>导航契约(顶部模块 + 子页面),路由自动生成的唯一来源</td></tr>
<tr><td><code>router/index.ts</code></td><td>路由表:glob 自动发现 oa/pages/**,无文件回退 OaGenericPage;自定义路由手动加</td></tr>
<tr><td><code>oa/pages/&lt;模块&gt;/&lt;&gt;.vue</code></td><td>各业务页面(170 个)</td></tr>
<tr><td><code>oa/api/</code></td><td>HTTP 封装(http.ts+ 各模块 API + settingList.ts(通用台账)</td></tr>
<tr><td><code>oa/engine/</code></td><td>表单引擎(OaFormWindow/OaFormRenderer+ 模板</td></tr>
<tr><td><code>components/erp/</code></td><td>MasterDataPage、ErpDataTable、ErpPageHeader、ErpStatusTag、ErpDrawer …</td></tr>
<tr><td><code>styles/tokens.css</code></td><td>设计令牌 --erp-*(颜色/字号/间距/圆角)</td></tr>
</table></div><div class="page"><span class="chap">第三章</span><h2>后端核心约定</h2>
<h3>统一返回信封 ApiResp</h3>
<pre>public record ApiResp&lt;T&gt;(int code, String message, T data) {
static ok(data) -&gt; {code:0, message:"ok", data}
static error(code,msg)
}</pre>
<p><code>code==0</code> 成功,非 0 是业务错误。前端 http.ts 自动拆信封,<code>code!=0</code> 抛 OaApiError。</p>
<div class="warn"><b>易错点</b>:业务错误返回的是 <b>HTTP 200 + body 里 code≠0</b>(如未登录 code:401)。只有受保护接口未带 token 时才返 HTTP 401。排查时别只看 HTTP 状态,要看 body 的 code。</div>
<h3>认证</h3>
<ul>
<li>登录 <code>POST /api/oa/auth/login</code>,请求体字段是 <code>loginName</code>/<code>password</code>(不是 username</li>
<li>返回 token,前端存 localStorage <code>oa.token</code>,每次请求带 <code>Authorization: Bearer &lt;token&gt;</code></li>
<li><code>AuthInterceptor</code><code>/api/oa/**</code>,白名单:login / session / logout</li>
<li>密码 PBKDF2-HMAC-SHA256<code>pbkdf2$120000$&lt;saltB64&gt;$&lt;hashB64&gt;</code></li>
</ul>
<h3>CORS(隧道用)</h3>
<ul>
<li><code>CorsConfig.allowedOriginPatterns</code><code>"*"</code>——隧道 Origin 才不被拦(curl 不带 Origin 永远通,浏览器才触发)</li>
<li>认证用 Bearer token 不用 cookie,所以反射任意 Origin 安全</li>
<li>前端所有请求带 <code>ngrok-skip-browser-warning: true</code> 头,绕过 ngrok-free 的浏览器拦截页</li>
</ul></div><div class="page"><span class="chap">第四章</span><h2>数据模型(核心实体与互链)</h2>
<p>复杂领域走专用实体,靠外键字段互链;通用台账走 BizRecord。核心互链字段:</p>
<div style="margin:8px 0">
<span class="ent"><b>Project 项目</b><small>id / name / phase / status / progress</small></span>
<span class="ent"><b>Contract 合同</b><small>code / status / amount<br>projectId · <b>parentId(总包→分包树)</b><br>paidAmount / invoicedAmount</small></span>
<span class="ent"><b>Bid 投标</b><small>status / bidAmount<br>opportunityId(→商机) · projectId(中标后)</small></span>
<span class="ent"><b>SealUse 用印</b><small>status / docName<br>contractId · projectId</small></span>
<span class="ent"><b>Invoice 发票</b><small>type(销项/进项) / status<br>contractId · projectId</small></span>
<span class="ent"><b>Payment 付款</b><small>status / amount<br>contractId · projectId · invoiceId</small></span>
<span class="ent"><b>BizRecord 通用台账</b><small>bizType · dataJson<br>(49 个台账页共用)</small></span>
</div>
<h3>互链关系</h3>
<table>
<tr><th>关系</th><th>字段</th></tr>
<tr><td>项目 ← 合同/投标/发票/付款/用印</td><td>各实体的 projectId 指向 Project.id</td></tr>
<tr><td>合同 ← 用印/发票/付款</td><td>各实体的 contractId 指向 Contract.id</td></tr>
<tr><td>总包 → 分包(合同树)</td><td>Contract.parentId 自引用</td></tr>
<tr><td>商机 → 投标 → 项目</td><td>Bid.opportunityId / Bid.projectId</td></tr>
</table>
<div class="tip">这些互链字段就是「项目360 一屏聚合」和「自动联动回写」的基础——加新关联只要补一个 xxxId 字段 + repo.findByXxxId。</div></div><div class="page"><span class="chap">第五章</span><h2>自动联动引擎(TriggerRuleEngine</h2>
<p>系统「会自己跑」的核心。某审批事项<b>办结</b>时自动写下游单据/状态。</p>
<h3>触发链路</h3>
<div class="flow">
<span class="fb">事项办结</span><span class="ar"></span>
<span class="fb">WorkflowService.finalize()</span><span class="ar"></span>
<span class="fb">triggerDownstream()</span><span class="ar"></span>
<span class="fb db">TriggerRuleEngine.fire(inst, now)</span>
</div>
<p><code>fire()</code><code>category + templateName</code> 关键词命中<b>一条</b>规则 → 写下游实体 + 一条 <code>AutomationLog</code>(数据中心可查)。</p>
<h3>规则表</h3>
<table>
<tr><th>关键词</th><th>下游动作(含链式接力)</th></tr>
<tr><td>付款/报销/请款</td><td>生成「待付」付款单(钱权分离,不放款)</td></tr>
<tr><td>收款/到账/回款</td><td>生成「待开」销项发票草稿(用项目反查合同拿甲方)</td></tr>
<tr><td>用印/盖章</td><td>用印台账置「已用印」</td></tr>
<tr><td>验收/结项/竣工</td><td>项目置「验收」<b> + 链式:自动生成项目档案</b></td></tr>
<tr><td>立项</td><td>新建项目台账</td></tr>
<tr><td>供应商/准入</td><td>供应商置「合格」</td></tr>
<tr><td>合同</td><td>合同置「已生效」<b> + 链式:用章中心生成待用印单</b></td></tr>
</table>
<p>另有 <code>BidController</code>(中标→建项目+总包合同)、<code>OpportunityController</code>(商机→投标)、<code>InvoiceController</code>(开票→回写合同已开票额)。</p>
<div class="warn"><b>必懂的坑</b>:真实表单按 <code>field.id</code>(英文 contractName)提交,规则按中文 label(合同名称)查找。<code>enrichWithLabels()</code> 读模板 schema 把值按中文 label 补一份键——<b>没有这步,自动联动在真实表单上全部静默失效</b></div>
<h3>加一条新规则</h3>
<ul>
<li>① 在 <code>fire()</code><code>else if (containsAny(tag, "关键词"))</code> 分支</li>
<li>② 写 <code>ruleXxx()</code> 方法(写下游 + safeLog</li>
<li>③ 在 <code>ruleKeyFor()</code> 映射动作→稳定 ruleKey(幂等键,(instanceId,ruleKey) 只触发一次)</li>
<li>④ 注意顺序:具体关键词在前(如收款/验收要排在「立项」之前,避免「项目发起」类目误命中)</li>
</ul></div><div class="page"><span class="chap">第六章</span><h2>通用台账机制(BizRecord</h2>
<p>49 个补齐的部室台账页(运营工艺/HR资质/财务应收/行政后勤…)共用一套<b>行级真后端</b>,而非各写一个实体。</p>
<table>
<tr><td><code>domain/BizRecord</code></td><td>行级实体:id + <code>bizType</code>(台账类型)+ <code>dataJson</code>(灵活字段)+ 时间戳</td></tr>
<tr><td><code>web/BizRecordController</code></td><td>REST<code>GET/POST /api/oa/biz/{type}</code><code>PUT/DELETE /biz/{type}/{id}</code>,对外扁平化为 {id, ...字段}</td></tr>
<tr><td>前端 <code>oa/api/settingList.ts</code></td><td><code>settingListStore(bizType, seed)</code> → 调 /biz/{type},喂给 MasterDataPage;首次空则把 seed 播种成真记录</td></tr>
</table>
<div class="tip">每条台账记录 = biz_record 表一行真实数据,可查询、可统计,统一 REST。复杂领域(合同/付款/审批/用印)<b>仍走各自专用实体 + 链式自动化</b>,不混进 BizRecord。</div></div><div class="page"><span class="chap">第七章</span><h2>前端机制</h2>
<h3>导航契约 oaModules.ts</h3>
<p>顶部导航 + 每个模块的子页面(key/label/kind/path)都在这一个文件。<b>路由从它自动生成</b>,加页面通常只改这里。</p>
<h3>路由自动发现</h3>
<pre><span class="c">// router/index.ts</span>
const pages = import.meta.glob('../oa/pages/**/*.vue')
<span class="c">// 有 oa/pages/&lt;模块&gt;/&lt;key&gt;.vue 就用它,没有则回退 OaGenericPage</span>
<span class="c">// 带参/不进导航的页(/orgview/dept、/collab/handle)手动加进 routes[]</span></pre>
<h3>通用列表页 MasterDataPage</h3>
<p><code>columns / load / create / update / remove / createFields</code> 就得到「列表 + 搜索 + 新建 + 详情抽屉 + 增删改」。绝大多数台账页就是它 + 一个 store。</p>
<h3>设计规范(硬约束)</h3>
<ul>
<li>颜色/字号/间距/圆角一律用 <code>tokens.css</code><code>--erp-*</code> 令牌</li>
<li><b>绝不用渐变色</b>(纯色,蓝色主调);<b>绝不用 emoji</b>(图标只用 @element-plus/icons-vue</li>
<li>状态着色走 ErpStatusTag(语义映射:失败红/完成绿/进行中蓝/待办黄)</li>
</ul></div><div class="page"><span class="chap">第八章</span><h2>构建与部署(精确命令)</h2>
<h3>① 构建前端(产物自动进 static)</h3>
<pre>cd ofbiz-framework/plugins/modern-ui/app
npm run build <span class="c"># = vue-tsc --noEmit && vite build</span>
<span class="c"># 输出到 oa-backend/src/main/resources/staticemptyOutDir</span></pre>
<h3>② 打 jar(把 SPA 封进去)</h3>
<pre>cd oa-backend
JAVA_HOME=/Users/qiu/Desktop/ERP/.jdks/jdk-17.0.19+10/Contents/Home \
./gradlew bootJar
<span class="c"># 产物:oa-backend/build/libs/oa-backend-0.1.0.jar</span></pre>
<h3>③ 运行(单 jar 跑全栈)</h3>
<pre>JAVA_HOME=/Users/qiu/Desktop/ERP/.jdks/jdk-17.0.19+10/Contents/Home \
java -jar build/libs/oa-backend-0.1.0.jar
<span class="c"># http://localhost:8090 —— SPA + API 同端口</span></pre>
<div class="warn">改了前端必须<b>先 npm run build 再 bootJar</b>(jar 在打包时封入 static,光改前端不重打 jar 不生效)。改了后端只需 bootJar。</div></div><div class="page"><span class="chap">第九章</span><h2>运维手册</h2>
<h3>重启(关键:只杀 8090 自己的进程)</h3>
<pre>PID=$(lsof -ti tcp:8090 -sTCP:LISTEN)
ps -p $PID -o command= | grep oa-backend-0.1.0.jar <span class="c"># 确认是本服务</span>
kill $PID
<span class="c"># 再 nohup java -jar ... 启动</span></pre>
<div class="warn"><b>绝不</b><code>pkill java</code> / <code>killall java</code>——会杀掉机器上别的 Java 进程(如 IDE/构建)。永远只杀 8090 LISTEN 的那个 pid。</div>
<table>
<tr><td>日志</td><td><code>/tmp/oa-backend.log</code>nohup 重定向)</td></tr>
<tr><td>数据库</td><td><code>oa-backend/data/oa.db</code>(SQLite 单文件,备份直接拷贝该文件)</td></tr>
<tr><td>建表/加列</td><td>ddl-auto=update 启动时自动同步(加实体字段会自动加列,不丢数据)</td></tr>
<tr><td>JDK</td><td><code>/Users/qiu/Desktop/ERP/.jdks/jdk-17.0.19+10/Contents/Home</code></td></tr>
<tr><td>公网隧道</td><td>ngrok http 8090(演示用);隧道挂了重起 ngrok 即可,不用重启服务</td></tr>
<tr><td>账号</td><td>admin / zhangwei / lina …,初始密码 123456</td></tr>
</table></div><div class="page"><span class="chap">第十章</span><h2>坑与硬约束(必读)</h2>
<table>
<tr><th></th><th>对策</th></tr>
<tr><td><b>vite 白屏</b>manualChunks 单拆 vendor-vueVue 与耦合库(tiptap/yjs/vuedraggable)跨块 TDZ「Cannot access 'X' before initialization」</td><td>Vue + 所有 Vue 耦合库<b>必须同一个 chunk</b><b>构建绿≠能跑</b>,构建后必须浏览器验 #app 已挂载</td></tr>
<tr><td><b>SQLite 多表启动崩溃</b></td><td>已配 <code>hbm2ddl.jdbc_metadata_extraction_strategy=individually</code></td></tr>
<tr><td><b>SQLite 写争用</b>:快速连续 PUT 会 500</td><td>操作间隔 / busy_timeout;批量写别并发轰</td></tr>
<tr><td><b>自动联动静默失效</b>:表单 field.id 英文 vs 规则中文 label</td><td>TriggerRuleEngine.enrichWithLabels() 读模板 schema 补中文 label 键</td></tr>
<tr><td><b>排查方向错</b>:只看 HTTP 状态</td><td>业务错误是 HTTP 200 + body code≠0,要看 body</td></tr>
<tr><td><b>误杀进程</b></td><td>重启只杀 8090 LISTEN pid,绝不 pkill/killall java</td></tr>
<tr><td><b>脚本批量生成页面</b></td><td>cwd 要在 oa/pages(否则相对 import 失效);数组元素逗号分隔;加类型注解 :MdColumn[]/:MdField[]MdField 无 'date' 类型用 text</td></tr>
<tr><td><b>ngrok-free 拦截页</b></td><td>所有请求带 ngrok-skip-browser-warning 头</td></tr>
</table>
<div class="tip">铁律:① 绝不渐变色 ② 绝不 emoji(图标用 @element-plus/icons-vue)③ 重启只杀 8090 pid ④ 改前端必重打 jar ⑤ 构建后必浏览器验挂载。</div></div><div class="page"><div class="cover"><h1 style="font-size:28px">一个 jar 跑全栈,靠流程引擎自己接力</h1>
<div class="csub" style="max-width:170mm;margin-top:14px">前端 Vue 打进后端 jar,单端口 8090;90 实体 / 98 控制器;<br>自动联动引擎 + 通用台账 BizRecord 是两个最该先读懂的设计。</div>
<div class="meta">凯迪 ERP 技术文档 · 凯迪科技 · 2026</div></div></div></body></html>