昨天,我們總算把廣播相關的資料表補得差不多了。
broadcasts、broadcast_targets、broadcast_confirmations、replies,該有的東西基本上都有了。
甚至連昨天一直在講的那棵樹,也真的種進 PostgreSQL 了。
所以今天終於可以寫 API 了?
不急。
又不急?
因為我突然發現一個問題。
我們昨天雖然把「資料要怎麼存」想得差不多了,但資料庫其實根本不知道這套系統要怎麼運作。
例如:
問題來了。
PostgreSQL 怎麼會知道我們學校幾點放學?
你可以告訴它啊。
……
好像也是。
但這就是今天真正要處理的問題。
昨天做的事情,大部分都是 Database Constraint 很擅長的。
例如:
BroadcastTarget
PRIMARY KEY (broadcast_id, group_id)
這代表同一則廣播不能把同一個班級 Target 加兩次。
Foreign Key 也可以保證 author_id 不能指向一個根本不存在的 Account。
這些規則有一個共同點:
它們描述的是「資料長什麼樣」。
但:
下午 3 點後發出的廣播,確認期限是隔天下午 4:30。
這就不是單純看 Schema 能知道的事情了。
於是我們終於來到了 Service Layer。
如果說 Database 在管:
這筆資料可不可以存在?
那 Service Layer 管的大概就是:
這件事情在這套系統裡可不可以發生?
先從昨天留下來最大的問題開始。
我們已經決定,每天的切點是:
15:00
15:00 前發出的廣播,確認責任算在今天;15:00 後發出的,則算到下一個週期。
例如:
10/06 14:59 發送
→ Deadline:10/06 16:30
10/06 15:00 發送
→ Deadline:10/07 16:30
看起來很簡單。
直到我想到:
如果星期五下午 3:30 發呢?
星期六下午 4:30?
你們星期六上課嗎?
那國定假日呢?
補班補課呢?
校慶呢?
……
閉嘴。
我們當然可以做 School Calendar,把國定假日、學校行事曆、特殊上課日全部塞進去。
聽起來非常完整。
也非常麻煩。
更重要的是——
我們真的需要嗎?
如果有老師星期五下午 3 點多突然發一則廣播,然後期待星期六下午 4:30 前資訊股長一定要登入確認……
我覺得問題可能不在系統。
所以最後決定非常簡單:
不管。
15:00 後就是下一個 Calendar Day。
星期五後面就是星期六,國定假日也照算。
有時候最簡單的 Business Rule,不是把現實世界建模得鉅細靡遺。
而是先決定:
哪些現實世界的複雜度,我根本不打算處理。
接著又冒出另一個問題。
假設今天 15:20 發了一則廣播。
照剛才的規則:
確認週期:明天
Deadline:明天 16:30
那這則廣播什麼時候出現?
答案是:現在。
因為「明天才需要負責確認」跟「明天才能看到」根本是兩回事。
所以:
發布時間 → 決定何時看得到
確認週期 → 決定何時應該確認
兩件事情正式拆開。
同樣的道理,如果資訊股長今天 15:21 就順手把明天週期的廣播按掉,也完全沒問題。
沒有必要跳出一個:
不行喔,這是明天的工作,請你明天再來按。
神經病。
所以週期限制的是責任,不是操作權。
那超過 16:30 呢?
按照現在舊系統的行為:
還是可以按。
只是你遲到了。
所以 ack_deadline_at 是 Deadline,不是 Lock Time。
我們只需要比較:
confirmed_at <= ack_deadline_at
→ 準時確認
confirmed_at > ack_deadline_at
→ 逾期確認
如果 Deadline 都過了還沒按,就是逾期未確認。
也因此根本沒有必要再往資料庫塞一個 is_late。
它不是新的事實,只是既有資料算出來的結果。
再來是每週顯示。
如果上星期有一則廣播,399 到星期日都沒確認,到了星期一,要不要繼續出現在這星期的待確認清單?
最後決定:
不要。
上週就是上週,這週就是這週。
不然一個班只要哪天漏掉一則,之後每個星期打開系統都有一個紅點跟著你,頗有一種陰魂不散的感覺。
但回到上週的歷史紀錄,它依然會顯示:
逾期未確認
而且還是可以補按。
補按之後就變成:
逾期確認
所以:
上週欠的,不計入這週工作量;但上週欠過,系統不會忘記。
我們目前學生帳號的職位權限大概是:
| 職位 | 查看 | 確認 | 回覆 |
|---|---|---|---|
| 一般學生 | ✓ | ✗ | ✗ |
| 資訊股長 | ✓ | ✓ | ✓ |
| 班代表 | ✓ | ✓ | ✓ |
所以確認的判斷是:
這個 Account 所在的班級,是不是這則 Broadcast 的 Target?它的職位又有沒有確認權限?
而 Confirmation 本身仍然屬於:
Broadcast × Group
假設 399 資訊股長先按了,那 399 就已經確認。
至於到底是誰按的,則交給:
confirmed_by_account_id
留下紀錄。
責任狀態屬於班級,操作紀錄則留下實際執行的 Account。
Broadcast 的發送權限反而簡單很多。
目前先直接定:
account_type == TEACHER
才能發。
同時 Teacher 也不需要「屬於」某個班級。
例如:
Teacher
↓
Broadcast
├── 398
├── 399
└── 400
完全合法。
不然行政或教師端如果還得先成為 399 的 Group Member 才能發廣播給 399,整件事情反而很奇怪。
所以班級 Group 是收件範圍,不是老師的權限 Domain。
另外,一則 Broadcast 至少要有一個 Target。
我們沒有 Draft。
按下發送,就是發送。
接著是一個我覺得很重要的決定:
Broadcast 發送後不可修改,也不可刪除。
假設老師發:
明天記得帶平板。
399 已經按確認。
結果老師偷偷改成:
明天記得帶平板跟 500 元。
那資料庫雖然還會告訴我們「399 已確認」,但 399 到底確認了哪個版本?
不知道。
Confirmation 的稽核意義直接被我們自己打爛。
所以乾脆:
發出去就不准改。
真的寫錯?
重發一則。
目前也不做 revision、superseded_by 之類的關係。
新的一則就是新的一則。
然後我們回頭看了一眼昨天才種好的 Reply Tree。
目前 replies 大概是:
replies
├── broadcast_id
├── author_id
├── content
└── ref_id
直到出現:
Broadcast #100
├── 398
├── 399
└── 400
399 的資訊股長回:
老師,明天需要帶平板嗎?
398 要不要看到?
不要。
每個班的 Reply 應該是獨立的。
一開始想說,那就在 replies 加個 group_id。
但仔細看昨天的 Schema,其實已經有一個東西完美描述:
「某則 Broadcast 發給某個 Group。」
就是:
broadcast_targets
-----------------
broadcast_id
group_id
所以 Reply 真正應該掛的不是單純的 Broadcast。
而是 BroadcastTarget。
Broadcast #100
│
├── Target 398
│ └── Reply Tree
├── Target 399
│ └── Reply Tree
└── Target 400
└── Reply Tree
這樣 (broadcast_id, group_id) 就可以直接指向 broadcast_targets。
資料庫甚至可以直接保證:
你不能在一個根本沒收到這則廣播的班級底下憑空長出 Reply。
這比單純指向 groups.id 更精確。
既然 Reply 屬於 BroadcastTarget,那昨天的 Tree Invariant 也得升級。
如果 Reply 有 ref_id,Parent 必須同時滿足:
reply.broadcast_id == parent.broadcast_id
reply.group_id == parent.group_id
不能跨 Broadcast,也不能跨 Target Group。
不然我們前面辛苦把每個班的 Thread 隔離,結果 ref_id 一接,又全部長回去了。
最後是 Reply 權限。
一般學生可以看自己班的 Reply Tree,但不能回覆。
資訊股長跟班代表可以。
Teacher 則比較有趣。
所有 Teacher 都可以查看所有 Broadcast,以及各個 Target 底下的 Reply。
但只有這則 Broadcast 的 Author可以回覆。
所以:
Teacher A 發 Broadcast #100
Teacher B 查看 → ✓
Teacher B 回覆 → ✗
Teacher A 回覆 → ✓
學生則只能存取自己班的 Thread。
這也讓 Read 跟 Write 正式分開:
看得到,不代表有權修改。
而 Reply 跟 Confirmation 也是兩回事。
你回了一句「收到」,不代表系統就幫你按確認。
確認就是確認,回覆就是回覆。
另外,Reply 也不跟著確認週期過期。
因為「週期」從頭到尾處理的都是確認責任,不是 Broadcast 或 Reply Thread 的死亡時間。
所以上週的廣播,照樣可以繼續回覆。
繞了一大圈之後,今天其實沒有寫多少很炫的程式。
反而大部分時間都在回答:
「這種情況到底算什麼?」
但這些問題如果今天不回答,最後就一定會有人替我們回答。
可能是 Route。
可能是 Frontend。
也可能是半年後連我自己都不知道為什麼存在的一段 if。
所以 Database 跟 Service Layer 的分工其實變得很清楚。
Database 可以說:
這個 Reply 指向的 BroadcastTarget 必須存在。
Service Layer 則說:
你到底有沒有資格在這個 Target 裡 Reply?
Database 可以說:
同一個 Broadcast × Group 只能有一筆 Confirmation。
Service Layer則說:
這個 Account 到底能不能替這個 Group 確認?
Database 可以保存:
ack_deadline_at
但它根本不需要知道為什麼是那一天的 16:30。
資料庫:你們學校幾點放學關我屁事。
……
好啦。
確實不關你的事。
這些規則,就留給 Service Layer。
至於實作?
規格都定成這樣了,就交給 Agent 去把它寫完吧。
明天再來看看,要怎麼把這些規則真正接到 API 上。