Skip to content

SDD-045 - Journey Management & Activation Engine ​

Yêu cầu: PRD-005. Nguồn: SRC-1077 (SDD mẫu của chủ dự án, 27.09.2026). Chỗ nào bản này khác SDD mẫu thì ghi rõ lý do và mã quyết định.

1. Hình dạng tổng thể ​

text
                 registry.ts (Journey Matrix, policies)
                          |
facts.ts  ----->  engine.ts (thuần)  <-----  learner_journey_states (ảnh chụp lần trước)
(Learner State)        |
                       v
             portfolio + foreground + next action
                       |
          service.ts: ghi ảnh chụp + journey_events (idempotent)
                       |
         routes.ts: learner view  |  admin trace
                       |
   learn /ielts (JourneyNextUp)   admin #/journeys

Mã: workers/api/src/modules/journeys/ (rules.ts, registry.ts, facts.ts, engine.ts, service.ts, routes.ts). Giao diện: apps/learn/src/JourneyNextUp.tsx, apps/admin/src/pages/Journeys.tsx. Dữ liệu: migration 0301_journey_engine_state_and_events.sql.

2. Bốn mối quan tâm tách nhau ​

Mối quan tâmCâu hỏiChỗ
Journey DefinitionHành trình này là gì?registry.ts
Journey StateLearner có đang ở trong nó không?engine.ts#deriveState
PriorityNó quan trọng tới đâu lúc này?engine.ts#scoreJourney + policy
Next Best ActionLearner nên làm gì?engine.ts#pickAction + catalog

Engine là hàm THUẦN: không chạm D1, không đọc đồng hồ (nhận now). Mọi kịch bản của §13 chạy không cần cơ sở dữ liệu, và test không đỏ theo lịch.

3. Trạng thái suy ra, không ghi làm nguồn (✍️ Q-159) ​

SDD mẫu lưu journey_instances làm nguồn sự thật. Bản này suy ra trạng thái từ dữ liệu học mỗi lần đánh giá, cùng lý do ielts/journey.ts cũ từ chối lưu "đang ở bước mấy": một cột trạng thái sẽ lệch khỏi sự thật ngay lần đầu learner xoá mục tiêu. D1 chỉ giữ ba thứ không suy ra được: ảnh chụp lần trước (để biết lần này có ĐỔI không, và hành trình nào đang đứng trước), mốc snoozed_until, và sổ sự kiện.

Luật suy trạng thái (deriveState), theo thứ tự:

  1. Luật xong đúng: lifecycle thì COMPLETED; episodic chỉ COMPLETED nếu lần trước đã ở trong đợt (ELIGIBLE/ACTIVE/PAUSED/COMPLETED/EXPIRED), nếu không thì NOT_ELIGIBLE. Learner chưa từng nghỉ thì Recovery là NOT_ELIGIBLE, không phải COMPLETED.
  2. Luật hết hạn đúng: EXPIRED nếu đã ở trong đợt, nếu không thì NOT_ELIGIBLE.
  3. Không đủ điều kiện: NOT_ELIGIBLE.
  4. snoozed_until còn hạn: PAUSED.
  5. Luật kích hoạt đúng: ACTIVE, không thì ELIGIBLE.

recurring (Daily Learning) không có luật xong (registry test chặn).

4. Journey Registry (✍️ Q-160) ​

Định nghĩa sống trong registry.ts, sửa bằng PR, không bằng màn admin: đổi một luật là đổi hành vi của mọi learner, và thay đổi ấy cần review, test và một commit nói vì sao. Registry là DỮ LIỆU (luật khai báo, không phải hàm), nên ngày chuyển vào D1 chỉ là đổi chỗ đọc.

ts
JourneyDefinition {
  id, version, name, purpose, horizon, owner, status, kind,   // lifecycle | episodic | recurring
  eligibility?, activation?, completion?, expiration?,        // Rule
  factors: { urgency, goal_relevance, need, impact, readiness }, // Factor
  policy, actions: JourneyAction[], success_metrics
}

validateRegistry() chặn ở test: id trùng, policy không tồn tại, catalog rỗng, gợi ý cuối có điều kiện (phải có gợi ý mặc định), hành trình recurring có luật xong, trọng số policy không cộng bằng 1, và em dash trong chữ gợi ý. Đổi luật thì tăng version: sổ ghi phiên bản đã dùng.

5. Luật và dữ kiện ​

Luật (rules.ts): { all: [...] } hoặc { any: [...] }, lồng được; mỗi điều kiện { fact, op, value } với op trong == != < <= > >=. Kết quả luôn kèm vết từng điều kiện (fact, op, expected, actual, pass) - chính thứ trang admin bày. Dữ kiện null (chưa biết) chỉ khớp == null: "chưa có ngày thi" không được lọt qua days_remaining <= 30.

Nhân tố: hằng số, hoặc bậc thang trên một dữ kiện (steps: [[op, ngưỡng, giá trị]], bậc đầu khớp thắng, else nếu không bậc nào). Boolean đọc như 1/0.

