b856f5fec8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
670 lines
65 KiB
HTML
670 lines
65 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>开发手册 — 岩美酒库</title>
|
||
<style>
|
||
:root{
|
||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
|
||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||
}
|
||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||
body{margin:0;background:var(--paper);color:var(--ink);line-height:1.6;}
|
||
main{max-width:980px;margin:0 auto;padding:26px 30px 60px;}
|
||
h1{font-size:22px;margin:0 0 4px;}
|
||
.sub{color:var(--muted);font-size:13px;margin-bottom:6px;}
|
||
h2{font-size:17px;margin:38px 0 6px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
|
||
h3{font-size:14px;margin:20px 0 8px;color:var(--primary-dark);}
|
||
.gdesc{font-size:12.5px;color:var(--muted);margin:6px 0 12px;}
|
||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin:14px 0;}
|
||
table{width:100%;border-collapse:collapse;font-size:12.5px;margin:6px 0;}
|
||
th{background:var(--head);color:var(--primary-dark);font-weight:600;font-size:11.5px;text-align:left;padding:7px 9px;border-bottom:1px solid var(--border);}
|
||
td{padding:6px 9px;border-bottom:1px solid #EEF1F6;vertical-align:top;}
|
||
tr:last-child td{border-bottom:0;}
|
||
.mono{font-family:ui-monospace,Menlo,monospace;}
|
||
code{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||
pre{background:#232934;color:#E8ECF3;border-radius:8px;padding:12px 14px;font-size:12px;line-height:1.55;overflow:auto;}
|
||
pre code{background:none;color:inherit;padding:0;}
|
||
.tag{display:inline-block;font-size:10px;padding:0 6px;border-radius:9px;font-weight:600;line-height:16px;white-space:nowrap;}
|
||
.tag.pub{background:var(--success-bg);color:var(--success);}
|
||
.tag.jwt{background:#E5EEF9;color:var(--primary);}
|
||
.tag.admin{background:var(--warn-bg);color:var(--warn);}
|
||
.tag.super{background:var(--danger-bg);color:var(--danger);}
|
||
.tag.rl{background:#EEE;color:var(--muted);}
|
||
.callout{border-left:4px solid var(--primary);background:#F0F6FF;padding:9px 13px;border-radius:4px;margin:8px 0 12px;font-size:12.5px;}
|
||
.callout.danger{border-color:var(--danger);background:var(--danger-bg);}
|
||
.callout.warn{border-color:var(--warn);background:var(--warn-bg);}
|
||
.callout.ok{border-color:var(--success);background:var(--success-bg);}
|
||
.toc{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:6px 18px;background:#fff;border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin:16px 0;}
|
||
.toc a{font-size:13px;color:var(--primary);text-decoration:none;}
|
||
.toc a:hover{text-decoration:underline;}
|
||
.toc b{grid-column:1/-1;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:4px;}
|
||
.svgwrap{background:#fff;border:1px solid var(--border);border-radius:10px;padding:14px;margin:12px 0;}
|
||
svg{display:block;width:100%;height:auto;max-width:900px;margin:0 auto;}
|
||
svg text{font-family:-apple-system,"PingFang SC",sans-serif;}
|
||
.m{font-family:ui-monospace,Menlo,monospace;font-weight:700;font-size:11px;}
|
||
.m.get{color:var(--success);} .m.post{color:var(--primary);} .m.put{color:var(--warn);} .m.del{color:var(--danger);} .m.patch{color:var(--accent);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>岩美酒库 · 开发手册</h1>
|
||
<div class="sub">面向后续开发者/维护者 · server v1.1.1 / client v1.1.1 · 更新于 2026-07-03 · 来源以仓库代码为准(<code>backend/</code> <code>client/</code> <code>deploy/</code> <code>scripts/</code>)</div>
|
||
|
||
<div class="toc">
|
||
<b>目录</b>
|
||
<a href="#arch">1. 系统架构总览</a>
|
||
<a href="#backend">2. 后端(Go / Gin)</a>
|
||
<a href="#frontend">3. 前端(Flutter)</a>
|
||
<a href="#db">4. 数据库</a>
|
||
<a href="#api">5. API 参考</a>
|
||
<a href="#ops">6. 数据操作与运维</a>
|
||
<a href="#workflow">7. 开发环境与工作流</a>
|
||
</div>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="arch">1. 系统架构总览</h2>
|
||
<p class="gdesc">五端 Flutter 客户端统一访问 <code>https://jiu.51yanmei.com</code>。阿里云主机上 nginx 1.24 终结 TLS(443 ssl+http2,certbot webroot 自动续期,80 → 443 跳转),按 location 分发:API/健康检查/版本反代到本机 8081 的 Go 后端(systemd <code>jiu.service</code>),静态资源(Flutter Web <code>/app</code>、商品图 <code>/images</code>、安装包 <code>/downloads</code>、营销站根路径)直接由 nginx 服务;<code>/product/:id</code> 与 <code>/app/product/:id</code>(扫码分享页)回代后端注入 OG 标签。后端连本机 Docker 容器 <code>jiu_mysql</code>(MySQL 8,127.0.0.1:3306)。未鉴权的 <code>/api/v1/(public|auth)/</code> 在 nginx 层再加一道 per-IP 限流(10r/s burst 20)。源:<code>deploy/nginx-jiu-ali.conf</code>、<code>deploy/jiu.service</code>。</p>
|
||
|
||
<div class="svgwrap">
|
||
<svg viewBox="0 0 900 470" role="img" aria-label="系统架构图">
|
||
<defs>
|
||
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||
</marker>
|
||
</defs>
|
||
<!-- clients -->
|
||
<g font-size="12" text-anchor="middle">
|
||
<rect x="30" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="100" y="43" fill="#154072">Windows</text>
|
||
<rect x="200" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="270" y="43" fill="#154072">macOS</text>
|
||
<rect x="370" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="440" y="43" fill="#154072">Web (/app)</text>
|
||
<rect x="540" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="610" y="43" fill="#154072">Android</text>
|
||
<rect x="710" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="780" y="43" fill="#154072">iOS</text>
|
||
</g>
|
||
<line x1="100" y1="56" x2="430" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<line x1="270" y1="56" x2="437" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<line x1="440" y1="56" x2="443" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<line x1="610" y1="56" x2="450" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<line x1="780" y1="56" x2="457" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<text x="450" y="88" text-anchor="middle" font-size="11" fill="#6E7888">https://jiu.51yanmei.com</text>
|
||
<!-- nginx -->
|
||
<rect x="30" y="100" width="840" height="170" rx="8" fill="#fff" stroke="#154072" stroke-width="1.5"/>
|
||
<text x="450" y="122" text-anchor="middle" font-size="13" font-weight="700" fill="#154072">ali · nginx 1.24 — 443 ssl+http2(certbot webroot 自动续期)· 80 → 443</text>
|
||
<g font-size="11" text-anchor="middle">
|
||
<rect x="50" y="136" width="190" height="52" rx="5" fill="#E6F3EC" stroke="#2E8B57"/>
|
||
<text x="145" y="157" fill="#2E8B57">静态:/app(Flutter Web)</text>
|
||
<text x="145" y="173" fill="#2E8B57">/images · /downloads · / 营销站</text>
|
||
<rect x="260" y="136" width="190" height="52" rx="5" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="355" y="157" fill="#154072">反代:/api /health /version</text>
|
||
<text x="355" y="173" fill="#154072">→ 127.0.0.1:8081</text>
|
||
<rect x="470" y="136" width="190" height="52" rx="5" fill="#FFF4E5" stroke="#B45309"/>
|
||
<text x="565" y="157" fill="#B45309">限流反代:/api/v1/(public|auth)</text>
|
||
<text x="565" y="173" fill="#B45309">10r/s · burst 20(per-IP)</text>
|
||
<rect x="680" y="136" width="170" height="52" rx="5" fill="#FDECEC" stroke="#8B2331"/>
|
||
<text x="765" y="157" fill="#8B2331">/product · /app/product</text>
|
||
<text x="765" y="173" fill="#8B2331">OG 注入 → 回代后端</text>
|
||
<text x="450" y="252" fill="#6E7888" font-size="11">证书 /etc/letsencrypt/live/jiu.51yanmei.com/ · client_max_body_size 20m · /import 超时 300s · 安全头 HSTS/XFO/nosniff</text>
|
||
</g>
|
||
<line x1="450" y1="270" x2="450" y2="308" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<text x="470" y="294" font-size="11" fill="#6E7888">proxy_pass 127.0.0.1:8081</text>
|
||
<!-- backend -->
|
||
<rect x="180" y="310" width="540" height="56" rx="8" fill="#fff" stroke="#2563AC" stroke-width="1.5"/>
|
||
<text x="450" y="333" text-anchor="middle" font-size="13" font-weight="700" fill="#154072">jiu-server(Go / Gin)— systemd jiu.service</text>
|
||
<text x="450" y="352" text-anchor="middle" font-size="11" fill="#6E7888">/opt/jiu/backend/jiu-server · EnvironmentFile=/opt/jiu/config/production.env(SERVER_PORT=8081)</text>
|
||
<line x1="450" y1="366" x2="450" y2="402" stroke="#6E7888" marker-end="url(#ar)"/>
|
||
<!-- mysql -->
|
||
<rect x="280" y="404" width="340" height="50" rx="8" fill="#fff" stroke="#2E8B57" stroke-width="1.5"/>
|
||
<text x="450" y="425" text-anchor="middle" font-size="13" font-weight="700" fill="#2E8B57">MySQL 8 — docker 容器 jiu_mysql</text>
|
||
<text x="450" y="443" text-anchor="middle" font-size="11" fill="#6E7888">127.0.0.1:3306 · 库 jiu_db · 每日备份见 §6</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="callout warn">阿里机 8080 已被 pay 项目 <code>payd</code> 占用,后端端口固定 <b>8081</b>;本地开发默认仍是 8080。改 nginx 时只动 jiu 的 server block(<code>/etc/nginx/conf.d/jiu.conf</code>),别碰同机其他站点。</div>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="backend">2. 后端(Go / Gin)</h2>
|
||
|
||
<h3>2.1 目录分层</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:32%">目录 / 文件</th><th>职责</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">main.go</td><td>入口:配置加载、生产前置检查(CORS/JWT 密钥)、连库、AutoMigrate、启动回填、SetTrustedProxies、挂路由</td></tr>
|
||
<tr><td class="mono">cmd/</td><td>一次性/运维工具(seed、gencode、import-history、fix-inventory-products、backfill-pinyin,详见 §6.4)</td></tr>
|
||
<tr><td class="mono">config/</td><td>Viper 配置结构体 + <code>config.yaml</code>(本地,不入 git)+ <code>version.yaml</code>(发版清单,/version 实时读)</td></tr>
|
||
<tr><td class="mono">internal/handler/</td><td>HTTP 处理器,每业务模块一个文件;参数校验 + 组织响应,<code>*_test.go</code> 为 SQLite in-memory 集成测试</td></tr>
|
||
<tr><td class="mono">internal/service/</td><td>业务逻辑层:auth(登录/失败锁定)、license(兑换券)、stock(审核/库存事务)、migrate(启动回填)、session_cleanup</td></tr>
|
||
<tr><td class="mono">internal/model/</td><td>GORM 模型;<code>base.go</code> 提供 Base/TenantBase/Date/JSON 公共类型</td></tr>
|
||
<tr><td class="mono">internal/middleware/</td><td>JWT 鉴权 + 会话校验、角色拦截(ReadOnly/AdminOnly/SuperAdminOnly)、LicenseGuard、限流</td></tr>
|
||
<tr><td class="mono">internal/util/</td><td>通用工具(<code>pinyin.go</code> 拼音搜索列生成等)</td></tr>
|
||
<tr><td class="mono">internal/router/router.go</td><td>全部路由注册与中间件编排(§5 的唯一真相源)</td></tr>
|
||
<tr><td class="mono">schema/schema.sql</td><td>完整建表 DDL(新装用;线上结构演进靠 AutoMigrate)</td></tr>
|
||
<tr><td class="mono">seeds/</td><td>门店种子数据 SQL(S001 全量测试数据、S002 空门店)</td></tr>
|
||
<tr><td class="mono">testutil/setup.go</td><td>测试工具:SQLite in-memory DB、CreateTestXxx 工厂、GetAuthToken</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>2.2 JWT 与多租户隔离</h3>
|
||
<p class="gdesc">源:<code>backend/internal/middleware/auth.go</code>。JWT(HS256,Access 60 分钟 / Refresh 7 天)Claims 携带 <code>user_id / shop_id / role / sid / lic_exp</code>。带 <code>sid</code> 的 token 每请求校验 <code>user_sessions</code>(撤销/禁用即时下线,<code>last_seen_at</code> 30s 节流刷新)。</p>
|
||
<div class="callout danger"><b>铁律:</b><code>shop_id</code> 永远用 <code>middleware.GetShopID(c)</code> 从 JWT 取,<b>绝不</b>从请求参数/URL/请求体读;所有查询必须带 <code>WHERE shop_id = ?</code>。违反即多租户越权。</div>
|
||
|
||
<h3>2.3 角色中间件</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:26%">中间件</th><th style="width:30%">挂载位置</th><th>行为</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">ReadOnly()</td><td>业务路由组全局 + /license</td><td><code>role=readonly</code> 的非 GET 请求 → 403(<code>code: READONLY_USER</code>)</td></tr>
|
||
<tr><td class="mono">AdminOnly()</td><td>/users 组、/shop 写、/sessions/:id 删</td><td>仅 <code>admin / superadmin</code> 放行</td></tr>
|
||
<tr><td class="mono">SuperAdminOnly()</td><td>/admin 组</td><td>仅 <code>superadmin</code> 放行(清数据/对账/错误报告/反馈后台)</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
<p class="gdesc">角色顺序:<code>superadmin > admin > operator > readonly</code>。新增写路由默认已受 ReadOnly 保护;仅管理员可用的要再包 AdminOnly 子组。</p>
|
||
|
||
<h3>2.4 LicenseGuard(授权过期降级)</h3>
|
||
<p class="gdesc">源:<code>middleware/license_guard.go</code>。按<b>当前 DB</b> 的有效授权实时算 phase(每店 30s 缓存,激活/续费调用 <code>InvalidateLicensePhase</code> 即时生效),不信任登录时 token 快照:</p>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:18%">phase</th><th style="width:28%">条件</th><th>效果</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="tag pub">normal</span></td><td>未过期 / 永久授权</td><td>正常读写</td></tr>
|
||
<tr><td><span class="tag admin">grace</span></td><td>过期 0–7 天</td><td>可写,前端展示提醒横幅</td></tr>
|
||
<tr><td><span class="tag admin">readonly</span></td><td>过期 7–15 天</td><td>非 GET → 403(<code>phase: readonly</code>)</td></tr>
|
||
<tr><td><span class="tag super">locked</span></td><td>过期 15+ 天,或授权全部被停用(吊销)</td><td>所有业务请求 403(<code>phase: locked</code>)</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
<p class="gdesc"><code>/license/*</code> 与 登出/心跳/在线会话 挂在 LicenseGuard <b>之外</b>——锁定时仍能查看授权状态并激活新码。</p>
|
||
|
||
<h3>2.5 限流</h3>
|
||
<p class="gdesc">源:<code>middleware/ratelimit.go</code>(进程内令牌桶 + janitor 清理,<code>config.C.RateLimit.Enabled=false</code> 可整体关闭)。两个维度:</p>
|
||
<ul style="font-size:13px">
|
||
<li><b>per-IP</b>(未鉴权接口逐条挂):login / refresh / register / errors / 公开商品读 / 店铺商品列表各自独立配额,IP 取自 nginx 写入的 <code>X-Real-IP</code>(main.go 只信任 127.0.0.1 代理,不可伪造)。</li>
|
||
<li><b>per-Shop</b>(挂在 JWT 之后,作用于全部已鉴权路由):按 <code>shop_id</code> 限 RPS+burst,防单店打爆共享后端。</li>
|
||
</ul>
|
||
<p class="gdesc">超限返回 429 + <code>Retry-After: 60</code>。nginx 层对 <code>/api/v1/(public|auth)/</code> 还有最外层 10r/s 兜底(§1)。</p>
|
||
|
||
<h3>2.6 单据审核流状态机</h3>
|
||
<p class="gdesc">入库单 / 出库单同一状态机(<code>service/stock.go</code> + <code>handler/stock_in.go</code>、<code>stock_out.go</code>)。审核通过时同一事务内:更新 <code>inventories</code>(FOR UPDATE 锁行)+ 写 <code>inventory_logs</code> + 生成财务应收/应付;出库先校验库存充足,不足回滚。</p>
|
||
<div class="svgwrap">
|
||
<svg viewBox="0 0 900 300" role="img" aria-label="单据审核流状态机">
|
||
<defs>
|
||
<marker id="ar2" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||
</marker>
|
||
</defs>
|
||
<g font-size="13" text-anchor="middle" font-weight="700">
|
||
<rect x="50" y="90" width="140" height="46" rx="23" fill="#EEF2F8" stroke="#6E7888"/><text x="120" y="118" fill="#232934">draft 草稿</text>
|
||
<rect x="330" y="90" width="150" height="46" rx="23" fill="#FFF4E5" stroke="#B45309"/><text x="405" y="118" fill="#B45309">pending 审核中</text>
|
||
<rect x="640" y="30" width="160" height="46" rx="23" fill="#E6F3EC" stroke="#2E8B57"/><text x="720" y="58" fill="#2E8B57">approved 已审核</text>
|
||
<rect x="640" y="160" width="160" height="46" rx="23" fill="#FDECEC" stroke="#D14343"/><text x="720" y="188" fill="#D14343">rejected 已拒绝</text>
|
||
</g>
|
||
<g font-size="11.5" fill="#6E7888">
|
||
<!-- submit -->
|
||
<line x1="190" y1="107" x2="328" y2="107" stroke="#6E7888" marker-end="url(#ar2)"/>
|
||
<text x="259" y="99" text-anchor="middle">PUT /:id/submit</text>
|
||
<!-- withdraw back edge -->
|
||
<path d="M340,136 C280,180 220,180 195,140" fill="none" stroke="#B45309" stroke-dasharray="5 4" marker-end="url(#ar2)"/>
|
||
<text x="268" y="185" text-anchor="middle" fill="#B45309">withdraw 撤回(管理员任意单 / 操作员限本人单)</text>
|
||
<!-- approve -->
|
||
<line x1="480" y1="98" x2="637" y2="60" stroke="#2E8B57" marker-end="url(#ar2)"/>
|
||
<text x="545" y="65" text-anchor="middle" fill="#2E8B57">approve 审核通过</text>
|
||
<!-- reject -->
|
||
<line x1="480" y1="122" x2="637" y2="176" stroke="#D14343" marker-end="url(#ar2)"/>
|
||
<text x="545" y="165" text-anchor="middle" fill="#D14343">reject 拒绝</text>
|
||
<!-- approved actions -->
|
||
<rect x="560" y="230" width="320" height="58" rx="6" fill="#F0F6FF" stroke="#2563AC"/>
|
||
<line x1="720" y1="76" x2="720" y2="228" stroke="#2563AC" stroke-dasharray="4 4" marker-end="url(#ar2)"/>
|
||
<text x="720" y="250" text-anchor="middle" fill="#154072">approved 后续动作(POST,service 内判权):</text>
|
||
<text x="720" y="268" text-anchor="middle" fill="#154072">return 退单(冲库存+冲应收/应付) · confirm-cost 确认进价(入库)</text>
|
||
<text x="720" y="283" text-anchor="middle" fill="#154072">confirm-sale 确认售价(出库,重算应收/利润)</text>
|
||
<!-- draft edit note -->
|
||
<text x="120" y="160" text-anchor="middle">draft 可改/可删</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3>2.7 AutoMigrate 与启动回填</h3>
|
||
<ul style="font-size:13px">
|
||
<li><b>AutoMigrate</b>(main.go):启动时对 31 个 model 只增不删地同步表结构;另幂等显式建 <code>uk_shop_code(products)</code> 唯一索引与 <code>idx_so_items_shop_code(stock_out_items)</code>(嵌入字段无法用 struct tag 表达)。</li>
|
||
<li><b>BackfillPricingColumns</b>(<code>service/migrate.go</code>):2026-07 定价字段消歧的一次性迁移,旧列(unit_price/total_price/total_amount)值拷入新列(cost_price/cost_amount/cost_total/sale_total/sale_amount/profit_total),幂等(新列为 0 才拷),旧列观察一版后手动 DROP;全新安装(无旧列)整体跳过。</li>
|
||
<li><b>backfillPartnerPinyin</b>(main.go):为 <code>name_pinyin</code> 为空的存量往来单位补拼音搜索列;products 同机制由 handler 写入时生成 + 启动兜底。</li>
|
||
</ul>
|
||
|
||
<h3>2.8 拼音搜索</h3>
|
||
<p class="gdesc">源:<code>internal/util/pinyin.go</code>(go-pinyin)。<code>ToPinyin(name)</code> 返回全拼 + 首字母,Create/Update 时自动写入 <code>name_pinyin / name_initials</code> 两列;搜索 SQL 同时 LIKE 匹配 name、code、name_pinyin、name_initials(汉字/全拼/首字母三种输入都命中)。新增需要拼音搜索的实体直接复用 <code>ToPinyin()</code>。</p>
|
||
|
||
<h3>2.9 客户端异常上报</h3>
|
||
<p class="gdesc">客户端 <code>POST /api/v1/public/errors</code>(per-IP 限流)→ <code>error_reports</code> 表;超管在 <code>GET /api/v1/admin/errors</code> 查看。前端两处统一捕获(main.dart 的 runZonedGuarded + Dio 拦截器上报 5xx),业务代码只在技术性异常处手动 <code>reportError(e, st)</code>,已知业务错误(AppException)不上报。</p>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="frontend">3. 前端(Flutter)</h2>
|
||
|
||
<h3>3.1 目录分层</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:26%">目录</th><th>职责</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">lib/core/</td><td>横切基建:api(Dio 封装,401 自动刷新)、auth(AuthNotifier + 持久化)、config(AppConfig,BASE_URL 注入)、router(go_router 登录重定向)、theme(token 三主题)、responsive、update(应用内更新)、errors(reportError)</td></tr>
|
||
<tr><td class="mono">lib/models/</td><td>与后端 JSON 对应的数据模型</td></tr>
|
||
<tr><td class="mono">lib/providers/</td><td>Riverpod 状态管理,每业务模块一个 provider(列表缓存/刷新/心跳等)</td></tr>
|
||
<tr><td class="mono">lib/repositories/</td><td>数据访问层:封装 API 调用,供 provider 消费</td></tr>
|
||
<tr><td class="mono">lib/screens/</td><td>页面(auth/shell/stock_in/stock_out/inventory/partners/finance/products/devices/settings/about/public/shared)</td></tr>
|
||
<tr><td class="mono">lib/widgets/</td><td>共享组件;<code>widgets/ds/</code> 为设计系统组件库(唯一样式来源,见 3.3)</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>3.2 数据流</h3>
|
||
<p class="gdesc">单向:Screen 只组合 ds 组件并 watch provider;provider 调 repository;repository 走 Dio(ApiClient 统一注入 token、401 自动刷新、5xx 自动上报)。</p>
|
||
<div class="svgwrap">
|
||
<svg viewBox="0 0 900 90" role="img" aria-label="前端数据流">
|
||
<defs>
|
||
<marker id="ar3" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||
</marker>
|
||
</defs>
|
||
<g font-size="12.5" text-anchor="middle" font-weight="700">
|
||
<rect x="20" y="22" width="140" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="90" y="43" fill="#154072">Screen</text><text x="90" y="59" font-size="10.5" font-weight="400" fill="#6E7888">screens/ + ds 组件</text>
|
||
<rect x="205" y="22" width="150" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="280" y="43" fill="#154072">Provider</text><text x="280" y="59" font-size="10.5" font-weight="400" fill="#6E7888">Riverpod 状态/缓存</text>
|
||
<rect x="400" y="22" width="150" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="475" y="43" fill="#154072">Repository</text><text x="475" y="59" font-size="10.5" font-weight="400" fill="#6E7888">API 调用封装</text>
|
||
<rect x="595" y="22" width="140" height="46" rx="8" fill="#FFF4E5" stroke="#B45309"/><text x="665" y="43" fill="#B45309">Dio</text><text x="665" y="59" font-size="10.5" font-weight="400" fill="#6E7888">401 刷新 · 5xx 上报</text>
|
||
<rect x="780" y="22" width="100" height="46" rx="8" fill="#E6F3EC" stroke="#2E8B57"/><text x="830" y="49" fill="#2E8B57">API</text>
|
||
</g>
|
||
<line x1="160" y1="45" x2="203" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||
<line x1="355" y1="45" x2="398" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||
<line x1="550" y1="45" x2="593" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||
<line x1="735" y1="45" x2="778" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3>3.3 ds 设计系统(单一真相源)</h3>
|
||
<p class="gdesc">UI 的真相源是原型 <code>.superpowers/prototype/</code>(tokens + <code>atoms.css</code>);Flutter 侧一对一镜像,屏只组合 ds 组件、不自己堆样式。令牌层由 <code>lib/core/theme/token_source/tokens.css</code> codegen 生成 <code>app_tokens.g.dart / app_dims.g.dart / app_chrome.g.dart</code>——改样式改源后重跑脚本,<b>禁止手改 .g 文件、禁止硬编码色值</b>。</p>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:30%">widgets/ds/ 文件</th><th>镜像对象</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">ds_atoms.dart</td><td>atoms.css 原子组件全集(按钮/输入/徽章/卡片…),尺寸引 AppDims、颜色引 context.tokens</td></tr>
|
||
<tr><td class="mono">ds_table.dart</td><td>原型 .table + .toolbar + .pager(toolbar 与表连成一卡,pager 卡外透明)</td></tr>
|
||
<tr><td class="mono">ds_kpi.dart</td><td>原型 .kpi 汇总卡(图标块 .ic 五种 tone)</td></tr>
|
||
<tr><td class="mono">ds_menu.dart</td><td>原型 .menu 下拉(定位规则照抄 shell.js openMenu)</td></tr>
|
||
<tr><td class="mono">ds_toast.dart</td><td>原型 .toast 1:1(单例顶替、2.2s 自动消失)</td></tr>
|
||
<tr><td class="mono">ds_bar_chart.dart</td><td>原型 finance.html 收支趋势分组柱状图(度量逐像素照抄)</td></tr>
|
||
<tr><td class="mono">grid_combo_cell.dart</td><td>表格内联可搜索下拉单元格(入库/出库编辑网格用)</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
<p class="gdesc"><b>闸机 1 — 代码端</b>:<code>client/tool/check_ds_code.mjs</code> 扫描 lib/ 禁 <code>Color(0x..)</code> 与 <code>Colors.x</code> 硬编码(合理特例需 <code>// ds-ignore: 理由</code>),<code>--changed --strict</code> 供 pre-commit。</p>
|
||
|
||
<h3>3.4 golden × 3 主题 + fidelity 像素闸</h3>
|
||
<ul style="font-size:13px">
|
||
<li><b>golden</b>:<code>client/test/golden/*_golden_test.dart</code> 对每屏 × 三主题(a/b/c)出基准图 <code>test/golden/goldens/<prefix>_<theme>.png</code>;更新用 <code>flutter test --update-goldens</code>。</li>
|
||
<li><b>闸机 2 — fidelity</b>:<code>tools/fidelity.mjs</code> 把<b>原型截图当基准</b>(Playwright 截 <code>.superpowers/prototype/screens/*.html</code>,注入 Noto Sans SC 统一字体)与 Flutter golden 做 pixelmatch diff,超逐屏校准阈值即 exit 1;支持 zones 分区阈值抓局部错位。屏注册表在 <code>tools/screens.mjs</code>(fidelity 与人工目检 <code>tools/ds-compare.mjs</code> 共用)。</li>
|
||
</ul>
|
||
<pre><code>cd client && flutter test --update-goldens test/golden/inventory_list_golden_test.dart
|
||
node tools/fidelity.mjs inventory # 仓库根运行;--themes a,b,c;--update 刷新原型基准</code></pre>
|
||
|
||
<h3>3.5 响应式</h3>
|
||
<p class="gdesc">断点统一 <code>context.isMobile</code>(宽 < 600,<code>core/responsive/responsive.dart</code>),弹窗宽度 <code>context.dialogWidth(X)</code>(≤ 屏宽 92%)。列表屏 <code>DataTableCard</code> 必传 <code>mobileCards</code>(窄屏卡片流),窄屏导航为 Drawer 抽屉。平台判断先 <code>kIsWeb</code> 再 <code>dart:io Platform</code>。后端 URL 一律 <code>AppConfig</code>(<code>--dart-define=BASE_URL=...</code> 注入),禁止硬编码。</p>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="db">4. 数据库</h2>
|
||
<p class="gdesc">MySQL 8 · 所有业务表含 <code>shop_id</code> 租户隔离列。<code>schema.sql</code> 定义 26 张表;另有 5 张仅由 AutoMigrate 创建(4 张商品属性字典 + <code>error_reports</code>),共 31 张。<b>完整列级文档见 <a href="../db-schema.html">db-schema.html</a></b>。</p>
|
||
|
||
<h3>4.1 分组速览</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:14%">分组</th><th style="width:26%">表</th><th>要点</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>租户与账号</td><td class="mono">shops<br>users<br>user_sessions<br>login_attempts</td><td>shops=租户根(code 唯一);users 角色 ENUM 四档、<code>uk_shop_username</code>;user_sessions 支撑 sid 会话校验/踢人(revoked_at、last_seen_at);login_attempts 记失败登录供锁定</td></tr>
|
||
<tr><td>授权</td><td class="mono">licenses<br>license_devices<br>license_codes</td><td>时长兑换券模型:license_codes 为平台码池(短码),激活兑入 licenses(expires_at 驱动 LicenseGuard),license_devices 记设备绑定</td></tr>
|
||
<tr><td>商品与字典</td><td class="mono">products<br>product_categories<br>product_images<br>product_*_options ×6<br>product_description_docs</td><td><b>product = 特有产品/序列号(非 SKU)</b>:code 同店唯一(uk_shop_code),name/series/spec=品牌/型号/版本,含 name_pinyin/name_initials 搜索列、public_id 公开页;字典表(名称/系列/规格/产地/保质期/储存/介绍)供入库选取后建 product</td></tr>
|
||
<tr><td>仓库与往来</td><td class="mono">warehouses<br>partners</td><td>partners.type=supplier/customer,含拼音搜索列</td></tr>
|
||
<tr><td>入库</td><td class="mono">stock_in_orders<br>stock_in_items</td><td>单据状态机(§2.6)、<code>uk_order_no(shop_id,order_no)</code>;明细含快照列 + cost_price/cost_amount + 批次/生产日期/有效期</td></tr>
|
||
<tr><td>出库</td><td class="mono">stock_out_orders<br>stock_out_items</td><td>同状态机;单头 sale_total/profit_total;明细 cost_price(成本快照)+ sale_price/sale_amount;<code>idx_so_items_shop_code</code> 支撑按编码反查</td></tr>
|
||
<tr><td>库存</td><td class="mono">inventories<br>inventory_logs<br>inventory_checks<br>inventory_check_items</td><td>inventories=批次行(每行一个入库批,stock_in_item_id 溯源,idx_fifo 支撑先进先出);logs 记每次变动 qty_before/after + ref;盘点单 draft→completed</td></tr>
|
||
<tr><td>财务</td><td class="mono">finance_records</td><td>type=receivable/payable/receipt/payment,balance=操作后余额,status=open/closed,ref_type/ref_id 关联单据</td></tr>
|
||
<tr><td>其他</td><td class="mono">number_rules<br>feedbacks<br>error_reports</td><td>number_rules 每店每类型一行(uk_shop_type),单号=前缀+日期+6 位序号;feedbacks 意见反馈;error_reports 客户端异常</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>4.2 定价字段口径(2026-07 消歧)</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:30%">新列</th><th style="width:18%">旧列(弃用)</th><th>口径</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">stock_in_items.cost_price</td><td class="mono">unit_price</td><td>进价 · 单瓶</td></tr>
|
||
<tr><td class="mono">stock_in_items.cost_amount</td><td class="mono">total_price</td><td>总进价 = quantity × cost_price</td></tr>
|
||
<tr><td class="mono">stock_in_orders.cost_total</td><td class="mono">total_amount</td><td>应付合计 = Σ 明细 cost_amount</td></tr>
|
||
<tr><td class="mono">stock_out_items.cost_price</td><td class="mono">unit_price</td><td>成本单价(入库成本快照)</td></tr>
|
||
<tr><td class="mono">stock_out_items.cost_amount</td><td class="mono">total_price</td><td>成本小计 = quantity × cost_price</td></tr>
|
||
<tr><td class="mono">stock_out_items.sale_price</td><td class="mono">—(新增)</td><td>实际销售单价,0 = 待定价</td></tr>
|
||
<tr><td class="mono">stock_out_items.sale_amount</td><td class="mono">—(新增)</td><td>售价小计 = quantity × sale_price(待定价 = 0)</td></tr>
|
||
<tr><td class="mono">stock_out_orders.sale_total</td><td class="mono">total_amount</td><td>应收合计 = Σ 明细 sale_amount</td></tr>
|
||
<tr><td class="mono">stock_out_orders.profit_total</td><td class="mono">—(新增)</td><td>总利润 = Σ(sale_price>0 ? (sale_price−cost_price)×qty : 0),建单落库,确认售价/进价联动重算</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
<div class="callout warn">行利润只统计<b>已定价</b>行(sale_price>0);待定价行不计利润也不计应收。旧列已停止读写,由启动回填拷值(§2.7),观察一版后手动 DROP——期间<b>勿</b>新代码引用旧列。</div>
|
||
|
||
<h3>4.3 关键机制</h3>
|
||
<ul style="font-size:13px">
|
||
<li><b>批次行 + 快照列</b>:<code>inventories</code> 每行 = 某 product 在某仓的一个入库批;出入库明细与库存行都带 <code>product_code/product_name/series/spec/...</code> 快照列(导入/审核时拷贝),显示优先 product、回退快照(<code>COALESCE</code> / 前端 <code>lineOrProduct</code>)。<b>快照列保留,勿删。</b></li>
|
||
<li><b>生成列</b>:<code>inventory_check_items.diff_qty</code> 为 <code>GENERATED ALWAYS AS (actual_qty - system_qty) STORED</code>,不可写。</li>
|
||
<li><b>FOR UPDATE 并发锁</b>:单号生成(number_rules)、库存扣减(inventories)等 check-then-act 一律事务内锁行;<code>products.code</code> 撞 <code>uk_shop_code</code> 时 max+1 重试(最多 5 次)。</li>
|
||
<li><b>custom_fields JSON</b>:新业务字段优先放 JSON 扩展列,不轻易加物理列。</li>
|
||
</ul>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="api">5. API 参考</h2>
|
||
<p class="gdesc">逐条核对自 <code>backend/internal/router/router.go</code>。权限图例:<span class="tag pub">公开</span> 无需登录(多带 per-IP 限流);<span class="tag jwt">JWT</span> 登录即可(readonly 角色写操作被 ReadOnly 拦);<span class="tag admin">Admin</span> = AdminOnly;<span class="tag super">Super</span> = SuperAdminOnly;<span class="tag rl">限流</span> = 独立 IP 限流。所有 <span class="tag jwt">JWT</span> 路由另受 per-Shop 限流;业务组(商品及以下)还受 LicenseGuard(§2.4)。</p>
|
||
|
||
<h3>5.1 根路由 / 认证 / 公开接口</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m get">GET</td><td class="mono">/health</td><td>健康检查(连通性探测)</td><td><span class="tag pub">公开</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/version</td><td>版本清单(实时读 version.yaml)</td><td><span class="tag pub">公开</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/product/:public_id</td><td>扫码分享页(OG 标签注入的 index.html)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/login</td><td>登录(失败锁定 + IP 限流双保险)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/refresh</td><td>刷新 token</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/products/:public_id</td><td>公开商品详情(扫码页数据)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/shops/:shop_code/products</td><td>店铺公开商品列表(限流最紧)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/release</td><td>版本 + 下载链接 + changelog(官网/更新用)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/public/errors</td><td>客户端异常上报 → error_reports</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/public/register</td><td>门店注册</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.2 会话 / 授权(豁免 LicenseGuard)</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/logout</td><td>登出(撤销会话)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/ping</td><td>在线心跳</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/sessions</td><td>在线会话列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/sessions/:id</td><td>强制下线</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/info</td><td>授权状态(与 LicenseGuard 同一取数口径)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/activate</td><td>兑换/激活授权码(锁定期仍可用)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/verify</td><td>校验当前授权</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/deactivate</td><td>解绑设备</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/devices</td><td>已绑定设备列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/purchase</td><td>在线购买/续费下单,返回 pay 收银台 pay_url(契约 pay-contract v1.0.0)</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/purchase/:out_trade_no</td><td>购买单状态(支付结果页轮询,本店可见)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/pay/callback</td><td>pay 支付成功 webhook(HMAC 验签+幂等+金额核对后续期)</td><td><span class="tag pub">公开</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.3 商品 / 仓库 / 往来单位</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products</td><td>商品列表(汉字/全拼/首字母/编码搜索)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products</td><td>新建商品(独立序列号,code 自增 max+1)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products/find-or-create</td><td>按编号找/建 product(导入用;禁止按名称合并)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/detail</td><td>商品详情(含图片/字典属性)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/price-history</td><td>进价/售价历史</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/qrcode</td><td>公开页二维码</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/products/:id</td><td>更新商品</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/products/:id</td><td>删除商品(软删)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products/:id/images</td><td>上传商品图片</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/products/:id/images/:image_id</td><td>删除商品图片</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/warehouses</td><td>仓库列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/warehouses</td><td>新建仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/warehouses/:id</td><td>更新仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/warehouses/:id</td><td>删除仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/partners</td><td>往来单位列表(?type&keyword,拼音搜索)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/partners</td><td>新建往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/partners/:id</td><td>更新往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/partners/:id</td><td>删除往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.4 入库 / 出库</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/orders</td><td>入库单列表(?status&keyword,编码双路匹配)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/summary</td><td>入库汇总(KPI 卡)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>入库单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders</td><td>创建入库单(每明细行新建独立 product)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>更新(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>删除(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/submit</td><td>提交审核 draft→pending</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/approve</td><td>审核通过(事务:+库存 +流水 +应付)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/reject</td><td>拒绝</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/withdraw</td><td>撤回 pending→draft(管理员任意/操作员本人)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders/:id/return</td><td>退单(库存删除 + 冲应付;service 内判管理员)</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders/:id/confirm-cost</td><td>确认进价(暂估 0 价→真实价,前向补偿)</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/orders</td><td>出库单列表(?status&keyword)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/summary</td><td>出库汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>出库单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders</td><td>创建出库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>更新(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>删除(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/submit</td><td>提交审核</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/approve</td><td>审核通过(校验库存充足,事务扣减 FIFO)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/reject</td><td>拒绝</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/withdraw</td><td>撤回 pending→draft</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders/:id/return</td><td>退单(库存加回 + 冲应收;管理员或本人)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders/:id/confirm-sale</td><td>确认售价(先出后定价,重算应收/利润)</td><td><span class="tag admin">Admin</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.5 库存 / 财务 / 用户 / 店铺 / 反馈 / 编号规则</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory</td><td>库存列表(?warehouse_id&keyword&series&spec&in_stock)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/summary</td><td>库存汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/logs</td><td>库存流水</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/inventory/:id/remark</td><td>更新库存行备注</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/inventory/checks</td><td>创建盘点单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/checks/:id</td><td>盘点单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/inventory/checks/:id/complete</td><td>完成盘点(盘盈盘亏落库)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/records</td><td>财务流水列表(含日期区间)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/finance/records</td><td>手工记账(收款/付款)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/finance/records/:id/close</td><td>结清单条应收/应付</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/finance/records/close-by-ref</td><td>按关联单据结清</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/summary</td><td>财务汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/trend</td><td>收支趋势(柱状图数据)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/users</td><td>用户列表</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/users</td><td>新建用户</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/users/:id</td><td>更新用户(角色/启用)</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/users/:id/reset-password</td><td>重置密码</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/shop/info</td><td>店铺信息</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/shop/info</td><td>更新店铺信息</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/shop/logo</td><td>上传店铺 logo</td><td><span class="tag admin">Admin</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/feedback</td><td>提交意见反馈</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/feedback/images</td><td>上传反馈附图</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/number-rules</td><td>单号规则列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/number-rules/:id</td><td>更新单号规则</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.6 数据导入(Excel)</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/products</td><td>导入商品</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/partners</td><td>导入往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-names</td><td>导入名称字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-series</td><td>导入系列字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-specs</td><td>导入规格字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-codes</td><td>导入商品编码</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/stock-in</td><td>导入历史入库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/stock-out</td><td>导入历史出库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/inventory</td><td>导入库存(按编号建 product,禁按名称合并)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.7 基础数据字典(/api/v1/product-options)</h3>
|
||
<p class="gdesc">八组字典同构:每组 <span class="m get">GET</span>(支持 <code>?keyword</code> 服务端搜索)/ <span class="m post">POST</span> / <span class="m put">PUT /:id</span> / <span class="m del">DELETE /:id</span>,共 32 条路由,权限均为 <span class="tag jwt">JWT</span>。</p>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:32%">子路径</th><th>字典</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">/names</td><td>商品名称(品牌)</td></tr>
|
||
<tr><td class="mono">/series</td><td>系列(型号)</td></tr>
|
||
<tr><td class="mono">/specs</td><td>规格(版本)</td></tr>
|
||
<tr><td class="mono">/categories</td><td>香型 / 分类</td></tr>
|
||
<tr><td class="mono">/origins</td><td>产地</td></tr>
|
||
<tr><td class="mono">/shelf-lives</td><td>保质期</td></tr>
|
||
<tr><td class="mono">/storages</td><td>储存方式</td></tr>
|
||
<tr><td class="mono">/description-docs</td><td>商品介绍文档</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>5.8 超级管理员(/api/v1/admin)</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="m post">POST</td><td class="mono">/api/v1/admin/clear-data</td><td>清空本店业务数据</td><td><span class="tag super">Super</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/reconcile</td><td>库存对账(inventories vs logs)</td><td><span class="tag super">Super</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/errors</td><td>客户端异常报告列表</td><td><span class="tag super">Super</span></td></tr>
|
||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/feedback</td><td>意见反馈列表</td><td><span class="tag super">Super</span></td></tr>
|
||
<tr><td class="m patch">PATCH</td><td class="mono">/api/v1/admin/feedback/:id</td><td>更新反馈处理状态</td><td><span class="tag super">Super</span></td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="ops">6. 数据操作与运维</h2>
|
||
|
||
<h3>6.1 Schema 管理</h3>
|
||
<p class="gdesc"><code>backend/schema/schema.sql</code> = 新装环境的完整 DDL(docker 初始化时执行);<b>线上结构演进只靠启动时 AutoMigrate(只增不删)</b>,没有独立迁移文件。表结构变更 = 同步改 schema.sql + 对应 model。改生产库数据前<b>先备份</b>。</p>
|
||
|
||
<h3>6.2 种子数据</h3>
|
||
<pre><code>sh scripts/dev.sh seed S001 # 清空并写入 S001 全量测试数据(docker exec 注入 MySQL 容器)
|
||
sh scripts/dev.sh seed S002 # 空门店(基础数据相同,无单据/库存)</code></pre>
|
||
<p class="gdesc">每店一个 <code>backend/seeds/<shop_code>.sql</code>,文件顶部含 TRUNCATE,每次完整重建。</p>
|
||
|
||
<h3>6.3 每日备份</h3>
|
||
<p class="gdesc"><code>.gitea/workflows/backup.yml</code>:每日北京时间 02:00(cron UTC 18:00)在 mac runner(开发者本机)跑 <code>scripts/ci/backup-db.sh</code>——SSH 到阿里主库机 dump MySQL,落到本机 <code>~/jiu-db-backups</code>,Telegram 通知结果;也可 workflow_dispatch 手动触发(外网 nginx 挡 API 触发,需 Forgejo UI 操作)。</p>
|
||
|
||
<h3>6.4 一次性工具(backend/cmd/)</h3>
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:26%">工具</th><th>用途</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="mono">cmd/seed</td><td>初始化/重置测试数据(<code>--reset</code> 删表重建、<code>--clear</code> 清数据)</td></tr>
|
||
<tr><td class="mono">cmd/gencode</td><td>平台方批量生成授权兑换券写入码池:<code>go run ./cmd/gencode -type annual -days 365 -count 100 -batch 2026-summer</code></td></tr>
|
||
<tr><td class="mono">cmd/import-history</td><td>旧系统历史进销存一次性迁入(历史只读、4 列快照直存、不回放库存)</td></tr>
|
||
<tr><td class="mono">cmd/fix-inventory-products</td><td>修复历史库存导入按「名称|系列|规格」错误合并 product 的数据(按快照编号重指)</td></tr>
|
||
<tr><td class="mono">cmd/backfill-pinyin</td><td>为存量 products 补 name_pinyin/name_initials(现启动已自动兜底)</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
|
||
<h3>6.5 生产环境配置</h3>
|
||
<p class="gdesc">生产 env 在 <code>/opt/jiu/config/production.env</code>(systemd EnvironmentFile 加载):<code>SERVER_PORT=8081</code>、<code>DATABASE_DSN</code>、<code>JWT_SECRET</code>(生产必须非默认值否则拒绝启动)、CORS origin(生产禁 <code>*</code>)。密钥不入 git,需要时从 Bitwarden 取。</p>
|
||
|
||
<!-- ============================================================ -->
|
||
<h2 id="workflow">7. 开发环境与工作流</h2>
|
||
|
||
<h3>7.1 本地开发</h3>
|
||
<pre><code># 首次:起 MySQL 容器(127.0.0.1:3306 + Adminer :8888)
|
||
docker compose -f deploy/docker-compose.yml up -d
|
||
|
||
# 一键起前后端(后端代码未变则跳过重启;--force 强制)
|
||
sh scripts/dev.sh run
|
||
sh scripts/dev.sh stop
|
||
|
||
# 测试数据 + 账号(门店编号 S001,密码均 password123)
|
||
sh scripts/dev.sh seed S001 # admin=管理员 operator=操作员 test=只读</code></pre>
|
||
<p class="gdesc">Go 命令前须 <code>export PATH="/opt/homebrew/bin:$PATH"</code>(dev.sh 已内置)。发版前自测「指向线上后端」的 macOS 包:<code>sh scripts/local_test.sh</code>(BASE_URL 固定生产域名,版本取最新 client tag patch+1)。</p>
|
||
|
||
<h3>7.2 完成标准(DoD 门禁)</h3>
|
||
<pre><code># 后端
|
||
cd backend && go build ./... && go vet ./... && go test ./...
|
||
|
||
# 前端
|
||
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings && flutter test
|
||
|
||
# 设计系统闸(改了 UI 时)
|
||
cd client && node tool/check_ds_code.mjs --changed # 禁硬编码色
|
||
node tools/fidelity.mjs # golden vs 原型像素闸(仓库根)</code></pre>
|
||
<div class="callout danger">任一项失败禁止提交、禁止打 tag 发版。通用检查:多租户隔离未破坏(查询都带 shop_id)、schema.sql 与 model 同步、新接口有测试覆盖。</div>
|
||
|
||
<h3>7.3 Git 提交规范</h3>
|
||
<pre><code>{类型}({模块}): {简短说明} # 类型: feat|fix|test|docs|chore|security|refactor
|
||
# 模块: backend|client|db|deploy|docs
|
||
feat(backend): 新增财务报表接口
|
||
docs(api): 财务报表 API 接口文档</code></pre>
|
||
|
||
<h3>7.4 三条发版流水线</h3>
|
||
<p class="gdesc">client / site / server 三条互不影响的流水线,各有 tag 前缀、独立版本序列、独立 CHANGELOG,由 Forgejo CI 按 tag 前缀路由(<code>.gitea/workflows/deploy-*.yml</code> + <code>scripts/ci/</code>)。用 <code>/release <part> [version]</code>:本地 build → test → 更新 CHANGELOG → commit → tag → push;省略版本则自增最新 tag 的 patch。回滚用 <code>manual.yml</code> 输入带前缀 tag,从 Forgejo Release 下载产物重部署。</p>
|
||
|
||
<div class="svgwrap">
|
||
<svg viewBox="0 0 900 400" role="img" aria-label="发版流水线拓扑">
|
||
<defs>
|
||
<marker id="ar4" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||
</marker>
|
||
</defs>
|
||
<g font-size="11.5">
|
||
<!-- lane labels -->
|
||
<rect x="20" y="20" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="75" y="37" text-anchor="middle" font-weight="700" fill="#154072">client-v*</text>
|
||
<text x="75" y="52" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-client</text>
|
||
<rect x="20" y="230" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="75" y="247" text-anchor="middle" font-weight="700" fill="#154072">site-v*</text>
|
||
<text x="75" y="262" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-site</text>
|
||
<rect x="20" y="310" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="75" y="327" text-anchor="middle" font-weight="700" fill="#154072">server-v*</text>
|
||
<text x="75" y="342" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-server</text>
|
||
|
||
<!-- client lane: workflow box -->
|
||
<rect x="160" y="20" width="560" height="180" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||
<text x="440" y="40" text-anchor="middle" font-weight="700" fill="#154072">deploy-client.yml — 五端构建矩阵</text>
|
||
<!-- mac serial chain -->
|
||
<text x="180" y="63" fill="#6E7888" font-size="10.5">mac runner(本机,容量 1,串行链):</text>
|
||
<rect x="180" y="72" width="118" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="239" y="93" text-anchor="middle" fill="#2E8B57">build-client-web</text>
|
||
<rect x="322" y="72" width="100" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="372" y="93" text-anchor="middle" fill="#2E8B57">build-macos</text>
|
||
<rect x="446" y="72" width="108" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="500" y="93" text-anchor="middle" fill="#2E8B57">build-android</text>
|
||
<rect x="578" y="72" width="90" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="623" y="93" text-anchor="middle" fill="#2E8B57">build-ios</text>
|
||
<line x1="298" y1="89" x2="320" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<line x1="422" y1="89" x2="444" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<line x1="554" y1="89" x2="576" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<!-- windows parallel -->
|
||
<text x="180" y="130" fill="#6E7888" font-size="10.5">windows runner(并行):</text>
|
||
<rect x="180" y="138" width="150" height="34" rx="5" fill="#FFF4E5" stroke="#B45309"/><text x="255" y="159" text-anchor="middle" fill="#B45309">build-windows (Inno)</text>
|
||
<!-- release job -->
|
||
<rect x="520" y="138" width="180" height="40" rx="5" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="610" y="155" text-anchor="middle" fill="#154072" font-weight="700">release-deploy-client</text>
|
||
<text x="610" y="170" text-anchor="middle" fill="#6E7888" font-size="10">收齐产物 → Release → 部署</text>
|
||
<line x1="623" y1="106" x2="617" y2="136" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<line x1="330" y1="155" x2="518" y2="157" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<text x="440" y="192" text-anchor="middle" fill="#6E7888" font-size="10">Android 需 ANDROID_* / iOS TestFlight 需 IOS_*·APPSTORE_* secrets,未配置优雅跳过</text>
|
||
<line x1="130" y1="40" x2="158" y2="40" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
|
||
<!-- site lane -->
|
||
<rect x="160" y="230" width="560" height="44" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||
<text x="440" y="257" text-anchor="middle" fill="#154072">deploy-site.yml — Eleventy 构建 → rsync /opt/jiu/marketing(nginx 根路径)</text>
|
||
<line x1="130" y1="250" x2="158" y2="250" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
|
||
<!-- server lane -->
|
||
<rect x="160" y="310" width="560" height="44" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||
<text x="440" y="337" text-anchor="middle" fill="#154072">deploy-server.yml — go build+test → Release → 部署 jiu-server + nginx/systemd</text>
|
||
<line x1="130" y1="330" x2="158" y2="330" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
|
||
<!-- deploy target -->
|
||
<rect x="750" y="130" width="130" height="130" rx="8" fill="#E6F3EC" stroke="#2E8B57" stroke-width="1.5"/>
|
||
<text x="815" y="170" text-anchor="middle" font-weight="700" fill="#2E8B57">阿里云主机</text>
|
||
<text x="815" y="190" text-anchor="middle" fill="#2E8B57" font-size="10.5">/opt/jiu/*</text>
|
||
<text x="815" y="207" text-anchor="middle" fill="#2E8B57" font-size="10.5">Forgejo Release</text>
|
||
<line x1="700" y1="158" x2="748" y2="175" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<line x1="720" y1="252" x2="748" y2="225" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<line x1="720" y1="332" x2="748" y2="250" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
<!-- telegram -->
|
||
<rect x="750" y="290" width="130" height="40" rx="8" fill="#F0F4FF" stroke="#2563AC"/>
|
||
<text x="815" y="315" text-anchor="middle" fill="#154072">Telegram 通知</text>
|
||
<line x1="815" y1="260" x2="815" y2="288" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="card"><table>
|
||
<thead><tr><th style="width:12%">part</th><th style="width:34%">范围</th><th style="width:14%">tag 前缀</th><th>要点</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>client</b></td><td class="mono">client/ 全平台 + version.yaml</td><td class="mono">client-v*</td><td>version.yaml 归 client;后端 /version 与 /public/release <b>每请求实时读</b>——client 发版立即生效,不重启后端、不重建官网</td></tr>
|
||
<tr><td><b>site</b></td><td class="mono">web/ 营销站(不含 Web 版 app)</td><td class="mono">site-v*</td><td>官网下载页运行时 fetch /public/release 动态刷新版本徽章/链接/更新日志</td></tr>
|
||
<tr><td><b>server</b></td><td class="mono">backend/ + nginx/systemd 基建</td><td class="mono">server-v*</td><td>nginx conf、jiu.service 归 server 流水线</td></tr>
|
||
</tbody>
|
||
</table></div>
|
||
<pre><code>/release client 1.1.1 # 指定版本
|
||
/release server # 省略则自增最新 server-v* tag 的 patch</code></pre>
|
||
<p class="gdesc">CI 脚本在 <code>scripts/ci/</code>:<code>lib-forgejo.sh</code>(公共函数)+ <code>compile-{client-web,site,backend,macos,android,ios,windows}.sh</code> + <code>release-*.sh</code> + <code>deploy-*.sh</code>。runner:mac = 开发者本机(launchd 看门狗)、windows = 另一台机(nssm)、Forgejo 在 NAS(git.51yanmei.com)。</p>
|
||
|
||
<hr style="border:none;border-top:1px solid var(--border);margin:36px 0 12px">
|
||
<div class="sub">相关文档:<a href="../db-schema.html">数据库 Schema(列级)</a> · <a href="../context/project.md">项目上下文 project.md</a> · <a href="../index.html">文档索引</a></div>
|
||
</main>
|
||
</body>
|
||
</html>
|