iloom/docs/architecture.html
Chever John 9c39c8cbd7
init: iloom WMS monorepo with K8s deployment manifests
Go workspace (go.work) with 5 microservices + shared library:
- gateway (8080), auth-service (8081), purchaser-service (8082)
- textile-service (8083), washing-service (8084)
- shared: proto definitions, common packages

Infrastructure: docker-compose for local dev, K8s manifests for
K3s cluster deployment (mysql/redis/etcd + traefik ingress).

Frontend (iloom-flatten) added as git submodule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 11:33:31 +08:00

1564 lines
66 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iloom × muyu-apiserver 系统架构图</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0e27;
color: #e0e6ed;
min-height: 100vh;
overflow-x: hidden;
}
/* ── Header ── */
.header {
text-align: center;
padding: 32px 20px 16px;
background: linear-gradient(180deg, #111640 0%, #0a0e27 100%);
border-bottom: 1px solid rgba(99,102,241,0.2);
}
.header h1 {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #818cf8, #6ee7b7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 6px;
}
.header p { font-size: 14px; color: #64748b; }
/* ── Legend ── */
.legend {
display: flex;
justify-content: center;
gap: 24px;
padding: 12px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #94a3b8;
}
.legend-dot {
width: 12px; height: 12px;
border-radius: 50%;
}
.legend-line {
width: 32px; height: 3px;
border-radius: 2px;
}
/* ── Canvas ── */
.canvas {
position: relative;
max-width: 1440px;
margin: 0 auto;
padding: 20px;
}
/* ── SVG Connections ── */
.connections-svg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 1;
}
.conn-line {
fill: none;
stroke-width: 2;
opacity: 0.5;
transition: opacity 0.3s, stroke-width 0.3s;
}
.conn-line.highlight {
opacity: 1;
stroke-width: 3;
}
.flow-particle {
r: 4;
opacity: 0.9;
}
/* ── Grid Layout ── */
.arch-grid {
display: grid;
grid-template-columns: 200px 1fr 320px;
grid-template-rows: auto auto auto;
gap: 20px;
position: relative;
z-index: 2;
min-height: 800px;
}
/* ── Service Card ── */
.service-card {
background: rgba(30, 35, 60, 0.85);
border: 1px solid rgba(99, 102, 241, 0.15);
border-radius: 14px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
backdrop-filter: blur(8px);
}
.service-card:hover {
border-color: rgba(99, 102, 241, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.15);
}
.service-card.active {
border-color: #818cf8;
box-shadow: 0 0 24px rgba(99, 102, 241, 0.3);
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.card-icon {
width: 36px; height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: #e2e8f0;
}
.card-port {
font-size: 11px;
color: #64748b;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.card-tag {
display: inline-block;
font-size: 10px;
padding: 2px 8px;
border-radius: 20px;
font-weight: 500;
margin-top: 4px;
}
/* ── Color Themes ── */
.theme-frontend .card-icon { background: rgba(251,191,36,0.15); color: #fbbf24; }
.theme-frontend { border-color: rgba(251,191,36,0.2); }
.theme-frontend:hover { border-color: rgba(251,191,36,0.5); }
.theme-gateway .card-icon { background: rgba(129,140,248,0.15); color: #818cf8; }
.theme-gateway { border-color: rgba(129,140,248,0.2); }
.theme-iloom .card-icon { background: rgba(52,211,153,0.15); color: #34d399; }
.theme-iloom { border-color: rgba(52,211,153,0.2); }
.theme-iloom:hover { border-color: rgba(52,211,153,0.5); }
.theme-muyu .card-icon { background: rgba(251,113,133,0.15); color: #fb7185; }
.theme-muyu { border-color: rgba(251,113,133,0.2); }
.theme-muyu:hover { border-color: rgba(251,113,133,0.5); }
.theme-db .card-icon { background: rgba(96,165,250,0.15); color: #60a5fa; }
.theme-db { border-color: rgba(96,165,250,0.2); }
.theme-db:hover { border-color: rgba(96,165,250,0.5); }
.theme-infra .card-icon { background: rgba(168,85,247,0.15); color: #a855f7; }
.theme-infra { border-color: rgba(168,85,247,0.2); }
/* ── Section Titles ── */
.section-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
padding: 8px 0;
margin-bottom: 8px;
}
.section-title.iloom { color: #34d399; border-bottom: 2px solid rgba(52,211,153,0.3); }
.section-title.muyu { color: #fb7185; border-bottom: 2px solid rgba(251,113,133,0.3); }
.section-title.data { color: #60a5fa; border-bottom: 2px solid rgba(96,165,250,0.3); }
/* ── Zone Boxes ── */
.zone {
border: 1.5px dashed rgba(99,102,241,0.2);
border-radius: 16px;
padding: 16px;
position: relative;
}
.zone-label {
position: absolute;
top: -10px; left: 16px;
background: #0a0e27;
padding: 0 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
}
.zone.iloom { border-color: rgba(52,211,153,0.25); }
.zone.iloom .zone-label { color: #34d399; }
.zone.muyu { border-color: rgba(251,113,133,0.25); }
.zone.muyu .zone-label { color: #fb7185; }
.zone.db { border-color: rgba(96,165,250,0.25); }
.zone.db .zone-label { color: #60a5fa; }
/* ── Detail Panel ── */
.detail-panel {
position: fixed;
right: -480px;
top: 0;
width: 460px;
height: 100vh;
background: #111640;
border-left: 1px solid rgba(99,102,241,0.2);
z-index: 100;
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
padding: 24px;
}
.detail-panel.open { right: 0; }
.detail-panel .close-btn {
position: absolute;
top: 16px; right: 16px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: #94a3b8;
width: 32px; height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.detail-panel .close-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }
.detail-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.detail-subtitle {
font-size: 13px;
color: #64748b;
margin-bottom: 20px;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h3 {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: #818cf8;
margin-bottom: 10px;
}
.endpoint-list {
list-style: none;
font-size: 12px;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.endpoint-list li {
padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
display: flex;
align-items: center;
gap: 8px;
}
.method {
display: inline-block;
padding: 1px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
min-width: 38px;
text-align: center;
}
.method.get { background: rgba(52,211,153,0.15); color: #34d399; }
.method.post { background: rgba(251,191,36,0.15); color: #fbbf24; }
.method.put { background: rgba(96,165,250,0.15); color: #60a5fa; }
.method.delete { background: rgba(251,113,133,0.15); color: #fb7185; }
.method.patch { background: rgba(168,85,247,0.15); color: #a855f7; }
.method.grpc { background: rgba(129,140,248,0.15); color: #818cf8; }
.method.ws { background: rgba(6,182,212,0.15); color: #06b6d4; }
.table-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 11px;
font-family: 'SF Mono', 'Fira Code', monospace;
margin: 2px;
}
.table-badge.ilm { background: rgba(52,211,153,0.1); color: #34d399; border: 1px solid rgba(52,211,153,0.2); }
.table-badge.sys { background: rgba(251,113,133,0.1); color: #fb7185; border: 1px solid rgba(251,113,133,0.2); }
.table-badge.inv { background: rgba(251,191,36,0.1); color: #fbbf24; border: 1px solid rgba(251,191,36,0.2); }
.flow-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
margin: 2px;
}
.flow-badge.read { background: rgba(52,211,153,0.1); color: #6ee7b7; }
.flow-badge.write { background: rgba(251,113,133,0.1); color: #fca5a5; }
.flow-badge.grpc { background: rgba(129,140,248,0.1); color: #a5b4fc; }
.flow-badge.http { background: rgba(251,191,36,0.1); color: #fcd34d; }
/* ── Overlay ── */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 99;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.overlay.show { opacity: 1; pointer-events: auto; }
/* ── Animations ── */
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@keyframes flowDash {
to { stroke-dashoffset: -20; }
}
.animated-flow {
stroke-dasharray: 8 6;
animation: flowDash 0.8s linear infinite;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.service-card { animation: fadeIn 0.5s ease both; }
.service-card:nth-child(1) { animation-delay: 0.05s; }
.service-card:nth-child(2) { animation-delay: 0.1s; }
.service-card:nth-child(3) { animation-delay: 0.15s; }
.service-card:nth-child(4) { animation-delay: 0.2s; }
.service-card:nth-child(5) { animation-delay: 0.25s; }
/* ── Sub layout ── */
.services-col { display: flex; flex-direction: column; gap: 12px; }
.frontend-col {
grid-row: 1 / 3;
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 40px;
}
.iloom-zone { grid-column: 2; grid-row: 1 / 2; }
.muyu-zone { grid-column: 3; grid-row: 1 / 2; }
.db-zone { grid-column: 1 / 4; grid-row: 3; }
.iloom-services {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.iloom-gateway { grid-column: 1 / 3; }
.muyu-services { display: flex; flex-direction: column; gap: 12px; }
.db-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
/* ── Flow Arrows between zones ── */
.flow-label {
position: absolute;
font-size: 10px;
font-weight: 600;
padding: 3px 10px;
border-radius: 12px;
white-space: nowrap;
z-index: 5;
pointer-events: none;
}
/* ── Data flow diagram ── */
.dataflow-section {
max-width: 1440px;
margin: 40px auto;
padding: 0 20px;
}
.dataflow-title {
font-size: 22px;
font-weight: 700;
text-align: center;
margin-bottom: 24px;
background: linear-gradient(135deg, #818cf8, #6ee7b7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.flow-diagram {
display: flex;
flex-direction: column;
gap: 2px;
background: rgba(30,35,60,0.5);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(99,102,241,0.15);
}
.flow-row {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.flow-row:last-child { border-bottom: none; }
.flow-row-header {
display: contents;
}
.flow-row-head {
font-weight: 700;
color: #64748b;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
}
.flow-from {
width: 160px;
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
}
.flow-arrow {
width: 80px;
flex-shrink: 0;
text-align: center;
position: relative;
}
.flow-arrow::before {
content: '';
position: absolute;
top: 50%;
left: 10px;
right: 10px;
height: 2px;
border-radius: 1px;
}
.flow-arrow::after {
content: '';
position: absolute;
top: 50%; right: 8px;
width: 0; height: 0;
border-left: 6px solid;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
transform: translateY(-50%);
}
.flow-arrow.green::before { background: #34d399; }
.flow-arrow.green::after { border-left-color: #34d399; }
.flow-arrow.pink::before { background: #fb7185; }
.flow-arrow.pink::after { border-left-color: #fb7185; }
.flow-arrow.blue::before { background: #60a5fa; }
.flow-arrow.blue::after { border-left-color: #60a5fa; }
.flow-arrow.purple::before { background: #a855f7; }
.flow-arrow.purple::after { border-left-color: #a855f7; }
.flow-arrow.yellow::before { background: #fbbf24; }
.flow-arrow.yellow::after { border-left-color: #fbbf24; }
.flow-to {
width: 160px;
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
}
.flow-desc {
flex: 1;
font-size: 12px;
color: #94a3b8;
padding-left: 16px;
}
.flow-protocol {
width: 70px;
flex-shrink: 0;
text-align: center;
}
/* ── JWT Flow ── */
.jwt-flow {
display: flex;
align-items: stretch;
gap: 0;
margin: 32px 0;
overflow-x: auto;
padding-bottom: 10px;
}
.jwt-step {
flex: 1;
min-width: 180px;
background: rgba(30,35,60,0.85);
border: 1px solid rgba(99,102,241,0.15);
border-radius: 12px;
padding: 16px;
position: relative;
text-align: center;
}
.jwt-step:not(:last-child)::after {
content: '→';
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: #818cf8;
z-index: 3;
}
.jwt-step-num {
width: 28px; height: 28px;
border-radius: 50%;
background: rgba(129,140,248,0.2);
color: #818cf8;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
margin: 0 auto 8px;
}
.jwt-step-title {
font-size: 13px;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 6px;
}
.jwt-step-desc {
font-size: 11px;
color: #64748b;
line-height: 1.5;
}
.jwt-step code {
background: rgba(129,140,248,0.1);
padding: 1px 5px;
border-radius: 4px;
font-size: 10px;
color: #a5b4fc;
}
/* ── Tables section ── */
.tables-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 16px;
}
.table-group {
background: rgba(30,35,60,0.6);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(99,102,241,0.1);
}
.table-group h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.table-group ul {
list-style: none;
font-size: 11px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: #94a3b8;
}
.table-group ul li {
padding: 3px 0;
display: flex;
align-items: center;
gap: 6px;
}
.table-group ul li::before {
content: '';
width: 6px; height: 6px;
border-radius: 2px;
flex-shrink: 0;
}
.table-group.ilm ul li::before { background: #34d399; }
.table-group.sys ul li::before { background: #fb7185; }
.table-group.inv ul li::before { background: #fbbf24; }
/* ── Responsive: Tablet ── */
@media (max-width: 1100px) {
.arch-grid {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
min-height: auto;
}
.frontend-col {
grid-row: auto;
grid-column: 1 / 3;
flex-direction: row;
padding-top: 0;
gap: 12px;
}
.frontend-col .service-card { flex: 1; }
.iloom-zone { grid-column: 1; grid-row: auto; }
.muyu-zone { grid-column: 2; grid-row: auto; }
.db-zone { grid-column: 1 / 3; grid-row: auto; }
.db-grid { grid-template-columns: 1fr 1fr; }
.detail-panel { width: 420px; right: -420px; }
}
/* ── Responsive: Mobile ── */
@media (max-width: 768px) {
html { scroll-behavior: smooth; }
body { -webkit-text-size-adjust: 100%; }
.header { padding: 20px 16px 12px; }
.header h1 { font-size: 20px; }
.header p { font-size: 12px; }
.legend {
gap: 10px 16px;
padding: 10px 16px;
justify-content: flex-start;
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.legend::-webkit-scrollbar { display: none; }
.legend-item { flex-shrink: 0; font-size: 11px; }
.canvas { padding: 12px; }
/* ── Hide SVG on mobile ── */
.connections-svg { display: none; }
.arch-grid {
display: flex;
flex-direction: column;
gap: 16px;
min-height: auto;
}
.frontend-col {
flex-direction: row;
gap: 10px;
padding-top: 0;
}
.frontend-col .service-card { flex: 1; min-width: 0; }
.zone { padding: 12px; border-radius: 12px; }
.zone-label { font-size: 10px; top: -9px; }
.iloom-services {
grid-template-columns: 1fr;
}
.iloom-gateway { grid-column: 1; }
.service-card {
padding: 12px;
border-radius: 10px;
min-height: 44px;
}
.card-header { gap: 8px; margin-bottom: 6px; }
.card-icon { width: 32px; height: 32px; font-size: 16px; border-radius: 8px; }
.card-title { font-size: 13px; }
.card-port { font-size: 10px; }
.card-tag { font-size: 9px; padding: 2px 6px; }
.db-grid { grid-template-columns: 1fr; gap: 10px; }
.table-group { padding: 12px; }
.table-group h4 { font-size: 12px; }
/* ── Mobile flow arrows between zones ── */
.zone::after {
content: '▼';
display: block;
text-align: center;
color: rgba(99,102,241,0.3);
font-size: 20px;
margin-top: 8px;
}
.zone:last-child::after,
.db-zone::after { display: none; }
/* ── Detail Panel: Bottom Sheet ── */
.detail-panel {
width: 100%;
height: 85vh;
max-height: 85vh;
top: auto;
bottom: 0;
right: 0;
left: 0;
transform: translateY(100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 20px 20px 0 0;
border-left: none;
border-top: 1px solid rgba(99,102,241,0.3);
padding: 16px;
padding-top: 8px;
}
.detail-panel.open {
transform: translateY(0);
right: 0;
}
.detail-panel::before {
content: '';
display: block;
width: 40px;
height: 4px;
background: rgba(255,255,255,0.2);
border-radius: 2px;
margin: 0 auto 12px;
}
.detail-panel .close-btn {
top: 12px; right: 12px;
width: 36px; height: 36px;
}
.detail-title { font-size: 17px; padding-right: 44px; }
.detail-subtitle { font-size: 12px; margin-bottom: 14px; }
.detail-section { margin-bottom: 14px; }
.endpoint-list { font-size: 11px; }
.endpoint-list li {
padding: 6px 0;
flex-wrap: wrap;
gap: 4px 8px;
line-height: 1.5;
}
.method { font-size: 9px; min-width: 32px; padding: 1px 4px; }
.table-badge { font-size: 10px; padding: 2px 6px; }
.flow-badge { font-size: 10px; padding: 2px 8px; }
/* ── Dataflow section ── */
.dataflow-section { padding: 0 12px; margin: 24px auto; }
.dataflow-title { font-size: 18px; margin-bottom: 16px; }
/* ── JWT Flow: Vertical ── */
.jwt-flow {
flex-direction: column;
gap: 8px;
padding-bottom: 0;
}
.jwt-step {
min-width: auto;
display: flex;
align-items: flex-start;
gap: 12px;
text-align: left;
padding: 12px;
}
.jwt-step:not(:last-child)::after {
content: '↓';
position: static;
transform: none;
font-size: 16px;
color: #818cf8;
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
}
.jwt-step-num {
margin: 0;
flex-shrink: 0;
width: 32px;
height: 32px;
}
.jwt-step-body { flex: 1; min-width: 0; }
.jwt-step-title { font-size: 13px; margin-bottom: 4px; }
.jwt-step-desc { font-size: 11px; }
.jwt-step code { font-size: 9px; word-break: break-all; }
/* ── Flow Diagram: Card layout ── */
.flow-diagram { padding: 12px; border-radius: 12px; gap: 0; }
.flow-row {
flex-direction: column;
align-items: stretch;
padding: 0;
gap: 0;
border-bottom: none;
}
.flow-row-head { display: none; }
.flow-row-header {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.flow-from, .flow-to {
width: auto;
font-size: 12px;
}
.flow-arrow {
width: 32px;
height: auto;
flex-shrink: 0;
}
.flow-arrow::before {
top: 50%;
left: 2px;
right: 8px;
height: 2px;
}
.flow-arrow::after {
top: 50%;
right: 2px;
left: auto;
transform: translateY(-50%);
}
.flow-protocol { width: auto; text-align: left; }
.flow-desc {
padding-left: 0;
padding-top: 4px;
font-size: 11px;
line-height: 1.5;
}
.flow-desc code { font-size: 10px; word-break: break-all; }
/* ── Flow Row as mini-card ── */
.flow-row:not(.flow-row-head) {
background: rgba(255,255,255,0.02);
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
border-bottom: none;
}
.section-title { font-size: 12px; letter-spacing: 1px; }
}
/* ── Small phones ── */
@media (max-width: 380px) {
.header h1 { font-size: 17px; }
.frontend-col { flex-direction: column; }
.card-title { font-size: 12px; }
.detail-panel { height: 90vh; max-height: 90vh; }
}
</style>
</head>
<body>
<div class="header">
<h1>iloom × muyu-apiserver 系统架构</h1>
<p>纺织供应链管理 · 微服务架构 · 点击服务卡片查看详情</p>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#fbbf24"></div>Frontend</div>
<div class="legend-item"><div class="legend-dot" style="background:#818cf8"></div>Gateway</div>
<div class="legend-item"><div class="legend-dot" style="background:#34d399"></div>iloom 服务</div>
<div class="legend-item"><div class="legend-dot" style="background:#fb7185"></div>muyu 服务</div>
<div class="legend-item"><div class="legend-dot" style="background:#60a5fa"></div>数据层</div>
<div class="legend-item"><div class="legend-line animated-flow" style="background:none;border-top:2px dashed #34d399;"></div>HTTP</div>
<div class="legend-item"><div class="legend-line" style="background:#818cf8;"></div>gRPC</div>
<div class="legend-item"><div class="legend-line animated-flow" style="background:none;border-top:2px dashed #60a5fa;"></div>SQL</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- MAIN ARCHITECTURE DIAGRAM -->
<!-- ════════════════════════════════════════ -->
<div class="canvas" id="canvas">
<svg class="connections-svg" id="connSvg"></svg>
<div class="arch-grid">
<!-- ── Column 1: Frontend ── -->
<div class="frontend-col">
<div class="service-card theme-frontend" data-service="frontend" id="card-frontend">
<div class="card-header">
<div class="card-icon"></div>
<div>
<div class="card-title">React SPA</div>
<div class="card-port">:3015</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,191,36,0.12);color:#fbbf24;">Frontend</div>
</div>
<div class="service-card theme-frontend" data-service="muyu-portal" id="card-muyu-portal" style="opacity:0.65">
<div class="card-header">
<div class="card-icon" style="font-size:14px">🖥</div>
<div>
<div class="card-title">muyu-portal</div>
<div class="card-port">:8080 (Nginx)</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">muyu 前端</div>
</div>
</div>
<!-- ── Column 2: iloom Services ── -->
<div class="zone iloom" id="zone-iloom">
<span class="zone-label">iloom 微服务集群</span>
<div class="iloom-services">
<div class="service-card theme-gateway iloom-gateway" data-service="gateway" id="card-gateway">
<div class="card-header">
<div class="card-icon">🔀</div>
<div>
<div class="card-title">API Gateway</div>
<div class="card-port">:8080</div>
</div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">JWT 验证</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">CORS</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">路由分发</div>
<div class="card-tag" style="background:rgba(129,140,248,0.12);color:#818cf8;">限流</div>
</div>
</div>
<div class="service-card theme-iloom" data-service="auth" id="card-auth">
<div class="card-header">
<div class="card-icon">🔐</div>
<div>
<div class="card-title">Auth Service</div>
<div class="card-port">:8081</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">认证 · 公司 · 成员</div>
</div>
<div class="service-card theme-iloom" data-service="purchaser" id="card-purchaser">
<div class="card-header">
<div class="card-icon">🛒</div>
<div>
<div class="card-title">Purchaser Service</div>
<div class="card-port">:8082</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">采购 · 计划 · 产品</div>
</div>
<div class="service-card theme-iloom" data-service="textile" id="card-textile">
<div class="card-header">
<div class="card-icon">🧵</div>
<div>
<div class="card-title">Textile Service</div>
<div class="card-port">:8083</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">纺织 · 纱线 · 工序</div>
</div>
<div class="service-card theme-iloom" data-service="washing" id="card-washing">
<div class="card-header">
<div class="card-icon">💧</div>
<div>
<div class="card-title">Washing Service</div>
<div class="card-port">:8084</div>
</div>
</div>
<div class="card-tag" style="background:rgba(52,211,153,0.12);color:#34d399;">水洗 · 成品 · 出库</div>
</div>
</div>
</div>
<!-- ── Column 3: muyu Services ── -->
<div class="zone muyu" id="zone-muyu">
<span class="zone-label">muyu-apiserver</span>
<div class="muyu-services">
<div class="service-card theme-muyu" data-service="muyu-gateway" id="card-muyu-gw">
<div class="card-header">
<div class="card-icon">🌐</div>
<div>
<div class="card-title">muyu Gateway</div>
<div class="card-port">:8888 (REST)</div>
</div>
</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">go-zero API</div>
</div>
<div class="service-card theme-muyu" data-service="system-rpc" id="card-system-rpc">
<div class="card-header">
<div class="card-icon">👤</div>
<div>
<div class="card-title">System RPC</div>
<div class="card-port">:9001 (gRPC)</div>
</div>
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">用户</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">角色</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">菜单</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">权限</div>
</div>
</div>
<div class="service-card theme-muyu" data-service="inventory-rpc" id="card-inv-rpc">
<div class="card-header">
<div class="card-icon">📦</div>
<div>
<div class="card-title">Inventory RPC</div>
<div class="card-port">:9002 (gRPC)</div>
</div>
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">产品</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">库存</div>
<div class="card-tag" style="background:rgba(251,113,133,0.12);color:#fb7185;">盘点</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<div class="service-card theme-infra" style="flex:1" id="card-redis">
<div class="card-header">
<div class="card-icon" style="width:28px;height:28px;font-size:14px;"></div>
<div>
<div class="card-title" style="font-size:12px;">Redis</div>
<div class="card-port">:6379</div>
</div>
</div>
</div>
<div class="service-card theme-infra" style="flex:1" id="card-etcd">
<div class="card-header">
<div class="card-icon" style="width:28px;height:28px;font-size:14px;">🔧</div>
<div>
<div class="card-title" style="font-size:12px;">etcd</div>
<div class="card-port">:2379</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Row 3: Database ── -->
<div class="zone db db-zone" id="zone-db">
<span class="zone-label">MySQL 8.0 · muyu_wms · :3306</span>
<div class="db-grid">
<div class="table-group ilm">
<h4 style="color:#34d399;">ilm_* 表 (iloom 拥有 · 直接 SQL 读写)</h4>
<ul>
<li>ilm_company <span style="color:#475569">— 公司</span></li>
<li>ilm_company_member <span style="color:#475569">— 成员</span></li>
<li>ilm_company_relationship <span style="color:#475569">— 公司关系</span></li>
<li>ilm_product_ext <span style="color:#475569">— 产品扩展</span></li>
<li>ilm_production_plan <span style="color:#475569">— 生产计划</span></li>
<li>ilm_plan_factory <span style="color:#475569">— 计划工厂</span></li>
<li>ilm_yarn_ratio <span style="color:#475569">— 纱线配比</span></li>
<li>ilm_process_step <span style="color:#475569">— 生产工序</span></li>
<li>ilm_warehouse <span style="color:#475569">— 仓库</span></li>
<li>ilm_inventory_record <span style="color:#475569">— 库存记录</span></li>
</ul>
</div>
<div class="table-group ilm">
<h4 style="color:#34d399;">ilm_* 表 (续)</h4>
<ul>
<li>ilm_product_inventory <span style="color:#475569">— 产品出入库</span></li>
<li>ilm_product_outbound <span style="color:#475569">— 产品出库</span></li>
<li>ilm_washing_plan <span style="color:#475569">— 水洗计划</span></li>
<li>ilm_washing_step <span style="color:#475569">— 水洗工序</span></li>
<li>ilm_washing_completion <span style="color:#475569">— 水洗完工</span></li>
<li>ilm_finished_product <span style="color:#475569">— 成品</span></li>
<li>ilm_finished_product_inventory <span style="color:#475569">— 成品出入库</span></li>
<li>ilm_yarn_stock <span style="color:#475569">— 纱线库存</span></li>
<li>ilm_yarn_stock_record <span style="color:#475569">— 纱线记录</span></li>
<li>ilm_accounts_payable <span style="color:#475569">— 应付账款</span></li>
<li>ilm_ap_item <span style="color:#475569">— 账款明细</span></li>
<li>ilm_payment <span style="color:#475569">— 收付款</span></li>
<li>ilm_notification <span style="color:#475569">— 通知</span></li>
<li>ilm_share_link <span style="color:#475569">— 分享链接</span></li>
<li>ilm_audit_log <span style="color:#475569">— 审计日志</span></li>
<li>ilm_product_price_history <span style="color:#475569">— 产品价格历史</span></li>
<li>ilm_production_price_history <span style="color:#475569">— 生产价格历史</span></li>
</ul>
</div>
<div class="table-group sys">
<h4 style="color:#fb7185;">sys_* / inv_* 表 (muyu 拥有 · 只读, 写走 gRPC)</h4>
<ul>
<li>sys_user <span style="color:#475569">— 用户</span></li>
<li>sys_role <span style="color:#475569">— 角色</span></li>
<li>sys_user_role <span style="color:#475569">— 用户角色绑定</span></li>
<li>sys_menu <span style="color:#475569">— 菜单</span></li>
<li>sys_role_menu <span style="color:#475569">— 角色菜单权限</span></li>
<li>sys_operation_log <span style="color:#475569">— 操作日志</span></li>
<li>sys_config <span style="color:#475569">— 系统配置</span></li>
<li style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06);">inv_product <span style="color:#475569">— 产品</span></li>
<li>inv_stock_check <span style="color:#475569">— 盘点</span></li>
<li>inv_stock_adjust <span style="color:#475569">— 调整</span></li>
<li style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06);">casbin_rule <span style="color:#475569">— RBAC 策略</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- DATA FLOW DIAGRAM -->
<!-- ════════════════════════════════════════ -->
<div class="dataflow-section">
<div class="dataflow-title">数据流向 · 接口调用一览</div>
<!-- JWT Flow -->
<div style="margin-bottom:32px;">
<div class="section-title iloom" style="text-align:center;">JWT 认证流程</div>
<div class="jwt-flow">
<div class="jwt-step">
<div class="jwt-step-num">1</div>
<div class="jwt-step-body">
<div class="jwt-step-title">用户登录</div>
<div class="jwt-step-desc">Frontend → Gateway<br><code>POST /api/v1/auth/login</code><br>{username, password}</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">2</div>
<div class="jwt-step-body">
<div class="jwt-step-title">gRPC 验证</div>
<div class="jwt-step-desc">Auth Service → System RPC<br><code>SystemService.Login()</code><br>muyu 验证密码</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">3</div>
<div class="jwt-step-body">
<div class="jwt-step-title">查询公司</div>
<div class="jwt-step-desc">Auth Service → MySQL<br><code>sys_user JOIN ilm_company_member</code><br>获取 company_id, role</div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">4</div>
<div class="jwt-step-body">
<div class="jwt-step-title">签发 Token</div>
<div class="jwt-step-desc">JWT Claims:<br><code>{uid, cid, role, master, uname}</code><br>Secret: <code>muyu-wms-jwt-secret-key-2026</code></div>
</div>
</div>
<div class="jwt-step">
<div class="jwt-step-num">5</div>
<div class="jwt-step-body">
<div class="jwt-step-title">请求鉴权</div>
<div class="jwt-step-desc">Gateway 验证 JWT → 注入 Headers<br><code>X-User-ID</code> <code>X-Company-ID</code><br><code>X-User-Role</code> <code>X-Is-Master</code></div>
</div>
</div>
</div>
</div>
<!-- Main Data Flow -->
<div class="section-title iloom" style="text-align:center;">服务间调用 · 数据流向</div>
<div class="flow-diagram">
<div class="flow-row flow-row-head">
<div class="flow-from">调用方</div>
<div class="flow-arrow" style="visibility:hidden"></div>
<div class="flow-to">目标</div>
<div class="flow-protocol">协议</div>
<div class="flow-desc">说明</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#fbbf24;">React SPA</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#818cf8;">Gateway :8080</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">所有 API 请求 + WebSocket 升级,<code>Authorization: Bearer &lt;token&gt;</code></div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Auth Service</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/auth/*</code>, <code>/api/v1/common/*</code> → 反向代理到 :8081</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/purchaser/*</code> → :8082 (role=purchaser)</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Textile Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/textile/*</code> → :8083 (role=textile)</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#818cf8;">Gateway</div>
<div class="flow-arrow green"></div>
<div class="flow-to" style="color:#34d399;">Washing Svc</div>
<div class="flow-protocol"><span class="method post">HTTP</span></div>
</div>
<div class="flow-desc"><code>/api/v1/washing/*</code> → :8084 (role=washing)</div>
</div>
<div class="flow-row" style="background:rgba(129,140,248,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Auth Service</div>
<div class="flow-arrow purple"></div>
<div class="flow-to" style="color:#fb7185;">System RPC</div>
<div class="flow-protocol"><span class="method grpc">gRPC</span></div>
</div>
<div class="flow-desc"><code>Login()</code> 密码验证, <code>CreateUser()</code> 注册, <code>GetUser()</code> 查询</div>
</div>
<div class="flow-row" style="background:rgba(129,140,248,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-arrow purple"></div>
<div class="flow-to" style="color:#fb7185;">Inventory RPC</div>
<div class="flow-protocol"><span class="method grpc">gRPC</span></div>
</div>
<div class="flow-desc"><code>CreateProduct()</code>, <code>GetProduct()</code>, <code>ListProduct()</code> 产品CRUD</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Textile Svc</div>
<div class="flow-arrow yellow"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">内部服务调用 — 查询采购商计划、工厂信息</div>
</div>
<div class="flow-row">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">Washing Svc</div>
<div class="flow-arrow yellow"></div>
<div class="flow-to" style="color:#34d399;">Purchaser Svc</div>
<div class="flow-protocol"><span class="method get">HTTP</span></div>
</div>
<div class="flow-desc">内部服务调用 — 查询采购商计划、工厂信息</div>
</div>
<div class="flow-row" style="background:rgba(96,165,250,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#34d399;">iloom 全部服务</div>
<div class="flow-arrow blue"></div>
<div class="flow-to" style="color:#60a5fa;">MySQL</div>
<div class="flow-protocol"><span class="method" style="background:rgba(96,165,250,0.15);color:#60a5fa;">SQL</span></div>
</div>
<div class="flow-desc"><code>ilm_*</code> 表直接读写;<code>sys_user</code> 只读查询 (JOIN)</div>
</div>
<div class="flow-row" style="background:rgba(96,165,250,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#fb7185;">muyu RPC 服务</div>
<div class="flow-arrow blue"></div>
<div class="flow-to" style="color:#60a5fa;">MySQL</div>
<div class="flow-protocol"><span class="method" style="background:rgba(96,165,250,0.15);color:#60a5fa;">SQL</span></div>
</div>
<div class="flow-desc"><code>sys_*</code>, <code>inv_*</code>, <code>casbin_rule</code> 全权读写</div>
</div>
<div class="flow-row" style="background:rgba(168,85,247,0.04);">
<div class="flow-row-header">
<div class="flow-from" style="color:#fb7185;">muyu RPC 服务</div>
<div class="flow-arrow" style="visibility:hidden"></div>
<div class="flow-to" style="color:#a855f7;">Redis / etcd</div>
<div class="flow-protocol"><span class="method" style="background:rgba(168,85,247,0.15);color:#a855f7;">TCP</span></div>
</div>
<div class="flow-desc">Redis: 缓存 + 会话 | etcd: 服务注册发现</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════ -->
<!-- DETAIL PANEL -->
<!-- ════════════════════════════════════════ -->
<div class="overlay" id="overlay"></div>
<div class="detail-panel" id="detailPanel">
<button class="close-btn" id="closeBtn"></button>
<div id="detailContent"></div>
</div>
<script>
// ── Service Detail Data ──
const serviceData = {
frontend: {
title: 'React SPA (iloom Frontend)',
subtitle: ':3015 · React 18 + TailwindCSS + Zustand',
sections: [
{ title: 'Tech Stack', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">React 18.3</span><span class="table-badge ilm">Webpack 5</span><span class="table-badge ilm">TailwindCSS</span><span class="table-badge ilm">Zustand</span><span class="table-badge ilm">React Router v6</span><span class="table-badge ilm">Recharts</span><span class="table-badge ilm">Framer Motion</span></div>' },
{ title: 'API 模块', content: '<ul class="endpoint-list"><li><span class="method post">AUTH</span> authApi — 登录/注册/刷新/用户信息</li><li><span class="method get">API</span> purchaserApi — 采购商全部接口</li><li><span class="method get">API</span> textileApi — 纺织厂全部接口</li><li><span class="method get">API</span> washingApi — 水洗厂全部接口</li><li><span class="method get">API</span> commonApi — 公司/成员/通知</li><li><span class="method ws">WS</span> WebSocket 实时推送</li></ul>' },
{ title: '认证机制', content: '<div style="font-size:12px;color:#94a3b8;line-height:1.8">• Access Token 存内存<br>• Refresh Token 存 Cookie (credentials: include)<br>• 401 自动刷新 + 重试<br>• Token 过期触发 auth:expired 事件 → 跳转登录</div>' }
]
},
gateway: {
title: 'API Gateway',
subtitle: ':8080 · Gin + 反向代理',
sections: [
{ title: '中间件链', content: '<ul class="endpoint-list"><li>RequestID() — 请求追踪</li><li>Logger() — 请求日志</li><li>Recovery() — Panic 恢复</li><li>CORS() — 跨域 (ALLOW_ORIGINS)</li><li>RateLimit() — 限流 100 req/s</li><li>JWTAuth() — JWT 验证 + Claims 提取</li></ul>' },
{ title: '路由规则 (JWT 验证后注入 Headers)', content: '<ul class="endpoint-list"><li><span class="method post">ANY</span>/api/v1/auth/* → auth-service:8081 <span style="color:#475569">(免 JWT)</span></li><li><span class="method post">ANY</span>/api/v1/common/* → auth-service:8081</li><li><span class="method post">ANY</span>/api/v1/purchaser/* → purchaser:8082 <span style="color:#475569">(role=purchaser)</span></li><li><span class="method post">ANY</span>/api/v1/textile/* → textile:8083 <span style="color:#475569">(role=textile)</span></li><li><span class="method post">ANY</span>/api/v1/washing/* → washing:8084 <span style="color:#475569">(role=washing)</span></li><li><span class="method ws">WS</span>/ws/v1/* → 对应服务 WebSocket</li></ul>' },
{ title: '注入的 Headers', content: '<div style="font-size:12px;font-family:monospace;color:#a5b4fc;line-height:2">X-User-ID: {claims.UserID}<br>X-Company-ID: {claims.CompanyID}<br>X-User-Role: {claims.Role}<br>X-Is-Master: {claims.IsMaster}<br>X-Username: {claims.Username}</div>' }
]
},
auth: {
title: 'Auth Service',
subtitle: ':8081 · 认证 + 公司管理 + 成员管理 + 通知',
sections: [
{ title: 'Auth 端点', content: '<ul class="endpoint-list"><li><span class="method post">POST</span>/auth/login — gRPC→SystemRPC.Login()→签发JWT</li><li><span class="method post">POST</span>/auth/register — gRPC→CreateUser()→建公司→建成员</li><li><span class="method post">POST</span>/auth/refresh — 刷新 access_token</li><li><span class="method get">GET</span>/auth/me — GetWithCompany() 用户+公司信息</li><li><span class="method post">POST</span>/auth/logout — 登出</li></ul>' },
{ title: 'Common 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/common/companies — 公司关系列表</li><li><span class="method get">GET</span>/common/companies/:id — 公司详情</li><li><span class="method get">GET</span>/common/members — 公司成员列表</li><li><span class="method post">POST</span>/common/members — gRPC→CreateUser()+建成员</li><li><span class="method delete">DEL</span>/common/members/:id — 删除成员关系</li><li><span class="method get">GET</span>/common/notifications — 通知列表</li><li><span class="method patch">PATCH</span>/common/notifications/:id — 标记已读</li></ul>' },
{ title: 'gRPC 调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ SystemRPC.Login()</span><span class="flow-badge grpc">→ SystemRPC.CreateUser()</span><span class="flow-badge grpc">→ SystemRPC.GetUser()</span></div>' },
{ title: '数据库访问', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge read">读 sys_user</span><span class="flow-badge write">写 ilm_company</span><span class="flow-badge write">写 ilm_company_member</span><span class="flow-badge read">读 ilm_company_relationship</span><span class="flow-badge write">写 ilm_notification</span></div>' }
]
},
purchaser: {
title: 'Purchaser Service',
subtitle: ':8082 · 采购商:计划 + 产品 + 应付账款',
sections: [
{ title: '计划管理', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/plans — 计划列表</li><li><span class="method post">POST</span>/plans — 创建生产计划</li><li><span class="method get">GET</span>/plans/:id — 计划详情 (含工序/库存)</li><li><span class="method put">PUT</span>/plans/:id — 更新计划</li><li><span class="method delete">DEL</span>/plans/:id — 删除计划</li><li><span class="method post">POST</span>/plans/:id/factories — 分配工厂</li><li><span class="method get">GET</span>/plans/:id/steps — 工序步骤</li><li><span class="method get">GET</span>/plans/:id/inventory — 库存记录</li></ul>' },
{ title: '产品管理', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/products — 产品列表</li><li><span class="method post">POST</span>/products — 创建产品 (gRPC→inv_product + ilm_product_ext)</li><li><span class="method put">PUT</span>/products/:id — 更新产品</li><li><span class="method post">POST</span>/products/:id/inbound — 入库</li><li><span class="method post">POST</span>/products/:id/outbound — 出库</li><li><span class="method put">PUT</span>/products/:id/price — 更新价格</li><li><span class="method get">GET</span>/products/:id/price-history — 价格历史</li></ul>' },
{ title: '其他', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘统计</li><li><span class="method get">GET</span>/factories — 工厂列表</li><li><span class="method get">GET</span>/accounts-payable — 应付账款</li><li><span class="method post">POST</span>/accounts-payable/:id/pay — 付款</li><li><span class="method post">POST</span>/washing-plans — 创建水洗计划</li><li><span class="method ws">WS</span>/ws/v1/purchaser — 实时推送</li></ul>' },
{ title: 'gRPC 调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ InventoryRPC.CreateProduct()</span><span class="flow-badge grpc">→ InventoryRPC.GetProduct()</span><span class="flow-badge grpc">→ InventoryRPC.ListProduct()</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_production_plan</span><span class="table-badge ilm">ilm_plan_factory</span><span class="table-badge ilm">ilm_yarn_ratio</span><span class="table-badge ilm">ilm_process_step</span><span class="table-badge ilm">ilm_product_ext</span><span class="table-badge ilm">ilm_product_inventory</span><span class="table-badge ilm">ilm_product_outbound</span><span class="table-badge ilm">ilm_product_price_history</span><span class="table-badge ilm">ilm_accounts_payable</span><span class="table-badge ilm">ilm_ap_item</span><span class="table-badge ilm">ilm_washing_plan</span></div>' }
]
},
textile: {
title: 'Textile Service',
subtitle: ':8083 · 纺织厂:工序 + 纱线 + 库存 + 收款',
sections: [
{ title: 'API 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘</li><li><span class="method get">GET</span>/plans — 生产计划 (只读)</li><li><span class="method get">GET</span>/plans/:id — 计划详情</li><li><span class="method post">POST</span>/plans/:id/steps/:stepId/complete — 完成工序</li><li><span class="method post">POST</span>/plans/:id/steps/:stepId/reject — 驳回工序</li><li><span class="method post">POST</span>/plans/:id/inventory — 入库记录</li><li><span class="method get">GET</span>/yarn-stock — 纱线库存</li><li><span class="method post">POST</span>/yarn-stock — 创建纱线</li><li><span class="method post">POST</span>/yarn-stock/:id/records — 纱线出入库</li><li><span class="method get">GET</span>/payments — 收款列表</li><li><span class="method post">POST</span>/payments/:id/confirm — 确认收款</li><li><span class="method ws">WS</span>/ws/v1/textile — 实时推送</li></ul>' },
{ title: '服务间调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge http">→ Purchaser :8082 (HTTP)</span></div><div style="font-size:11px;color:#64748b;margin-top:4px">查询采购商计划、工厂信息</div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_production_plan</span><span class="table-badge ilm">ilm_process_step</span><span class="table-badge ilm">ilm_inventory_record</span><span class="table-badge ilm">ilm_warehouse</span><span class="table-badge ilm">ilm_yarn_stock</span><span class="table-badge ilm">ilm_yarn_stock_record</span><span class="table-badge ilm">ilm_payment</span></div>' }
]
},
washing: {
title: 'Washing Service',
subtitle: ':8084 · 水洗厂:水洗计划 + 成品 + 出库 + 收款',
sections: [
{ title: 'API 端点', content: '<ul class="endpoint-list"><li><span class="method get">GET</span>/dashboard/stats — 仪表盘</li><li><span class="method get">GET</span>/plans — 水洗计划列表</li><li><span class="method get">GET</span>/plans/pending — 待处理计划</li><li><span class="method get">GET</span>/plans/completed — 已完成计划</li><li><span class="method get">GET</span>/plans/:id — 计划详情</li><li><span class="method patch">PATCH</span>/plans/:id/status — 更新状态</li><li><span class="method post">POST</span>/plans/:id/complete — 标记完成</li><li><span class="method get">GET</span>/finished-products — 成品列表</li><li><span class="method post">POST</span>/finished-products — 创建成品</li><li><span class="method post">POST</span>/finished-products/:id/outbound — 成品出库</li><li><span class="method get">GET</span>/payments — 收款列表</li><li><span class="method post">POST</span>/payments/:id/confirm — 确认收款</li><li><span class="method ws">WS</span>/ws/v1/washing — 实时推送</li></ul>' },
{ title: '服务间调用', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge http">→ Purchaser :8082 (HTTP)</span></div><div style="font-size:11px;color:#64748b;margin-top:4px">查询采购商计划、工厂信息</div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge ilm">ilm_washing_plan</span><span class="table-badge ilm">ilm_washing_step</span><span class="table-badge ilm">ilm_washing_completion</span><span class="table-badge ilm">ilm_finished_product</span><span class="table-badge ilm">ilm_finished_product_inventory</span><span class="table-badge ilm">ilm_payment</span></div>' }
]
},
'system-rpc': {
title: 'System RPC (muyu-apiserver)',
subtitle: ':9001 · go-zero gRPC · 用户/角色/权限管理',
sections: [
{ title: 'gRPC 方法', content: '<ul class="endpoint-list"><li><span class="method grpc">RPC</span>Login(username, password, ip) → LoginResp</li><li><span class="method grpc">RPC</span>CreateUser(username, password, realName, phone, roleID) → IdResp</li><li><span class="method grpc">RPC</span>GetUser(userID) → UserInfo</li><li><span class="method grpc">RPC</span>UpdateUser / DeleteUser / ListUser</li><li><span class="method grpc">RPC</span>UpdateUserStatus(userID, status)</li><li><span class="method grpc">RPC</span>CreateRole / GetRole / ListRole / DeleteRole</li><li><span class="method grpc">RPC</span>SetRolePermissions(roleID, menuIDs)</li><li><span class="method grpc">RPC</span>CreateMenu / GetMenu / ListMenu / DeleteMenu</li><li><span class="method grpc">RPC</span>CreateOperationLog / ListOperationLog</li></ul>' },
{ title: '调用者', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">← iloom Auth Service</span><span class="flow-badge grpc">← muyu Gateway :8888</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge sys">sys_user</span><span class="table-badge sys">sys_role</span><span class="table-badge sys">sys_user_role</span><span class="table-badge sys">sys_menu</span><span class="table-badge sys">sys_role_menu</span><span class="table-badge sys">sys_operation_log</span><span class="table-badge sys">sys_config</span><span class="table-badge sys">casbin_rule</span></div>' }
]
},
'inventory-rpc': {
title: 'Inventory RPC (muyu-apiserver)',
subtitle: ':9002 · go-zero gRPC · 产品/库存管理',
sections: [
{ title: 'gRPC 方法', content: '<ul class="endpoint-list"><li><span class="method grpc">RPC</span>CreateProduct(name, sku, ...) → IdResp</li><li><span class="method grpc">RPC</span>GetProduct(productID) → ProductInfo</li><li><span class="method grpc">RPC</span>ListProduct(page, pageSize, ...) → ListProductResp</li><li><span class="method grpc">RPC</span>UpdateProduct / DeleteProduct</li><li><span class="method grpc">RPC</span>ImportProducts(batch) → ImportResp</li><li><span class="method grpc">RPC</span>GetStockSummary / GetStockGroup</li><li><span class="method grpc">RPC</span>CreateStockCheck / ConfirmStockCheck</li><li><span class="method grpc">RPC</span>CreateStockAdjust / ApproveStockAdjust</li></ul>' },
{ title: '调用者', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">← iloom Purchaser Service</span><span class="flow-badge grpc">← muyu Gateway :8888</span></div>' },
{ title: '数据库表', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="table-badge inv">inv_product</span><span class="table-badge inv">inv_stock_check</span><span class="table-badge inv">inv_stock_adjust</span></div>' }
]
},
'muyu-gateway': {
title: 'muyu Gateway',
subtitle: ':8888 · go-zero REST API · 管理后台接口',
sections: [
{ title: '端点 (JWT + Casbin RBAC)', content: '<ul class="endpoint-list"><li><span class="method post">POST</span>/api/v1/auth/login — 登录</li><li><span class="method get">GET</span>/api/v1/auth/info — 用户信息</li><li><span class="method get">GET</span>/api/v1/system/users — 用户管理</li><li><span class="method get">GET</span>/api/v1/system/roles — 角色管理</li><li><span class="method get">GET</span>/api/v1/system/menus — 菜单管理</li><li><span class="method get">GET</span>/api/v1/inventory/products — 产品管理</li><li><span class="method get">GET</span>/api/v1/inventory/stock-check — 盘点</li><li><span class="method get">GET</span>/api/v1/inventory/stock-adjust — 调整</li></ul>' },
{ title: '下游 gRPC', content: '<div style="display:flex;gap:6px;flex-wrap:wrap"><span class="flow-badge grpc">→ System RPC :9001</span><span class="flow-badge grpc">→ Inventory RPC :9002</span></div>' }
]
}
};
// ── Detail Panel Logic ──
const panel = document.getElementById('detailPanel');
const overlay = document.getElementById('overlay');
const detailContent = document.getElementById('detailContent');
function openDetail(key) {
const data = serviceData[key];
if (!data) return;
let html = `<div class="detail-title">${data.title}</div><div class="detail-subtitle">${data.subtitle}</div>`;
data.sections.forEach(s => {
html += `<div class="detail-section"><h3>${s.title}</h3>${s.content}</div>`;
});
detailContent.innerHTML = html;
panel.classList.add('open');
overlay.classList.add('show');
document.querySelectorAll('.service-card').forEach(c => c.classList.remove('active'));
const activeCard = document.querySelector(`[data-service="${key}"]`);
if (activeCard) activeCard.classList.add('active');
}
function closeDetail() {
panel.classList.remove('open');
overlay.classList.remove('show');
document.querySelectorAll('.service-card').forEach(c => c.classList.remove('active'));
}
document.getElementById('closeBtn').addEventListener('click', closeDetail);
overlay.addEventListener('click', closeDetail);
// Swipe down to close on mobile
let touchStartY = 0;
panel.addEventListener('touchstart', (e) => {
if (panel.scrollTop <= 0) {
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
panel.addEventListener('touchmove', (e) => {
if (panel.scrollTop <= 0 && touchStartY > 0) {
const dy = e.touches[0].clientY - touchStartY;
if (dy > 80) {
closeDetail();
touchStartY = 0;
}
}
}, { passive: true });
panel.addEventListener('touchend', () => { touchStartY = 0; }, { passive: true });
document.querySelectorAll('.service-card[data-service]').forEach(card => {
card.addEventListener('click', () => openDetail(card.dataset.service));
});
// ── Draw SVG Connections ──
function drawConnections() {
const svg = document.getElementById('connSvg');
if (window.innerWidth <= 768) {
svg.innerHTML = '';
return;
}
const canvas = document.getElementById('canvas');
const canvasRect = canvas.getBoundingClientRect();
svg.setAttribute('width', canvas.scrollWidth);
svg.setAttribute('height', canvas.scrollHeight);
svg.innerHTML = '';
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
// Gradient: green
const gGreen = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gGreen.setAttribute('id', 'gGreen'); gGreen.setAttribute('x1','0%'); gGreen.setAttribute('y1','0%'); gGreen.setAttribute('x2','100%'); gGreen.setAttribute('y2','0%');
gGreen.innerHTML = '<stop offset="0%" style="stop-color:#34d399"/><stop offset="100%" style="stop-color:#6ee7b7"/>';
defs.appendChild(gGreen);
// Gradient: purple (gRPC)
const gPurple = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gPurple.setAttribute('id', 'gPurple'); gPurple.setAttribute('x1','0%'); gPurple.setAttribute('y1','0%'); gPurple.setAttribute('x2','100%'); gPurple.setAttribute('y2','0%');
gPurple.innerHTML = '<stop offset="0%" style="stop-color:#818cf8"/><stop offset="100%" style="stop-color:#a855f7"/>';
defs.appendChild(gPurple);
// Gradient: blue (DB)
const gBlue = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gBlue.setAttribute('id', 'gBlue'); gBlue.setAttribute('x1','0%'); gBlue.setAttribute('y1','0%'); gBlue.setAttribute('x2','0%'); gBlue.setAttribute('y2','100%');
gBlue.innerHTML = '<stop offset="0%" style="stop-color:#60a5fa"/><stop offset="100%" style="stop-color:#3b82f6"/>';
defs.appendChild(gBlue);
// Gradient: yellow
const gYellow = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gYellow.setAttribute('id', 'gYellow'); gYellow.setAttribute('x1','0%'); gYellow.setAttribute('y1','0%'); gYellow.setAttribute('x2','100%'); gYellow.setAttribute('y2','0%');
gYellow.innerHTML = '<stop offset="0%" style="stop-color:#fbbf24"/><stop offset="100%" style="stop-color:#fcd34d"/>';
defs.appendChild(gYellow);
svg.appendChild(defs);
function getCenter(id) {
const el = document.getElementById(id);
if (!el) return null;
const r = el.getBoundingClientRect();
return {
x: r.left + r.width / 2 - canvasRect.left,
y: r.top + r.height / 2 - canvasRect.top,
top: r.top - canvasRect.top,
bottom: r.bottom - canvasRect.top,
left: r.left - canvasRect.left,
right: r.right - canvasRect.left,
};
}
function drawCurve(from, to, stroke, dashArray, fromSide, toSide) {
const a = getCenter(from);
const b = getCenter(to);
if (!a || !b) return;
let x1, y1, x2, y2;
if (fromSide === 'right') { x1 = a.right; y1 = (a.top + a.bottom) / 2; }
else if (fromSide === 'bottom') { x1 = (a.left + a.right) / 2; y1 = a.bottom; }
else { x1 = (a.left + a.right) / 2; y1 = (a.top + a.bottom) / 2; }
if (toSide === 'left') { x2 = b.left; y2 = (b.top + b.bottom) / 2; }
else if (toSide === 'top') { x2 = (b.left + b.right) / 2; y2 = b.top; }
else { x2 = (b.left + b.right) / 2; y2 = (b.top + b.bottom) / 2; }
const dx = x2 - x1;
const dy = y2 - y1;
let cx1, cy1, cx2, cy2;
if (fromSide === 'right' && toSide === 'left') {
cx1 = x1 + dx * 0.4; cy1 = y1;
cx2 = x2 - dx * 0.4; cy2 = y2;
} else if (fromSide === 'bottom' && toSide === 'top') {
cx1 = x1; cy1 = y1 + dy * 0.4;
cx2 = x2; cy2 = y2 - dy * 0.4;
} else {
cx1 = x1 + dx * 0.3; cy1 = y1;
cx2 = x2 - dx * 0.3; cy2 = y2;
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M${x1},${y1} C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`);
path.setAttribute('stroke', stroke);
path.setAttribute('class', 'conn-line');
if (dashArray) {
path.setAttribute('stroke-dasharray', dashArray);
path.classList.add('animated-flow');
}
svg.appendChild(path);
}
// Frontend → Gateway
drawCurve('card-frontend', 'card-gateway', 'url(#gYellow)', '6 4', 'right', 'left');
// Gateway → Services
drawCurve('card-gateway', 'card-auth', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-purchaser', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-textile', 'url(#gGreen)', '6 4', 'bottom', 'top');
drawCurve('card-gateway', 'card-washing', 'url(#gGreen)', '6 4', 'bottom', 'top');
// Auth → System RPC (gRPC)
drawCurve('card-auth', 'card-system-rpc', 'url(#gPurple)', null, 'right', 'left');
// Purchaser → Inventory RPC (gRPC)
drawCurve('card-purchaser', 'card-inv-rpc', 'url(#gPurple)', null, 'right', 'left');
// Textile → Purchaser (internal HTTP)
drawCurve('card-textile', 'card-purchaser', 'url(#gYellow)', '4 3', 'right', 'left');
// Washing → Purchaser (internal HTTP)
drawCurve('card-washing', 'card-purchaser', 'url(#gYellow)', '4 3', 'right', 'left');
// Services → DB zone
drawCurve('card-auth', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-purchaser', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-textile', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-washing', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-system-rpc', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
drawCurve('card-inv-rpc', 'zone-db', 'url(#gBlue)', '4 3', 'bottom', 'top');
}
// Run after layout
window.addEventListener('load', () => { setTimeout(drawConnections, 100); });
window.addEventListener('resize', drawConnections);
</script>
</body>
</html>