給工程同事

工程規格與改動清單

以下對應現有 internx-me/frontend 的檔案,是建議的最小改動路徑、不是要求重寫。資料模型、狀態機、API、權限、通知、話題牆都列在下面;更完整的整合全貌與流程圖見 交接首頁的 INTEGRATION.md

本頁 17 節: 前端 1 票券模型・2 表單驗證・3 拖曳・4 FormField; 後端 5 報名狀態機・6 金流・8 API・10 Firestore 規則・9 通知; 身分/整合 7 帳號認證並行・11 話題牆; 收尾 12 改動檔案清單・13 驗收標準; 增補 14 撥款結算+社團・15 帳號選單・16 活動個人化; 職涯學院 17 Academy 課程平台(原型已完成:入口地圖・預留事項・資料流・字幕・整併風險速查 17.6)。

1 · 票券資料模型(核心)

目前票券只有名稱與價格,缺少「販售時間」與「數量」。建議把 feeItems{ name, price } 擴充為完整票券物件。

現況 · lib/form-schema/activity-form-schema.ts:328
feeItems: Array<{ name: string; price: number }> — 無時間、無數量,故早鳥過期仍可選、無法限量。

// 建議的票券型別(新增 data/ticket.ts,或擴充 activity.ts)
interface Ticket {
  id: string;
  name: string;            // 票種名稱
  price: number;           // 0 = 免費
  quantity: number | null;  // + 數量上限,null = 不限
  sold: number;             // + 已售(後端維護,前端唯讀)
  saleStart: Timestamp;     // + 販售開始
  saleEnd: Timestamp;       // + 販售結束
  description?: string;
  order: number;           // 排序用
}

狀態由系統計算,不另存欄位

報名端與編輯端共用同一個函式,避免主辦方手動上下架。

function ticketStatus(t: Ticket, now = new Date()) {
  if (t.quantity != null && t.sold >= t.quantity) return 'soldout';  // 已售完
  if (now < t.saleStart) return 'soon';                            // 尚未開賣
  if (now > t.saleEnd)   return 'ended';                           // 販售已截止
  return 'live';                                                  // 販售中(唯一可購買)
}

報名送出時後端需再驗一次 ticketStatus === 'live'sold < quantity,避免前端被繞過或併發超賣。

2 · 表單驗證調整

activity-form-schema.tsfeeItems.validation 需加上時間與數量檢查:

規則訊息
saleStart < saleEnd販售開始需早於結束時間
quantity == null || quantity >= 1數量上限需 ≥ 1,或留空表示不限
price >= 0(現有)價格不可為負
付費活動至少 1 張票請至少新增一個票種

原本獨立的 feeAmount(單一費用)與 參加名額上限(schema:371)可由票券模型取代;保留與否視既有報名資料相容性決定。

3 · 報名表單排序:用拖曳(@dnd-kit)

現況 · components/Activities/FormBuilder/FormBuilder.tsx:88
已有 handleReorderFields(fromIndex, toIndex),但拖曳是自製的、不穩、手機難用,所以才難實作。

改法:保留既有的 handleReorderFields,改用成熟套件 @dnd-kit 來驅動拖曳。內建指標 / 觸控 / 鍵盤支援,工程端不用自己處理拖曳事件,抓把手就能順暢拖曳

npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
// FormPreview:用 SortableContext 包欄位,handleReorderFields 不用改
<DndContext onDragEnd={({ active, over }) => {
  if (over && active.id !== over.id) {
    const from = fields.findIndex(f => f.id === active.id);
    const to   = fields.findIndex(f => f.id === over.id);
    handleReorderFields(from, to);   // 既有函式直接重用
  }
}}>
  <SortableContext items={fields.map(f => f.id)}>
    {fields.map(f => <SortableFieldRow key={f.id} field={f} />)}
  </SortableContext>
</DndContext>
// SortableFieldRow 用 useSortable({id}) 取得把手 listeners,套到 grip 圖示上。

系統必填欄位(姓名 / Email)標記 locked:不給拖曳把手、固定最上方,並用 dnd-kit 的 modifiers 限制不可被拖到其上方。完整步驟見 INTEGRATION.md

