@ifrankwang/openspec-agents 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +91 -0
  2. package/assets/agents/openspec-architect.md +61 -0
  3. package/assets/agents/openspec-developer.md +21 -0
  4. package/assets/agents/openspec-main.md +35 -0
  5. package/assets/agents/openspec-reviewer-architecture.md +60 -0
  6. package/assets/agents/openspec-reviewer-maintainability.md +59 -0
  7. package/assets/agents/openspec-reviewer-performance.md +49 -0
  8. package/assets/agents/openspec-reviewer-security.md +51 -0
  9. package/assets/agents/openspec-reviewer-style.md +50 -0
  10. package/assets/agents/openspec-reviewer-task.md +40 -0
  11. package/assets/agents/openspec-reviewer-tool.md +30 -0
  12. package/assets/dashboard/index.html +295 -0
  13. package/assets/skills/api-conventions/SKILL.md +60 -0
  14. package/assets/skills/api-test/SKILL.md +175 -0
  15. package/assets/skills/architecture-evolution/SKILL.md +60 -0
  16. package/assets/skills/db-conventions/SKILL.md +62 -0
  17. package/assets/skills/ddd-architecture/SKILL.md +261 -0
  18. package/assets/skills/environment-cleanup/SKILL.md +44 -0
  19. package/assets/skills/java-api-conventions/SKILL.md +30 -0
  20. package/assets/skills/java-code-maintainability/SKILL.md +62 -0
  21. package/assets/skills/java-code-performance/SKILL.md +49 -0
  22. package/assets/skills/java-code-style/SKILL.md +54 -0
  23. package/assets/skills/java-db-conventions/SKILL.md +44 -0
  24. package/assets/skills/java-ddd-architecture/SKILL.md +533 -0
  25. package/assets/skills/java-dev-practices/SKILL.md +110 -0
  26. package/assets/skills/java-quality-gate/SKILL.md +455 -0
  27. package/assets/skills/java-quality-gate/reference/pmd-rules.md +16 -0
  28. package/assets/skills/java-quality-tool-improve/SKILL.md +142 -0
  29. package/assets/skills/java-security/SKILL.md +59 -0
  30. package/assets/skills/java-technical-debt/SKILL.md +36 -0
  31. package/assets/skills/orchestrator/SKILL.md +65 -0
  32. package/assets/skills/quality-gate/SKILL.md +84 -0
  33. package/assets/skills/security-baseline/SKILL.md +101 -0
  34. package/assets/skills/technical-debt/SKILL.md +78 -0
  35. package/assets/workflows/task.yaml +177 -0
  36. package/package.json +79 -0
  37. package/src/adapters/agent-md.ts +37 -0
  38. package/src/adapters/claude-code/index.ts +35 -0
  39. package/src/adapters/codex/index.ts +161 -0
  40. package/src/adapters/mcp-common/cli.ts +64 -0
  41. package/src/adapters/mcp-common/index.ts +137 -0
  42. package/src/adapters/mcp-common/json-schema.ts +53 -0
  43. package/src/adapters/mcp-common/server.ts +157 -0
  44. package/src/adapters/opencode/agents.ts +80 -0
  45. package/src/adapters/opencode/dashboard.ts +64 -0
  46. package/src/adapters/opencode/index.ts +34 -0
  47. package/src/adapters/opencode/json-schema.ts +60 -0
  48. package/src/adapters/opencode/skills.ts +19 -0
  49. package/src/adapters/opencode/tools.ts +78 -0
  50. package/src/adapters/plugin-common/index.ts +173 -0
  51. package/src/adapters/zcode/index.ts +32 -0
  52. package/src/core/constants.ts +46 -0
  53. package/src/core/dashboard.ts +101 -0
  54. package/src/core/derive.ts +146 -0
  55. package/src/core/exemptions.ts +168 -0
  56. package/src/core/git.ts +373 -0
  57. package/src/core/index.ts +76 -0
  58. package/src/core/namespace.ts +12 -0
  59. package/src/core/paths.ts +23 -0
  60. package/src/core/provider.ts +74 -0
  61. package/src/core/state.ts +357 -0
  62. package/src/core/task-children.ts +59 -0
  63. package/src/core/tasks-md.ts +89 -0
  64. package/src/core/tools/gate.ts +165 -0
  65. package/src/core/tools/lifecycle.ts +645 -0
  66. package/src/core/tools/schemas.ts +363 -0
  67. package/src/core/tools/submit.ts +863 -0
  68. package/src/core/tools/types.ts +129 -0
  69. package/src/core/types.ts +135 -0
  70. package/src/core/views.ts +195 -0
  71. package/src/core/workflow/collector.ts +150 -0
  72. package/src/core/workflow/engine.ts +576 -0
  73. package/src/core/workflow/index.ts +56 -0
  74. package/src/core/workflow/loader.ts +277 -0
  75. package/src/core/workflow/poller.ts +159 -0
  76. package/src/core/workflow/reset.ts +93 -0
  77. package/src/core/workflow/status.ts +1010 -0
  78. package/src/core/workflow/submit.ts +331 -0
  79. package/src/core/workflow/types.ts +100 -0
  80. package/src/core/workflow/writeback.ts +48 -0
  81. package/src/dashboard/page.ts +19 -0
  82. package/src/index.ts +2 -0
  83. package/src/skills/resolve.ts +95 -0
  84. package/src/skills/scan.ts +29 -0
