Skip to content

SDD-025 — Anchor ​

Never lose the commitment. Thiết kế kỹ thuật cho PRD-003 phần Anchor.

Nếu Dory là trí nhớ, Anchor là lương tâm: nó giữ những thứ Nemo12 đã hứa mà chưa làm, và không cho chúng biến mất một cách êm ái.

1. Nguyên tắc ​

  1. Một lời hứa không có chủ và hạn thì không lưu được. owner_user_id và due_at là NOT NULL trên commitments. Cho phép để trống nghĩa là phần lớn sẽ trống, và một danh sách lời hứa không hạn chính là danh sách những thứ sẽ không xảy ra.
  2. Đóng case cần bằng chứng, không cần một cú bấm. resolution_note bắt buộc; muốn đóng mà chưa có thì phải nói rõ đang đóng bằng ngoại lệ (§6).
  3. Xong chưa chắc là xong. Mỗi resolution tự đặt một lần follow-up (§7). Một vấn đề đóng hôm nay mà hai tuần sau quay lại thì lần đóng đó đã sai.
  4. Escalation cứu việc, không bắt lỗi người. Không có bảng xếp hạng Dolphin nào sinh ra từ dữ liệu này (PRD-003 §8).
  5. Danh sách ngắn mới có tác dụng. Anchor cố ý không nhận mọi ý nghĩ thoáng qua — thứ đó thuộc về ghi chú của Dory. Vào Anchor là thành nghĩa vụ.

2. Dữ liệu ​

text
cases
  id · family_id → families · learner_id? → learners
  title · description?
  category(academic|logistics|billing|relationship|technical|other)
  priority(low|normal|high|urgent)
  status(open|in_progress|waiting_family|resolved|closed|reopened)
  opened_by → users · owner_user_id → users
  source_interaction_id? → family_interactions      -- sinh ra từ một cuộc gọi
  due_at?                                            -- SLA tính ra, sửa tay được
  resolved_at? · resolution_note? · resolution_evidence_json?
  closed_at? · reopened_count
  created_at · updated_at

commitments                 -- "chúng tôi sẽ..."
  id · case_id? → cases · family_id → families
  statement                 -- nguyên văn điều đã hứa
  owner_user_id NOT NULL → users
  due_at NOT NULL
  made_at · made_to_contact_id? → family_contacts
  source_interaction_id? → family_interactions
  status(open|kept|missed|cancelled)
  kept_at? · evidence_note?
  created_at · updated_at

case_actions                -- việc cụ thể để xong một case
  id · case_id → cases · title · assignee_user_id? → users
  due_at? · status(todo|doing|done|dropped) · done_at? · created_at

case_followups              -- kiểm lại sau khi đóng
  id · case_id → cases · due_at · question
  status(pending|confirmed_ok|reopened) · checked_by? · checked_at? · note?

case_events                 -- vết xử lý, chỉ nối thêm
  id · case_id · actor_user_id · event_type · from_status? · to_status?
  detail? · created_at

commitments tách khỏi cases chứ không phải một cột trong đó. Lý do: rất nhiều lời hứa không có case nào cả — "em gửi anh lịch thi thử trước thứ Sáu" không phải một vấn đề, nó là một việc. Bắt phải mở case trước mới ghi được lời hứa là dựng một thủ tục khiến người ta thôi ghi.

statement lưu nguyên văn điều đã hứa, không phải bản tóm tắt. Khi cãi nhau về việc đã hứa gì, bản tóm tắt là thứ vô dụng nhất.

3. Vòng đời Case (REQ-ANC-01) ​

text
open → in_progress → waiting_family ⇄ in_progress → resolved → closed
                                                        ↓
                                                    reopened → in_progress

waiting_family là trạng thái riêng chứ không gộp vào in_progress, vì đồng hồ SLA dừng ở đó (§5). Không tách thì mọi case chờ phụ huynh trả lời đều trở thành case trễ, và bảng cảnh báo đầy thứ không ai làm gì được — sau vài tuần thì không ai nhìn bảng đó nữa.

reopened_count để trần: một case mở lại ba lần là dấu hiệu resolution lần đầu chưa bao giờ đúng, và đó là thông tin đáng nhìn hơn con số "đã đóng bao nhiêu case".

4. Commitment (REQ-ANC-02) ​

Ba đường sinh ra một commitment, xếp theo mức hay dùng:

  1. Từ Interaction Capture của Dory — vừa gọi xong, bấm "tôi vừa hứa gì đó".
  2. Từ trong một Case.
  3. Tạo thẳng ở màn gia đình.

status chỉ có bốn giá trị, và missed là một trạng thái thật, không phải lỗi. Một hệ thống không cho phép ghi nhận "đã lỡ hẹn" sẽ được dùng bằng cách im lặng đổi due_at — và khi đó mọi số liệu về độ tin cậy đều là số giả.

5. SLA (REQ-ANC-04) ​

Hạn tính theo category × priority, ghi trong config chứ không rải trong mã:

