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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
<!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>