@@ -0,0 +1,295 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>编排进度看板 · 5 列看板</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
9
+ :root{
10
+ --bg:#f8fafc;--surface:#fff;--surface-2:#f1f5f9;
11
+ --ink:#0f172a;--muted:#475569;--quiet:#94a3b8;
12
+ --line:#e2e8f0;--line-2:#cbd5e1;
13
+ --blue:#2563eb;--blue-soft:#eff6ff;
14
+ --green:#16a34a;--green-soft:#f0fdf4;
15
+ --amber:#d97706;--amber-soft:#fffbeb;
16
+ --red:#dc2626;--red-soft:#fef2f2;
17
+ --violet:#7c3aed;--violet-soft:#f5f3ff;
18
+ --teal:#0d9488;--teal-soft:#f0fdfa;
19
+ --radius:12px;--radius-sm:8px;--pill:999px;
20
+ --shadow:0 1px 2px rgba(15,23,42,.04),0 6px 16px rgba(15,23,42,.04);
21
+ --sans:'Inter',system-ui,sans-serif;
22
+ --mono:'JetBrains Mono',ui-monospace,monospace;
23
+ }
24
+ *{box-sizing:border-box}html{font-size:15px}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55;-webkit-font-smoothing:antialiased}
25
+ button{font:inherit;cursor:pointer}button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
26
+ .shell{max-width:1440px;margin:0 auto;padding:32px 28px 80px}
27
+ /* header */
28
+ .topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;padding-bottom:26px;border-bottom:1px solid var(--line)}
29
+ .brand{display:flex;flex-direction:column;gap:8px}
30
+ .badge{display:inline-flex;align-items:center;gap:8px;font:600 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
31
+ .badge::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px var(--green-soft)}
32
+ h1{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.03em}
33
+ .meta{display:flex;gap:28px;text-align:right;font-size:.8rem;color:var(--muted)}
34
+ .meta span{display:flex;flex-direction:column;gap:5px}.meta strong{color:var(--ink);font-weight:600}
35
+ /* 5 列看板 */
36
+ .board{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;align-items:start;padding-top:24px}
37
+ .col{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column}
38
+ .col-head{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px;border-bottom:1px solid var(--line)}
39
+ .col-head h3{margin:0;font-size:.92rem;font-weight:700}
40
+ .col-count{font:600 .78rem/1 var(--mono);padding:4px 10px;background:var(--surface-2);border-radius:var(--pill);color:var(--muted)}
41
+ .col-body{max-height:calc(100vh - 280px);overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px}
42
+ .board-empty{grid-column:1/-1;padding:48px 0;text-align:center;color:var(--quiet);font-size:.9rem}
43
+ /* 卡片 */
44
+ .card{position:relative;background:var(--surface);border:1px solid var(--line);border-left-width:3px;border-radius:var(--radius-sm);box-shadow:0 1px 2px rgba(15,23,42,.04);padding:12px 14px;display:flex;flex-direction:column;gap:8px}
45
+ .card.task{border-left-color:var(--blue)}
46
+ .card.issue{border-left-color:var(--violet)}
47
+ .card.suspended{border-style:dashed;opacity:.94}
48
+ .card-head{display:flex;align-items:center;gap:8px}
49
+ .type-tag{flex:0 0 auto;width:22px;height:22px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font:700 .7rem/1 var(--mono);color:#fff}
50
+ .type-tag.task{background:var(--blue)}.type-tag.issue{background:var(--violet)}
51
+ .src-icon{flex:0 0 auto;font-size:.85rem;color:var(--quiet);line-height:1}
52
+ .card-title{margin:0;font-size:.9rem;font-weight:700;line-height:1.4;flex:1}
53
+ .card-desc{font-size:.78rem;color:var(--muted);line-height:1.5}
54
+ .card-desc.collapsed{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
55
+ .desc-toggle{margin-top:-4px;font-size:.72rem;color:var(--blue);background:none;border:none;padding:0;cursor:pointer;text-align:left}
56
+ .card-labels{display:flex;flex-wrap:wrap;gap:6px}
57
+ .chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--pill);font:500 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
58
+ .card-children{display:flex;flex-direction:column;gap:6px;border-top:1px dashed var(--line);padding-top:8px}
59
+ .child-row{display:flex;align-items:center;gap:6px;font-size:.76rem;color:var(--muted)}
60
+ .child-type{flex:0 0 auto;width:16px;height:16px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font:700 .58rem/1 var(--mono);color:#fff}
61
+ .child-type.task{background:var(--blue)}.child-type.issue{background:var(--violet)}
62
+ .child-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
63
+ .child-phase{flex:0 0 auto;padding:1px 6px;border-radius:var(--pill);font:600 .62rem/1.3 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
64
+ .card-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:8px;margin-top:auto;border-top:1px solid var(--line);padding-top:8px}
65
+ .card-verdicts{display:flex;flex-wrap:wrap;gap:6px}
66
+ .verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
67
+ .verdict.passed{background:var(--green-soft);color:var(--green)}
68
+ .verdict.failed{background:var(--red-soft);color:var(--red)}
69
+ .verdict.pending{background:var(--surface-2);color:var(--quiet)}
70
+ .card-meta{display:flex;align-items:center;gap:6px;flex-shrink:0}
71
+ .step-tag{font:.68rem/1.4 var(--mono);color:var(--quiet)}
72
+ .suspend-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--pill);font:600 .7rem/1.4 var(--mono);background:var(--amber-soft);color:var(--amber);border:1px solid var(--amber);cursor:pointer}
73
+ .suspend-reason{margin-top:4px;font-size:.72rem;color:var(--amber);background:var(--amber-soft);border-radius:var(--radius-sm);padding:6px 8px}
74
+ /* severity 色条 */
75
+ .sev-bar{position:absolute;top:0;left:0;right:0;height:4px;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
76
+ .sev-bar.critical{background:var(--red)}.sev-bar.high{background:var(--amber)}.sev-bar.medium{background:var(--violet)}.sev-bar.low{background:var(--teal)}.sev-bar.info{background:var(--line-2)}
77
+ /* WorkItem 详情弹窗 */
78
+ .modal-open{overflow:hidden}
79
+ .modal-overlay{position:fixed;inset:0;z-index:100;background:rgba(15,23,42,.45);display:flex;align-items:flex-start;justify-content:center;padding:48px 16px;overflow:auto}
80
+ .modal-overlay[hidden]{display:none}
81
+ .modal-dialog{background:var(--surface);border-radius:var(--radius);box-shadow:0 20px 50px rgba(15,23,42,.2);max-width:640px;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column}
82
+ .modal-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--line)}
83
+ .modal-title{flex:1;font-size:.92rem;font-weight:700}
84
+ .modal-close{background:none;border:none;font-size:1.2rem;color:var(--muted);line-height:1;padding:4px 8px;border-radius:6px}
85
+ .modal-close:hover{background:var(--surface-2);color:var(--ink)}
86
+ .modal-back{background:none;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.78rem;color:var(--muted);padding:4px 10px}
87
+ .modal-back:hover{background:var(--surface-2);color:var(--ink)}
88
+ .modal-back[hidden]{display:none}
89
+ .modal-body{padding:18px;overflow:auto;display:flex;flex-direction:column;gap:14px}
90
+ .detail-title{margin:0;font-size:1.1rem;font-weight:700}
91
+ .detail-block{display:flex;flex-direction:column;gap:8px}
92
+ .detail-block h5{margin:0;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;color:var(--quiet)}
93
+ .meta-row{display:flex;gap:10px;font-size:.8rem;padding:4px 0;border-bottom:1px dashed var(--line)}
94
+ .meta-key{flex:0 0 96px;color:var(--quiet);font:500 .76rem/1.4 var(--mono)}
95
+ .meta-val{flex:1;color:var(--ink);word-break:break-word}
96
+ .detail-desc{margin:0;font-size:.82rem;color:var(--muted);white-space:pre-wrap}
97
+ .detail-verdicts{display:flex;flex-wrap:wrap;gap:6px}
98
+ .detail-verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
99
+ .detail-verdict.passed{background:var(--green-soft);color:var(--green)}
100
+ .detail-verdict.failed{background:var(--red-soft);color:var(--red)}
101
+ .detail-verdict.pending{background:var(--surface-2);color:var(--quiet)}
102
+ .detail-children{display:flex;flex-direction:column;gap:6px}
103
+ .detail-child{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.8rem;cursor:pointer}
104
+ .detail-child:hover{background:var(--surface-2);border-color:var(--line-2)}
105
+ @media(max-width:1024px){.board{grid-template-columns:repeat(2,1fr)}}
106
+ @media(max-width:640px){.board{grid-template-columns:1fr}.topbar{flex-direction:column;gap:14px}.meta{text-align:left}}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="shell">
111
+ <header class="topbar">
112
+ <div class="brand">
113
+ <div class="badge" id="connBadge"><span>实时同步</span></div>
114
+ <h1>编排进度看板</h1>
115
+ </div>
116
+ <div class="meta">
117
+ <span>变更编号<strong id="changeId">-</strong></span>
118
+ <span>基准分支<strong id="baseBranch">-</strong></span>
119
+ <span>最后更新<strong id="updatedAt">-</strong></span>
120
+ </div>
121
+ </header>
122
+ <main id="dashboard"></main>
123
+ </div>
124
+ <div class="modal-overlay" id="wiModal" hidden>
125
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-label="工作项详情">
126
+ <header class="modal-head">
127
+ <button class="modal-back" id="wiBack" hidden>← 返回</button>
128
+ <span class="modal-title">工作项详情</span>
129
+ <button class="modal-close" id="wiClose" aria-label="关闭">×</button>
130
+ </header>
131
+ <div class="modal-body" id="wiBody"></div>
132
+ </div>
133
+ </div>
134
+ <script>
135
+ const COLUMNS=[['todo','待办'],['in_progress','处理中'],['review','审核中'],['done','完成'],['cancelled','已取消']];
136
+ const SOURCE_ICONS={openspec:'◈',ado:'◆',custom:'✚'};
137
+ const VERDICT_ICONS={passed:'✓',failed:'✕',pending:'○'};
138
+ const AGENT_SHORT={'openspec-architect':'architect','openspec-developer':'dev','openspec-reviewer-tool':'tool','openspec-reviewer-task':'task','openspec-reviewer-style':'style','openspec-reviewer-architecture':'arch','openspec-reviewer-performance':'perf','openspec-reviewer-security':'sec','openspec-reviewer-maintainability':'maint'};
139
+ let dashboardState={};
140
+ function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\'':'&#39;','"':'&quot;'}[c]))}
141
+ function cls(v){return String(v??'').replace(/[^a-zA-Z0-9_-]/g,'_')}
142
+ function shortAgent(key){if(AGENT_SHORT[key])return AGENT_SHORT[key];const m=/^openspec-reviewer-(.+)$/.exec(key)||/^openspec-(.+)$/.exec(key);return m?m[1]:(key||'')}
143
+ function descBlock(text){if(!text)return '';if(text.length<90)return `<div class="card-desc">${esc(text)}</div>`;return `<div class="card-desc collapsed">${esc(text)}</div><button class="desc-toggle">展开</button>`}
144
+ function verdictDot(v){return `<span class="verdict ${cls(v.verdict)}" title="${esc(v.stepId)}">${esc(shortAgent(v.agentKey))}:${VERDICT_ICONS[v.verdict]||'○'}</span>`}
145
+ function renderCard(c){
146
+ const isIssue=c.type==='issue';
147
+ const typeTag=`<span class="type-tag ${isIssue?'issue':'task'}">${isIssue?'I':'T'}</span>`;
148
+ const srcIcon=`<span class="src-icon" title="${esc(c.source||'')}">${SOURCE_ICONS[c.source]||'●'}</span>`;
149
+ const sevBar=isIssue&&c.severity?`<span class="sev-bar ${cls(String(c.severity).toLowerCase())}" title="${esc(c.severity)}"></span>`:'';
150
+ const labels=(c.labels||[]).map(l=>`<span class="chip">${esc(l)}</span>`).join('');
151
+ const step=c.currentStep?`<span class="step-tag">${esc(c.currentStep)}</span>`:'';
152
+ const suspTag=c.suspended?'<span class="suspend-tag" role="button" tabindex="0">⏸ 暂停</span>':'';
153
+ const suspReason=c.suspended?`<div class="suspend-reason" hidden>${esc(c.suspendReason||'未提供暂停原因')}</div>`:'';
154
+ const verdicts=(c.agentVerdicts||[]).map(verdictDot).join('');
155
+ const children=(c.children||[]).map(ch=>`<div class="child-row"><span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span><span class="child-title">${esc(ch.title)}</span>${ch.type==='task'?`<span class="child-phase">${esc(ch.phase)}</span>`:''}</div>`).join('');
156
+ return `<article class="card ${isIssue?'issue':'task'}${c.suspended?' suspended':''}" data-id="${esc(c.id)}">${sevBar}
157
+ <div class="card-head">${typeTag}${srcIcon}<h4 class="card-title">${esc(c.title)}</h4></div>
158
+ ${descBlock(c.description)}
159
+ ${labels?`<div class="card-labels">${labels}</div>`:''}
160
+ ${children?`<div class="card-children">${children}</div>`:''}
161
+ <div class="card-foot">${verdicts?`<div class="card-verdicts">${verdicts}</div>`:'<span></span>'}<div class="card-meta">${step}${suspTag}</div></div>
162
+ ${suspReason}
163
+ </article>`;
164
+ }
165
+ function renderBoard(){
166
+ const cards=dashboardState.workItemCards||[];
167
+ if(!cards.length)return `<section class="board"><div class="board-empty">暂无工作项数据(旧格式状态或未初始化)</div></section>`;
168
+ return `<section class="board">${COLUMNS.map(([key,label])=>{
169
+ const items=cards.filter(c=>c.phase===key);
170
+ return `<div class="col" data-col="${key}"><header class="col-head"><h3>${label}</h3><span class="col-count">${items.length}</span></header><div class="col-body">${items.map(renderCard).join('')}</div></div>`;
171
+ }).join('')}</section>`;
172
+ }
173
+ function render(){
174
+ document.getElementById('changeId').textContent=dashboardState.changeId||'-';
175
+ document.getElementById('baseBranch').textContent=dashboardState.baseBranch||'-';
176
+ document.getElementById('updatedAt').textContent=dashboardState.updatedAt?new Date(dashboardState.updatedAt).toLocaleString():'-';
177
+ document.getElementById('dashboard').innerHTML=renderBoard();
178
+ document.querySelectorAll('.desc-toggle').forEach(btn=>btn.addEventListener('click',e=>{
179
+ const d=e.target.previousElementSibling;
180
+ if(d&&d.classList.contains('card-desc')){d.classList.toggle('collapsed');e.target.textContent=d.classList.contains('collapsed')?'展开':'收起'}
181
+ }));
182
+ document.querySelectorAll('.suspend-tag').forEach(tag=>tag.addEventListener('click',e=>{
183
+ const card=e.target.closest('.card');
184
+ const reason=card&&card.querySelector('.suspend-reason');
185
+ if(reason)reason.hidden=!reason.hidden;
186
+ }));
187
+ if(modalState.id)refreshModal();
188
+ }
189
+ /* WorkItem 详情弹窗 */
190
+ let modalState={id:null,back:[]};
191
+ function findWorkItem(items,id){
192
+ for(const it of items||[]){
193
+ if(it.id===id)return it;
194
+ if(it.children&&it.children.length){
195
+ const found=findWorkItem(it.children,id);
196
+ if(found)return found;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ function renderTags(tags){
202
+ const verdicts=Object.entries(tags||{}).map(([key,verdict])=>{
203
+ const sep=key.indexOf(':');
204
+ const stepId=sep===-1?key:key.slice(0,sep);
205
+ const agentKey=sep===-1?key:key.slice(sep+1);
206
+ return `<span class="detail-verdict ${cls(verdict)}" title="${esc(key)}">${esc(shortAgent(agentKey))}:${VERDICT_ICONS[verdict]||'○'}</span>`;
207
+ }).join('');
208
+ return verdicts?`<div class="detail-block"><h5>Agent 裁决</h5><div class="detail-verdicts">${verdicts}</div></div>`:'';
209
+ }
210
+ function renderWriteback(wb){
211
+ if(!wb)return '';
212
+ const rows=Object.entries(wb).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
213
+ const val=typeof v==='object'?JSON.stringify(v):String(v);
214
+ return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
215
+ }).join('');
216
+ return rows?`<div class="detail-block"><h5>写回</h5>${rows}</div>`:'';
217
+ }
218
+ function renderMeta(metadata){
219
+ const rows=Object.entries(metadata||{}).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
220
+ const val=typeof v==='object'?JSON.stringify(v):String(v);
221
+ return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
222
+ }).join('');
223
+ return rows?`<div class="detail-block"><h5>元数据</h5>${rows}</div>`:'';
224
+ }
225
+ function renderChildren(children){
226
+ if(!children||!children.length)return '';
227
+ const rows=(children||[]).map(ch=>`<div class="detail-child" data-id="${esc(ch.id)}" role="button" tabindex="0">
228
+ <span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span>
229
+ <span class="child-title">${esc(ch.title)}</span>
230
+ <span class="child-phase">${esc(ch.phase)}</span>
231
+ </div>`).join('');
232
+ return `<div class="detail-block"><h5>Children</h5><div class="detail-children">${rows}</div></div>`;
233
+ }
234
+ function renderDetail(item){
235
+ const rows=[['类型',item.type],['来源',item.source],['ID',item.id]];
236
+ if(item.externalId)rows.push(['外部ID',item.externalId]);
237
+ rows.push(['阶段',item.phase]);
238
+ if(item.severity)rows.push(['严重级别',item.severity]);
239
+ rows.push(['当前步骤',item.currentStep||'-']);
240
+ const kv=rows.map(([k,v])=>`<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(v)}</span></div>`).join('');
241
+ return `<h3 class="detail-title">${esc(item.title)}</h3>
242
+ ${item.suspended?`<div class="suspend-reason">⏸ 暂停:${esc(typeof (item.metadata||{})['suspend_reason']==='string'?(item.metadata||{})['suspend_reason']:'未提供暂停原因')}</div>`:''}
243
+ ${item.description?`<div class="detail-block"><h5>描述</h5><p class="detail-desc">${esc(item.description)}</p></div>`:''}
244
+ <div class="detail-block"><h5>字段</h5>${kv}</div>
245
+ ${item.labels&&item.labels.length?`<div class="detail-block"><h5>标签</h5><div class="card-labels">${item.labels.map(l=>`<span class="chip">${esc(l)}</span>`).join('')}</div></div>`:''}
246
+ ${renderTags(item.tags)}
247
+ ${renderWriteback(item.writeback)}
248
+ ${renderMeta(item.metadata)}
249
+ ${renderChildren(item.children)}`;
250
+ }
251
+ function refreshModal(){
252
+ const modal=document.getElementById('wiModal');
253
+ const item=findWorkItem(dashboardState.workItems||[],modalState.id);
254
+ if(!item){closeDetail();return;}
255
+ document.getElementById('wiBody').innerHTML=renderDetail(item);
256
+ document.getElementById('wiBack').hidden=!modalState.back.length;
257
+ modal.hidden=false;
258
+ document.body.classList.add('modal-open');
259
+ }
260
+ function openDetail(id,back){
261
+ modalState={id,back:back||[]};
262
+ refreshModal();
263
+ }
264
+ function closeDetail(){
265
+ modalState={id:null,back:[]};
266
+ const modal=document.getElementById('wiModal');
267
+ if(modal)modal.hidden=true;
268
+ document.body.classList.remove('modal-open');
269
+ }
270
+ document.addEventListener('click',e=>{
271
+ if(e.target.closest('.desc-toggle,.suspend-tag'))return;
272
+ const card=e.target.closest('.card');
273
+ if(card&&card.dataset.id)openDetail(card.dataset.id,[]);
274
+ });
275
+ document.getElementById('wiModal').addEventListener('click',e=>{
276
+ if(e.target.closest('#wiBack')){
277
+ const prev=modalState.back[modalState.back.length-1];
278
+ openDetail(prev,modalState.back.slice(0,-1));
279
+ return;
280
+ }
281
+ if(e.target.classList.contains('modal-overlay')){closeDetail();return;}
282
+ if(e.target.closest('.modal-close')){closeDetail();return;}
283
+ const child=e.target.closest('.detail-child');
284
+ if(child&&child.dataset.id)openDetail(child.dataset.id,[...modalState.back,modalState.id]);
285
+ });
286
+ document.addEventListener('keydown',e=>{
287
+ if(e.key==='Escape'){const modal=document.getElementById('wiModal');if(modal&&!modal.hidden)closeDetail();}
288
+ });
289
+ async function loadState(){try{const res=await fetch('/api/state');if(!res.ok)throw new Error('fetch failed');const data=await res.json();if(data&&data.active===true){dashboardState=data;render()}}catch(e){console.error('[dashboard] load state failed',e)}}
290
+ render();
291
+ loadState();
292
+ setInterval(loadState,2000);
293
+ </script>
294
+ </body>
295
+ </html>
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: api-conventions
3
+ description: REST API 设计规范——URL 路径约定、HTTP 动词映射、分层职责、输入校验、HTTP 状态码错误映射、统一响应结构。技术栈无关。当新增 REST 接口或设计错误响应时使用。
4
+ capabilities: ["api-design", "architecture"]
5
+ ---
6
+
7
+ > **项目规范优先**:本 skill 所列约定为推荐标准。若项目已有明确规范且与本 skill 不一致,以项目规范为准。
8
+
9
+ ## 路径约定
10
+
11
+ | # | 规则 | 说明 |
12
+ |---|------|------|
13
+ | 1 | 路径统一 `{context-path}/api/v1/` 前缀 + kebab-case | 如 `{context-path}/api/v1/orders/{orderId}/items` |
14
+ | 2 | RESTful 风格 | 禁止 POST 包打天下;GET 查询、POST 创建、PUT/PATCH 更新、DELETE 删除 |
15
+
16
+ ## 接口层职责
17
+
18
+ 接口层(Controller / Handler)职责边界:
19
+
20
+ | 职责 | 说明 |
21
+ |------|------|
22
+ | 参数校验 | 校验入参合法性(必填、格式、长度、枚举值),校验失败返回 4xx |
23
+ | 调用下层 | 校验通过后调用应用层服务编排业务 |
24
+ | 响应转换 | 将业务结果转换为响应格式(DTO) |
25
+ | **禁止** | 不写业务逻辑、不直接访问持久化层 |
26
+
27
+ ## 统一响应结构
28
+
29
+ 所有接口统一返回结构,含以下三部分:
30
+
31
+ - `code`:业务错误码(数字),成功时为约定成功值
32
+ - `message`:错误描述,支持动态占位参数
33
+ - `data`:业务数据泛型
34
+
35
+ 文件下载等特殊场景可例外。
36
+
37
+ ## 异常响应体系
38
+
39
+ 接口层不直接处理异常,由全局异常处理器统一拦截。HTTP 状态码与异常语义映射:
40
+
41
+ | 异常语义 | HTTP 状态码 | 含义 |
42
+ |---------|------------|------|
43
+ | 资源不存在 | 404 | 请求的资源未找到 |
44
+ | 状态冲突 | 409 | 如重复提交、状态不允许 |
45
+ | 文件类型不合法 | 415 | 不支持的媒体类型 |
46
+ | 参数校验失败 | 400 | 输入不合法 |
47
+ | 服务端业务异常 | 500 | 业务处理错误 |
48
+ | 外部服务异常 | 500 | 依赖的外部服务调用失败 |
49
+ | 其他未捕获异常 | 500 | 兜底 |
50
+
51
+ 业务错误码作为响应体中独立数字字段,与 HTTP 状态码解耦。HTTP 码供客户端/网关判断大类,业务错误码供前端精确分支处理。
52
+
53
+ ## 泛化设计原则(非强约束)
54
+
55
+ 以下为推荐模式,不强制:
56
+
57
+ | 原则 | 说明 |
58
+ |------|------|
59
+ | 输入校验 | 入参 String 字段限制长度和特殊字符(InfoSec 合规) |
60
+ | 文件上传 MIME | 上传接口接口层校验 MIME/Content-Type,允许类型由项目配置决定 |
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: api-test
3
+ description: Web 应用 API 自动化测试规范——.http 脚本格式、目录结构、SQL 前置数据脚本、认证发现、覆盖标准与执行方式。
4
+ capabilities: ["api-testing"]
5
+ boundary_hints:
6
+ directories: ["api-tests/"]
7
+ ---
8
+
9
+ > **项目规范优先**:本 skill 所列约定为推荐标准。若项目已有明确规范且与本 skill 不一致,以项目规范为准。
10
+
11
+ ## 目录结构
12
+
13
+ API 测试素材独立于项目源代码目录(不放入 src/test/ 等构建工具测试源码目录),避免与单元测试混淆。
14
+
15
+ ```
16
+ <project-root>/api-tests/
17
+ ├── data/ # SQL 前置数据脚本(按场景准备数据库数据)
18
+ └── script/ # API 测试脚本
19
+ ```
20
+
21
+ 目录位置可据项目现有惯例微调,但必须与构建工具的测试源码目录隔离。
22
+
23
+ ## 脚本格式
24
+
25
+ API 测试脚本使用 IntelliJ IDEA HTTP Client(.http 文件)格式。每个场景以 `###` 分隔。
26
+
27
+ ```http
28
+ ### 正常路径:创建订单
29
+ POST {{BASE_URL}}/api/v1/orders
30
+ Content-Type: application/json
31
+ Authorization: Bearer {{TOKEN}}
32
+
33
+ {"orderNo": "SO202608140001", "customerName": "华东供应链", "amount": 12800.50, "status": "CREATED"}
34
+
35
+ > {%
36
+ client.test("创建订单返回 201", function() {
37
+ client.assert(response.status === 201, "期望 201,实际 " + response.status);
38
+ client.assert(response.body.data.orderNo === "SO202608140001", "订单号回显不一致");
39
+ });
40
+ %}
41
+
42
+ ### 边界:缺必填字段(缺 orderNo)
43
+ POST {{BASE_URL}}/api/v1/orders
44
+ Content-Type: application/json
45
+ Authorization: Bearer {{TOKEN}}
46
+
47
+ {"customerName": "华东供应链"}
48
+
49
+ > {%
50
+ client.test("缺必填字段返回 4xx", function() {
51
+ client.assert(response.status >= 400 && response.status < 500, "期望 4xx,实际 " + response.status);
52
+ });
53
+ %}
54
+
55
+ ### 边界:非法值(orderNo 为空串)
56
+ POST {{BASE_URL}}/api/v1/orders
57
+ Content-Type: application/json
58
+ Authorization: Bearer {{TOKEN}}
59
+
60
+ {"orderNo": "", "customerName": "华东供应链"}
61
+
62
+ > {%
63
+ client.test("非法值返回 4xx", function() {
64
+ client.assert(response.status >= 400 && response.status < 500, "期望 4xx,实际 " + response.status);
65
+ });
66
+ %}
67
+ ```
68
+
69
+ ## 旧脚本迁移
70
+
71
+ 若 `api-tests/script/` 目录已存在 shell(`.sh`)+ curl + jq 旧版脚本,逐文件转为 `.http` 格式:
72
+ - 每个 `curl` 命令及其 headers 和 body 转为一条 `###` 分隔的请求
73
+ - 状态码和字段校验的 shell 断言(`jq` + `if`)转为 `client.test()` + `client.assert()`
74
+ - Shell 变量(如 `BASE`, `TOKEN`)转为 `{{ }}` 环境变量,来源写入 `api-tests/http-client.env.json`
75
+ - 迁移后删除 `.sh` 文件,不留残余
76
+
77
+ ## SQL 前置数据脚本
78
+
79
+ SQL 脚本按场景准备无法通过接口构造的数据库数据(历史数据状态、多表联动前置条件)。要求幂等:
80
+
81
+ - PostgreSQL: `INSERT INTO ... ON CONFLICT DO NOTHING`
82
+ - MySQL: `INSERT IGNORE INTO ...`
83
+ - H2: `MERGE INTO ...` 或 `INSERT ... ON CONFLICT DO NOTHING`
84
+
85
+ 按项目实际 DB 类型选择写法。
86
+
87
+ ## 认证发现
88
+
89
+ API 测试脚本运行前需获取有效认证凭证。常见模式:
90
+
91
+ 1. **Dev-only login 端点**:检查项目配置中是否有 profile 专属的免登入口(如 local/dev profile 下的登录 API)
92
+ 2. **静态 Token**:检查项目是否有 dev profile 专属的 JWT 密钥配置,可用相同密钥签发测试 token
93
+ 3. **Basic Auth**:检查项目安全配置中 dev profile 是否有固定凭证或免登入口
94
+ 4. **无认证**:若 dev profile 完全关闭认证,无需 token
95
+
96
+ 从项目配置文件和安全配置类中查找。
97
+
98
+ ## 执行顺序
99
+
100
+ ```
101
+ 1. **MUST** 启动基础设施(Docker compose 项目名带隔离标识:`docker compose -p <namespace> up -d`,避免与并发 change 的容器冲突)
102
+ 2. **MUST** 准备 SQL 前置数据(写入本 change 的隔离容器内 DB)
103
+ 3. **MUST** 启动服务(端口用编排会话建议端口:`--server.port=<建议端口>`,避免并发 change 端口冲突)
104
+ 4. API 测试脚本(BASE_URL 使用上述端口)
105
+ 5. **MUST** 停止服务并清理隔离环境(`docker compose -p <namespace> down`),随后幂等自检:本 change 隔离标识下无残留容器(`docker compose -p <namespace> ps` 为空)、建议端口无进程监听
106
+ ```
107
+
108
+ 以上第 1、2、3、5 条为 MUST:隔离(独立 compose 项目名、隔离 DB、建议端口、down 清理)是并发安全硬约束,不适用本文件开头「项目规范优先」的推荐豁免。
109
+
110
+ - SQL 在前:先准备数据,再启动应用确保应用启动时读取到完整数据
111
+ - 隔离标识 `<namespace>` 与建议端口来自编排会话上下文
112
+ - 启动服务:按项目构建文件确定的启动方式(mvn / gradle / npm 等),端口用编排会话建议端口
113
+ - API 脚本:BASE_URL 使用上述建议端口,依赖运行中的服务
114
+ - 停止服务:测试完成后停止服务并清理隔离环境
115
+
116
+ ## 覆盖要求
117
+
118
+ API 测试脚本必须覆盖所有新增/变更接口:
119
+
120
+ | 维度 | 覆盖内容 |
121
+ |------|---------|
122
+ | 正常路径 | 按 spec 请求结构传入合法值,验证响应状态码(2xx) 和响应结构 |
123
+ | 关键边界 | 缺必填字段 → 4xx;非法值(空串/超长/类型错误) → 4xx;极值 → 正确响应或合理错误 |
124
+ | 业务闭环 | spec 含列表/详情/状态流转/删除接口时,测试须端到端闭环:创建后用返回标识在列表/详情检索成功、状态变更后可见性反映、删除/停用后不可见 |
125
+
126
+ spec 描述为多个行为一并发生的(如状态变更与数据入队),每个行为都须有断言证据;断言范围不限于响应状态码与响应结构,须覆盖变更引发的真实行为(状态落库、消息入队、下游可消费等连带效果)。
127
+
128
+ 「业务闭环」与「一并操作行为组」划界:行为组=同一请求内多个行为同时发生的断言;闭环=跨请求链路的可见性验证(创建 → 检索 → 变更 → 删除),二者互补,均须覆盖。
129
+
130
+ ## 测试数据
131
+
132
+ http 请求体与 SQL 前置数据均按 spec 业务语义构造:使用真实字段值与业务含义明确的数据(如按 spec 字段语义构造的订单号、状态名、业务名称),禁止无意义占位值(如「测试数据」「aaa」「test1」)。列表/详情检索断言须基于真实业务标识(创建接口返回的 id/编码),使检索结果可预期、可追溯。
133
+
134
+ ## 适用范围
135
+
136
+ 涉及业务逻辑改动,且导致接口契约或数据口径变化时,属于 API 变更范围,MUST 执行本章所有后续约束。
137
+
138
+ 以下情况不属于 API 变更,可豁免本章约束:
139
+ - 纯配置文件(非接口契约配置)、构建脚本、GitHub Actions、Docker 文件
140
+ - 仅 SQL init 文件改动且无 API 契约影响
141
+ - 纯格式、注释、import 排序等无逻辑变更
142
+ - 仅测试代码变更(无生产代码、接口契约、构建与测试配置改动)
143
+ - 纯文档变更
144
+
145
+ ## 执行约束
146
+
147
+ MUST 遵守以下约束,不得以编译通过或单元测试通过替代 API 测试:
148
+
149
+ - MUST 不得以接口返回成功与状态字段正确作为唯一验收;spec 含一并操作行为组时,须提供每个行为真实发生的断言证据。
150
+
151
+ ### 脚本准备
152
+
153
+ MUST 确保 api-tests/script/ 目录下存在与所有变更接口对应的 .http 测试脚本:
154
+ - 已有对应脚本 → 更新为匹配变更后契约
155
+ - 尚无对应脚本 → 创建新脚本(不得因尚无已有脚本而跳过创建),同步准备 api-tests/data/ 下 SQL 前置数据脚本
156
+
157
+ ### 脚本执行
158
+
159
+ API 验证须通过 .http 兼容执行工具运行脚本——不得使用 curl、wget 等通用 HTTP 命令行工具替代。
160
+
161
+ #### 执行工具
162
+
163
+ API 测试脚本使用 IntelliJ HTTP Client CLI(`ijhttp`)执行,它是 JetBrains 官方提供的独立 CLI 工具,与 IDE 中 .http 文件的解析器和执行引擎完全一致。
164
+
165
+ - **工具**: `ijhttp`(IntelliJ HTTP Client CLI)
166
+ - **安装 (macOS)**: `brew install --cask ijhttp`
167
+ - **Docker**: `docker pull jetbrains/intellij-http-client`
168
+ - **直接下载**: `curl -f -L -o ijhttp.zip "https://jb.gg/ijhttp/latest"`
169
+ - **执行**: `ijhttp api-tests/script/<文件名>.http`
170
+ - **批量执行**: `ijhttp api-tests/script/*.http`
171
+ - **环境变量**: `ijhttp <文件> --env-file api-tests/http-client.env.json --env <环境名>`
172
+ - **测试报告 (JUnit XML)**: `ijhttp <文件>.http --report`
173
+ - **依赖**: 需 Java 17+(macOS 可用 `brew install --cask temurin` 安装)
174
+
175
+ API 验证须通过 `api-tests/script/` 下的 `.http` 脚本执行,不得使用 shell 命令或临时脚本替代。
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: architecture-evolution
3
+ description: 架构演进审查(架构维度)——方案/变更与项目整体架构脉络的契合度、长期迭代演进支持、拓展性与兼容性、重构对历史逻辑与既有行为语义的贴合。技术栈无关。
4
+ capabilities: ["architecture"]
5
+ ---
6
+
7
+ > **项目规范优先**:本 skill 所列约定为推荐标准。若项目已有明确规范且与本 skill 不一致,以项目规范为准。
8
+ > 本文列出**架构维度架构演进审查的最低基线**,不限于所列维度。AI 审查时必须结合自身架构知识做拓展覆盖。
9
+
10
+ ## 定位
11
+
12
+ 本 skill 是架构维度的前瞻性审查框架——评估方案/变更与项目整体架构脉络的契合度与演进合理性。只覆盖架构维度可归口的演进类问题,不替代各维度规范:识别出的问题按各维度规范归口,同一问题不在多个维度重复报。
13
+
14
+ ### 归口边界
15
+
16
+ | 演进类信号 | 归口 |
17
+ |-----------|------|
18
+ | 已存在的债务识别与处置分级(过时选型/迁移中断残留/平行实现/临时方案固化/契约不一致) | 技术债识别职责 |
19
+ | 代码级重复、死代码、结构债 | 可维护性维度 |
20
+ | 术语/命名与通用语言漂移 | 命名与通用语言约定 |
21
+ | 分层规则合规、依赖方向违规(绕过分层直连) | 架构分层约定 |
22
+ | 方案设计时未复用既有能力/平行引入新实现 | 方案复用与平行实现约定 |
23
+ | 本 skill 四类检查与上述职责的相邻边界见各类检查中的划界说明 | 本 skill |
24
+
25
+ ## 检查类别
26
+
27
+ ### 1. 整体架构契合
28
+
29
+ 方案/变更是否延续项目整体架构方向——分层风格、模块边界、架构风格延续、与既有模块的边界关系。
30
+
31
+ - 识别信号:新模块/新实现与项目既有架构风格相悖;割裂既有模块边界;架构方向上与项目既有路线冲突
32
+ - 判据:方案/变更与项目整体架构脉络明显冲突且无说明理由
33
+ - 处置:方案/变更与项目整体架构方向冲突且无理由 → 至少 Medium 级别 issue
34
+ - 划界:分层规则合规(是否违反分层依赖方向)归架构分层约定;本类关注方向性冲突(风格/边界/路线)
35
+
36
+ ### 2. 长期迭代逻辑
37
+
38
+ 设计是否支持后续迭代演进——新场景/新类型/新渠道扩展时是否需要返工、是否堵死演进路径、是否与项目迭代方向一致。
39
+
40
+ - 识别信号:扩展新场景需改动核心链路多处;设计固化单一场景假设;演进路径被结构性堵死
41
+ - 判据:可预见的后续迭代在现有设计下需结构性返工且方案未考虑
42
+ - 处置:可预见的后续迭代需结构性返工且方案未考虑 → 至少 Medium 级别 issue
43
+
44
+ ### 3. 拓展性与兼容性
45
+
46
+ 变更是否破坏既有契约——接口签名、数据格式、行为语义;破坏时是否有兼容或过渡安排;应预留拓展点而未预留。
47
+
48
+ - 识别信号:修改既有接口签名/响应结构/字段语义且调用方未同步;存量数据/历史行为不兼容;破坏性变更无过渡方案
49
+ - 判据:破坏既有契约且无兼容/过渡安排
50
+ - 处置:破坏既有契约且无兼容/过渡安排 → 至少 Medium 级别 issue
51
+ - 划界:跨接口/模块同一语义表达不一致(枚举 vs 数字码)归契约不一致职责;本类关注变更对既有契约的破坏与兼容安排
52
+
53
+ ### 4. 重构贴合历史逻辑
54
+
55
+ 重构是否保持既有行为语义与历史调用方契约——行为变化是否显式声明、是否无意改变既有语义。
56
+
57
+ - 识别信号:重构改动既有行为逻辑/边界条件/错误处理语义;重构后行为与重构前不一致且未声明;借重构之名改变契约
58
+ - 判据:重构改变既有行为语义且未显式声明
59
+ - 处置:重构改变既有行为语义且未显式声明 → 至少 Medium 级别 issue
60
+ - 划界:重构场景下破坏契约归本类;非重构场景的新契约破坏归第 3 类。平行引入新实现(另起一套)归方案复用与平行实现约定
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: db-conventions
3
+ description: 数据库设计通用约定——表命名、主键选择、审计字段、逻辑删除、索引命名、确定性标记(状态/类别)与字典字段、操作人来源链路。技术栈无关(不绑定具体 SQL 方言)。
4
+ capabilities: ["db-design", "architecture"]
5
+ ---
6
+
7
+ > **项目规范优先**:本 skill 所列约定为推荐标准。若项目已有明确规范且与本 skill 不一致,以项目规范为准。
8
+
9
+ ## 表结构规范
10
+
11
+ | 项 | 约定 | 说明 |
12
+ |----|------|------|
13
+ | 表名前缀 | `{project_prefix}_` + 业务域名 | 如 `{project_prefix}_order`、`{project_prefix}_order_item` |
14
+ | 主键 | UUID | 全局唯一,不含业务含义 |
15
+ | 状态/类别等确定性标记 | TINYINT | 确定性取值集(码集固定,非字典管理) |
16
+ | 字典类标记 | VARCHAR(原值 code) | 非确定性取值集,由字典动态管理 |
17
+ | 半结构化字段 | JSON / JSONB | 建索引(如 GIN)加速查询 |
18
+
19
+ 数据库类型和具体 SQL 语法由项目技术栈确定。
20
+
21
+ ## 审计字段
22
+
23
+ 所有表必须包含以下审计字段(字段名固定,SQL 类型按项目 DB 调整):
24
+
25
+ | 字段 | 含义 | 备注 |
26
+ |------|------|------|
27
+ | `created_at` | 创建时间 | NOT NULL |
28
+ | `updated_at` | 更新时间 | NOT NULL |
29
+ | `creator` | 创建人标识 | NOT NULL |
30
+ | `updater` | 更新人标识 | NOT NULL |
31
+ | `deleted_at` | 逻辑删除时间 | NULL = 未删除 |
32
+
33
+ ## 逻辑删除
34
+
35
+ 采用 `deleted_at IS NOT NULL` 表示已删除,配合**部分唯一索引**约束未删除行的唯一性:
36
+
37
+ ```sql
38
+ -- 仅对未删除行建立唯一约束,已删除行不受限
39
+ CREATE UNIQUE INDEX uk_{table}_{column} ON {table} (business_key) WHERE deleted_at IS NULL;
40
+ ```
41
+
42
+ 部分唯一索引方案(`WHERE deleted_at IS NULL`)比用 `deleted` 字段存主键值的方案语义更清晰。
43
+
44
+ ## 索引命名
45
+
46
+ | 类型 | 前缀 | 示例 |
47
+ |------|------|------|
48
+ | 唯一索引 | `uk_` | `uk_order_code` |
49
+ | 普通索引 | `idx_` | `idx_order_item_order` |
50
+ | 部分唯一索引 | `uk_` + `WHERE deleted_at IS NULL` | 见上方逻辑删除 |
51
+
52
+ 索引名使用 snake_case。
53
+
54
+ ## 操作人来源(概念层)
55
+
56
+ 典型鉴权链路:后端通过 OAuth2/OIDC 授权码流程登录并签发标准 JWT,操作人标识从 JWT 还原:
57
+
58
+ 1. 安全过滤器校验 JWT,将身份写入请求上下文
59
+ 2. 拦截器从请求上下文取操作人标识,写入请求级鉴权上下文
60
+ 3. 持久化层从鉴权上下文取标识填充 `creator`/`updater` 审计字段
61
+
62
+ 具体 IDP(如 Azure AD、Keycloak)和框架实现按项目技术栈选择。