Learner State Provider (facts.ts) - một chỗ đọc duy nhất; mỗi truy vấn bọc try/catch trả "chưa có" (code có thể lên trước migration):

Dữ kiệnNguồn
goal.exists, goal.target_date, goal.days_remaining, goal.overalllearner_ielts_goals active. Ngày mục tiêu dùng làm ngày thi (✍️ Q-162)
profile.answeredlearner_ielts_profile_facts + learner_ielts_declarations
diagnosis.exam_quiz, diagnosis.measuredielts_exam_quiz_attempts; assessment_sessions diagnostic completed hoặc learner_ielts_baselines
plan.milestones, progress.milestone_due_days_agolearner_ielts_milestones của mục tiêu đang dùng
progress.reviewed, goal.reached_reviewedjourney_events kind action_accepted sau ngày mốc / sau lần sửa mục tiêu
activity.ever, activity.inactive_days, activity.todaylearning_events, chỉ các action practice, review, submit, read (✍️ Q-163) tính theo ngày học (04:00 VN)
capability.measured_overall, capability.reachedảnh chụp Learner Model IELTS mới nhất (readIeltsModel), cần đủ bốn kỹ năng
focus.skill, focus.gap, focus.whykỹ năng xa mục tiêu nhất; chưa đo thì kỹ năng ít luyện nhất 14 ngày

6. Ưu tiên ​

Policy: trọng số năm nhân tố (cộng bằng 1), min_score, eligible_multiplier (0.6).

text
score = Σ weight_f × factor_f   × (ACTIVE ? 1 : eligible_multiplier)

Hành trình không ACTIVE/ELIGIBLE có điểm 0. Năm policy MVP: setup_v1, daily_v1, recovery_v1, exam_urgency_v1, goal_progress_v1 (ngưỡng 0.75, để Goal Achievement chưa đạt không bao giờ đứng trước). Trọng số là phỏng đoán có lý do ghi trong mã, đo lại sau bốn tuần (Q-172).

Suppression được diễn bằng nhân tố thay vì một luật riêng: exam_preparation.readiness rơi về 0.3 khi learner nghỉ >= 7 ngày, nên Recovery thắng (PRD-005 §8).

7. Dữ liệu (migration 0301) ​

BảngVai trò
learner_journey_states (PK learner_id, journey_id)Ảnh chụp lần đánh giá gần nhất: state, definition_version, priority_score, is_foreground, action_id, snoozed_until, evaluated_at
journey_eventsSổ chỉ chèn: transition (from, to), foreground, action_accepted, snoozed; kèm definition_version, reasons_json

SDD mẫu liệt kê mười một bảng (definitions, versions, stages, rules, policies...). Chín bảng không có vì registry nằm trong mã (§4) và trạng thái suy ra (§3).

8. Hành trình đứng trước ​

  1. Ứng viên = ACTIVE hoặc ELIGIBLE, không PAUSED, điểm >= min_score của policy.
  2. Xếp: điểm, rồi urgency, goal_relevance, impact, rồi thứ tự trong registry.
  3. Giữ mạch: hành trình đang đứng trước (ảnh chụp) ở lại nếu kém đứng đầu <= 0.05.
  4. Không ứng viên nào: không có thẻ (learner vẫn còn bốn lối vào tự chọn trên trang chủ).

Mỗi hành trình không phải ứng viên mang not_candidate_reason (đang hoãn, trạng thái, điểm dưới ngưỡng); hành trình thắng mang foreground_reason.

9. API ​

ĐườngAiLàm gì
GET /v1/learners/{id}/journeyscanAccessLearnerĐánh giá, ghi sổ, trả bản learner: foreground, next_action, journeys[] {id, name, horizon, state}. Không có điểm, nhân tố
POST /v1/learners/{id}/journeys/re-evaluatenhư trênĐánh giá lại tường minh (FR8)
POST /v1/learners/{id}/journeys/{journeyId}/snoozenhư trên"Để sau" 24 giờ; 409 nếu hành trình không ACTIVE/ELIGIBLE
POST /v1/learners/{id}/journeys/{journeyId}/actions/{actionId}/acceptnhư trênGhi learner đã bấm; 404 nếu action không có trong catalog
GET /v1/admin/journeysadminRegistry, policy, lỗi cấu hình, số learner theo (hành trình, trạng thái)
GET /v1/admin/learners/{id}/journeysadminVết đầy đủ: facts, ảnh chụp trước, từng journey (luật, nhân tố, điểm, lý do), sổ sự kiện. Không ghi gì

Endpoint cũ GET /v1/learners/{id}/ielts-journey (năm bước) đã bỏ cùng ielts/journey.ts.

10. Đánh giá đồng bộ, ghi idempotent (✍️ Q-161) ​