4 · 報名表單欄位型別(FormField)

報名者端依此 schema 渲染與驗證。對應 components/Activities/FormBuilder/types.ts

type FieldType =
  'text' | 'textarea' | 'email' | 'phone' | 'number'
  | 'select' | 'radio' | 'checkbox' | 'date' | 'file' | 'agreement';

interface FormField {
  id: string; type: FieldType; label: string;
  required: boolean; locked?: boolean;     // 系統必填(姓名/Email):不可刪/拖、固定最上
  order: number; placeholder?: string; helperText?: string;
  options?: string[]; allowOther?: boolean;        // 選項型
  accept?: string; maxFileSize?: number; maxFiles?: number;  // 檔案型
  allowedDomains?: string[];                       // Email 限定網域
  min?: number; max?: number;                       // 數字
  agreementText?: string;                          // 同意條款
}
型別前端驗證後端必驗(前端可被繞過)
file副檔名 ∈ accept、size ≤ maxFileSize、數量 ≤ maxFiles同左 + 類型/病毒掃描
email格式 + 網域 ∈ allowedDomains同左
numbermin ≤ v ≤ max同左
select/radio/checkbox值 ∈ options(或 allowOther)同左

5 · 報名資料模型與狀態機

「報名者審核」是這次新增的概念,與「活動發布審核」(approvalStatus) 不同:前者主辦方審個別報名者,後者平台審活動能否上架。對應 data/registration.ts

type RegistrationStatus =
  'pending'    // 待主辦方審核
  | 'approved' // 已通過,待繳費(系統已寄通知)
  | 'paid'     // 已繳費(付款 webhook 回寫)
  | 'rejected' | 'cancelled' | 'refunded';

interface Registration {
  id: string; activityId: string; userUid: string;
  ticketId: string; qty: number; amount: number;   // 送出當下金額快照
  answers: Record<string, unknown>;                 // key = FormField.id
  status: RegistrationStatus;
  createdAt: Timestamp; approvedAt?: Timestamp; paidAt?: Timestamp;
  rejectedAt?: Timestamp; rejectReason?: string;
  paymentId?: string; reviewerUid?: string;
}
// 狀態機
送出 ──▶ pending ──(主辦通過)──▶ approved ──(線上繳費)──▶ paid
            │ (主辦拒絕)              │ (逾期/取消)
            ▼                       ▼
         rejected               cancelled         paid ──(退款)──▶ refunded
轉換觸發者後端動作 / 副作用
→ pending報名者送出建 registration;sold += qty(佔位);通知主辦方
pending → approved主辦方寫 approvedAt;寄信 + 站內通知繳費
pending → rejected主辦方sold -= qty(釋放);通知報名者
approved → paid付款 webhook寫 paidAt / paymentId;後台自動顯示已付款;寄收據
paid → refunded平台退款sold -= qty;退款記錄

佔位策略:pending 即扣 sold,避免審核期間名額被搶光。若不希望待審佔名額,改 approved 才扣,但要在當下重驗 ticketStatus==='live' 且未超賣。

6 · 金流:平台代收代付(主辦方不對帳)

金流由實習通統一處理,主辦方只審核、不碰錢。沿用 components/Payments/Payments.tsx

步驟動作
1報名者送出 → registration 建為 pending
2主辦方「通過」→ approved系統自動寄信通知可繳費
3報名者於實習通線上繳費(點數折抵 / 轉帳;金額 = 票券 × 張數)
4付款 webhook → paid,後台自動顯示已付款(無人工標記、無末五碼對帳)
5退款同樣由平台處理

主辦方端對付款狀態唯讀;registration 存 ticketId / qty / status / paidAt

7 · 帳號與認證:並行邏輯

沒有「創作者帳號」這種東西。一個帳號 + 一個 verified-creator 標章;「主辦單位 = 認證創作者 = 同一標章」。取得標章有兩個可並行的入口,共用同一套審核(verifiedRoleApplications)。

