Part 6
案件追蹤系統完整實戰
以 ASP.NET Core Web API + PostgreSQL 完整走過需求、規格、架構、資料模型、API、實作、測試與交付。ChatGPT 負責加速分析與產出,人負責需求權限、證據與最終核准。
01|案例與完成範圍
案件追蹤系統
角色:承辦人、主管、系統管理員。核心功能:建立案件、指派承辦人、合法狀態轉換、處理紀錄、查詢、稽核。
| 本次垂直切片 | 完成條件 |
|---|---|
| 變更案件狀態 | 權限、狀態機、並行控制、稽核、API 錯誤、整合測試完整 |
| 不納入 | 附件儲存、通知、全文檢索與完整前端;列入 backlog,不假裝已完成 |
02|把 ChatGPT 放進 SDLC,而不是取代 SDLC
1 需求
找缺口、矛盾、角色、狀態、驗收。輸出「需求/假設/待確認」表。
2 設計
狀態機、API contract、schema、constraint、並行與稽核策略。
3 實作
先最小垂直切片;每次變更都能編譯、測試與 review。
4 驗證
單元、整合、權限、並行、migration 與失敗情境。
5 交付
OpenAPI、ADR、migration、測試證據、操作與回復說明。
6 回饋
把缺陷回寫需求與測試;不讓文件、schema 與 code 漂移。
第一輪 Prompt: 先不要寫程式。針對案件狀態變更,找出會改變 權限、狀態機、資料一致性、稽核與驗收測試的未知事項。 依風險排序問 7 題;每題說明若不確認的後果。
03|從需求建立可驗證設計
狀態轉換
| 目前 | 可轉換 | 角色 | 必要條件 |
|---|---|---|---|
| Draft | Submitted | 承辦人 | 必要欄位完整 |
| Submitted | InReview | 主管 | 已指派審查人 |
| InReview | Approved / Rejected | 主管 | 意見不可空白 |
| Rejected | Draft | 承辦人 | 建立修正紀錄 |
資料庫先守住不變條件
create table cases (
id uuid primary key,
title varchar(200) not null,
status varchar(30) not null
check (status in ('Draft','Submitted','InReview','Approved','Rejected')),
version integer not null default 1,
updated_at timestamptz not null
);
create table case_status_history (
id uuid primary key,
case_id uuid not null references cases(id),
from_status varchar(30) not null,
to_status varchar(30) not null,
changed_by uuid not null,
reason text,
changed_at timestamptz not null
);04|API 與核心實作(閱讀示範)
本節是閱讀示範:用來看懂設計意圖,不是可直接部署的實作。可執行的部分放在 §06。
| 項目 | Contract |
|---|---|
| Endpoint | POST /api/cases/{id}/status-transitions |
| 輸入 | targetStatus、reason、expectedVersion |
| 成功 | 200,回傳新狀態、版本、updatedAt |
| 錯誤 | 400 格式/規則、403 權限、404 不存在、409 並行衝突 |
public sealed record ChangeStatusRequest(
CaseStatus TargetStatus,
string? Reason,
int ExpectedVersion);
public Result ChangeStatus(
CaseStatus target, Guid actorId, string? reason, int expectedVersion)
{
if (Version != expectedVersion)
return Result.Conflict("CASE_VERSION_CONFLICT");
if (!StatusTransitions.IsAllowed(Status, target))
return Result.Invalid("INVALID_STATUS_TRANSITION");
var previous = Status;
Status = target;
Version++;
UpdatedAt = DateTimeOffset.UtcNow;
_history.Add(CaseStatusChanged.Create(
Id, previous, target, actorId, reason, UpdatedAt));
return Result.Success();
}上面那段版本檢查,擋不住兩個服務實例
if (Version != expectedVersion) 是記憶體內的比對。它能擋掉「客戶端拿著舊版本送出」,但擋不住「兩個請求各自讀到同一筆舊資料、各自比對通過」。下面這個時序,兩邊都會通過檢查:
| 時點 | 請求 A | 請求 B | 學習重點 |
|---|---|---|---|
| 1 | 讀取版本 7 | 讀取版本 7 | 兩個應用程式中的物件各自持有相同舊值。 |
| 2 | expectedVersion = 7,比對通過 | expectedVersion = 7,比對通過 | 記憶體比對不能阻止另一個請求也通過。 |
| 3 | 嘗試寫入版本 8 | 嘗試寫入版本 8 | 會不會衝突,要看資料庫寫入條件與影響列數。 |
| 4 | 若更新沒有版本條件,兩次寫入可能都被接受。 | 前置檢查與原子更新不是同一件事。 | |
版本必須參與寫入條件
真正的關鍵機制,是把版本放進資料庫更新的 WHERE 條件,再用影響列數判斷有沒有成功。EF Core 的 optimistic concurrency 就是這個做法:把 concurrency token 放進更新條件,找不到符合舊 token 的列時,回報並行衝突。
-- 概念 SQL;不是可直接部署的完整 repository 實作。
-- 角色、案件可見範圍、合法轉換與理由必填,仍須先驗證。
UPDATE cases
SET status = @target_status,
version = version + 1,
updated_at = @now
WHERE id = @id
AND version = @expected_version
AND status = @expected_status
RETURNING id, status, version;
-- 0 列:這次狀態轉換沒有完成;依 contract 處理衝突或不存在。
-- 1 列:才可以在同一個交易內寫入對應的 history。
-- 任一步驟失敗,整個交易必須回滾。
rowversion 範例是 SQL Server 的型別,不要直接當成 PostgreSQL 的欄位型別來用。第二,EF Core 單次 SaveChanges 在支援交易的 provider 下具備原子性;一旦你拆成多次儲存或混用原始 SQL,就必須自己確認它們用的是同一個交易與連線。第三,這段是設計方向,不代表任何特定程式已經正確實作——要用 §06 的雙請求測試證明。05|測試不是只有 happy path
單元測試
- 合法與非法狀態轉換
- 理由必填規則
- 版本衝突
- 歷程事件內容
整合測試
- 實際 PostgreSQL constraint
- transaction 回滾
- 兩個請求同時更新
- HTTP 400/403/404/409 contract
安全測試
- 未登入與錯誤角色
- 跨單位案件越權
- 稽核紀錄不可竄改
- 輸入長度與惡意內容
Migration 測試
- 既有資料能否轉換
- upgrade/rollback 路徑
- 鎖表與停機風險
- 備份與復原演練
06|畢業實作與驗收(可執行實驗)
這裡才是要動手跑的部分。請把它和 §04 的閱讀示範分開看:示範讓你看懂,實驗才產生證據。
- 寫 10 條可驗收需求,標示來源與待確認。
- 完成狀態轉換表、API contract、schema 與 ADR。
- 建立可啟動的 ASP.NET Core 專案與 PostgreSQL migration。
- 完成一條狀態變更垂直切片。
- 至少 12 個測試:規則、權限、並行、資料庫與 HTTP。
- 執行 build、test、migration 驗證,保存輸出。
- 產生 OpenAPI、README、部署與回復說明。
- 讓 ChatGPT 以 hostile reviewer 反查,再由你判斷是否接受。
真正完成
不是「ChatGPT 已產生 code」,而是新環境能按 README 啟動;測試可重跑;資料庫規則存在;每項需求能追到設計與測試;未知與風險未被隱藏。
驗收契約:要觀察到什麼,什麼不算數
| 驗收項目 | 應觀察到的結果 | 不足的替代品 |
|---|---|---|
| 相同舊版本,兩個獨立請求同時轉換 | 只有一筆成功轉換,另一筆依 contract 回報衝突;歷程筆數與成功次數相符。 | 對同一個物件連續呼叫兩次。 |
| 寫入歷程時故意觸發失敗 | 主狀態與版本都回復到交易前。 | 只 assert 回傳了錯誤訊息。 |
| 錯誤角色與跨單位存取 | 拒絕未授權操作,且沒有任何資料變更。 | 只在畫面上隱藏按鈕。 |
| 空白理由與非法狀態轉換 | 拒絕不符規則的輸入,並有對應測試。 | 只測成功路徑。 |
| 新環境依 README 啟動 | 可重跑 migration 與整合測試,附執行命令、版本與結果。 | AI 生成的「預期會通過」文字。 |
07|20 題綜合情境測驗
總分至少 85,且四個能力軸均達 70% 才通過。
官方技術參考
本章維護紀錄
| 項目 | 內容 |
|---|---|
| 查核日期/依據 | 2026-09-13;依 2026-09 現況查核報告(內部紀錄,未發布於網站) 的 F02、F03、F12 修訂。 |
| 主張與來源 | 並行控制:EF Core — Handling Concurrency Conflicts;交易原子性:EF Core — Using Transactions;子集合唯一性:PostgreSQL — Partial Indexes。 |
| 已執行的步驟 | 已查閱官方文件並改寫正文;未在本 repo 建立可啟動專案,未執行任何資料庫或併發測試。 |
| 待確認 | 尚不存在於本 repo 的 .NET/PostgreSQL 實驗專案(starter、migration、整合測試)。 |