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>
@@ -0,0 +1,32 @@
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#ffffff; color:#1d2433; }
|
||||
.wrap { width:1560px; padding:40px 48px; }
|
||||
h1 { font-size:30px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.sub { font-size:15px; color:#667085; margin-bottom:28px; }
|
||||
.phase { display:flex; align-items:stretch; margin-bottom:14px; }
|
||||
.plabel { flex:0 0 120px; display:flex; flex-direction:column; justify-content:center; font-weight:700; font-size:16px; color:#2764e7; padding-right:16px; border-right:3px solid #2764e7; margin-right:18px; }
|
||||
.plabel small { font-weight:400; font-size:12px; color:#8a94a6; margin-top:3px; }
|
||||
.row { display:flex; align-items:center; flex-wrap:wrap; gap:8px; flex:1; }
|
||||
.node { background:#fff; border:1.5px solid #d9dee8; border-radius:10px; padding:11px 16px; font-size:15px; font-weight:600; color:#1d2433; white-space:nowrap; }
|
||||
.node small { display:block; font-weight:400; font-size:11px; color:#8a94a6; margin-top:2px; }
|
||||
.node.start { border-color:#2764e7; color:#2764e7; background:#eef3ff; }
|
||||
.node.auto { border-color:#16845b; background:#e9f6ef; color:#16845b; }
|
||||
.node.fin { border-color:#b86b00; background:#fff6e6; color:#9a5b00; }
|
||||
.arrow { color:#2764e7; font-size:22px; font-weight:700; }
|
||||
.arrow.a { color:#16845b; }
|
||||
.atag { display:inline-block; background:#16845b; color:#fff; font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px; margin-right:6px; vertical-align:middle; }
|
||||
.down { text-align:left; padding-left:138px; color:#2764e7; font-size:20px; line-height:1; margin:2px 0 8px; }
|
||||
.legend { margin-top:24px; display:flex; gap:24px; font-size:13px; color:#667085; }
|
||||
.dot { display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:6px; vertical-align:middle; }
|
||||
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:14px 36px; }
|
||||
.rule { display:flex; align-items:center; gap:10px; padding:12px 16px; border:1px solid #e8ebf1; border-radius:10px; background:#fbfcfe; }
|
||||
.rule .src { flex:0 0 230px; font-weight:600; font-size:14px; }
|
||||
.rule .eff { font-size:13px; color:#16845b; font-weight:600; }
|
||||
.rule .num { flex:0 0 26px; height:26px; border-radius:50%; background:#2764e7; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; }
|
||||
.mods { display:grid; grid-template-columns: repeat(4,1fr); gap:12px; }
|
||||
.band { margin-bottom:16px; }
|
||||
.band h3 { font-size:15px; color:#2764e7; font-weight:700; margin-bottom:8px; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:7px; }
|
||||
.chip { background:#fff; border:1px solid #d9dee8; border-radius:8px; padding:7px 12px; font-size:13px; color:#1d2433; }
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 598 KiB |
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""极简 CDP 截图器(仅标准库)。
|
||||
用法: cdp_shot.py <ws_port> <out.png> <width> <height> <mobile:0|1> <path> <token>
|
||||
流程: 导航到 8090 -> 注入 oa.token 到 localStorage -> 重载进入登录态 -> 设备视口 -> 截图。
|
||||
"""
|
||||
import sys, json, socket, base64, hashlib, struct, os, urllib.request, time
|
||||
|
||||
def http_json(port, path):
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=10) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
class WS:
|
||||
def __init__(self, url):
|
||||
# ws://127.0.0.1:port/devtools/page/XXXX
|
||||
assert url.startswith("ws://")
|
||||
hostport, _, path = url[5:].partition("/")
|
||||
host, _, port = hostport.partition(":")
|
||||
self.path = "/" + path
|
||||
self.sock = socket.create_connection((host, int(port)), timeout=15)
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET {self.path} HTTP/1.1\r\nHost: {hostport}\r\n"
|
||||
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n")
|
||||
self.sock.sendall(req.encode())
|
||||
# read handshake response headers
|
||||
buf = b""
|
||||
while b"\r\n\r\n" not in buf:
|
||||
buf += self.sock.recv(4096)
|
||||
self.rest = buf.split(b"\r\n\r\n", 1)[1]
|
||||
self._id = 0
|
||||
|
||||
def send(self, method, params=None):
|
||||
self._id += 1
|
||||
msg = json.dumps({"id": self._id, "method": method, "params": params or {}}).encode()
|
||||
header = bytearray([0x81]) # FIN + text
|
||||
mask = os.urandom(4)
|
||||
n = len(msg)
|
||||
if n < 126:
|
||||
header.append(0x80 | n)
|
||||
elif n < 65536:
|
||||
header.append(0x80 | 126); header += struct.pack(">H", n)
|
||||
else:
|
||||
header.append(0x80 | 127); header += struct.pack(">Q", n)
|
||||
header += mask
|
||||
masked = bytes(b ^ mask[i % 4] for i, b in enumerate(msg))
|
||||
self.sock.sendall(bytes(header) + masked)
|
||||
return self._id
|
||||
|
||||
def _read(self, n):
|
||||
while len(self.rest) < n:
|
||||
chunk = self.sock.recv(65536)
|
||||
if not chunk:
|
||||
raise IOError("socket closed")
|
||||
self.rest += chunk
|
||||
out, self.rest = self.rest[:n], self.rest[n:]
|
||||
return out
|
||||
|
||||
def recv_msg(self):
|
||||
# assemble one full (possibly fragmented) text message
|
||||
data = b""
|
||||
while True:
|
||||
b0, b1 = self._read(2)
|
||||
fin = b0 & 0x80
|
||||
opcode = b0 & 0x0F
|
||||
length = b1 & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", self._read(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", self._read(8))[0]
|
||||
payload = self._read(length) if length else b""
|
||||
if opcode == 0x9: # ping -> pong
|
||||
continue
|
||||
if opcode == 0x8: # close
|
||||
raise IOError("ws closed by peer")
|
||||
data += payload
|
||||
if fin:
|
||||
return json.loads(data.decode("utf-8", "replace"))
|
||||
|
||||
def call(self, method, params=None, timeout=30):
|
||||
mid = self.send(method, params)
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
m = self.recv_msg()
|
||||
if m.get("id") == mid:
|
||||
return m
|
||||
raise TimeoutError(method)
|
||||
|
||||
def main():
|
||||
port, out, w, h, mobile, path, token = (sys.argv[1], sys.argv[2], int(sys.argv[3]),
|
||||
int(sys.argv[4]), sys.argv[5] == "1",
|
||||
sys.argv[6], sys.argv[7])
|
||||
targets = http_json(port, "/json")
|
||||
page = next(t for t in targets if t.get("type") == "page")
|
||||
ws = WS(page["webSocketDebuggerUrl"])
|
||||
ws.call("Page.enable")
|
||||
ws.call("Runtime.enable")
|
||||
ws.call("Emulation.setDeviceMetricsOverride",
|
||||
{"width": w, "height": h, "deviceScaleFactor": 2, "mobile": mobile})
|
||||
# 1) load origin so localStorage is writable for it
|
||||
ws.call("Page.navigate", {"url": "http://127.0.0.1:8090/login"})
|
||||
time.sleep(2.5)
|
||||
# 2) inject token + go to target path
|
||||
ws.call("Runtime.evaluate", {"expression":
|
||||
f"localStorage.setItem('oa.token','{token}'); 'ok'"})
|
||||
ws.call("Page.navigate", {"url": f"http://127.0.0.1:8090{path}"})
|
||||
time.sleep(4.5) # let SPA boot + data load
|
||||
shot = ws.call("Page.captureScreenshot", {"format": "png", "captureBeyondViewport": False})
|
||||
img = base64.b64decode(shot["result"]["data"])
|
||||
with open(out, "wb") as f:
|
||||
f.write(img)
|
||||
print(f"OK {out} {len(img)} bytes")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#ffffff; color:#1d2433; }
|
||||
.wrap { width:1560px; padding:40px 48px; }
|
||||
h1 { font-size:30px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.sub { font-size:15px; color:#667085; margin-bottom:28px; }
|
||||
.phase { display:flex; align-items:stretch; margin-bottom:14px; }
|
||||
.plabel { flex:0 0 120px; display:flex; flex-direction:column; justify-content:center; font-weight:700; font-size:16px; color:#2764e7; padding-right:16px; border-right:3px solid #2764e7; margin-right:18px; }
|
||||
.plabel small { font-weight:400; font-size:12px; color:#8a94a6; margin-top:3px; }
|
||||
.row { display:flex; align-items:center; flex-wrap:wrap; gap:8px; flex:1; }
|
||||
.node { background:#fff; border:1.5px solid #d9dee8; border-radius:10px; padding:11px 16px; font-size:15px; font-weight:600; color:#1d2433; white-space:nowrap; }
|
||||
.node small { display:block; font-weight:400; font-size:11px; color:#8a94a6; margin-top:2px; }
|
||||
.node.start { border-color:#2764e7; color:#2764e7; background:#eef3ff; }
|
||||
.node.auto { border-color:#16845b; background:#e9f6ef; color:#16845b; }
|
||||
.node.fin { border-color:#b86b00; background:#fff6e6; color:#9a5b00; }
|
||||
.arrow { color:#2764e7; font-size:22px; font-weight:700; }
|
||||
.arrow.a { color:#16845b; }
|
||||
.atag { display:inline-block; background:#16845b; color:#fff; font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px; margin-right:6px; vertical-align:middle; }
|
||||
.down { text-align:left; padding-left:138px; color:#2764e7; font-size:20px; line-height:1; margin:2px 0 8px; }
|
||||
.legend { margin-top:24px; display:flex; gap:24px; font-size:13px; color:#667085; }
|
||||
.dot { display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:6px; vertical-align:middle; }
|
||||
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:14px 36px; }
|
||||
.rule { display:flex; align-items:center; gap:10px; padding:12px 16px; border:1px solid #e8ebf1; border-radius:10px; background:#fbfcfe; }
|
||||
.rule .src { flex:0 0 230px; font-weight:600; font-size:14px; }
|
||||
.rule .eff { font-size:13px; color:#16845b; font-weight:600; }
|
||||
.rule .num { flex:0 0 26px; height:26px; border-radius:50%; background:#2764e7; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; }
|
||||
.mods { display:grid; grid-template-columns: repeat(4,1fr); gap:12px; }
|
||||
.band { margin-bottom:16px; }
|
||||
.band h3 { font-size:15px; color:#2764e7; font-weight:700; margin-bottom:8px; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:7px; }
|
||||
.chip { background:#fff; border:1px solid #d9dee8; border-radius:8px; padding:7px 12px; font-size:13px; color:#1d2433; }
|
||||
</style></head><body><div class="wrap">
|
||||
<h1>凯迪 ERP 业务逻辑链总图</h1>
|
||||
<div class="sub">从市场线索到归档全自动接力 ·(<span style="color:#16845b;font-weight:700">绿色=系统自动触发</span>,共 10+ 条自动联动链)</div>
|
||||
|
||||
<div class="phase"><div class="plabel">① 获取<small>市场→中标</small></div><div class="row">
|
||||
<span class="node start">情报线索<small>统一情报中心</small></span><span class="arrow">→</span>
|
||||
<span class="node">商机<small>统一CRM</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>投标台账<small>商机一键转投标</small></span><span class="arrow">→</span>
|
||||
<span class="node fin">中标<small>统一招投标</small></span>
|
||||
</div></div>
|
||||
<div class="down">↓</div>
|
||||
|
||||
<div class="phase"><div class="plabel">② 立项签约<small>合同→盖章</small></div><div class="row">
|
||||
<span class="node auto"><span class="atag">自动</span>项目台账 + 总包合同草稿<small>中标自动建</small></span><span class="arrow">→</span>
|
||||
<span class="node">合同 OA 审批<small>含法务/合约节点</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>合同生效 + 待用印单<small>审批通过自动进用章中心</small></span><span class="arrow">→</span>
|
||||
<span class="node fin">已盖章<small>用章中心</small></span><span class="arrow">→</span>
|
||||
<span class="node">总包→分包<small>分包不超总包·成本控制</small></span>
|
||||
</div></div>
|
||||
<div class="down">↓</div>
|
||||
|
||||
<div class="phase"><div class="plabel">③ 履约收支<small>开票/付款</small></div><div class="row">
|
||||
<span class="node">收款确认<small>项目收款</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>待开销项发票<small>自动带金额·关联合同</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>开具→回写合同已开票额<small>入账</small></span>
|
||||
<span style="flex-basis:100%;height:8px"></span>
|
||||
<span class="node">付款审批<small>含人材机审核</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>待付付款单<small>钱权分离</small></span><span class="arrow">→</span>
|
||||
<span class="node fin">财务放款</span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>记账凭证<small>写入财务</small></span>
|
||||
</div></div>
|
||||
<div class="down">↓</div>
|
||||
|
||||
<div class="phase"><div class="plabel">④ 收尾<small>验收归档</small></div><div class="row">
|
||||
<span class="node">项目验收<small>验收登记</small></span><span class="arrow a">→</span>
|
||||
<span class="node auto"><span class="atag">自动</span>项目档案<small>档案库自动归集全周期资料</small></span>
|
||||
</div></div>
|
||||
|
||||
<div class="legend">
|
||||
<span><i class="dot" style="background:#eef3ff;border:1.5px solid #2764e7"></i>起点/业务节点</span>
|
||||
<span><i class="dot" style="background:#e9f6ef;border:1.5px solid #16845b"></i>系统自动触发</span>
|
||||
<span><i class="dot" style="background:#fff6e6;border:1.5px solid #b86b00"></i>状态里程碑</span>
|
||||
</div>
|
||||
</div></body></html>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#ffffff; color:#1d2433; }
|
||||
.wrap { width:1560px; padding:40px 48px; }
|
||||
h1 { font-size:30px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.sub { font-size:15px; color:#667085; margin-bottom:28px; }
|
||||
.phase { display:flex; align-items:stretch; margin-bottom:14px; }
|
||||
.plabel { flex:0 0 120px; display:flex; flex-direction:column; justify-content:center; font-weight:700; font-size:16px; color:#2764e7; padding-right:16px; border-right:3px solid #2764e7; margin-right:18px; }
|
||||
.plabel small { font-weight:400; font-size:12px; color:#8a94a6; margin-top:3px; }
|
||||
.row { display:flex; align-items:center; flex-wrap:wrap; gap:8px; flex:1; }
|
||||
.node { background:#fff; border:1.5px solid #d9dee8; border-radius:10px; padding:11px 16px; font-size:15px; font-weight:600; color:#1d2433; white-space:nowrap; }
|
||||
.node small { display:block; font-weight:400; font-size:11px; color:#8a94a6; margin-top:2px; }
|
||||
.node.start { border-color:#2764e7; color:#2764e7; background:#eef3ff; }
|
||||
.node.auto { border-color:#16845b; background:#e9f6ef; color:#16845b; }
|
||||
.node.fin { border-color:#b86b00; background:#fff6e6; color:#9a5b00; }
|
||||
.arrow { color:#2764e7; font-size:22px; font-weight:700; }
|
||||
.arrow.a { color:#16845b; }
|
||||
.atag { display:inline-block; background:#16845b; color:#fff; font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px; margin-right:6px; vertical-align:middle; }
|
||||
.down { text-align:left; padding-left:138px; color:#2764e7; font-size:20px; line-height:1; margin:2px 0 8px; }
|
||||
.legend { margin-top:24px; display:flex; gap:24px; font-size:13px; color:#667085; }
|
||||
.dot { display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:6px; vertical-align:middle; }
|
||||
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:14px 36px; }
|
||||
.rule { display:flex; align-items:center; gap:10px; padding:12px 16px; border:1px solid #e8ebf1; border-radius:10px; background:#fbfcfe; }
|
||||
.rule .src { flex:0 0 230px; font-weight:600; font-size:14px; }
|
||||
.rule .eff { font-size:13px; color:#16845b; font-weight:600; }
|
||||
.rule .num { flex:0 0 26px; height:26px; border-radius:50%; background:#2764e7; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; }
|
||||
.mods { display:grid; grid-template-columns: repeat(4,1fr); gap:12px; }
|
||||
.band { margin-bottom:16px; }
|
||||
.band h3 { font-size:15px; color:#2764e7; font-weight:700; margin-bottom:8px; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:7px; }
|
||||
.chip { background:#fff; border:1px solid #d9dee8; border-radius:8px; padding:7px 12px; font-size:13px; color:#1d2433; }
|
||||
</style></head><body><div class="wrap">
|
||||
<h1>自动联动链 · 触发规则图</h1>
|
||||
<div class="sub">某个业务动作办结 → 系统自动写下游单据/状态(数据中心「自动联动留痕」全程可查)</div>
|
||||
<div class="grid">
|
||||
<div class="rule"><span class="num">1</span><span class="src">商机点「转投标」</span><span class="arrow a">→</span><span class="eff">自动建投标台账(投标准备)</span></div>
|
||||
<div class="rule"><span class="num">2</span><span class="src">投标状态改「中标」</span><span class="arrow a">→</span><span class="eff">自动建项目台账 + 总包合同草稿</span></div>
|
||||
<div class="rule"><span class="num">3</span><span class="src">合同审批办结</span><span class="arrow a">→</span><span class="eff">合同自动置「已生效」</span></div>
|
||||
<div class="rule"><span class="num">4</span><span class="src">合同生效</span><span class="arrow a">→</span><span class="eff">用章中心自动生成「待用印」单</span></div>
|
||||
<div class="rule"><span class="num">5</span><span class="src">用印申请办结</span><span class="arrow a">→</span><span class="eff">用印台账自动置「已用印」</span></div>
|
||||
<div class="rule"><span class="num">6</span><span class="src">收款确认单办结</span><span class="arrow a">→</span><span class="eff">自动生成「待开」销项发票草稿</span></div>
|
||||
<div class="rule"><span class="num">7</span><span class="src">发票开具(待开→已开)</span><span class="arrow a">→</span><span class="eff">自动回写合同「已开票额」</span></div>
|
||||
<div class="rule"><span class="num">8</span><span class="src">付款/报销/请款办结</span><span class="arrow a">→</span><span class="eff">自动生成「待付」付款单(钱权分离)</span></div>
|
||||
<div class="rule"><span class="num">9</span><span class="src">财务确认放款</span><span class="arrow a">→</span><span class="eff">自动生成记账凭证 + 回写合同已付额</span></div>
|
||||
<div class="rule"><span class="num">10</span><span class="src">立项审批办结</span><span class="arrow a">→</span><span class="eff">自动建项目台账(phase=立项)</span></div>
|
||||
<div class="rule"><span class="num">11</span><span class="src">项目验收办结</span><span class="arrow a">→</span><span class="eff">项目自动置「验收」</span></div>
|
||||
<div class="rule"><span class="num">12</span><span class="src">项目验收办结</span><span class="arrow a">→</span><span class="eff">档案库自动生成项目档案</span></div>
|
||||
<div class="rule"><span class="num">13</span><span class="src">供应商准入办结</span><span class="arrow a">→</span><span class="eff">供应商自动置「合格」</span></div>
|
||||
<div class="rule"><span class="num">14</span><span class="src">总包合同「派生分包」</span><span class="arrow a">→</span><span class="eff">甲乙方自动倒换 + 校验分包不超总包</span></div>
|
||||
</div>
|
||||
</div></body></html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#ffffff; color:#1d2433; }
|
||||
.wrap { width:1560px; padding:40px 48px; }
|
||||
h1 { font-size:30px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.sub { font-size:15px; color:#667085; margin-bottom:28px; }
|
||||
.phase { display:flex; align-items:stretch; margin-bottom:14px; }
|
||||
.plabel { flex:0 0 120px; display:flex; flex-direction:column; justify-content:center; font-weight:700; font-size:16px; color:#2764e7; padding-right:16px; border-right:3px solid #2764e7; margin-right:18px; }
|
||||
.plabel small { font-weight:400; font-size:12px; color:#8a94a6; margin-top:3px; }
|
||||
.row { display:flex; align-items:center; flex-wrap:wrap; gap:8px; flex:1; }
|
||||
.node { background:#fff; border:1.5px solid #d9dee8; border-radius:10px; padding:11px 16px; font-size:15px; font-weight:600; color:#1d2433; white-space:nowrap; }
|
||||
.node small { display:block; font-weight:400; font-size:11px; color:#8a94a6; margin-top:2px; }
|
||||
.node.start { border-color:#2764e7; color:#2764e7; background:#eef3ff; }
|
||||
.node.auto { border-color:#16845b; background:#e9f6ef; color:#16845b; }
|
||||
.node.fin { border-color:#b86b00; background:#fff6e6; color:#9a5b00; }
|
||||
.arrow { color:#2764e7; font-size:22px; font-weight:700; }
|
||||
.arrow.a { color:#16845b; }
|
||||
.atag { display:inline-block; background:#16845b; color:#fff; font-size:11px; font-weight:700; padding:2px 7px; border-radius:5px; margin-right:6px; vertical-align:middle; }
|
||||
.down { text-align:left; padding-left:138px; color:#2764e7; font-size:20px; line-height:1; margin:2px 0 8px; }
|
||||
.legend { margin-top:24px; display:flex; gap:24px; font-size:13px; color:#667085; }
|
||||
.dot { display:inline-block; width:12px; height:12px; border-radius:3px; margin-right:6px; vertical-align:middle; }
|
||||
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:14px 36px; }
|
||||
.rule { display:flex; align-items:center; gap:10px; padding:12px 16px; border:1px solid #e8ebf1; border-radius:10px; background:#fbfcfe; }
|
||||
.rule .src { flex:0 0 230px; font-weight:600; font-size:14px; }
|
||||
.rule .eff { font-size:13px; color:#16845b; font-weight:600; }
|
||||
.rule .num { flex:0 0 26px; height:26px; border-radius:50%; background:#2764e7; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; }
|
||||
.mods { display:grid; grid-template-columns: repeat(4,1fr); gap:12px; }
|
||||
.band { margin-bottom:16px; }
|
||||
.band h3 { font-size:15px; color:#2764e7; font-weight:700; margin-bottom:8px; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:7px; }
|
||||
.chip { background:#fff; border:1px solid #d9dee8; border-radius:8px; padding:7px 12px; font-size:13px; color:#1d2433; }
|
||||
</style></head><body><div class="wrap">
|
||||
<h1>凯迪 ERP 系统功能全景</h1>
|
||||
<div class="sub">33 个模块 · 170 个页面 · 后端 90 实体 / 98 控制器 · 甲方需求表契合度 100%</div>
|
||||
|
||||
<div class="band"><h3>组织视图(导航层)</h3><div class="chips"><span class="chip">凯迪组织视图(10中心/29部室/260功能块全可点)</span></div></div>
|
||||
<div class="band"><h3>协同 · 平台</h3><div class="chips"><span class="chip">协同工作/BPM</span><span class="chip">应用定制平台</span><span class="chip">报表中心</span><span class="chip">数据中心·自动联动</span><span class="chip">移动工作台</span><span class="chip">会议管理</span><span class="chip">文档协作</span></div></div>
|
||||
<div class="band"><h3>市场获取</h3><div class="chips"><span class="chip">统一CRM·商机漏斗</span><span class="chip">统一招投标</span><span class="chip">统一情报中心·查网</span></div></div>
|
||||
<div class="band"><h3>研发 · 设计 · 检测</h3><div class="chips"><span class="chip">研发创新中心·立项/知产/申报/费用归集</span><span class="chip">设计研究中心</span><span class="chip">实验室·检测</span><span class="chip">工程监理</span></div></div>
|
||||
<div class="band"><h3>生产 · 运营 · 工程</h3><div class="chips"><span class="chip">制造管理中心·工单/库存</span><span class="chip">运营管理中心·污水工艺/药剂/污泥</span><span class="chip">目标管理·项目360流程链</span><span class="chip">预算成本中心·BOM/量价</span><span class="chip">统一EHS·三体系</span></div></div>
|
||||
<div class="band"><h3>合同 · 资金(链式自动化核心)</h3><div class="chips"><span class="chip">合同中心·台账/分包父子</span><span class="chip">资金支付中心·付款/资金池</span><span class="chip">用章中心</span><span class="chip">财务管理·凭证/应收应付/税务/资产</span><span class="chip">主数据中心·供应商/客户/印章/发票</span></div></div>
|
||||
<div class="band"><h3>管控 · 支持</h3><div class="chips"><span class="chip">审计监察·含舞弊/经济责任</span><span class="chip">法务合规中心·诉讼/合规</span><span class="chip">HR管理·资质/招聘/电子合同</span><span class="chip">行政后勤中心·车辆/宿舍/食堂</span><span class="chip">信息中心·运维/IT预算</span><span class="chip">档案知识中心</span><span class="chip">知识社区</span><span class="chip">文化建设·媒体/评优</span></div></div>
|
||||
</div></body></html>
|
||||
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
CSS = """
|
||||
@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;}
|
||||
"""
|
||||
def page(inner): return f'<div class="page">{inner}</div>'
|
||||
|
||||
cover = """<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>"""
|
||||
|
||||
toc = """<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>"""
|
||||
|
||||
ch1 = """<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 createWebHistory(base '/',干净路径无 #);图标只用 @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>SQLite(org.hibernate.community.dialect.SQLiteDialect)</td><td>jdbc:sqlite:./data/oa.db;ddl-auto=update 自动建表加列</td></tr>
|
||||
<tr><td>实时</td><td>WebSocket + yjs CRDT</td><td>文档协同实时编辑(doccollab)</td></tr>
|
||||
<tr><td>部署</td><td>单 jar(gradlew 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>"""
|
||||
|
||||
ch2 = """<span class="chap">第二章</span><h2>代码结构</h2>
|
||||
<h3>后端 oa-backend(com.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 JpaRepository(findByXxx 派生查询)</td></tr>
|
||||
<tr><td><code>web/</code></td><td>98</td><td>@RestController,REST 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、PasswordUtil(PBKDF2)</td></tr>
|
||||
<tr><td><code>config/</code></td><td>5</td><td>AuthInterceptor、CorsConfig、JacksonConfig、SpaWebConfig(SPA 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/<模块>/<页>.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>"""
|
||||
|
||||
ch3 = """<span class="chap">第三章</span><h2>后端核心约定</h2>
|
||||
<h3>统一返回信封 ApiResp</h3>
|
||||
<pre>public record ApiResp<T>(int code, String message, T data) {
|
||||
static ok(data) -> {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 <token></code></li>
|
||||
<li><code>AuthInterceptor</code> 守 <code>/api/oa/**</code>,白名单:login / session / logout</li>
|
||||
<li>密码 PBKDF2-HMAC-SHA256:<code>pbkdf2$120000$<saltB64>$<hashB64></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>"""
|
||||
|
||||
ch4 = """<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>"""
|
||||
|
||||
ch5 = """<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>"""
|
||||
|
||||
ch6 = """<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>"""
|
||||
|
||||
ch7 = """<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/<模块>/<key>.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>"""
|
||||
|
||||
ch8 = """<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/static(emptyOutDir)</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>"""
|
||||
|
||||
ch9 = """<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>"""
|
||||
|
||||
ch10 = """<span class="chap">第十章</span><h2>坑与硬约束(必读)</h2>
|
||||
<table>
|
||||
<tr><th>坑</th><th>对策</th></tr>
|
||||
<tr><td><b>vite 白屏</b>:manualChunks 单拆 vendor-vue,Vue 与耦合库(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>"""
|
||||
|
||||
backc = """<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>"""
|
||||
|
||||
secs=[cover,toc,ch1,ch2,ch3,ch4,ch5,ch6,ch7,ch8,ch9,ch10,backc]
|
||||
out=f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}</style></head><body>"
|
||||
out+="".join(page(s) for s in secs)
|
||||
out+="</body></html>"
|
||||
open('/Users/qiu/Desktop/ERP/diagrams/techdoc.html','w').write(out)
|
||||
print("techdoc.html written,", len(secs), "pages")
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import html
|
||||
|
||||
# section: dict(kind, ...)
|
||||
# kind 'tutorial': {chapter, title, intro, img, ar(图宽高比), markers:[(n,x,y)], callouts:[(x,y,dir,text)], steps:[...], note}
|
||||
SECTIONS = [
|
||||
{'kind':'cover'},
|
||||
{'kind':'toc'},
|
||||
{'kind':'intro'},
|
||||
{'kind':'tutorial','chapter':'第一步 · 登录','title':'1 登录系统','img':'shots/01_login.png',
|
||||
'intro':'打开系统网址(内测:https://resonant-elated-launder.ngrok-free.dev),用内测账号登录。',
|
||||
'markers':[(1,37,46),(2,37,53),(3,50,60),(4,50,68)],
|
||||
'callouts':[(62,46,'left','输入用户名(admin)')],
|
||||
'steps':['① 在「用户名」框输入账号,如 admin','② 在「密码」框输入密码 123456','③ 点蓝色「登录」按钮进入系统','④ 内测账号:admin / zhangwei / lina,初始密码都是 123456'],
|
||||
'note':'登录后默认进入个人空间,顶部第一个模块就是「凯迪组织视图」。'},
|
||||
{'kind':'tutorial','chapter':'第二步 · 总览','title':'2 凯迪组织视图:一眼看全','img':'shots/02_orgview.png',
|
||||
'intro':'这是整套系统的门户。它按甲方需求表的组织架构(10 个中心 / 29 个部室 / 260 个功能块)排列,并实时标出每块做到了什么程度。',
|
||||
'markers':[(1,6,9),(2,9,30),(3,46,29),(4,18,55),(5,12,62)],
|
||||
'callouts':[(20,26,'down','100% = 甲方需求表全部功能块都已落地')],
|
||||
'steps':['① 顶部导航第一个模块「凯迪组织视图」','② 左上大数字=表格契合度(已做到 100%)','③ 绿色进度条=已建可用 260 / 共 260 项','④ 下面 10 个中心卡片,每个显示该中心完成度','⑤ 点中心里的任意「部室」蓝色按钮 → 进入部室工作台'],
|
||||
'note':'所有中心都已 100%,整条进度条全绿。'},
|
||||
{'kind':'tutorial','chapter':'第三步 · 找功能','title':'3 部室工作台:找到你部门的活','img':'shots/03_dept.png',
|
||||
'intro':'点任意部室进来,就是这个部门要用的全部功能——每个功能块都能点开进入真实页面。',
|
||||
'markers':[(1,12,16),(2,12,27),(3,33,27),(4,24,48),(5,44,52),(6,12,84)],
|
||||
'callouts':[(48,48,'left','绿色=可用,点「打开」进真实页面')],
|
||||
'steps':['① 面包屑:组织视图 / 中心 / 部室','② 该部室用到的系统标签(OA / 财务T+ / ERP)','③ 覆盖度','④ 功能板块卡片:绿=可用、蓝=部分、灰=规划中','⑤ 点「打开」直接进入该功能的真实页面','⑥ 底部「与其他部门协作」:和哪些部门自动交换数据'],
|
||||
'note':'这一层是「部门视图」——按你的部门组织,找功能不用记菜单。'},
|
||||
{'kind':'tutorial','chapter':'第四步 · 最简单的操作','title':'4 录一条业务台账(增删改查)','img':'shots/04_ledger_ops.png',
|
||||
'intro':'最基础的操作:任何业务台账(这里是污水厂「工艺运行台账」)都能新建、编辑、删除,数据存进真实后端、刷新仍在。',
|
||||
'markers':[(1,88,18),(2,12,30),(3,30,56),(4,72,56)],
|
||||
'callouts':[(88,12,'down','点这里新建一条记录')],
|
||||
'steps':['① 点右上「+ 新建」打开录入表单','② 填写字段(日期、进出水量、COD、状态…)后保存','③ 列表里每行可点进去「编辑 / 删除」','④ 状态列自动语义着色(达标绿 / 超标红)','⑤ 顶部搜索框可按关键字快速过滤'],
|
||||
'note':'数据写入后端 biz_record 真实数据表(不是前端假数据),可查询、可统计。'},
|
||||
{'kind':'tutorial','chapter':'第五步 · 进阶','title':'5 合同台账 + 一键派生分包','img':'shots/05_contract.png',
|
||||
'intro':'合同中心管理全部合同;总包合同可一键「派生分包」,系统自动倒换甲乙方、并校验分包累计不超总包(成本控制)。',
|
||||
'markers':[(1,88,18),(2,15,42),(3,15,70)],
|
||||
'callouts':[(18,66,'up','HT-B2-F1/F2 就是自动派生出来的分包合同')],
|
||||
'steps':['① 合同列表(含总包、分包,分包编号带 -F1/-F2)','② 点任一合同行进入详情抽屉','③ 详情里:履约进度、关联发票/付款/用印、变更记录','④ 总包合同详情有「派生分包」按钮 → 填分包商和金额','⑤ 系统自动:我方变发包甲方、分包商为乙方;分包超总包会被拦截'],
|
||||
'note':'合同的生效、用印、回写已开票/已付款额都由流程自动联动(见第八章)。'},
|
||||
{'kind':'tutorial','chapter':'第六步 · 业务流程(起点)','title':'6 商机 → 转投标','img':'shots/06_crm.png',
|
||||
'intro':'业务流程链的起点。在 CRM 看到机会,点商机详情里的「转投标」,系统自动建一张投标台账——不用去投标模块重录。',
|
||||
'markers':[(1,88,18),(2,30,45)],
|
||||
'callouts':[(33,40,'left','点「详情」进抽屉,里面有「转投标」按钮')],
|
||||
'steps':['① 商机管理列出所有销售机会','② 点某商机的「详情」打开抽屉','③ 抽屉右上「转投标」→ 自动生成投标台账(投标准备)','④ 商机阶段自动推进到「方案报价」'],
|
||||
'note':'链路:情报线索 → 商机 →(转投标)→ 投标 → 中标 → …'},
|
||||
{'kind':'tutorial','chapter':'第七步 · 业务流程(中标自动化)','title':'7 中标 → 自动建项目 + 总包合同','img':'shots/07_bidding.png',
|
||||
'intro':'把投标状态改成「中标」,系统自动建项目台账 + 总包合同草稿——金额、甲方自动带入,不用手工再录。',
|
||||
'markers':[(1,30,40),(2,72,40)],
|
||||
'callouts':[(72,34,'down','状态改「中标」→ 自动建项目+合同')],
|
||||
'steps':['① 投标台账管理所有投标项目','② 把某投标状态改为「中标」','③ 系统自动:建项目台账(phase=中标) + 总包合同草稿','④ 自动联动留痕里能看到这两个动作'],
|
||||
'note':'这条就是「招投标 → 项目 → 合同」的自动接力。'},
|
||||
{'kind':'tutorial','chapter':'第八步 · 流程闭环(核心)','title':'8 项目360:整条流程链一屏看全','img':'shots/08_project360.png','tall':True,
|
||||
'intro':'这是整套系统的灵魂页。选一个项目,把它从商机线索到归档的整条生命周期画成一条链,绿色「自动」徽标标出哪几步是系统替你接力做的。',
|
||||
'markers':[(1,88,16),(2,10,30),(3,46,44),(4,15,72)],
|
||||
'callouts':[(46,36,'down','业务流程链:7 段闭环,绿色徽标=系统自动触发')],
|
||||
'steps':['① 右上选项目','② 资金汇总:合同总额 / 已付款 / 已开票','③ 业务流程链:商机线索→投标→合同→用印→开票收款→付款→验收归档','④ 带「自动」徽标的步骤=系统自动接力(合同/开票/付款)','⑤ 下面卡片:该项目关联的合同/发票/付款/用印/档案全在一屏'],
|
||||
'note':'这页就是回答「这套系统到底替凯迪把活干通了没有」——一屏说清。'},
|
||||
{'kind':'tutorial','chapter':'第九步 · 看系统替你干了什么','title':'9 自动联动留痕','img':'shots/09_automation.png',
|
||||
'intro':'所有自动接力都在这里留痕。一个业务事项办结后,系统自动写了哪些下游单据/状态,全程可追溯。',
|
||||
'markers':[(1,15,40),(2,70,40)],
|
||||
'callouts':[(18,34,'down','来源事项 → 系统自动产生的联动动作')],
|
||||
'steps':['① 数据中心 → 自动联动留痕','② 左列「来源事项」:触发的业务办结','③ 右列「联动动作」:系统自动写的下游(生成付款/用印/发票/归档…)','④ 一个事项可产生多个联动动作(如验收→项目验收+项目归档)'],
|
||||
'note':'这是「流程驱动」的证据——不是人工搬数据,是系统自动接力。'},
|
||||
{'kind':'logic'},
|
||||
{'kind':'rules'},
|
||||
{'kind':'back'},
|
||||
]
|
||||
|
||||
CSS = """
|
||||
@page { size: A4 landscape; margin: 11mm 12mm; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; color:#1d2433; font-size:13px; line-height:1.6; }
|
||||
.page { page-break-after:always; position:relative; }
|
||||
.page:last-child { page-break-after:auto; }
|
||||
h1 { font-size:26px; font-weight:700; }
|
||||
.cover { height:182mm; display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center; }
|
||||
.cover .badge { display:inline-block; background:#2764e7; color:#fff; font-size:13px; padding:5px 16px; border-radius:20px; margin-bottom:24px; }
|
||||
.cover h1 { font-size:46px; color:#1947aa; margin-bottom:14px; }
|
||||
.cover .csub { font-size:18px; color:#667085; margin-bottom:40px; }
|
||||
.cover .stats { display:flex; gap:40px; margin-top:10px; }
|
||||
.cover .stat { text-align:center; }
|
||||
.cover .stat b { display:block; font-size:30px; color:#2764e7; }
|
||||
.cover .stat span { font-size:13px; color:#8a94a6; }
|
||||
.cover .foot { position:absolute; bottom:0; font-size:12px; color:#8a94a6; }
|
||||
.chap { display:inline-block; background:#eef3ff; color:#2764e7; font-weight:700; font-size:13px; padding:4px 12px; border-radius:6px; margin-bottom:8px; }
|
||||
.ttl { font-size:22px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.lead { font-size:13px; color:#667085; margin-bottom:12px; max-width:250mm; }
|
||||
.body { display:flex; gap:18px; align-items:flex-start; }
|
||||
.shotwrap { flex:0 0 63%; position:relative; border:1px solid #d9dee8; border-radius:10px; overflow:hidden; line-height:0; }
|
||||
.shotwrap.tall { flex:0 0 58%; }
|
||||
.shotwrap img { width:100%; display:block; }
|
||||
.mk { position:absolute; width:24px; height:24px; border-radius:50%; background:#e23b3b; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; transform:translate(-50%,-50%); box-shadow:0 1px 4px rgba(0,0,0,.35); border:2px solid #fff; }
|
||||
.callout { position:absolute; background:#1d2433; color:#fff; font-size:11px; padding:5px 9px; border-radius:6px; max-width:190px; line-height:1.4; transform:translate(-50%,-50%); z-index:5; }
|
||||
.callout.cl-left { transform:translate(-100%,-50%); }
|
||||
.callout.cl-down::after { content:''; position:absolute; left:50%; top:100%; border:6px solid transparent; border-top-color:#1d2433; transform:translateX(-50%); }
|
||||
.callout.cl-up::after { content:''; position:absolute; left:50%; bottom:100%; border:6px solid transparent; border-bottom-color:#1d2433; transform:translateX(-50%); }
|
||||
.callout.cl-left::after { content:''; position:absolute; left:100%; top:50%; border:6px solid transparent; border-left-color:#1d2433; transform:translateY(-50%); }
|
||||
.steps { flex:1; }
|
||||
.steps h3 { font-size:14px; color:#2764e7; margin-bottom:8px; }
|
||||
.steps ol { list-style:none; }
|
||||
.steps li { font-size:12.5px; padding:5px 0 5px 0; border-bottom:1px dashed #e8ebf1; color:#1d2433; }
|
||||
.note { margin-top:12px; background:#e9f6ef; border-left:3px solid #16845b; padding:9px 14px; font-size:12px; color:#16845b; border-radius:0 6px 6px 0; }
|
||||
.toc-list { columns:2; column-gap:40px; margin-top:18px; }
|
||||
.toc-list div { padding:7px 0; font-size:14px; border-bottom:1px solid #eef1f5; }
|
||||
.toc-list b { color:#2764e7; margin-right:10px; }
|
||||
.intro-grid { display:flex; gap:24px; margin-top:14px; }
|
||||
.intro-txt { flex:1; }
|
||||
.intro-txt p { margin-bottom:10px; font-size:13px; }
|
||||
.intro-txt .k { font-weight:700; color:#1947aa; }
|
||||
.intro-img { flex:0 0 52%; }
|
||||
.intro-img img { width:100%; border:1px solid #d9dee8; border-radius:10px; }
|
||||
.sysrow { display:flex; gap:12px; margin:14px 0; }
|
||||
.syscard { flex:1; border:1px solid #d9dee8; border-radius:10px; padding:12px 14px; }
|
||||
.syscard b { color:#2764e7; }
|
||||
.fullimg { width:100%; border:1px solid #d9dee8; border-radius:10px; margin-top:10px; }
|
||||
.logiccap { font-size:13px; color:#667085; margin:6px 0 4px; }
|
||||
"""
|
||||
|
||||
def esc(s): return html.escape(str(s))
|
||||
|
||||
def render_tutorial(s):
|
||||
mks = "".join(f'<span class="mk" style="left:{x}%;top:{y}%">{n}</span>' for n,x,y in s.get('markers',[]))
|
||||
cls = "".join(f'<span class="callout cl-{d}" style="left:{x}%;top:{y}%">{esc(t)}</span>' for x,y,d,t in s.get('callouts',[]))
|
||||
steps = "".join(f"<li>{esc(t)}</li>" for t in s['steps'])
|
||||
tall = ' tall' if s.get('tall') else ''
|
||||
note = f'<div class="note">{esc(s["note"])}</div>' if s.get('note') else ''
|
||||
return f"""<div class="page">
|
||||
<span class="chap">{esc(s['chapter'])}</span>
|
||||
<div class="ttl">{esc(s['title'])}</div>
|
||||
<div class="lead">{esc(s['intro'])}</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap{tall}"><img src="{s['img']}">{mks}{cls}</div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol>{steps}</ol>{note}</div>
|
||||
</div></div>"""
|
||||
|
||||
def render(s):
|
||||
k=s['kind']
|
||||
if k=='cover':
|
||||
return """<div class="page"><div class="cover">
|
||||
<span class="badge">企业内训 · 系统介绍 + 操作教程</span>
|
||||
<h1>凯迪 ERP 系统使用教程</h1>
|
||||
<div class="csub">ERP + OA 一体化平台 · 从登录到全流程闭环,手把手教会怎么用</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><b>33</b><span>功能模块</span></div>
|
||||
<div class="stat"><b>170</b><span>业务页面</span></div>
|
||||
<div class="stat"><b>10</b><span>自动联动链</span></div>
|
||||
<div class="stat"><b>100%</b><span>需求表契合度</span></div>
|
||||
</div>
|
||||
<div class="foot">凯迪科技 · 2026</div>
|
||||
</div></div>"""
|
||||
if k=='toc':
|
||||
items=[('一','系统是什么(介绍 + 功能全景)'),('二','登录系统'),('三','凯迪组织视图:一眼看全'),
|
||||
('四','部室工作台:找到你的活'),('五','录一条业务台账(增删改查)'),('六','合同台账 + 一键派生分包'),
|
||||
('七','商机 → 转投标'),('八','中标 → 自动建项目+合同'),('九','项目360:整条流程链闭环'),
|
||||
('十','自动联动留痕'),('十一','业务逻辑闭环示意图'),('十二','14 条自动联动规则')]
|
||||
rows="".join(f'<div><b>{a}</b>{esc(b)}</div>' for a,b in items)
|
||||
return f'<div class="page"><h1>目录</h1><div class="toc-list">{rows}</div></div>'
|
||||
if k=='intro':
|
||||
return """<div class="page"><span class="chap">第一章</span><div class="ttl">系统是什么</div>
|
||||
<div class="lead">凯迪 ERP 是一套「ERP + OA 一体化」平台,把市场、研发、工程、生产运营、合同资金、管控、行政全部业务搬到一个系统里,并用流程链自动串起来。</div>
|
||||
<div class="sysrow">
|
||||
<div class="syscard"><b>OA(协同层)</b><br>发起 / 审批 / 待办 / 通知 / 公文,全员每天用</div>
|
||||
<div class="syscard"><b>ERP(业务主干)</b><br>项目 / 合同 / 资金 / 招投标 / 研发 / 运营… 统一数据库 + 流程驱动</div>
|
||||
<div class="syscard"><b>财务 T+(核算)</b><br>做账 / 凭证 / 报表,与 ERP 打通</div>
|
||||
</div>
|
||||
<div class="intro-grid">
|
||||
<div class="intro-txt">
|
||||
<p><span class="k">规模</span>:33 个功能模块、170 个页面,后端 90 个数据实体 / 98 个接口控制器,全部真实持久化。</p>
|
||||
<p><span class="k">怎么组织</span>:上层是「部门视图」(按 10 中心 / 29 部室排列,找功能不用记菜单),底层是统一中台模块 + 流程链。</p>
|
||||
<p><span class="k">最大特点</span>:10+ 条业务自动联动链——中标自动建项目合同、合同批了自动出用印单、收款自动出发票、付款自动出凭证、验收自动归档,全程留痕。</p>
|
||||
<p><span class="k">契合度</span>:甲方需求表 10 中心 / 29 部室 / 260 功能块,<b style="color:#16845b">100% 全部落地</b>。</p>
|
||||
</div>
|
||||
<div class="intro-img"><img src="凯迪ERP-系统功能全景.png"></div>
|
||||
</div></div>"""
|
||||
if k=='logic':
|
||||
return """<div class="page"><span class="chap">第十一章</span><div class="ttl">业务逻辑闭环 · 示意图</div>
|
||||
<div class="logiccap">从市场线索一直到归档,整条业务怎么在系统里自动接力跑通:</div>
|
||||
<img class="fullimg" src="凯迪ERP-业务逻辑链总图.png">
|
||||
</div>"""
|
||||
if k=='rules':
|
||||
return """<div class="page"><span class="chap">第十二章</span><div class="ttl">14 条自动联动规则</div>
|
||||
<div class="logiccap">某个业务动作办结后,系统自动写下游单据/状态(数据中心「自动联动留痕」全程可查):</div>
|
||||
<img class="fullimg" src="凯迪ERP-自动联动链规则图.png">
|
||||
</div>"""
|
||||
if k=='back':
|
||||
return """<div class="page"><div class="cover">
|
||||
<h1 style="font-size:34px">把"组织架构图"做成了"会自己跑的系统"</h1>
|
||||
<div class="csub" style="max-width:200mm;margin-top:16px">部门只是导航视图,底层按业务对象 + 流程链组织;<br>甲方需求表 100% 落地,10 条流程自动接力,全程留痕可查。</div>
|
||||
<div class="foot">凯迪 ERP 系统使用教程 · 凯迪科技 · 2026</div>
|
||||
</div></div>"""
|
||||
if k=='tutorial':
|
||||
return render_tutorial(s)
|
||||
return ""
|
||||
|
||||
htmlout = f"<!doctype html><html><head><meta charset='utf-8'><style>{CSS}</style></head><body>"
|
||||
htmlout += "".join(render(s) for s in SECTIONS)
|
||||
htmlout += "</body></html>"
|
||||
open('/Users/qiu/Desktop/ERP/diagrams/tutorial.html','w').write(htmlout)
|
||||
print("tutorial.html written,", len(SECTIONS), "sections")
|
||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把指定实体的真·金额字段从 double 改为 BigDecimal(字段声明+getter+setter+import)。
|
||||
科学量/数量字段不传入,保持 double。"""
|
||||
import re, sys, os
|
||||
|
||||
BASE = "/Users/qiu/Desktop/ERP/oa-backend/src/main/java/com/kaidi/oa/domain"
|
||||
|
||||
# 文件 -> 该文件里需要转成 BigDecimal 的金额字段
|
||||
TARGETS = {
|
||||
"Contract.java": ["amount", "paidAmount", "invoicedAmount"],
|
||||
"RdProject.java": ["budget"],
|
||||
"ContractMilestone.java": ["amount"],
|
||||
"PolicyApplication.java": ["appliedAmount", "grantedAmount"],
|
||||
"Bid.java": ["controlPrice", "bidAmount", "deposit"],
|
||||
"Payment.java": ["amount"],
|
||||
"Invoice.java": ["amount", "taxAmount", "total"],
|
||||
"Patent.java": ["annualFee"],
|
||||
"Budget.java": ["budgetAmount", "actualAmount"],
|
||||
"Account.java": ["balance"],
|
||||
"ContractChange.java": ["amountBefore", "amountAfter"],
|
||||
"Declaration.java": ["amount"],
|
||||
"Opportunity.java": ["amount"],
|
||||
"CostCenter.java": ["budgetTotal", "actualTotal"],
|
||||
"StandardCost.java": ["materialCost", "laborCost", "overheadCost",
|
||||
"totalStandard", "actualCost", "variance"],
|
||||
"BomItem.java": ["unitPrice"], # bidQty/actualQty 保持 double
|
||||
"PriceItem.java": ["unitPrice"],
|
||||
"RdExpense.java": ["amount"],
|
||||
"Policy.java": ["fundAmount"],
|
||||
"Voucher.java": ["amount"],
|
||||
}
|
||||
|
||||
def cap(s):
|
||||
return s[0].upper() + s[1:]
|
||||
|
||||
def ensure_import(src):
|
||||
if "import java.math.BigDecimal;" in src:
|
||||
return src
|
||||
# 插在 package 行之后第一处空行/或第一条 import 前
|
||||
lines = src.split("\n")
|
||||
out, inserted = [], False
|
||||
for i, ln in enumerate(lines):
|
||||
out.append(ln)
|
||||
if not inserted and ln.startswith("package "):
|
||||
out.append("")
|
||||
out.append("import java.math.BigDecimal;")
|
||||
inserted = True
|
||||
# 若下一行本就是空行,跳过避免双空行
|
||||
if i + 1 < len(lines) and lines[i + 1].strip() == "":
|
||||
continue
|
||||
return "\n".join(out)
|
||||
|
||||
def xform(path, fields):
|
||||
src = open(path, encoding="utf-8").read()
|
||||
orig = src
|
||||
changed = []
|
||||
for f in fields:
|
||||
c = cap(f)
|
||||
# 字段声明:private double <f> -> private BigDecimal <f>
|
||||
s1 = re.sub(rf"(\bprivate\s+)double(\s+{re.escape(f)}\b)", r"\1BigDecimal\2", src)
|
||||
# getter:public double get<C>( -> public BigDecimal get<C>(
|
||||
s2 = re.sub(rf"(\bpublic\s+)double(\s+get{re.escape(c)}\s*\()", r"\1BigDecimal\2", s1)
|
||||
# setter 形参:set<C>(double -> set<C>(BigDecimal
|
||||
s3 = re.sub(rf"(\bset{re.escape(c)}\s*\(\s*)double(\s+)", r"\1BigDecimal\2", s2)
|
||||
if s3 != src:
|
||||
changed.append(f)
|
||||
src = s3
|
||||
else:
|
||||
print(f" !! {os.path.basename(path)}: 字段 {f} 未命中任何模式(需手查)")
|
||||
if changed:
|
||||
src = ensure_import(src)
|
||||
if src != orig:
|
||||
open(path, "w", encoding="utf-8").write(src)
|
||||
return changed
|
||||
|
||||
def main():
|
||||
total = 0
|
||||
for fn, fields in TARGETS.items():
|
||||
p = os.path.join(BASE, fn)
|
||||
if not os.path.exists(p):
|
||||
print(f" !! 缺文件 {fn}")
|
||||
continue
|
||||
ch = xform(p, fields)
|
||||
total += len(ch)
|
||||
print(f"{fn}: 改 {len(ch)}/{len(fields)} 字段 {ch}")
|
||||
print(f"\n合计改 {total} 个金额字段")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 234 KiB |
@@ -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 createWebHistory(base '/',干净路径无 #);图标只用 @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>SQLite(org.hibernate.community.dialect.SQLiteDialect)</td><td>jdbc:sqlite:./data/oa.db;ddl-auto=update 自动建表加列</td></tr>
|
||||
<tr><td>实时</td><td>WebSocket + yjs CRDT</td><td>文档协同实时编辑(doccollab)</td></tr>
|
||||
<tr><td>部署</td><td>单 jar(gradlew 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-backend(com.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 JpaRepository(findByXxx 派生查询)</td></tr>
|
||||
<tr><td><code>web/</code></td><td>98</td><td>@RestController,REST 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、PasswordUtil(PBKDF2)</td></tr>
|
||||
<tr><td><code>config/</code></td><td>5</td><td>AuthInterceptor、CorsConfig、JacksonConfig、SpaWebConfig(SPA 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/<模块>/<页>.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<T>(int code, String message, T data) {
|
||||
static ok(data) -> {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 <token></code></li>
|
||||
<li><code>AuthInterceptor</code> 守 <code>/api/oa/**</code>,白名单:login / session / logout</li>
|
||||
<li>密码 PBKDF2-HMAC-SHA256:<code>pbkdf2$120000$<saltB64>$<hashB64></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/<模块>/<key>.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/static(emptyOutDir)</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-vue,Vue 与耦合库(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>
|
||||
@@ -0,0 +1,148 @@
|
||||
<!doctype html><html><head><meta charset='utf-8'><style>
|
||||
@page { size: A4 landscape; margin: 11mm 12mm; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; color:#1d2433; font-size:13px; line-height:1.6; }
|
||||
.page { page-break-after:always; position:relative; }
|
||||
.page:last-child { page-break-after:auto; }
|
||||
h1 { font-size:26px; font-weight:700; }
|
||||
.cover { height:182mm; display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center; }
|
||||
.cover .badge { display:inline-block; background:#2764e7; color:#fff; font-size:13px; padding:5px 16px; border-radius:20px; margin-bottom:24px; }
|
||||
.cover h1 { font-size:46px; color:#1947aa; margin-bottom:14px; }
|
||||
.cover .csub { font-size:18px; color:#667085; margin-bottom:40px; }
|
||||
.cover .stats { display:flex; gap:40px; margin-top:10px; }
|
||||
.cover .stat { text-align:center; }
|
||||
.cover .stat b { display:block; font-size:30px; color:#2764e7; }
|
||||
.cover .stat span { font-size:13px; color:#8a94a6; }
|
||||
.cover .foot { position:absolute; bottom:0; font-size:12px; color:#8a94a6; }
|
||||
.chap { display:inline-block; background:#eef3ff; color:#2764e7; font-weight:700; font-size:13px; padding:4px 12px; border-radius:6px; margin-bottom:8px; }
|
||||
.ttl { font-size:22px; font-weight:700; color:#1d2433; margin-bottom:6px; }
|
||||
.lead { font-size:13px; color:#667085; margin-bottom:12px; max-width:250mm; }
|
||||
.body { display:flex; gap:18px; align-items:flex-start; }
|
||||
.shotwrap { flex:0 0 63%; position:relative; border:1px solid #d9dee8; border-radius:10px; overflow:hidden; line-height:0; }
|
||||
.shotwrap.tall { flex:0 0 58%; }
|
||||
.shotwrap img { width:100%; display:block; }
|
||||
.mk { position:absolute; width:24px; height:24px; border-radius:50%; background:#e23b3b; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; transform:translate(-50%,-50%); box-shadow:0 1px 4px rgba(0,0,0,.35); border:2px solid #fff; }
|
||||
.callout { position:absolute; background:#1d2433; color:#fff; font-size:11px; padding:5px 9px; border-radius:6px; max-width:190px; line-height:1.4; transform:translate(-50%,-50%); z-index:5; }
|
||||
.callout.cl-left { transform:translate(-100%,-50%); }
|
||||
.callout.cl-down::after { content:''; position:absolute; left:50%; top:100%; border:6px solid transparent; border-top-color:#1d2433; transform:translateX(-50%); }
|
||||
.callout.cl-up::after { content:''; position:absolute; left:50%; bottom:100%; border:6px solid transparent; border-bottom-color:#1d2433; transform:translateX(-50%); }
|
||||
.callout.cl-left::after { content:''; position:absolute; left:100%; top:50%; border:6px solid transparent; border-left-color:#1d2433; transform:translateY(-50%); }
|
||||
.steps { flex:1; }
|
||||
.steps h3 { font-size:14px; color:#2764e7; margin-bottom:8px; }
|
||||
.steps ol { list-style:none; }
|
||||
.steps li { font-size:12.5px; padding:5px 0 5px 0; border-bottom:1px dashed #e8ebf1; color:#1d2433; }
|
||||
.note { margin-top:12px; background:#e9f6ef; border-left:3px solid #16845b; padding:9px 14px; font-size:12px; color:#16845b; border-radius:0 6px 6px 0; }
|
||||
.toc-list { columns:2; column-gap:40px; margin-top:18px; }
|
||||
.toc-list div { padding:7px 0; font-size:14px; border-bottom:1px solid #eef1f5; }
|
||||
.toc-list b { color:#2764e7; margin-right:10px; }
|
||||
.intro-grid { display:flex; gap:24px; margin-top:14px; }
|
||||
.intro-txt { flex:1; }
|
||||
.intro-txt p { margin-bottom:10px; font-size:13px; }
|
||||
.intro-txt .k { font-weight:700; color:#1947aa; }
|
||||
.intro-img { flex:0 0 52%; }
|
||||
.intro-img img { width:100%; border:1px solid #d9dee8; border-radius:10px; }
|
||||
.sysrow { display:flex; gap:12px; margin:14px 0; }
|
||||
.syscard { flex:1; border:1px solid #d9dee8; border-radius:10px; padding:12px 14px; }
|
||||
.syscard b { color:#2764e7; }
|
||||
.fullimg { width:100%; border:1px solid #d9dee8; border-radius:10px; margin-top:10px; }
|
||||
.logiccap { font-size:13px; color:#667085; margin:6px 0 4px; }
|
||||
</style></head><body><div class="page"><div class="cover">
|
||||
<span class="badge">企业内训 · 系统介绍 + 操作教程</span>
|
||||
<h1>凯迪 ERP 系统使用教程</h1>
|
||||
<div class="csub">ERP + OA 一体化平台 · 从登录到全流程闭环,手把手教会怎么用</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><b>33</b><span>功能模块</span></div>
|
||||
<div class="stat"><b>170</b><span>业务页面</span></div>
|
||||
<div class="stat"><b>10</b><span>自动联动链</span></div>
|
||||
<div class="stat"><b>100%</b><span>需求表契合度</span></div>
|
||||
</div>
|
||||
<div class="foot">凯迪科技 · 2026</div>
|
||||
</div></div><div class="page"><h1>目录</h1><div class="toc-list"><div><b>一</b>系统是什么(介绍 + 功能全景)</div><div><b>二</b>登录系统</div><div><b>三</b>凯迪组织视图:一眼看全</div><div><b>四</b>部室工作台:找到你的活</div><div><b>五</b>录一条业务台账(增删改查)</div><div><b>六</b>合同台账 + 一键派生分包</div><div><b>七</b>商机 → 转投标</div><div><b>八</b>中标 → 自动建项目+合同</div><div><b>九</b>项目360:整条流程链闭环</div><div><b>十</b>自动联动留痕</div><div><b>十一</b>业务逻辑闭环示意图</div><div><b>十二</b>14 条自动联动规则</div></div></div><div class="page"><span class="chap">第一章</span><div class="ttl">系统是什么</div>
|
||||
<div class="lead">凯迪 ERP 是一套「ERP + OA 一体化」平台,把市场、研发、工程、生产运营、合同资金、管控、行政全部业务搬到一个系统里,并用流程链自动串起来。</div>
|
||||
<div class="sysrow">
|
||||
<div class="syscard"><b>OA(协同层)</b><br>发起 / 审批 / 待办 / 通知 / 公文,全员每天用</div>
|
||||
<div class="syscard"><b>ERP(业务主干)</b><br>项目 / 合同 / 资金 / 招投标 / 研发 / 运营… 统一数据库 + 流程驱动</div>
|
||||
<div class="syscard"><b>财务 T+(核算)</b><br>做账 / 凭证 / 报表,与 ERP 打通</div>
|
||||
</div>
|
||||
<div class="intro-grid">
|
||||
<div class="intro-txt">
|
||||
<p><span class="k">规模</span>:33 个功能模块、170 个页面,后端 90 个数据实体 / 98 个接口控制器,全部真实持久化。</p>
|
||||
<p><span class="k">怎么组织</span>:上层是「部门视图」(按 10 中心 / 29 部室排列,找功能不用记菜单),底层是统一中台模块 + 流程链。</p>
|
||||
<p><span class="k">最大特点</span>:10+ 条业务自动联动链——中标自动建项目合同、合同批了自动出用印单、收款自动出发票、付款自动出凭证、验收自动归档,全程留痕。</p>
|
||||
<p><span class="k">契合度</span>:甲方需求表 10 中心 / 29 部室 / 260 功能块,<b style="color:#16845b">100% 全部落地</b>。</p>
|
||||
</div>
|
||||
<div class="intro-img"><img src="凯迪ERP-系统功能全景.png"></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第一步 · 登录</span>
|
||||
<div class="ttl">1 登录系统</div>
|
||||
<div class="lead">打开系统网址(内测:https://resonant-elated-launder.ngrok-free.dev),用内测账号登录。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/01_login.png"><span class="mk" style="left:37%;top:46%">1</span><span class="mk" style="left:37%;top:53%">2</span><span class="mk" style="left:50%;top:60%">3</span><span class="mk" style="left:50%;top:68%">4</span><span class="callout cl-left" style="left:62%;top:46%">输入用户名(admin)</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 在「用户名」框输入账号,如 admin</li><li>② 在「密码」框输入密码 123456</li><li>③ 点蓝色「登录」按钮进入系统</li><li>④ 内测账号:admin / zhangwei / lina,初始密码都是 123456</li></ol><div class="note">登录后默认进入个人空间,顶部第一个模块就是「凯迪组织视图」。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第二步 · 总览</span>
|
||||
<div class="ttl">2 凯迪组织视图:一眼看全</div>
|
||||
<div class="lead">这是整套系统的门户。它按甲方需求表的组织架构(10 个中心 / 29 个部室 / 260 个功能块)排列,并实时标出每块做到了什么程度。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/02_orgview.png"><span class="mk" style="left:6%;top:9%">1</span><span class="mk" style="left:9%;top:30%">2</span><span class="mk" style="left:46%;top:29%">3</span><span class="mk" style="left:18%;top:55%">4</span><span class="mk" style="left:12%;top:62%">5</span><span class="callout cl-down" style="left:20%;top:26%">100% = 甲方需求表全部功能块都已落地</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 顶部导航第一个模块「凯迪组织视图」</li><li>② 左上大数字=表格契合度(已做到 100%)</li><li>③ 绿色进度条=已建可用 260 / 共 260 项</li><li>④ 下面 10 个中心卡片,每个显示该中心完成度</li><li>⑤ 点中心里的任意「部室」蓝色按钮 → 进入部室工作台</li></ol><div class="note">所有中心都已 100%,整条进度条全绿。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第三步 · 找功能</span>
|
||||
<div class="ttl">3 部室工作台:找到你部门的活</div>
|
||||
<div class="lead">点任意部室进来,就是这个部门要用的全部功能——每个功能块都能点开进入真实页面。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/03_dept.png"><span class="mk" style="left:12%;top:16%">1</span><span class="mk" style="left:12%;top:27%">2</span><span class="mk" style="left:33%;top:27%">3</span><span class="mk" style="left:24%;top:48%">4</span><span class="mk" style="left:44%;top:52%">5</span><span class="mk" style="left:12%;top:84%">6</span><span class="callout cl-left" style="left:48%;top:48%">绿色=可用,点「打开」进真实页面</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 面包屑:组织视图 / 中心 / 部室</li><li>② 该部室用到的系统标签(OA / 财务T+ / ERP)</li><li>③ 覆盖度</li><li>④ 功能板块卡片:绿=可用、蓝=部分、灰=规划中</li><li>⑤ 点「打开」直接进入该功能的真实页面</li><li>⑥ 底部「与其他部门协作」:和哪些部门自动交换数据</li></ol><div class="note">这一层是「部门视图」——按你的部门组织,找功能不用记菜单。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第四步 · 最简单的操作</span>
|
||||
<div class="ttl">4 录一条业务台账(增删改查)</div>
|
||||
<div class="lead">最基础的操作:任何业务台账(这里是污水厂「工艺运行台账」)都能新建、编辑、删除,数据存进真实后端、刷新仍在。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/04_ledger_ops.png"><span class="mk" style="left:88%;top:18%">1</span><span class="mk" style="left:12%;top:30%">2</span><span class="mk" style="left:30%;top:56%">3</span><span class="mk" style="left:72%;top:56%">4</span><span class="callout cl-down" style="left:88%;top:12%">点这里新建一条记录</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 点右上「+ 新建」打开录入表单</li><li>② 填写字段(日期、进出水量、COD、状态…)后保存</li><li>③ 列表里每行可点进去「编辑 / 删除」</li><li>④ 状态列自动语义着色(达标绿 / 超标红)</li><li>⑤ 顶部搜索框可按关键字快速过滤</li></ol><div class="note">数据写入后端 biz_record 真实数据表(不是前端假数据),可查询、可统计。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第五步 · 进阶</span>
|
||||
<div class="ttl">5 合同台账 + 一键派生分包</div>
|
||||
<div class="lead">合同中心管理全部合同;总包合同可一键「派生分包」,系统自动倒换甲乙方、并校验分包累计不超总包(成本控制)。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/05_contract.png"><span class="mk" style="left:88%;top:18%">1</span><span class="mk" style="left:15%;top:42%">2</span><span class="mk" style="left:15%;top:70%">3</span><span class="callout cl-up" style="left:18%;top:66%">HT-B2-F1/F2 就是自动派生出来的分包合同</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 合同列表(含总包、分包,分包编号带 -F1/-F2)</li><li>② 点任一合同行进入详情抽屉</li><li>③ 详情里:履约进度、关联发票/付款/用印、变更记录</li><li>④ 总包合同详情有「派生分包」按钮 → 填分包商和金额</li><li>⑤ 系统自动:我方变发包甲方、分包商为乙方;分包超总包会被拦截</li></ol><div class="note">合同的生效、用印、回写已开票/已付款额都由流程自动联动(见第八章)。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第六步 · 业务流程(起点)</span>
|
||||
<div class="ttl">6 商机 → 转投标</div>
|
||||
<div class="lead">业务流程链的起点。在 CRM 看到机会,点商机详情里的「转投标」,系统自动建一张投标台账——不用去投标模块重录。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/06_crm.png"><span class="mk" style="left:88%;top:18%">1</span><span class="mk" style="left:30%;top:45%">2</span><span class="callout cl-left" style="left:33%;top:40%">点「详情」进抽屉,里面有「转投标」按钮</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 商机管理列出所有销售机会</li><li>② 点某商机的「详情」打开抽屉</li><li>③ 抽屉右上「转投标」→ 自动生成投标台账(投标准备)</li><li>④ 商机阶段自动推进到「方案报价」</li></ol><div class="note">链路:情报线索 → 商机 →(转投标)→ 投标 → 中标 → …</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第七步 · 业务流程(中标自动化)</span>
|
||||
<div class="ttl">7 中标 → 自动建项目 + 总包合同</div>
|
||||
<div class="lead">把投标状态改成「中标」,系统自动建项目台账 + 总包合同草稿——金额、甲方自动带入,不用手工再录。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/07_bidding.png"><span class="mk" style="left:30%;top:40%">1</span><span class="mk" style="left:72%;top:40%">2</span><span class="callout cl-down" style="left:72%;top:34%">状态改「中标」→ 自动建项目+合同</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 投标台账管理所有投标项目</li><li>② 把某投标状态改为「中标」</li><li>③ 系统自动:建项目台账(phase=中标) + 总包合同草稿</li><li>④ 自动联动留痕里能看到这两个动作</li></ol><div class="note">这条就是「招投标 → 项目 → 合同」的自动接力。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第八步 · 流程闭环(核心)</span>
|
||||
<div class="ttl">8 项目360:整条流程链一屏看全</div>
|
||||
<div class="lead">这是整套系统的灵魂页。选一个项目,把它从商机线索到归档的整条生命周期画成一条链,绿色「自动」徽标标出哪几步是系统替你接力做的。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap tall"><img src="shots/08_project360.png"><span class="mk" style="left:88%;top:16%">1</span><span class="mk" style="left:10%;top:30%">2</span><span class="mk" style="left:46%;top:44%">3</span><span class="mk" style="left:15%;top:72%">4</span><span class="callout cl-down" style="left:46%;top:36%">业务流程链:7 段闭环,绿色徽标=系统自动触发</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 右上选项目</li><li>② 资金汇总:合同总额 / 已付款 / 已开票</li><li>③ 业务流程链:商机线索→投标→合同→用印→开票收款→付款→验收归档</li><li>④ 带「自动」徽标的步骤=系统自动接力(合同/开票/付款)</li><li>⑤ 下面卡片:该项目关联的合同/发票/付款/用印/档案全在一屏</li></ol><div class="note">这页就是回答「这套系统到底替凯迪把活干通了没有」——一屏说清。</div></div>
|
||||
</div></div><div class="page">
|
||||
<span class="chap">第九步 · 看系统替你干了什么</span>
|
||||
<div class="ttl">9 自动联动留痕</div>
|
||||
<div class="lead">所有自动接力都在这里留痕。一个业务事项办结后,系统自动写了哪些下游单据/状态,全程可追溯。</div>
|
||||
<div class="body">
|
||||
<div class="shotwrap"><img src="shots/09_automation.png"><span class="mk" style="left:15%;top:40%">1</span><span class="mk" style="left:70%;top:40%">2</span><span class="callout cl-down" style="left:18%;top:34%">来源事项 → 系统自动产生的联动动作</span></div>
|
||||
<div class="steps"><h3>操作步骤</h3><ol><li>① 数据中心 → 自动联动留痕</li><li>② 左列「来源事项」:触发的业务办结</li><li>③ 右列「联动动作」:系统自动写的下游(生成付款/用印/发票/归档…)</li><li>④ 一个事项可产生多个联动动作(如验收→项目验收+项目归档)</li></ol><div class="note">这是「流程驱动」的证据——不是人工搬数据,是系统自动接力。</div></div>
|
||||
</div></div><div class="page"><span class="chap">第十一章</span><div class="ttl">业务逻辑闭环 · 示意图</div>
|
||||
<div class="logiccap">从市场线索一直到归档,整条业务怎么在系统里自动接力跑通:</div>
|
||||
<img class="fullimg" src="凯迪ERP-业务逻辑链总图.png">
|
||||
</div><div class="page"><span class="chap">第十二章</span><div class="ttl">14 条自动联动规则</div>
|
||||
<div class="logiccap">某个业务动作办结后,系统自动写下游单据/状态(数据中心「自动联动留痕」全程可查):</div>
|
||||
<img class="fullimg" src="凯迪ERP-自动联动链规则图.png">
|
||||
</div><div class="page"><div class="cover">
|
||||
<h1 style="font-size:34px">把"组织架构图"做成了"会自己跑的系统"</h1>
|
||||
<div class="csub" style="max-width:200mm;margin-top:16px">部门只是导航视图,底层按业务对象 + 流程链组织;<br>甲方需求表 100% 落地,10 条流程自动接力,全程留痕可查。</div>
|
||||
<div class="foot">凯迪 ERP 系统使用教程 · 凯迪科技 · 2026</div>
|
||||
</div></div></body></html>
|
||||
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把机械文件里金额 setter 的实参包成 Money.of(...)(括号配平),并补 import。
|
||||
仅用于不含金额算术公式的文件。"""
|
||||
import os, re
|
||||
|
||||
BASE = "/Users/qiu/Desktop/ERP/oa-backend/src/main/java/com/kaidi/oa"
|
||||
|
||||
SETTERS = [
|
||||
"setAmount", "setPaidAmount", "setInvoicedAmount", "setBudget",
|
||||
"setAppliedAmount", "setGrantedAmount", "setControlPrice", "setBidAmount",
|
||||
"setDeposit", "setTaxAmount", "setTotal", "setAnnualFee", "setBudgetAmount",
|
||||
"setActualAmount", "setBalance", "setAmountBefore", "setAmountAfter",
|
||||
"setBudgetTotal", "setActualTotal", "setMaterialCost", "setLaborCost",
|
||||
"setOverheadCost", "setTotalStandard", "setActualCost", "setVariance",
|
||||
"setUnitPrice", "setFundAmount",
|
||||
]
|
||||
|
||||
FILES = [
|
||||
"web/BudgetController.java",
|
||||
"web/PaymentController.java",
|
||||
"web/RdProjectController.java",
|
||||
"web/OpportunityController.java",
|
||||
"web/ContractController.java",
|
||||
"web/BidController.java",
|
||||
"web/PolicyApplicationController.java",
|
||||
]
|
||||
|
||||
def find_call(s, start):
|
||||
"""返回 (open_idx, close_idx, setter) 第一个未包装的金额 setter 调用,从 start 起。"""
|
||||
best = None
|
||||
for setter in SETTERS:
|
||||
i = s.find("." + setter + "(", start)
|
||||
if i != -1 and (best is None or i < best[0]):
|
||||
best = (i, setter)
|
||||
if best is None:
|
||||
return None
|
||||
i, setter = best
|
||||
open_idx = s.index("(", i)
|
||||
depth, j = 0, open_idx
|
||||
while j < len(s):
|
||||
if s[j] == "(":
|
||||
depth += 1
|
||||
elif s[j] == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return (open_idx, j, setter)
|
||||
j += 1
|
||||
return None
|
||||
|
||||
def wrap_file(path):
|
||||
s = open(path, encoding="utf-8").read()
|
||||
out = []
|
||||
pos = 0
|
||||
count = 0
|
||||
while True:
|
||||
call = find_call(s, pos)
|
||||
if call is None:
|
||||
out.append(s[pos:])
|
||||
break
|
||||
open_idx, close_idx, setter = call
|
||||
inner = s[open_idx + 1:close_idx]
|
||||
out.append(s[pos:open_idx + 1])
|
||||
if inner.strip().startswith("Money.of(") and inner.strip().endswith(")"):
|
||||
out.append(inner) # 已包装,跳过
|
||||
else:
|
||||
out.append("Money.of(" + inner + ")")
|
||||
count += 1
|
||||
out.append(")")
|
||||
pos = close_idx + 1
|
||||
res = "".join(out)
|
||||
if count and "import com.kaidi.oa.common.Money;" not in res:
|
||||
res = re.sub(r"(package [^\n]+\n)", r"\1\nimport com.kaidi.oa.common.Money;\n", res, count=1)
|
||||
if res != s:
|
||||
open(path, "w", encoding="utf-8").write(res)
|
||||
return count
|
||||
|
||||
def main():
|
||||
for f in FILES:
|
||||
p = os.path.join(BASE, f)
|
||||
if not os.path.exists(p):
|
||||
print(f" !! 缺 {f}")
|
||||
continue
|
||||
n = wrap_file(p)
|
||||
print(f"{f}: 包装 {n} 处金额 setter")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 235 KiB |