// 入口 B:onboarding 選身分(新增一步)
function onOnboardingIdentitySelected(role) {
  createBasicAccount();                          // 一律先建一般帳號
  if (role === 'creator') goTo('/dashboard/verified-role-apply');
  else finishOnboarding();
}
// 入口 A:事後任意時間(設定頁、或點建立活動/撰寫文章時)
function onClickApplyVerification() { goTo('/dashboard/verified-role-apply'); }
// 兩入口都送同一支 → verifiedRoleApplications(pending) → 審核 → grantVerifiedBadgeToUser
submitVerifiedRoleApplication(payload);

// 能力判斷:只看標章 / admining,不看「是不是創作者帳號」
const isCreator       = profile.badges.includes('verified-creator');
const canHostActivity = profile.admining || isCreator;
const canPublishBlog  = isCreator;

審核期間(pending)仍可正常使用平台;通過後才解鎖辦活動/發文/主頁。建議預設走入口 A(事後申請),註冊零阻力。完整見 交接首頁 §10.1

8 · API / Cloud Functions 介面(建議)

審核與金流走後端,前端只呼叫、不直接改 status,避免被繞過。

// 主辦方審核(驗 caller 是該 activity 的 admining 成員)
approveRegistration({ registrationId })       // pending→approved + 寄繳費通知;非 pending 回 409
rejectRegistration({ registrationId, reason? }) // pending→rejected + 釋放 sold
// 報名者繳費
createPaymentSession({ registrationId })      // 限 approved;金額 = registration.amount → { paymentUrl }
onPaymentSucceeded(webhook)                   // approved→paid(後端,前端不可呼叫)
refundRegistration({ registrationId })        // paid→refunded
// 報名送出(務必用 transaction,見 §1 提示)
submitRegistration({ activityId, ticketId, qty, answers }) // → { registrationId }

錯誤碼:TICKET_NOT_ON_SALE / SOLD_OUT / NOT_PENDING / NOT_APPROVED / FORBIDDEN / VALIDATION_FAILED

9 · 通知事件(Email + 站內)

事件對象管道
新報名待審主辦方站內(可選 Email)
審核通過・待繳費報名者Email + 站內
審核未通過報名者Email + 站內(含 rejectReason)
繳費完成(收據)報名者Email
繳費完成主辦方站內(後台自動轉已付款)
退款完成報名者Email

10 · Firestore 安全規則(草稿)

sold / status / paidAt 全部只由後端寫,前端唯讀;報名者只能建自己的 pending

match /activities/{aid} {
  allow read: if true;                       // 公開活動頁
  allow write: if isActivityAdmin(aid);          // 僅該官方帳號成員
  match /tickets/{tid} {
    allow read: if true;
    allow write: if false;                  // sold 只由後端 transaction 改
  }
}
match /registrations/{rid} {
  allow create: if request.auth.uid == request.resource.data.userUid
                && request.resource.data.status == 'pending';
  allow read:   if isOwner(rid) || isActivityAdmin(resource.data.activityId);
  allow update, delete: if false;          // status 轉換走 Cloud Functions
}

11 · 話題牆(NeedsWall)串接

話題牆=既有 data/needs-wall-live.js(依行業 forumId 分版的話題;含 branches / poll)+論壇 data/chat.ts ChatRoom。Post / ChatMessage 已顯示 verified-creator 標章。這次只新增「活動 / 創作者 ↔ 話題牆」的關聯。

串接機制欄位 / 檔案
活動 → 話題牆活動上架(approved) 依行業 forumId 自動建/連話題;活動頁顯示「社群討論」新增 activity.needsWallTopicId;createNeedsWallTopic()
活動 → 論壇討論區付費 / internx_form 活動開專屬討論區新增 activity.chatRoomId;ChatRoom.create()
創作者 → 話題牆話題頁顯示「行業認證專家」沿用 verifiedRolePitch.expertiseForumIds 命中 forumId

活動對到哪個行業版:activity.ts 無行業欄位 → 由主辦公司產業推導,或建立活動時選一個 forumId(建議)。完整見 交接首頁 §17

12 · 改動檔案清單