categoryurgenthighnormallow
billing4h1 ngày2 ngày5 ngày
academic1 ngày2 ngày5 ngày10 ngày
relationship1 ngày2 ngày3 ngày7 ngày
logistics · technical · other1 ngày3 ngày5 ngày10 ngày
  • Đồng hồ dừng khi waiting_family, chạy lại khi về in_progress.
  • Cảnh báo trước khi trễ, ở mốc 80% thời gian — cảnh báo sau khi trễ chỉ là đưa tin buồn.
  • due_at sửa tay được, nhưng mỗi lần sửa ghi một dòng case_events. Gia hạn im lặng là cách một hệ thống theo dõi hạn tự vô hiệu hoá chính nó.

6. Resolution Evidence (REQ-ANC-07) ​

Chuyển sang resolved đòi:

  • resolution_note không rỗng, và
  • ít nhất một trong: một case_action đã done, một commitment đã kept, hoặc một family_interaction gắn với case này.

Thiếu thì API trả 409 kèm câu nói rõ còn thiếu gì — cùng khuôn publish_blockers đã dùng ở SDD-019 §4 và SDD-020 §4. Khuôn này đã chứng minh được: người dùng biết trước phải làm gì thay vì bấm rồi mới nhận lỗi.

Có một đường đóng ngoại lệ: closed với resolution_note bắt đầu bằng [no-evidence]. Nó tồn tại vì thực tế có case chết già (gia đình ngừng học, vấn đề tự hết) và một cổng không có lối thoát nào là một cổng người ta sẽ tìm cách lách. Nhưng những case đó hiện thành một mục riêng trong Commitment Model (closed_without_evidence) — hệ thống tự thú nhận chỗ nó đang bị dùng sai.

7. Follow-up (REQ-ANC-05) ​

Mỗi lần resolved, hệ tự tạo một case_followup với due_at = +14 ngày (billing +7). question sinh từ tiêu đề case: "Two weeks on, is [title] genuinely settled?"

Đến hạn, follow-up hiện trong danh sách việc của owner. Ba kết cục: confirmed_ok · reopened · gia hạn một lần. Không có nút bỏ qua — bỏ qua chính là thứ follow-up sinh ra để chặn.

8. Escalation (REQ-ANC-06) ​

Điều kiệnHành động
Case quá due_at 24hgắn cờ at_risk, hiện đầu danh sách của owner
Case quá due_at 72hthêm vào bảng của trưởng nhóm
Commitment quá hạnđánh dấu missed, sinh signal cho Dory (nhà này ta đã lỡ hẹn)
Case urgent chưa ai nhận sau 4hvào bảng trưởng nhóm ngay

Chạy trong cron chung với Family Signals §5. Mỗi lần escalate ghi case_events — có vết thì mới cãi lại được, kể cả cãi rằng luật escalation đang quá nhạy.

9. Audit (REQ-ANC-08) ​

case_events là vết nghiệp vụ (đọc được trên màn hình, kể lại câu chuyện của một case). audit_log là vết truy cập (ai xem dữ liệu gia đình nào lúc nào, AS-07.4). Hai thứ khác nhau và cùng phải có; gộp lại thì một trong hai sẽ bị cắt xén cho vừa cái kia.

case_events chỉ nối thêm, không có đường sửa hay xoá trong ứng dụng — cùng luật đã áp cho audit_log.

10. API ​

MethodPathQuyền
GET · POST/v1/mentor/cases🔐 role mentor
GET · PATCH/v1/mentor/cases/{id}🔐 role mentor
POST/v1/mentor/cases/{id}/resolve🔐 role mentor
POST/v1/mentor/cases/{id}/reopen🔐 role mentor
POST/v1/mentor/cases/{id}/actions · PATCH /v1/mentor/actions/{id}🔐 role mentor
GET · POST/v1/mentor/commitments🔐 role mentor
PATCH/v1/mentor/commitments/{id}🔐 role mentor
GET/v1/mentor/my-work🔐 role mentor
POST/v1/mentor/followups/{id}/check🔐 role mentor
GET/v1/mentor/escalations🔐 role mentor

/v1/mentor/my-work là màn hình mở đầu ca trực: commitments đến hạn · cases at-risk · follow-ups tới hạn, gộp một truy vấn.

11. Cái chưa làm ​

ViệcVì sao
Tự nhắn cho phụ huynh khi commitment tới hạnAnchor nhắc Dolphin, không thay Dolphin nói chuyện (PRD-003 §8)
Bảng xếp hạng Dolphin theo tỉ lệ giữ lờiEscalation để cứu việc, không để bắt lỗi người
SLA theo lịch làm việc (trừ cuối tuần)Đợt đầu tính theo giờ trôi; thêm lịch làm việc là một lớp phức tạp đáng làm sau khi biết dữ liệu thật trông thế nào
Case template theo loại vấn đềChờ đủ dữ liệu thật để biết loại nào lặp lại

Trace ​

REQ-ANC-01→§2-3 · REQ-ANC-02→§4 · REQ-ANC-03→§2 · REQ-ANC-04→§5 · REQ-ANC-05→§7 · REQ-ANC-06→§8 · REQ-ANC-07→§6 · REQ-ANC-08→§9.