Contoh Fail Sebenar
Sekarang kita buka fail betul-betul dalam sistem ini dan baca baris demi baris. Tetapi bukan semua 440 fail — cukup satu contoh setiap jenis. Faham 10 contoh ini, anda faham corak seluruh repo.
📑 Kandungan
- Cara guna panduan ini
- Kontrak (Zod) — penjaga bentuk data
- Route — pintu yang backend dengar
- Service — pekerja yang sentuh database
- Middleware — kunci di depan pintu
- Jejak satu permintaan (01→04 bersama)
- Migration mudah — tambah lajur
- Migration bahaya — cascade & backup
- Router — peta alamat → halaman
- Context provider + custom hook
- Komponen daun — pengawal laluan
- Hook custom — ambil data
- Seterusnya: 10 bentuk → 440 fail
Cara guna panduan ini
Panduan Kod 01 mengajar asas. Di sini kita guna asas itu untuk membaca fail sebenar. Kalau anda belum baca Kod 01, baca dulu — setiap anotasi di sini menganggap anda kenal simbol asas.
Rahsia: kod ini ada ~10 bentuk fail, bukan 440 benda berbeza
Sistem ini ada 440 fail. Bunyinya menakutkan. Tetapi rahsianya: fail-fail itu jatuh dalam segelintir bentuk yang berulang. Ada ~40 fail route — tetapi semuanya ikut corak yang sama. Faham satu route betul-betul, anda faham 40. Sebab itu kita pilih satu contoh terbaik setiap bentuk dan bedah sehabis-habisnya.
Cara baca anotasi (ulang kaji ringkas)
Setiap blok kod di bawah ada anak panah ←: sebelah kiri kod sebenar, sebelah kanan maksudnya dalam bahasa biasa. Kami juga memendekkan sesetengah baris (ganti butiran panjang dengan …) supaya fokus pada idea, bukan setiap aksara. Untuk versi penuh tanpa potong, buka fail itu sendiri atau tanya cikgu anda:
Saya baru belajar coding. Buka fail worker/routes/hospital.ts dan terangkan setiap baris penuh, dalam bahasa mudah. Jangan potong apa-apa.
Kontrak (Zod) — penjaga bentuk data
Sebelum apa-apa data masuk ke sistem, ia mesti lulus pemeriksaan bentuk. Fail kontrak ini ialah senarai "data ni mesti nampak macam mana".
Apa fail ini: worker/contracts/hospital.contracts.ts mentakrif bentuk sah untuk "identiti hospital" (nama, alamat, telefon, logo…). Ia guna pustaka bernama Zod. Anggap ia borang dengan peraturan: "telefon mesti teks", "e-mel mesti betul-betul nampak macam e-mel".
import { z } from 'zod'; ← bawa masuk Zod — alat penyemak bentuk data
const ContactSchema = z.object({ ← takrif bentuk "contact": satu objek dengan…
tel: z.string(), ← …medan 'tel' mesti teks (string)
email: z.string().email().or(z.literal('')), ← e-mel: format SAH, ATAU teks kosong
});
export const HospitalIdentitySchema = z.object({ ← bentuk PENUH satu hospital…
appName: z.string().min(1), ← nama app: teks, sekurang-kurangnya 1 huruf
hospitalName: z.string().min(1), ← nama hospital: wajib ada isi
contact: ContactSchema, ← guna semula bentuk 'contact' di atas
// …medan lain (address, letterhead, logos…)
});
export type HospitalIdentityInput = ← jana 'jenis' TypeScript automatik…
z.infer<typeof HospitalIdentitySchema>; ← …DARI bentuk di atas (kontrak & jenis sentiasa sepadan)
Kenapa ia penting: kontrak ditulis sekali, lalu diguna di dua tempat — backend pakai ia untuk menolak data buruk (lihat e2), dan TypeScript guna baris z.infer untuk menjana jenis automatik. Jadi kalau anda tambah medan baru, kedua-dua tempat dapat tahu serta-merta. Satu sumber kebenaran.
Terangkan worker/contracts/oncall-config.contracts.ts baris demi baris. Ia ikut corak yang sama dengan hospital.contracts.ts — apa beza dan kenapa?
Route — pintu yang backend dengar
Sebuah route ialah satu alamat (cth /api/hospital/config) yang backend dengar. Bila frontend "ketuk" alamat itu, fungsi route berjalan.
Apa fail ini: worker/routes/hospital.ts daftar dua pintu untuk tetapan hospital — satu untuk baca (GET), satu untuk kemas kini (PUT). Perhatikan: route sendiri tipis. Ia tidak sentuh database terus; ia upah seorang service (e3) untuk kerja sebenar.
export function registerHospitalRoutes(app) { ← fungsi yang mendaftar semua pintu hospital
app.get('/api/hospital/config', async (c) => { ← PINTU 1 (GET = baca). c = konteks (req+res)
const svc = new HospitalConfigService(c.env, 'system'); ← upah 'pekerja' yang sentuh DB
const config = await svc.get(); ← suruh ia ambil tetapan; tunggu siap (await)
return ok(c, config); ← balas { success:true, data: config }
});
app.put('/api/hospital/config', ← PINTU 2 (PUT = kemas kini)…
requireRole('superadmin'), async (c) => { ← …tapi KUNCI dulu: superadmin sahaja (e4)
const parsed = UpdateHospitalConfigRequest.safeParse(body); ← semak data ikut kontrak (e1)
if (!parsed.success) return bad(c, '…'); ← bentuk salah → tolak dgn mesej ralat
const updated = await svc.update(parsed.data); ← data sah → suruh pekerja simpan
return ok(c, updated);
});
}
Kenapa ia penting: ini corak setiap route dalam sistem: (1) kunci dengan requireRole kalau perlu, (2) semak data masuk dengan kontrak Zod, (3) hantar kerja kepada service, (4) balas dengan ok() atau bad(). Route kekal pendek dan mudah dibaca.
Terangkan worker/routes/oncall.ts baris demi baris — ia ikut corak route yang sama dengan hospital.ts di atas, cuma lebih banyak pintu.
Service — pekerja yang sentuh database
Kalau route ialah pelayan, service ialah tukang masak di dapur. Di sinilah kerja sebenar berlaku: baca & tulis database, gabung data, tulis jejak audit.
Apa fail ini: worker/services/HospitalConfigService.ts tahu cara baca dan simpan tetapan hospital di dalam database. Ia satu class — satu "bekas" yang membungkus data + tindakan yang berkaitan.
export class HospitalConfigService { ← 'pekerja' yang tahu baca/tulis tetapan hospital
constructor(private env, private actorId) {} ← bila diupah, ingat: env (akses DB) + siapa buat
async get() { ← tindakan: ambil tetapan
const row = await this.env.DB.prepare( ← sedia satu arahan SQL untuk DB…
'SELECT config FROM hospital_config WHERE id = 1', ← …ambil baris tunggal (id=1)
).first(); ← jalankan, ambil baris pertama
if (!row) return DEFAULT_HOSPITAL_IDENTITY; ← tiada baris? pulang nilai lalai (fallback)
// …sahkan ikut kontrak (e1) sebelum pulang
}
async update(patch) { ← tindakan: kemas kini sebahagian tetapan
const current = await this.get(); ← ambil yang sedia ada dulu
const merged = { ...current, ...patch }; ← gabung: mula dari lama, timpa dengan baru
const validated = HospitalIdentitySchema.parse(merged); ← sahkan hasil gabungan (kontrak e1)
await this.env.DB.prepare(`INSERT … ON CONFLICT … DO UPDATE`) ← simpan (cipta-atau-ganti)
.bind(JSON.stringify(validated), this.actorId).run();
await logAction(this.env, this.actorId, 'UPDATE_HOSPITAL_CONFIG'); ← jejak audit: siapa buat apa
return validated;
}
}
Kenapa ia penting: service memusatkan logik. Tindakan update di sini buat tiga benda yang mesti berlaku bersama: gabung & sahkan, simpan ke DB, dan tulis jejak audit. Kalau logik ini bertabur dalam banyak route, satu hari satu route lupa tulis audit — pepijat senyap. Dengan service, ia ditulis sekali, betul untuk semua.
Terangkan worker/services/OncallConfigService.ts. Bandingkan strukturnya dengan HospitalConfigService — constructor, get, update, dan kenapa ia tulis audit.
Middleware — kunci di depan pintu
Middleware ialah kod yang berjalan di tengah — selepas permintaan tiba, tetapi sebelum handler pintu sebenar. Di sini, tugasnya: semak pengguna dibenarkan.
Apa fail ini: worker/middleware/auth.ts ialah penjaga keselamatan. requireRole('superadmin') yang anda nampak di e2 datang dari sini.
export const requireRole = (...roles) => ← fungsi yang PULANGKAN satu penjaga…
async (c, next) => { ← …penjaga = fungsi yang lari SEBELUM handler
const jwt = c.get('jwtPayload'); ← ambil 'pas masuk' pengguna (token disahkan)
if (!jwt) return unauthorized(c); ← tiada pas → 401 (belum log masuk)
if (!roles.includes(jwt.role)) { ← pas ada, tapi peranan tak dibenarkan…
return forbidden(c, '…'); ← …→ 403 (dilarang)
}
return next(); ← lulus semua → teruskan ke handler pintu
};
Kenapa ia penting: tanpa middleware, setiap route perlu menyalin semula semakan log-masuk — mudah terlupa pada route baru, dan satu route terlupa = lubang keselamatan. Dengan requireRole, kawalan akses jadi satu baris di atas pintu. 401 bermaksud "siapa awak?" (belum log masuk); 403 bermaksud "saya kenal awak, tapi awak tak dibenarkan".
Dalam worker/middleware/auth.ts, apa beza requireAuth dan requireRole? Dan di mana jwtPayload mula-mula diletak sebelum middleware ini baca?
Jejak satu permintaan (01→04 bersama)
Empat bentuk tadi tidak hidup sendiri — mereka bekerja sebagai satu rantaian. Mari jejak satu permintaan "simpan tetapan hospital" dari mula sampai habis.
Pengguna tekan "Simpan" di halaman tetapan
Frontend hantar permintaan PUT /api/hospital/config dengan data borang.
Kunci: middleware (e4)
requireRole('superadmin') semak pas. Bukan superadmin → 403, berhenti di sini.
Pintu: route (e2)
Handler PUT terima data, hantar ke kontrak untuk disemak.
Penjaga bentuk: kontrak Zod (e1)
safeParse semak setiap medan. Bentuk salah → bad(), berhenti dengan mesej ralat.
Kerja sebenar: service (e3)
svc.update() gabung + sahkan + simpan ke DB + tulis audit.
Balas
Route pulang ok(c, updated). Frontend tunjuk "Berjaya disimpan".
Bila pengguna simpan tetapan hospital, senaraikan ikut turutan SETIAP fail yang terlibat, dari frontend sampai data masuk database. Saya nak nampak rantaian penuh.
Migration mudah — tambah lajur
Struktur database tidak diubah secara rawak. Setiap perubahan ditulis sebagai satu migration — fail SQL bernombor, dijalankan sekali, ikut turutan.
Apa fail ini: migrations/0002_add_tarikh_lahir.sql ialah migration paling mudah yang wujud — ia tambah satu lajur baru ke table sedia ada.
-- Add tarikh_lahir column to patients table ← komen: apa migration ini buat (untuk manusia)
ALTER TABLE patients ADD COLUMN tarikh_lahir TEXT; ← tambah satu lajur baru 'tarikh_lahir' (jenis teks)
Kenapa ia penting: ini jenis perubahan database yang selamat dan paling biasa — ia cuma menambah, tak menyentuh data sedia ada. Nombor 0002 di hadapan nama penting: ia memastikan migration berjalan ikut turutan, sekali sahaja setiap satu. Sistem ini mengutamakan ALTER TABLE … ADD COLUMN berbanding membina semula table — sebab membina semula table itu bahaya (e7).
Migration bahaya — cascade & backup
Kadang anda terpaksa bina semula sesebuah table, bukan sekadar tambah lajur. Di sinilah jebakan paling berbahaya dalam seluruh sistem bersembunyi.
Apa fail ini: migrations/0026_reten_batal_pulang_awal.sql perlu memperluas satu peraturan pada table visits. Untuk itu, ia terpaksa buang & bina semula table. Masalahnya: visits ada "anak-anak" table yang berpaut padanya dengan CASCADE.
-- DROP TABLE visits cetus DELETE tersembunyi → CASCADE padam anak.
-- Maka: salin anak DULU sebelum buang ibu.
CREATE TABLE _mig0026_backup_visit_doctors AS SELECT * FROM visit_doctors; ← salin anak (selamat)
CREATE TABLE _mig0026_backup_diagnoses AS SELECT * FROM diagnoses;
CREATE TABLE _mig0026_backup_managements AS SELECT * FROM managements;
CREATE TABLE visits__mig0026 ( /* …CHECK diperluas: tambah 'BATAL' */ ); ← bina visits baru
INSERT INTO visits__mig0026 SELECT * FROM visits; ← pindah data lama ke yang baru
DROP TABLE visits; ← buang yang lama (CASCADE padam anak — sebab itu kita salin!)
ALTER TABLE visits__mig0026 RENAME TO visits; ← namakan yang baru jadi 'visits'
INSERT OR IGNORE INTO visit_doctors SELECT * FROM _mig0026_backup_visit_doctors; ← pulang anak dari salinan
-- …sama untuk diagnoses & managements…
DROP TABLE _mig0026_backup_visit_doctors; ← kemas: buang salinan sementara
Kenapa ia penting: ini bukan kod cantik — ini kod yang menyelamatkan data pesakit. Tarian "salin → bina semula → pulang" itu wajib setiap kali table visits dibina semula. Inilah sebab peraturan asas projek: elak bina semula table; utamakan ADD COLUMN (e6). Bila terpaksa juga, ikut corak 0026 ini tepat-tepat.
Terangkan ON DELETE CASCADE macam saya budak 12 tahun, guna contoh table visits dan visit_doctors dalam sistem ini. Kenapa DROP TABLE visits boleh padam data anak?
Router — peta alamat → halaman
Sekarang kita pindah ke frontend (bahagian yang pengguna nampak). Fail pertama: router — peta yang menentukan "alamat URL ini tunjuk halaman mana".
Apa fail ini: src/main.tsx ialah titik mula frontend. Bahagian terpentingnya ialah router — senarai pasangan "alamat → halaman". Ini fail React pertama anda lihat, jadi ia bercampur HTML (dipanggil JSX) dengan TypeScript.
const router = createBrowserRouter([ ← bina 'peta': alamat URL → halaman mana
{ path: "/", element: <HomePage /> }, ← alamat '/' → tunjuk HomePage
{ path: "/oncall/jadual", ← alamat laman on-call awam…
element: <OncallPublicPage /> }, ← …tunjuk halaman jadual on-call
{
element: <AppLayout />, ← 'bekas' kongsi (header+menu) untuk anak di bawah
children: [ ← halaman-halaman di dalam bekas ini…
{
path: "/kaunter",
element: <PrivateRoute allowedRoles={['pendaftar']}> ← KUNCI depan: pendaftar sahaja (e10)
<KaunterLayout /></PrivateRoute>,
children: [ ← sub-halaman di bawah '/kaunter'…
{ index: true, element: <RegistrarDashboardPage /> }, ← '/kaunter' → dashboard
{ path: "daftar", element: <PatientRegistrationPage /> }, ← '/kaunter/daftar' → borang daftar
],
},
],
},
]);
Kenapa ia penting: router ialah peta keseluruhan app. Mahu tahu halaman apa wujud dan siapa boleh masuk? Baca fail ini. Perhatikan corak bersarang: alamat di dalam children mewarisi bekas (AppLayout) dan kunci (PrivateRoute) di atasnya. <HomePage /> itu JSX — ia nampak macam HTML tapi sebenarnya cara React kata "letak komponen HomePage di sini".
Buka src/main.tsx dan senaraikan semua alamat (path) untuk peranan 'doktor', dan halaman mana setiap satu tunjuk. Terangkan corak children bersarang.
Context provider + custom hook
Bagaimana satu maklumat (cth nama hospital) boleh dibaca oleh mana-mana halaman tanpa dihantar tangan-ke-tangan? Jawapannya: context — satu "siaran" global.
Apa fail ini: src/contexts/HospitalConfigProvider.tsx mengambil tetapan hospital dari backend (pintu e2) sekali, lalu "menyiarkannya" ke seluruh app. Ia memperkenalkan tiga idea teras React: useState (ingatan), useEffect (kesan sampingan), dan custom hook.
const HospitalConfigContext = createContext(DEFAULT_HOSPITAL_IDENTITY); ← cipta 'saluran siaran' global
export function HospitalConfigProvider({ children }) { ← komponen yang menyiarkan tetapan ke anak
const [config, setConfig] = useState(readCache() ?? DEFAULT_HOSPITAL_IDENTITY); ← ingatan: tetapan semasa
← useState beri [nilai, caraUbahNilai]
useEffect(() => { ← lepas komponen muncul, jalankan sekali…
fetch('/api/hospital/config') ← …ambil tetapan terkini dari backend (pintu e2)
.then(r => r.json())
.then(({ data }) => { if (data) update(data); }) ← dapat → kemas kini paparan
.catch(swallow('…')); ← gagal rangkaian → biar; guna cache/lalai
}, [update]); ← senarai kebergantungan (bila perlu jalan semula)
return (
<HospitalConfigContext.Provider value={config}> ← siarkan 'config' ke semua anak di dalam
{children}
</HospitalConfigContext.Provider>
);
}
export function useHospitalConfig() { ← 'hook' custom: cara mudah anak baca siaran
return useContext(HospitalConfigContext); ← mana-mana komponen panggil ini → dapat tetapan
}
Kenapa ia penting: tanpa context, "nama hospital" perlu dihantar sebagai prop melalui setiap lapisan komponen — menyusahkan. Context buat ia macam siaran radio: provider menyiar, dan mana-mana komponen "tala" guna hook useHospitalConfig(). useState ialah ingatan komponen; useEffect ialah "buat sesuatu selepas muncul" (di sini: ambil data). .catch(swallow(…)) sengaja membiarkan kegagalan rangkaian senyap — sebab kita masih ada nilai cache/lalai untuk dipapar.
Apa itu useState dan useEffect dalam React? Guna HospitalConfigProvider.tsx sebagai contoh. Kenapa useEffect ada senarai [update] di hujung?
Komponen daun — pengawal laluan
Komponen "daun" ialah kepingan UI kecil dengan satu tugas. Contoh ini, PrivateRoute, ialah pasangan frontend untuk middleware backend (e4): ia jaga siapa boleh lihat satu halaman.
Apa fail ini: src/components/PrivateRoute.tsx ialah pembalut. Anda nampak ia membalut halaman di router (e8). Tugasnya: kalau pengguna belum log masuk atau peranan salah, halau ke laman lain; jika tidak, tunjuk kandungan.
export function PrivateRoute({ children, allowedRoles }) { ← komponen pembalut: jaga siapa boleh masuk
const isAuthenticated = useAuthStore(s => s.isAuthenticated); ← baca status log masuk dari stor auth
const role = useAuthStore(s => s.role); ← baca peranan pengguna
if (!isAuthenticated || !role || !allowedRoles.includes(role)) { ← belum log masuk ATAU peranan salah…
return <Navigate to={systemToken ? '/pilih-fungsi' : '/'} replace />; ← …halau ke laman yang sesuai
}
return children; ← lulus → tunjuk kandungan yang dijaga
}
Kenapa ia penting: ini menunjukkan dua perkara. Pertama, keselamatan ada dua lapis: backend (e4) ialah lapisan sebenar yang melindungi data; PrivateRoute cuma pengalaman pengguna (elak tunjuk skrin yang mereka tak patut nampak). Kedua, ini komponen kecil yang bersih — satu tugas, mudah dibaca. Kebanyakan "daun" dalam src/components/ ikut bentuk ini.
Dalam PrivateRoute.tsx, kenapa ia halau ke '/pilih-fungsi' kalau ada systemToken, tapi ke '/' kalau tiada? Terangkan model auth dua-peringkat sistem ini.
Hook custom — ambil data
Bentuk terakhir: custom hook yang berdiri sendiri. Ia membungkus logik "ambil data dari backend" supaya banyak komponen boleh guna semula tanpa salin-tampal.
Apa fail ini: src/hooks/useServerTime.ts ambil masa semasa dari server. Ia contoh bersih corak yang berulang di mana-mana frontend: "muat data, simpan status sedang-muat, bersih bila komponen tutup".
export function useServerTime() { ← hook custom: dapatkan masa dari server
const [data, setData] = useState(null); ← simpan hasil (mula: belum ada)
const [loading, setLoading] = useState(true); ← simpan status 'sedang muat'
useEffect(() => { ← lepas guna kali pertama, ambil masa…
let cancelled = false; ← bendera: elak set data kalau komponen dah tutup
(async () => {
try {
const res = await api('/api/time'); ← panggil pintu '/api/time'
if (!cancelled) setData(res.data); ← simpan hasil (kalau masih relevan)
} catch {
if (!cancelled) setData(null); ← gagal → biar kosong
} finally {
if (!cancelled) setLoading(false); ← apa pun jadi, 'muat' tamat
}
})();
return () => { cancelled = true; }; ← bila komponen tutup, tanda 'batal'
}, []); ← [] = jalankan SEKALI sahaja
return { data, loading }; ← bagi balik kepada komponen yang guna
}
Kenapa ia penting: mana-mana komponen kini boleh tulis const { data, loading } = useServerTime() dan dapat masa server + status muat — logik ditulis sekali. Perhatikan bendera cancelled: kalau pengguna tutup halaman sebelum data tiba, kita tidak cuba kemas kini komponen yang dah hilang. Itu corak penting yang elak ralat React biasa.
Terangkan kenapa useServerTime guna bendera 'cancelled'. Apa ralat React yang berlaku kalau kita buang bendera itu? Beri contoh mudah.
10 bentuk → 440 fail
Anda baru sahaja membaca satu contoh setiap bentuk utama dalam sistem ini. Inilah kuncinya: hampir setiap fail lain ialah variasi salah satu bentuk ini.
| Bila anda buka… | Ia ikut bentuk… |
|---|---|
| worker/contracts/*.contracts.ts | Kontrak Zod (e1) |
| worker/routes/*.ts | Route (e2) — ~40 fail, corak sama |
| worker/services/*Service.ts | Service (e3) |
| worker/middleware/*.ts | Middleware (e4) |
| migrations/NNNN_*.sql | Migration mudah (e6) atau bina-semula (e7) |
| src/contexts/*Provider.tsx | Context provider + hook (e9) |
| src/components/*.tsx | Komponen daun (e10) |
| src/hooks/use*.ts | Custom hook (e11) |
| src/pages/[role]/*.tsx | Halaman = komponen besar (gabungan e9–e11) |
Saya dah faham 10 bentuk fail dalam Sistem OMFS. Sekarang saya nak faham ciri on-call. Senaraikan fail-fail utamanya, kelaskan setiap satu ikut bentuk (route/service/contract/komponen/hook), dan cadang turutan untuk saya baca.
🔬 Panduan Kod 03 — Ciri Sebenar →
Langkah seterusnya. Bagaimana 6 ciri penuh (laporan, e-mel, AI, keadilan on-call) berfungsi di sebalik tabir.
← Panduan Kod 01 (asas & bahasa)
Ulang kaji: apa itu kod, 4 bahasa, peta bina-dari-kosong, cara guna Claude Code.
🔨 Panduan Developer →
Rujukan teknikal padat: seni bina, fail penting, resipi perubahan bergred.