檔案動作說明
data/ticket.ts新增Ticket 型別 + ticketStatus(),編輯/報名端共用
data/activity.ts擴充活動關聯 tickets[](取代或相容 feeItems)
lib/form-schema/activity-form-schema.tsfeeItems 改為票券物件、新增驗證(:328、:371)
components/Activities/AddActivity.tsx沿用維持步驟式結構;票券步驟換成新的編輯器卡片
components/Activities/FormBuilder/FormPreview.tsx改用 @dnd-kit 拖曳排序(沿用 handleReorderFields)
components/Activities/FormBuilder/types.ts沿用FormField 型別已存在(accept / maxFileSize / options / allowedDomains…),§4
components/Payments/Payments.tsx沿用平台代收代付:通過→通知→線上繳費→自動回寫 paid;主辦方不對帳
data/registration.ts擴充status 狀態機 + ticketId / qty / amount / paidAt(§5)
Cloud Functions(後端)新增approve / reject / pay webhook / refund / submit(§8);status 只由後端改
通知服務新增 / 接審核 / 繳費 / 退款事件 Email + 站內(§9)
lib/config.js PROFESSIONAL_SERVICES側欄加「報名名單」「創作內容」;進入條件放寬為 admining OR verified-creator(§7)
pages/[lang]/professional/registrations新增報名名單 + 審核 UI(§5)
data/blog.ts + BlockEditor沿用部落格發佈權放寬給帶 verified-creator 標章者(一行條件)
Profile.tsxactiveSectionTab 加「部落格 / 主辦活動 / 活動紀錄」分頁
data/verified-role-application.ts沿用身分申請 + 審核(§7);兩入口共用
data/needs-wall-live.js / data/chat.ts活動上架建話題(needsWallTopicId)/ 專屬討論區(chatRoomId)(§11)
data/settlement.ts / data/payout.ts新增結算與撥款模型;後台「撥款結算」分頁(/backstage);對應金流規格 §10(§14.1)
data/club.ts / data/clubMember.ts新增社團=組織型主辦帳號(多管理員 / 換屆);activity.ownerType:'user'|'club'(§14.2)

13 · 驗收標準