SDD mẫu vẽ Event Router + Cloudflare Workflow + cache. MVP không cần: trạng thái suy ra từ dữ liệu học, nên mọi sự kiện domain (nộp bài, đổi mục tiêu, ngày tới gần) đã nằm trong dữ liệu facts.ts đọc; đánh giá lúc learner mở trang luôn đúng với dữ liệu mới nhất, không có cảnh "sự kiện chưa tới". Chín truy vấn nhỏ, không cần cache.

Đồng thời: mỗi dòng ảnh chụp đổi bằng UPDATE ... WHERE state = <đã đọc> AND is_foreground = <đã đọc> (optimistic concurrency); dòng transition/foreground chỉ chèn khi chính câu ấy đổi được dòng. Hai tab mở cùng lúc thì một lần chuyển, một dòng sổ.

Hỏng: ghi sổ lỗi thì nuốt, gợi ý vẫn trả về. Phía learn, lỗi đọc thì thẻ im lặng biến mất, không hiện "Journey Engine failed" (SDD mẫu §23).

Khi nào cần Workflows: hành trình phải CHỦ ĐỘNG đi ra ngoài (thư nhắc khi Recovery bật) - việc ấy cần một lịch chạy, không cần learner mở trang.

11. Giao diện ​

Learn - JourneyNextUp ở đầu cụm "What to do now" trên trang chủ /ielts (✍️ Q-164), và thay JourneyStrip năm bước trên trang "Con và IELTS" (/squid/ielts-profile/ielts, đọc lại sau khi nộp quiz). Thẻ: chấm cam, nhãn "Next up", tên hành trình (tiếng Anh), câu gợi ý và câu vì sao (tiếng Việt, do server viết), nút "Làm ngay" (ghi accept với keepalive rồi chuyển trang) và "Để sau" (hoãn rồi vẽ lại với hành trình kế tiếp). Chữ theo SRC-953 (✍️ Q-165).

Admin - tab Journeys (#/journeys): bảng registry kèm số learner mỗi trạng thái, bảng policy, ô tra learner. #/journeys/<learnerId>: việc tiếp theo và vì sao hành trình ấy đứng trước; từng hành trình theo thứ hạng với luật đủ điều kiện / kích hoạt / xong / hết hạn (mỗi điều kiện ✓ ✗ kèm giá trị thật), nhân tố, điểm, lý do không phải ứng viên; bảng dữ kiện; sổ sự kiện. Nút "Journeys →" trên mỗi dòng của tab Learners.

Trang công khai - năm bước thay bằng tám hành trình ở nemo12.com/ielts/journey, /{students|parents}/university/ielts/journey, khối trên trang môn IELTS của học sinh, và mọi nhãn liên kết (apps/web, dữ liệu chung ở ieltsJourneySteps.ts). URL giữ nguyên. SAT giữ năm bước (journeyLabel mặc định).

Bàn mentor (SDD-037 desk/steps.ts) giữ năm bước theo lô cho tới Q-169.

12. Bảo mật và riêng tư ​

Đường learner qua canAccessLearner (401 cho nhà khác, không lộ learner có tồn tại). Đường vết chỉ admin. Bản learner không có điểm, nhân tố hay dữ kiện thô. Registry chỉ đọc qua API; đổi luật là commit có review. Phụ huynh chưa có đường (Q-168).

13. Kiểm chứng ​

TầngFileGác
Engine thuầnjourneys/engine.test.ts (27 ca)Registry hợp lệ; luật và null; kịch bản learner mới, học đều, tới mốc, quay lại (3/7 ngày, xong khi học lại, chưa từng nghỉ), sắp thi (60/20 ngày, hết hạn), ba hành trình giành nhau, giữ mạch trong/ngoài biên, "Để sau" và hết hạn hoãn
API + D1 thậtjourneys/routes.test.ts (19 ca)Quyền (401, admin); bản learner không lộ điểm; dữ kiện thật đổi đúng hành trình; sổ không ghi hai lần; "Để sau"; 409; accept đóng Learning Progress; 404 action lạ; vết admin không ghi sổ
E2E learne2e/smoke.spec.tsThẻ một việc, "Để sau" nhường chỗ, thẻ đứng giữa "What to do now" và "Practise a skill"

14. Cố ý không làm (MVP) ​

  • Registry trong D1 và màn sửa luật (Q-160) · Event Router, Workflows, cache (Q-161) · bảng journey_stages (mỗi hành trình MVP có ít bước, catalog có điều kiện là đủ) · AI chọn hành trình · phễu phân tích theo thời gian (REQ-JRN-16).

15. Trace ​

REQMục
REQ-JRN-01§4
REQ-JRN-02§4, §5
REQ-JRN-03§5
REQ-JRN-04§3, §7
REQ-JRN-05§6
REQ-JRN-06§8
REQ-JRN-07§8, §11
REQ-JRN-08§9, §10
REQ-JRN-09§7, §9
REQ-JRN-10§9, §11
REQ-JRN-11§8, §9, §11
REQ-JRN-12§4 (PRD-005 §6)
REQ-JRN-13§11
REQ-JRN-14§9, §11
REQ-JRN-15§12
REQ-JRN-16§14