早鳥票販售結束時間過後,報名端自動鎖定、不可選。
每種票可獨立設數量,售完自動標記,且後端 transaction 防超賣。
表單欄位可用拖曳排序(@dnd-kit、含觸控與鍵盤),系統必填欄位固定最上方。
報名者審核狀態機:待審核→已通過→已付款/已拒絕,且 status 只由後端轉換。
金流平台代收代付:通過後自動通知、線上繳費、自動回寫已付款,主辦方不對帳。
審核 / 繳費 / 退款各觸發對應通知(Email + 站內)。
認證並行:事後申請與註冊時選兩入口共用同一套審核;通過才解鎖辦活動 / 發文。
活動上架後在對應行業話題牆建立 / 連結話題;話題頁顯示行業認證專家。
編輯器分四步、欄位固定寬度,輸入時版面不跳動。
視覺沿用既有 InternX token(#0182fd / Poppins + Noto Sans TC)。

14 · 撥款結算 + 社團(本次新增)

14.1 撥款 / 結算(代收代付的「撥款」那半)

主辦方後台(/backstage → 撥款結算)把單一「票券收入」拆成 總收款 / 已扣手續費 / 待撥款 / 已撥款 / 已退款,並列每場結算明細與撥款記錄。完整邏輯見內部規格 金流邏輯_平台代收代付.md §10。

// data/payout.ts(新)— 撥款給主辦方(一活動一批)
interface Payout {
  id; organizerId; activityId; orderIds: string[];
  gross; platformFeeTotal; gatewayFeeTotal; net;   // net = gross − 平台抽成 − 金流手續費
  status: 'pending'|'paid'|'failed';
  bankAccountRef; paidAt?;
}
// 結算週期:活動結束 T+N(如 T+7)→ settleAndPayout() 建 payout 撥款

主辦方端對金額 / 狀態唯讀;結算與撥款只在 Cloud Functions(settleAndPayout)計算。認證帳號 / 設定需新增銀行帳戶欄位供撥款。

14.2 社團=組織型主辦帳號(多管理員 / 換屆)

把「主辦方」從個人升級為可選的組織(社團):活動掛在 clubId 而非建立者個人。主頁見 /club、註冊見 /club-register

interface Club {
  id; name; school; type; verified; foundedYear; currentTerm;   // 屆數
}
interface ClubMember {
  clubId; userId; role: '社長'|'副社長'|'財務長'|'活動長'|'公關長';
  term; isOwner; status: 'active'|'invited'|'alumni';
}
// 活動歸屬:activity.ownerType: 'user' | 'club' + ownerId
// 換屆 = 換 owner,不換 club:移轉只更新 isOwner 與當期 term,
// 舊成員轉 alumni;活動 / 追蹤者 / 結算全掛 clubId,永久保留。

權限以 role 控管(owner 可移轉擁有權與刪社團;財務看撥款結算;活動長開活動;公關改主頁)。多管理員讓社團不卡在一個人、社長畢業也不中斷。

15 · 帳號選單 / 導覽(依角色)

關鍵原則:選單不是三套,而是「共用的個人選單 + 依旗標展開的條件區塊」。本次改版是往既有選單「加」東西,不是把里程碑、心得那些換掉。能力旗標存在 appStates.userData,選單定義在 lib/config.js(TOP_BAR_TABS / DASHBOARD_SERVICES / PROFESSIONAL_SERVICES / BADGES_CONFIG)與 components/TopBar/TopBar.jsx

15.1 能力旗標(userData)

旗標意義解鎖
(已登入)一般會員(學生)個人選單全套:里程碑徽章、分享心得、收藏公司/活動、我的小夥伴、帳號設定、申請認證標章、Coffee Chat 講師後台
badges: 'verified-creator'認證創作者(另有 industry-expert / collaboration-partner)撰寫文章、創作者主頁(canPublishBlog
admining官方帳號 / 主辦單位成員官方帳號平台(後台):儀表板、新增活動、活動成果,+本次新增「報名名單審核」「撥款結算」
adminingIsOwner官方帳號擁有者共同管理員(多管理員)+ 移轉擁有權(=社團換屆)
admin平台管理員管理員工具 /admin

15.2 帳號下拉選單 = 共用 + 條件展開

// 一律顯示(已登入)— 對齊 TopBar.jsx UserDropdownPanel 的「我的 / 功能選項」
我的:里程碑徽章 /dashboard/milestones · 分享心得 /share · 收藏公司/活動 · 我的小夥伴 · 帳號設定 · 申請認證標章 · Coffee Chat
// 依旗標展開
if (badges.includes('verified-creator'))  ➜ 撰寫文章、創作者主頁
if (admining)        ➜ 官方帳號平台後台(活動管理 / 報名審核 / 撥款結算 / 活動成果)
if (adminingIsOwner) ➜ 共同管理員(多管理員)+ 移轉擁有權(換屆)
if (admin)           ➜ 管理員工具

不要做的事:改版時把「里程碑徽章 / 分享心得 / 收藏 / 我的小夥伴」從選單拿掉。這些是平台既有的個人功能,主辦 / 創作者 / 社團只是額外的條件區塊,不是取代。

15.3 三種視角(旗標組合的結果)

學生創作者主辦單位(常為社團)
頭像點進去個人檔案創作者主頁社團 / 官方帳號主頁
個人選單(里程碑/心得/收藏/設定)
撰寫文章 / 創作者主頁視是否帶標章
官方帳號後台(活動/報名/撥款)
共同管理員 / 換屆✓(擁有者)
申請認證標章✓ 升級入口已認證已認證

15.4 社團 = 官方帳號的一種(重用,不是新系統)

本次的「社團多管理員 / 換屆」直接掛在既有機制上,不要另開一套:

本次概念= 既有平台檔案 / 條件
社團 = 組織型主辦帳號官方帳號(admining/professional
多管理員共同管理員PROFESSIONAL_SERVICES.members · /professional/membersadminingIsOwner 才見)
換屆交接移轉 adminingIsOwner新增「移轉擁有權」action;舊成員轉 alumni(§14.2)
撥款結算官方帳號後台新分頁/backstage(§14.1)

真實選單定義:components/TopBar/TopBar.jsx(UserDropdownPanel)、components/SideNav/SideNav.jsxlib/config.js。本 mockup 對應外殼:assets/cz/app.js(已補回里程碑 / 分享心得 / 收藏 / 我的小夥伴等既有項目)。

16 · 活動個人化(領域 / 推薦 / 追蹤)

建立在既有活動頁上,不重做。現有 /dashboard/activities 已有完整篩選(搜尋 + 類型 / 地區 / 方式 / 費用 / 日期)與三種排序(綜合 / 最新 / 報名截止日)。個人化只補三塊:領域篩選「綜合」排序接上推薦追蹤主辦,並重用既有 INDUSTRY_CATEGORIES(17 類,目前只用在公司評價)。

16.1 三個接點(最小改動)

接點現況改法
「綜合」排序UI 已有按鈕,但 activities.jsx:702 對 comprehensive 是 return 0(未實作)把推薦分數放這裡(16.3)
領域篩選ActivitiesFilters.tsx 有 類型/地區/方式/費用/日期,無領域加一列「領域」chips(多選)
活動標記data/activity.ts 無行業欄位industryTags: string[](建立活動時選)
使用者偏好userDatainterests[] 但沒用於活動;onboarding 不問領域preferredIndustries[](onboarding 選 2–3 個)
追蹤沒有追蹤主辦followedOrganizers[] +「你追蹤的主辦」區

16.2 資料模型

// data/activity.ts — 擴充
interface ActivityData { /* …現有… */ industryTags?: string[]; }   // 對應 INDUSTRY_CATEGORIES 的 key
// userData / profile — 擴充
preferredIndustries?: string[];   // onboarding 選;同一組 key
followedOrganizers?: string[];    // 追蹤的官方帳號 / 社團 / 創作者 id
// DashboardActivitiesFilters — 與 ActivitiesFilters 一致
industries: string[];            // 空陣列 = 全部

16.3 「綜合」排序 = 推薦分數(接在既有 sort)

// activities.jsx:comprehensive 分支從 return 0 換成 matchScore
if (sortBy === 'comprehensive') return score(b) - score(a);

function score(act) {
  let s = 0;
  const ind = userData.preferredIndustries || [];
  if (ind.length && act.industryTags?.some(t => ind.includes(t))) s += 100;   // 領域命中
  if (userData.followedOrganizers?.includes(act.ownerId)) s += 10;             // 追蹤的主辦
  s += deadlineHeat(act);   // 報名熱度 / 截止將近(沿用現有 deadline 桶邏輯)
  return s;
}

推薦要標出原因(「因為你選了金融」「你追蹤的 ●●」):命中哪個條件就顯示哪個 —— 透明的推薦比黑箱更被信任。原型見 /activities偏好只影響排序與預設,不隱藏任何活動(既有篩選照常運作)。

17 · InternX Academy 職涯課程平台(原型已完成;主平台整併後續排程)

本 repo 已含完整可操作原型:探索 /academy → 詳情+Mock 購買 /course → 教室播放器(含字幕 CC/classroom/my-courses;創作者後台 /academy-studio(4 步開課精靈/字幕與教材上傳/收益/留言管理)、管理後台 /academy-admin(審核/訂單/收益/分潤)。資料層在 assets/academy/academy.js:localStorage 持久化+PaymentProvider 抽象層(FakePay),Order → Payment → Enrollment → RevenueRecord 全流程真的會跑。整併進 internx.me 主平台仍屬後續排程(低優先),完整需求見 ACADEMY-PRD.md交接首頁可線上閱讀);下面的預留事項仍然成立。

17.1 入口地圖(各頁彼此怎麼進;詳版見 INTEGRATION.md §20.1)

頁面誰用進入方式(全部列出)
/academy 探索學生主導覽「職涯學院」(每頁都有,手機=漢堡選單)· Footer「職涯學院」· 活動頁「延伸學習資源→看更多」· 各頁麵包屑
/course?id=… 詳情學生探索頁課程卡 · 活動頁推薦卡 · 其他課程「推薦搭配」· 我的課程「課程頁」· 教室頂欄「課程介紹」· 完課 modal 推薦
/classroom?id=…&lesson=… 教室學生(未購=試看模式)購買成功 modal「進入教室」· 課程頁「開始/繼續上課」· 章節列表「免費試看」單元 · 探索頁「繼續上課」條 · 我的課程「繼續上課/複習」(自動接上次單元)
/my-courses學生帳號選單「我的課程」· 手機選單 · Footer · 課程頁(已購)· 購買成功 modal
/academy-studio?tab=…創作者(verified-creator)帳號選單「課程後台(開課)」· 手機選單;預覽未上架課 → /course(創作者預覽 banner)
/academy-admin?tab=…Admin帳號選單「課程管理(Admin)」;審核動線:Studio 送審 → 待審 badge → 通過即上架前台/退回理由回寫 Studio

URL 約定:/course?id=(無效自動回探索)、/classroom?id=&lesson=(lesson 省略=接上次進度)、/academy?career=pm,finance(給職缺頁/履歷健檢帶入預選)、後台 ?tab= 直達分頁。PRD 整合章 §3 的 8 個入口:Navbar/帳號選單/Footer/活動頁 demo 已做,個人頁/職缺頁/履歷健檢/公司頁/面試心得/AI 推薦頁預留(指向同一組 URL 即可)。

一句話定位:職涯導向課程平台(對標 Hahow 但不做泛知識)。課程綁五類標籤(職涯方向 / 技能 / 年級 / 求職階段 / 成果),與職缺、履歷健檢、諮詢、活動互相導流;講師=既有認證創作者,V1 金流用 Mock Payment 但資料流完整(Order → Payment → Enrollment → RevenueRecord)。

17.2 現在實作就該預留的 6 件事(重點)

Academy 本體晚點做沒關係,但下面這些「現在正在做的系統」設計時多想一步,未來就能直接共用:

預留點現況(本次交接)預留做法
金流資料模型§6 / §14.1 代收代付、結算欄位以「活動」為單位設計Order / Payment / 結算紀錄加 itemType: 'activity' | 'course'(+ itemId),欄位命名不要綁死 activity —— 未來課程訂單直接進同一套帳
Payment Provider 抽象層活動金流待串接(§6)先定 interface:createPayment / verifyPayment / refundPayment / getPaymentStatus。活動金流照這個寫,未來 Academy 只加 FakePaymentProvider→正式(ECPay / NewebPay / Stripe / TapPay)換 Provider、不重寫購買流程
標籤系統§16 剛加 industryTags(活動)、重用 INDUSTRY_CATEGORIES課程的職涯方向標籤用同一組 key;技能 / 年級 / 求職階段 / 成果標籤設計成通用 Tag,職缺・履歷健檢・課程共用(推薦匹配的基礎)
講師身分verified-role 申請+審核既有(§7)課程講師=verified-creator,同一帳號申請 creator role、共用審核,不另開帳號系統 —— 現行設計已滿足,不要分岔
Navbar / 入口現有 6 個主導覽 tab(§15)預留「職涯學院」入口位;職缺頁 / 履歷健檢結果頁 / 活動頁預留「推薦課程」區塊(V1 可先空著)
通知站內+Email 通知基建既有(§9)課程公告 / 購買 / 審核通知直接接同一 Notification,不另建

17.3 新增資料(全部是新 collection,不動既有)

// Academy 專用(未來新增;User/Auth/Order/Payment/Notification/Tag 共用主平台)
Courses / CourseSections / CourseLessons / LessonResources
Enrollments / CourseProgress / CourseComments / CourseAnnouncements
RevenueRecords / CreatorPayouts / CreatorProfiles / CourseReviews / AdminReviewLogs

// 字幕(V1 單語中文;2026-07-02 由「不做」改為 V1 需求)
lesson.subtitles: { t: number /*秒*/, text: string }[]   // 原型 cue 模型
// 正式版:講師逐單元上傳 SRT/VTT → 解析為 cue;LessonVideos.subtitleTracks[](多語預留);CC 開關偏好存使用者設定

// 分潤紀錄(每筆付款成功自動建立;預設抽成 15%,可依創作者 / 課程覆寫)
RevenueRecord {
  orderId, courseId, creatorId,
  grossAmount: 3000,           // 售價
  platformFeeRate: 0.15,      // → platformFeeAmount: 450
  creatorIncomeAmount: 2550,
  paymentStatus, payoutStatus, paidAt, payoutAt
}

17.4 V1 購買資料流(Mock 金流、資料先完整)

點購買 → Order(pending) → Payment(pending, FakeProvider)
  → 「模擬付款成功」→ Payment(paid) → Order(paid)
  → 建 Enrollment(取得觀看權限)→ 建 RevenueRecord(分潤)

和 §6 活動金流是同一套代收代付思路:學生付款 → 平台代收 → 依分潤撥給創作者。所以排程上建議等活動金流(§6 / §14.1)穩定後再排 Academy,屆時課程金流直接重用同一套 Provider 抽象與結算後台。

17.5 推薦 API(跨模組整合的核心)

// 職缺頁「申請前建議補強」/ 履歷健檢「下一步建議」/ 活動頁「延伸學習資源」共用一支
GET /recommendations/courses?source_type=job|company|interview_review|resume_review|event|consultation|profile|career_map&source_id=…
// 回傳 { courseId, title, reason, matchedTags, priorityScore }
// 匹配:Job.requiredSkills ↔ Course.skillTags;Job.category ↔ Course.careerTags;ResumeReview.weaknessTags ↔ Course.skillTags/outcomeTags

推薦一樣要標出原因(「此職缺要求產品企劃、使用者訪談…該課程可補足」)—— 與 §16 活動個人化同一個透明推薦原則。

17.6 整併風險速查(詳版必讀:INTEGRATION.md §21)

原則:產品邏輯照抄 demo(欄位/狀態機/文案),工程實作全部重做(demo 是 localStorage 假後端)。🔴=不解決不能上線。

風險一句話解法方向
🔴 訂單模型衝突活動金流若先上線且沒有 itemType/itemId,Academy 併入要 backfill 或雙軌帳現在就把活動金流做成通用 commerce record(唯一不等 Academy 排程的事)
🔴 狀態所有權反轉demo 全部前端寫;正式版 status/paidAt/enrollment/payout 只能後端寫Cloud Functions + Firestore rules + rules simulator 測試
🔴 Webhook 髒現實FakePay 掩蓋重複回調/亂序/偽造/雙訂單驗簽+冪等鍵+對帳 job;入帳後才建 Enrollment
🔴 權限分支沒被看到demo 一人三角色,看不到「非本人課程/被停權/未購買」等錯誤路徑後端逐條檢查(藏選單不算);講師要補收款帳戶核實
🔴 媒體管線全模擬影片儲存/轉檔/防盜連/上傳狀態機 demo 都是按鈕選型(Stream/Mux 類)+ signed URL 授權+轉檔狀態機
🟡 退款遇已撥款直接改狀態會弄壞歷史帳調整紀錄(adjustment)從下期撥款扣回
🟡 i18n/SEOdemo 繁中寫死、無 SSR;課程頁是天然 landing pageUI 字串進翻譯系統;課程頁 SSR+OG+Course schema
🟡 法務三件事發票義務、退款鑑賞期政策、創作者收款個資上線前要有書面答案(見 INTEGRATION §21.7)

上線策略:契約先行(commerce record+API contract+rules 草稿)→ 四條線並行(後端/學生端/創作者端/媒體)→ feature flag 內測(1–2 位真實創作者跑完整流程含一筆真退款)→ 公開。最小測試清單見 INTEGRATION.md §21.8

17.7 範圍速記

V1 做V1 不做(結構預留)
課程 CRUD+審核上架、五類職涯標籤、課程詳情頁(適合誰 / 學完獲得什麼 / 對應職缺 / 可產出履歷成果)、Mock 購買全流程、播放器+進度、教材下載、留言、創作者後台(Dashboard / 我的課程 / 建立課程)、Admin(課程審核 / 訂單 / 收益 / 分潤設定) 正式金流、自動撥款、發票、優惠碼、證書、作業批改、Quiz、直播、字幕、AI 課程推薦、評價系統、退款自動化、組合包(課程+諮詢 / 活動)、企業內訓、學校合作

本交接檔為獨立 repo,與 internx.me 程式碼分離;可作為 PR 描述與設計依據附在工程任務上。