diff --git a/AGENTS.md b/AGENTS.md index 93e7d3d..d20df06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,28 +7,174 @@ STOP. Your knowledge of Cloudflare Workers APIs and limits may be outdated. Alwa - https://developers.cloudflare.com/workers/ - MCP: `https://docs.mcp.cloudflare.com/mcp` -For all limits and quotas, retrieve from the product's `/platform/limits/` page. eg. `/workers/platform/limits` +For all limits and quotas, retrieve from the product's `/platform/limits/` page. e.g. `/workers/platform/limits` -## Commands +## Product Docs -| Command | Purpose | -|---------|---------| -| `npx wrangler dev` | Local development | -| `npx wrangler deploy` | Deploy to Cloudflare | -| `npx wrangler types` | Generate TypeScript types | - -Run `wrangler types` after changing bindings in wrangler.jsonc. - -## Node.js Compatibility - -https://developers.cloudflare.com/workers/runtime-apis/nodejs/ +Retrieve API references and limits from: +`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/` ## Errors - **Error 1102** (CPU/Memory exceeded): Retrieve limits from `/workers/platform/limits/` - **All errors**: https://developers.cloudflare.com/workers/observability/errors/ -## Product Docs +--- -Retrieve API references and limits from: -`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/` +# Project: tangfamily + +A Cloudflare Workers full-stack family tree website. The single Worker at `src/index.ts` serves all API routes; static HTML pages in `public/` are served via Cloudflare's asset pipeline (`env.ASSETS`). Data is stored in a KV namespace (`FAMILY_DATA`). Deployed at https://tangfamily.shumengya666.workers.dev. + +## Commands + +| Command | Purpose | +|---------|---------| +| `npm run dev` | Start local dev server (http://127.0.0.1:8787) | +| `npm run deploy` | Deploy to Cloudflare | +| `npm test` | Run all tests (vitest watch mode) | +| `npm run cf-typegen` | Regenerate `worker-configuration.d.ts` from `wrangler.jsonc` | + +**Run a single test file:** +```bash +npx vitest run test/index.spec.ts +``` + +**Run a single test by name:** +```bash +npx vitest run -t "test name here" +``` + +Always run `npm run cf-typegen` after changing any bindings in `wrangler.jsonc`. + +There is no lint script. Format with Prettier manually if needed: +```bash +npx prettier --write "src/**/*.ts" +``` + +## Project Structure + +``` +src/index.ts # Single-file Worker: all API handlers + routing + CORS +public/index.html # Visitor login page +public/tree.html # Family tree viewer (authenticated visitors) +public/admin-login.html # Admin login +public/admin.html # Admin CRUD panel +test/index.spec.ts # Vitest integration tests (runs inside Workers runtime) +test/tsconfig.json # Extends root tsconfig; adds vitest-pool-workers types +test/env.d.ts # Merges Env into cloudflare:test module +vitest.config.mts # Vitest config — uses defineWorkersConfig + wrangler.jsonc +wrangler.jsonc # Worker config: entry, KV binding, assets, compat flags +worker-configuration.d.ts # Auto-generated by cf-typegen — do not edit manually +``` + +## Architecture + +- All API logic lives in `src/index.ts`; there are no other Worker source files. +- `public/*.html` pages are pure HTML with inline CSS and inline ` + + diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..e0ec47c --- /dev/null +++ b/public/admin.html @@ -0,0 +1,357 @@ + + + + + + 唐氏族谱 - 后台管理 + + + +
+

族谱管理后台

+
+ + + +
+
+ +
+ +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + +
姓名出生年性别辈分父亲母亲操作
加载中...
+
+
+ +
+ + + + diff --git a/public/index.html b/public/index.html index e72f403..cecdba8 100644 --- a/public/index.html +++ b/public/index.html @@ -1,32 +1,89 @@ - - - - - - Hello, World! - - -

-

This page comes from a static asset stored at `public/index.html` as configured in `wrangler.jsonc`.

- - - - + + + + + + 唐家族谱 + + + +
+ +
+ + + +
密码错误,请重试
+
+
+ + diff --git a/public/tree.html b/public/tree.html new file mode 100644 index 0000000..37ba80d --- /dev/null +++ b/public/tree.html @@ -0,0 +1,452 @@ + + + + + + 唐氏族谱 + + + +
+

唐氏族谱

+
+ + +
+
+
+
加载中...
+
+ + + + diff --git a/src/index.ts b/src/index.ts index 1ecdb14..0f39f40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,203 @@ /** - * Welcome to Cloudflare Workers! This is your first worker. - * - * - Run `npm run dev` in your terminal to start a development server - * - Open a browser tab at http://localhost:8787/ to see your worker in action - * - Run `npm run deploy` to publish your worker - * - * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the - * `Env` object can be regenerated with `npm run cf-typegen`. - * - * Learn more at https://developers.cloudflare.com/workers/ + * 唐家族谱网站 - Cloudflare Worker */ -export default { - async fetch(request, env, ctx): Promise { - const url = new URL(request.url); - switch (url.pathname) { - case '/message': - return new Response('Hello, World!'); - case '/random': - return new Response(crypto.randomUUID()); - default: - return new Response('Not Found', { status: 404 }); +interface FamilyMember { + id: string; + name: string; + birthYear: number; + gender: '男' | '女'; + generation?: string; + fatherId?: string; + motherId?: string; +} + +interface Env { + FAMILY_DATA: KVNamespace; + ASSETS: Fetcher; +} + +// 密码常量 +const VISITOR_PASSWORD = 'tangfamily'; +const ADMIN_PASSWORD = 'shumengya5201314'; + +// 生成简单的 session token +function generateToken(): string { + return crypto.randomUUID(); +} + +// 验证 token +async function verifyToken(token: string | null, env: Env, isAdmin: boolean = false): Promise { + if (!token) return false; + const session = await env.FAMILY_DATA.get(`session:${token}`); + if (!session) return false; + const sessionData = JSON.parse(session); + return isAdmin ? sessionData.isAdmin === true : true; +} + +// 处理登录 +async function handleLogin(request: Request, env: Env): Promise { + try { + const body = await request.json() as { password: string; isAdmin?: boolean }; + const isAdmin = body.isAdmin === true; + const correctPassword = isAdmin ? ADMIN_PASSWORD : VISITOR_PASSWORD; + + if (body.password === correctPassword) { + const token = generateToken(); + const sessionData = { isAdmin, createdAt: Date.now() }; + await env.FAMILY_DATA.put(`session:${token}`, JSON.stringify(sessionData), { expirationTtl: 86400 }); // 24小时 + return new Response(JSON.stringify({ success: true, token, isAdmin }), { + headers: { 'Content-Type': 'application/json' } + }); } + return new Response(JSON.stringify({ success: false, error: '密码错误' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ success: false, error: '请求格式错误' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } +} + +// 获取所有家族成员 +async function handleGetMembers(request: Request, env: Env): Promise { + const token = request.headers.get('Authorization')?.replace('Bearer ', '') || null; + if (!await verifyToken(token, env)) { + return new Response(JSON.stringify({ error: '未授权' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + + const membersJson = await env.FAMILY_DATA.get('members'); + const members = membersJson ? JSON.parse(membersJson) : []; + return new Response(JSON.stringify(members), { + headers: { 'Content-Type': 'application/json' } + }); +} + +// 添加或更新成员(仅管理员) +async function handleSaveMember(request: Request, env: Env): Promise { + const token = request.headers.get('Authorization')?.replace('Bearer ', '') || null; + if (!await verifyToken(token, env, true)) { + return new Response(JSON.stringify({ error: '需要管理员权限' }), { + status: 403, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const member = await request.json() as FamilyMember; + if (!member.id) { + member.id = crypto.randomUUID(); + } + + const membersJson = await env.FAMILY_DATA.get('members'); + const members: FamilyMember[] = membersJson ? JSON.parse(membersJson) : []; + + const index = members.findIndex(m => m.id === member.id); + if (index >= 0) { + members[index] = member; + } else { + members.push(member); + } + + await env.FAMILY_DATA.put('members', JSON.stringify(members)); + return new Response(JSON.stringify({ success: true, member }), { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ success: false, error: '保存失败' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } +} + +// 删除成员(仅管理员) +async function handleDeleteMember(request: Request, env: Env): Promise { + const token = request.headers.get('Authorization')?.replace('Bearer ', '') || null; + if (!await verifyToken(token, env, true)) { + return new Response(JSON.stringify({ error: '需要管理员权限' }), { + status: 403, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const { id } = await request.json() as { id: string }; + const membersJson = await env.FAMILY_DATA.get('members'); + const members: FamilyMember[] = membersJson ? JSON.parse(membersJson) : []; + + const filtered = members.filter(m => m.id !== id); + await env.FAMILY_DATA.put('members', JSON.stringify(filtered)); + + return new Response(JSON.stringify({ success: true }), { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ success: false, error: '删除失败' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + const path = url.pathname; + + // CORS 头 + const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }; + + // 处理 OPTIONS 请求 + if (request.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + // API 路由 + if (path.startsWith('/api/')) { + let response: Response; + + switch (path) { + case '/api/login': + response = await handleLogin(request, env); + break; + case '/api/members': + if (request.method === 'GET') { + response = await handleGetMembers(request, env); + } else if (request.method === 'POST') { + response = await handleSaveMember(request, env); + } else { + response = new Response('Method not allowed', { status: 405 }); + } + break; + case '/api/members/delete': + response = await handleDeleteMember(request, env); + break; + default: + response = new Response('Not found', { status: 404 }); + } + + // 添加 CORS 头到响应 + const headers = new Headers(response.headers); + Object.entries(corsHeaders).forEach(([key, value]) => headers.set(key, value)); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers + }); + } + + // 静态文件由 assets 配置处理 + return env.ASSETS.fetch(request); }, } satisfies ExportedHandler; diff --git a/src/init-data.js b/src/init-data.js new file mode 100644 index 0000000..6cf7e58 --- /dev/null +++ b/src/init-data.js @@ -0,0 +1,47 @@ +/** + * 初始化家族数据脚本 + * 运行此脚本来初始化唐家族谱的初始数据 + */ + +// 初始家族成员数据 +const initialMembers = [ + { + id: 'tang-zhiming', + name: '唐治明', + birthYear: 1950, // 请根据实际情况修改 + gender: '男' + }, + { + id: 'shu-banglian', + name: '舒邦莲', + birthYear: 1952, // 请根据实际情况修改 + gender: '女' + }, + { + id: 'tang-yonghong', + name: '唐永洪', + birthYear: 1975, // 请根据实际情况修改 + gender: '男', + fatherId: 'tang-zhiming', + motherId: 'shu-banglian' + }, + { + id: 'tang-wei', + name: '唐伟', + birthYear: 1980, // 请根据实际情况修改 + gender: '男', + fatherId: 'tang-zhiming', + motherId: 'shu-banglian' + } +]; + +console.log('初始家族数据:'); +console.log(JSON.stringify(initialMembers, null, 2)); + +console.log('\n要初始化数据,请按以下步骤操作:'); +console.log('1. 运行 npm run dev 启动本地开发服务器'); +console.log('2. 访问 http://localhost:8787/admin-login.html'); +console.log('3. 使用管理员密码登录:shumengya5201314'); +console.log('4. 在管理后台逐个添加以上成员'); +console.log('\n或者,你也可以使用 Wrangler CLI 直接写入 KV:'); +console.log('wrangler kv:key put --binding=FAMILY_DATA "members" \'', JSON.stringify(initialMembers), '\''); diff --git a/test-fixed-sort.js b/test-fixed-sort.js new file mode 100644 index 0000000..4c7c226 --- /dev/null +++ b/test-fixed-sort.js @@ -0,0 +1,136 @@ +// 完整测试新算法 +const members = [{"id":"4661dd7b","name":"唐治明","birthYear":1960,"gender":"男","generation":"父亲","fatherId":"f24a95c7"},{"id":"d4498bfb","name":"舒邦莲","birthYear":1960,"gender":"女","generation":"父亲"},{"id":"f24a95c7","name":"唐有奎","birthYear":1900,"gender":"男","generation":"祖父"},{"id":"1be537aa","name":"唐治安","birthYear":1960,"gender":"男","generation":"父亲","fatherId":"f24a95c7"},{"id":"e6d9c5b3","name":"万华群","birthYear":1960,"gender":"女","generation":"父亲"},{"id":"4b78f55d","name":"唐治芳","birthYear":1960,"gender":"女","generation":"父亲","fatherId":"f24a95c7"},{"id":"a7bcc3b5","name":"邓能春","birthYear":1960,"gender":"男","generation":"父亲"}]; + +const map = {}; +members.forEach(m => map[m.id] = m); + +const families = { + '4661dd7b|d4498bfb': { fatherId: '4661dd7b', motherId: 'd4498bfb' }, + '1be537aa|e6d9c5b3': { fatherId: '1be537aa', motherId: 'e6d9c5b3' }, + 'a7bcc3b5|4b78f55d': { fatherId: 'a7bcc3b5', motherId: '4b78f55d' } +}; + +const familyKeyOfChild = { + '4661dd7b': 'f24a95c7|', + '1be537aa': 'f24a95c7|', + '4b78f55d': 'f24a95c7|' +}; + +const prevOrderIndex = { 'f24a95c7': 0 }; +const row = members; +const memberSet = new Set(row.map(m => m.id)); + +// 1. 识别配偶关系 +const spouseOf = {}; +Object.keys(families).forEach(key => { + const fam = families[key]; + if (!fam || !fam.fatherId || !fam.motherId) return; + if (memberSet.has(fam.fatherId) && memberSet.has(fam.motherId)) { + spouseOf[fam.fatherId] = fam.motherId; + spouseOf[fam.motherId] = fam.fatherId; + } +}); + +// 2. 按血缘分组 +const siblingGroups = {}; +row.forEach(m => { + const key = familyKeyOfChild[m.id] || `solo:${m.id}`; + if (!siblingGroups[key]) siblingGroups[key] = []; + siblingGroups[key].push(m); +}); + +console.log('兄弟姐妹分组:'); +Object.keys(siblingGroups).forEach(key => { + console.log(` ${key}: ${siblingGroups[key].map(m => m.name).join(', ')}`); +}); + +// 3. 构建家庭单元 +const familyUnits = []; +const globalProcessed = new Set(); + +function compareByBirthName(a, b) { + if (a.birthYear !== b.birthYear) return b.birthYear - a.birthYear; + return a.name.localeCompare(b.name, 'zh-Hans-CN'); +} + +Object.keys(siblingGroups).forEach(groupKey => { + const siblings = siblingGroups[groupKey].slice().sort(compareByBirthName); + const unitMembers = []; + + siblings.forEach(person => { + if (globalProcessed.has(person.id)) return; + globalProcessed.add(person.id); + + const spouseId = spouseOf[person.id]; + if (spouseId && memberSet.has(spouseId)) { + globalProcessed.add(spouseId); + const spouse = map[spouseId]; + if (person.gender === '男') { + unitMembers.push(person, spouse); + } else { + unitMembers.push(spouse, person); + } + } else { + unitMembers.push(person); + } + }); + + if (unitMembers.length > 0) { + familyUnits.push({ groupKey, members: unitMembers }); + } +}); + +console.log('\n家庭单元:'); +familyUnits.forEach((unit, i) => { + const names = unit.members.map(m => m.name).join(' + '); + console.log(` Unit ${i} (${unit.groupKey}): ${names}`); +}); + +// 4. 计算权重并排序 +function getUnitWeight(unit) { + const key = unit.groupKey; + if (key.startsWith('solo:')) { + return { parentIdx: 9999, birthYear: unit.members[0].birthYear }; + } + + const parts = key.split('|'); + const fId = parts[0] || null; + const mId = parts[1] || null; + const indices = []; + if (fId && prevOrderIndex[fId] !== undefined) indices.push(prevOrderIndex[fId]); + if (mId && prevOrderIndex[mId] !== undefined) indices.push(prevOrderIndex[mId]); + const parentIdx = indices.length > 0 ? Math.min(...indices) : 9999; + + const birthYear = Math.max(...unit.members.map(m => m.birthYear)); + + return { parentIdx, birthYear }; +} + +familyUnits.sort((a, b) => { + const wa = getUnitWeight(a); + const wb = getUnitWeight(b); + + if (wa.parentIdx !== wb.parentIdx) return wa.parentIdx - wb.parentIdx; + if (wa.birthYear !== wb.birthYear) return wb.birthYear - wa.birthYear; + return 0; +}); + +console.log('\n排序后的家庭单元:'); +familyUnits.forEach((unit, i) => { + const names = unit.members.map(m => m.name).join(' + '); + const weight = getUnitWeight(unit); + console.log(` ${i}. ${names} (parentIdx=${weight.parentIdx}, birthYear=${weight.birthYear})`); +}); + +// 5. 展开为最终结果 +const result = []; +familyUnits.forEach(unit => result.push(...unit.members)); + +console.log('\n最终排序结果:'); +console.log(result.map(m => m.name).join(', ')); + +console.log('\n期望结果:'); +console.log('唐治安, 万华群, 唐治芳, 邓能春, 唐治明, 舒邦莲, 唐有奎'); +console.log('\n说明: 唐治安、唐治芳、唐治明都是唐有奎的孩子,应该排在前面(parentIdx=0)'); +console.log(' 舒邦莲、万华群、邓能春是外来配偶,紧贴在丈夫/妻子旁边'); +console.log(' 唐有奎没有父母信息,排到最后(parentIdx=9999)'); diff --git a/test-layout.html b/test-layout.html new file mode 100644 index 0000000..1ffafe3 --- /dev/null +++ b/test-layout.html @@ -0,0 +1,77 @@ + + + + + Layout Test + + +

+    
+
+
diff --git a/test-left-right-spouse.js b/test-left-right-spouse.js
new file mode 100644
index 0000000..c3e6d09
--- /dev/null
+++ b/test-left-right-spouse.js
@@ -0,0 +1,172 @@
+// 测试新的配偶放置策略(左右分离)
+
+const members = [
+	{ id: '4661dd7b', name: '唐治明', birthYear: 1960, gender: '男', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'd4498bfb', name: '舒邦莲', birthYear: 1960, gender: '女', generation: '父亲' },
+	{ id: 'f24a95c7', name: '唐有奎', birthYear: 1900, gender: '男', generation: '祖父' },
+	{ id: '1be537aa', name: '唐治安', birthYear: 1960, gender: '男', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'e6d9c5b3', name: '万华群', birthYear: 1960, gender: '女', generation: '父亲' },
+	{ id: '4b78f55d', name: '唐治芳', birthYear: 1960, gender: '女', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'a7bcc3b5', name: '邓能春', birthYear: 1960, gender: '男', generation: '父亲' },
+	{ id: '208fb133', name: '唐伟', birthYear: 1990, gender: '男', generation: '自己', fatherId: '4661dd7b', motherId: 'd4498bfb' },
+	{ id: 'ab86880b', name: '唐永洪', birthYear: 1990, gender: '男', generation: '自己', fatherId: '4661dd7b', motherId: 'd4498bfb' },
+	{ id: 'de8e2fc8', name: '唐雪', birthYear: 1990, gender: '女', generation: '自己', fatherId: '1be537aa', motherId: 'e6d9c5b3' },
+	{ id: '08a22bcd', name: '唐波', birthYear: 1990, gender: '男', generation: '自己', fatherId: '1be537aa', motherId: 'e6d9c5b3' },
+	{ id: '1ebd23c6', name: '邓静', birthYear: 1990, gender: '女', generation: '自己', fatherId: 'a7bcc3b5', motherId: '4b78f55d' },
+	{ id: '4c3f6e87', name: '邓正悦', birthYear: 1990, gender: '女', generation: '自己', fatherId: 'a7bcc3b5', motherId: '4b78f55d' },
+	{ id: 'a74c5ba0', name: '张红', birthYear: 1990, gender: '女', generation: '自己' },
+	{ id: '95672071', name: '郭思琪', birthYear: 1990, gender: '女', generation: '自己' },
+	{ id: '6fb43781', name: '彭先生', birthYear: 1990, gender: '男', generation: '自己' },
+	{ id: 'a1fa7080', name: '唐世宁', birthYear: 2020, gender: '女', generation: '子女', fatherId: 'ab86880b', motherId: '95672071' },
+	{ id: '74ab3d7a', name: '唐瑾瑜', birthYear: 2020, gender: '女', generation: '子女', fatherId: '08a22bcd', motherId: 'a74c5ba0' },
+	{ id: '6365ca6a', name: '唐世严', birthYear: 2020, gender: '男', generation: '子女', fatherId: '08a22bcd', motherId: 'a74c5ba0' },
+	{ id: '4736c2bb', name: '彭瑾瑜', birthYear: 2020, gender: '女', generation: '子女', fatherId: '6fb43781', motherId: '1ebd23c6' }
+];
+
+const map = {};
+members.forEach(m => (map[m.id] = m));
+
+const families = {};
+const familyKeyOfChild = {};
+members.forEach(child => {
+	const fatherId = child.fatherId && map[child.fatherId] ? child.fatherId : '';
+	const motherId = child.motherId && map[child.motherId] ? child.motherId : '';
+	if (!fatherId && !motherId) return;
+	const key = `${fatherId}|${motherId}`;
+	familyKeyOfChild[child.id] = key;
+	if (!families[key]) families[key] = { fatherId: fatherId || null, motherId: motherId || null, children: [] };
+	families[key].children.push(child.id);
+});
+
+const gen2 = members.filter(m => m.generation === '自己');
+const memberSet = new Set(gen2.map(m => m.id));
+const spouseOf = {};
+Object.keys(families).forEach(key => {
+	const fam = families[key];
+	if (!fam || !fam.fatherId || !fam.motherId) return;
+	if (memberSet.has(fam.fatherId) && memberSet.has(fam.motherId)) {
+		spouseOf[fam.fatherId] = fam.motherId;
+		spouseOf[fam.motherId] = fam.fatherId;
+	}
+});
+
+const bloodMembers = [];
+const spouseMembers = new Set();
+
+gen2.forEach(m => {
+	const hasParents = (m.fatherId && map[m.fatherId]) || (m.motherId && map[m.motherId]);
+	const isSpouseOnly = !hasParents && spouseOf[m.id];
+
+	if (isSpouseOnly) {
+		spouseMembers.add(m.id);
+	} else {
+		bloodMembers.push(m);
+	}
+});
+
+const siblingGroups = {};
+bloodMembers.forEach(m => {
+	const key = familyKeyOfChild[m.id] || `solo:${m.id}`;
+	if (!siblingGroups[key]) siblingGroups[key] = [];
+	siblingGroups[key].push(m);
+});
+
+function compareByBirthName(a, b) {
+	if (a.birthYear !== b.birthYear) return b.birthYear - a.birthYear;
+	return a.name.localeCompare(b.name, 'zh-Hans-CN');
+}
+
+const familyUnits = [];
+const globalProcessed = new Set();
+
+Object.keys(siblingGroups).forEach(groupKey => {
+	const siblings = siblingGroups[groupKey].slice().sort(compareByBirthName);
+
+	const leftSpouses = [];
+	const coreMembers = [];
+	const rightSpouses = [];
+
+	siblings.forEach(person => {
+		if (globalProcessed.has(person.id)) return;
+		globalProcessed.add(person.id);
+		coreMembers.push(person);
+
+		const spouseId = spouseOf[person.id];
+		if (spouseId && memberSet.has(spouseId)) {
+			globalProcessed.add(spouseId);
+			const spouse = map[spouseId];
+
+			if (person.gender === '男') {
+				rightSpouses.push(spouse);
+			} else {
+				leftSpouses.push(spouse);
+			}
+		}
+	});
+
+	const unitMembers = [...leftSpouses, ...coreMembers, ...rightSpouses];
+
+	if (unitMembers.length > 0) {
+		familyUnits.push({ groupKey, members: unitMembers });
+	}
+});
+
+console.log('=== 新策略:家庭单元构建结果 ===');
+familyUnits.forEach((unit, idx) => {
+	const names = unit.members.map(m => {
+		const isSpouse = spouseMembers.has(m.id);
+		return isSpouse ? `[${m.name}]` : m.name;
+	});
+	console.log(`单元 ${idx + 1} (${unit.groupKey}):`);
+	console.log(`  ${names.join(' - ')}`);
+});
+
+// 排序(使用简化的父母位置)
+const gen1 = members.filter(m => m.generation === '父亲');
+const prevOrderIndex = {};
+gen1.forEach((m, idx) => {
+	prevOrderIndex[m.id] = idx;
+});
+
+function getUnitWeight(unit) {
+	const key = unit.groupKey;
+	if (key.startsWith('solo:')) {
+		return { parentIdx: 9999, birthYear: unit.members[0].birthYear };
+	}
+
+	const parts = key.split('|');
+	const fId = parts[0] || null;
+	const mId = parts[1] || null;
+	const indices = [];
+	if (fId && prevOrderIndex[fId] !== undefined) indices.push(prevOrderIndex[fId]);
+	if (mId && prevOrderIndex[mId] !== undefined) indices.push(prevOrderIndex[mId]);
+	const parentIdx = indices.length > 0 ? Math.min(...indices) : 9999;
+
+	const bloodMembersInUnit = unit.members.filter(m => !spouseMembers.has(m.id));
+	const birthYear = bloodMembersInUnit.length > 0 ? Math.max(...bloodMembersInUnit.map(m => m.birthYear)) : Math.max(...unit.members.map(m => m.birthYear));
+
+	return { parentIdx, birthYear };
+}
+
+familyUnits.sort((a, b) => {
+	const wa = getUnitWeight(a);
+	const wb = getUnitWeight(b);
+	if (wa.parentIdx !== wb.parentIdx) return wa.parentIdx - wb.parentIdx;
+	if (wa.birthYear !== wb.birthYear) return wb.birthYear - wa.birthYear;
+	return 0;
+});
+
+console.log('\n=== 最终排序结果(第三代)===');
+const result = [];
+familyUnits.forEach(unit => {
+	result.push(...unit.members);
+});
+console.log(result.map(m => m.name).join(' → '));
+
+console.log('\n=== 连接验证 ===');
+console.log('唐治明+舒邦莲 的孩子: 唐伟, 唐永洪');
+console.log('  位置:', result.findIndex(m => m.name === '唐伟'), result.findIndex(m => m.name === '唐永洪'));
+console.log('唐治安+万华群 的孩子: 唐波, 唐雪');
+console.log('  位置:', result.findIndex(m => m.name === '唐波'), result.findIndex(m => m.name === '唐雪'));
+console.log('唐治芳+邓能春 的孩子: 邓静, 邓正悦');
+console.log('  位置:', result.findIndex(m => m.name === '邓静'), result.findIndex(m => m.name === '邓正悦'));
diff --git a/test-new-sort.js b/test-new-sort.js
new file mode 100644
index 0000000..cd4fe94
--- /dev/null
+++ b/test-new-sort.js
@@ -0,0 +1,128 @@
+// 测试新的排序逻辑
+const members = [{"id":"4661dd7b","name":"唐治明","birthYear":1960,"gender":"男","generation":"父亲","fatherId":"f24a95c7"},{"id":"d4498bfb","name":"舒邦莲","birthYear":1960,"gender":"女","generation":"父亲"},{"id":"f24a95c7","name":"唐有奎","birthYear":1900,"gender":"男","generation":"祖父"},{"id":"1be537aa","name":"唐治安","birthYear":1960,"gender":"男","generation":"父亲","fatherId":"f24a95c7"},{"id":"e6d9c5b3","name":"万华群","birthYear":1960,"gender":"女","generation":"父亲"},{"id":"4b78f55d","name":"唐治芳","birthYear":1960,"gender":"女","generation":"父亲","fatherId":"f24a95c7"},{"id":"a7bcc3b5","name":"邓能春","birthYear":1960,"gender":"男","generation":"父亲"}];
+
+const map = {};
+members.forEach(m => map[m.id] = m);
+
+// 构建配偶关系
+const families = {
+  '4661dd7b|d4498bfb': { fatherId: '4661dd7b', motherId: 'd4498bfb' },
+  '1be537aa|e6d9c5b3': { fatherId: '1be537aa', motherId: 'e6d9c5b3' },
+  'a7bcc3b5|4b78f55d': { fatherId: 'a7bcc3b5', motherId: '4b78f55d' }
+};
+
+const familyKeyOfChild = {
+  '4661dd7b': 'f24a95c7|',
+  '1be537aa': 'f24a95c7|',
+  '4b78f55d': 'f24a95c7|'
+};
+
+const prevOrderIndex = { 'f24a95c7': 0 };
+
+// 识别配偶关系
+const spouseOf = {};
+Object.keys(families).forEach(key => {
+  const fam = families[key];
+  spouseOf[fam.fatherId] = fam.motherId;
+  spouseOf[fam.motherId] = fam.fatherId;
+});
+
+console.log('配偶关系:', spouseOf);
+
+// 按血缘分组(兄弟姐妹)
+const siblingGroups = {};
+members.forEach(m => {
+  const key = familyKeyOfChild[m.id] || `solo:${m.id}`;
+  if (!siblingGroups[key]) siblingGroups[key] = [];
+  siblingGroups[key].push(m);
+});
+
+console.log('\n兄弟姐妹分组:');
+Object.keys(siblingGroups).forEach(key => {
+  console.log(`  ${key}: ${siblingGroups[key].map(m => m.name).join(', ')}`);
+});
+
+// 构建家庭单元(每个兄弟姐妹 + 配偶)
+const familyUnits = [];
+Object.keys(siblingGroups).forEach(groupKey => {
+  const siblings = siblingGroups[groupKey];
+  const processed = new Set();
+  
+  siblings.forEach(person => {
+    if (processed.has(person.id)) return;
+    processed.add(person.id);
+    
+    const spouseId = spouseOf[person.id];
+    if (spouseId && map[spouseId] && !processed.has(spouseId)) {
+      processed.add(spouseId);
+      const spouse = map[spouseId];
+      familyUnits.push({
+        type: 'couple',
+        groupKey,
+        members: person.gender === '男' ? [person, spouse] : [spouse, person]
+      });
+    } else {
+      familyUnits.push({
+        type: 'single',
+        groupKey,
+        members: [person]
+      });
+    }
+  });
+});
+
+console.log('\n家庭单元:');
+familyUnits.forEach((unit, i) => {
+  const names = unit.members.map(m => m.name).join(' + ');
+  console.log(`  Unit ${i} (${unit.type}, ${unit.groupKey}): ${names}`);
+});
+
+// 对家庭单元排序
+function getUnitWeight(unit) {
+  const key = unit.groupKey;
+  if (key.startsWith('solo:')) {
+    return { parentIdx: null, birthYear: unit.members[0].birthYear };
+  }
+  
+  const parts = key.split('|');
+  const fId = parts[0] || null;
+  const indices = [];
+  if (fId && prevOrderIndex[fId] !== undefined) indices.push(prevOrderIndex[fId]);
+  const parentIdx = indices.length > 0 ? Math.min(...indices) : null;
+  const birthYear = Math.max(...unit.members.map(m => m.birthYear));
+  
+  return { parentIdx, birthYear };
+}
+
+familyUnits.sort((a, b) => {
+  const wa = getUnitWeight(a);
+  const wb = getUnitWeight(b);
+  
+  if (wa.parentIdx !== null && wb.parentIdx !== null) {
+    if (wa.parentIdx !== wb.parentIdx) return wa.parentIdx - wb.parentIdx;
+  } else if (wa.parentIdx !== null) {
+    return -1;
+  } else if (wb.parentIdx !== null) {
+    return 1;
+  }
+  
+  if (wa.birthYear !== wb.birthYear) return wb.birthYear - wa.birthYear;
+  return 0;
+});
+
+console.log('\n排序后的家庭单元:');
+familyUnits.forEach((unit, i) => {
+  const names = unit.members.map(m => m.name).join(' + ');
+  const weight = getUnitWeight(unit);
+  console.log(`  ${i}. ${names} (parentIdx=${weight.parentIdx}, birthYear=${weight.birthYear})`);
+});
+
+// 展开为最终结果
+const result = [];
+familyUnits.forEach(unit => result.push(...unit.members));
+
+console.log('\n最终排序结果:');
+console.log(result.map(m => m.name).join(', '));
+
+console.log('\n期望结果:');
+console.log('唐治安, 万华群, 唐治芳, 邓能春, 唐治明, 舒邦莲');
diff --git a/test-sort-siblings.js b/test-sort-siblings.js
new file mode 100644
index 0000000..6ecc17c
--- /dev/null
+++ b/test-sort-siblings.js
@@ -0,0 +1,20 @@
+// 测试 compareByBirthName 排序
+
+const members = [
+	{ id: 'de8e2fc8', name: '唐雪', birthYear: 1990, gender: '女' },
+	{ id: '08a22bcd', name: '唐波', birthYear: 1990, gender: '男' }
+];
+
+function compareByBirthName(a, b) {
+	if (a.birthYear !== b.birthYear) return b.birthYear - a.birthYear;
+	return a.name.localeCompare(b.name, 'zh-Hans-CN');
+}
+
+const sorted = members.slice().sort(compareByBirthName);
+console.log('排序前:', members.map(m => m.name).join(', '));
+console.log('排序后:', sorted.map(m => m.name).join(', '));
+
+// 测试中文姓名比较
+console.log('\n中文比较:');
+console.log('唐波 vs 唐雪:', '唐波'.localeCompare('唐雪', 'zh-Hans-CN'));
+console.log('唐雪 vs 唐波:', '唐雪'.localeCompare('唐波', 'zh-Hans-CN'));
diff --git a/test-spouse-fix.js b/test-spouse-fix.js
new file mode 100644
index 0000000..01c9337
--- /dev/null
+++ b/test-spouse-fix.js
@@ -0,0 +1,210 @@
+// 测试修复后的配偶排序逻辑
+
+const members = [
+	{ id: '4661dd7b', name: '唐治明', birthYear: 1960, gender: '男', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'd4498bfb', name: '舒邦莲', birthYear: 1960, gender: '女', generation: '父亲' },
+	{ id: 'f24a95c7', name: '唐有奎', birthYear: 1900, gender: '男', generation: '祖父' },
+	{ id: '1be537aa', name: '唐治安', birthYear: 1960, gender: '男', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'e6d9c5b3', name: '万华群', birthYear: 1960, gender: '女', generation: '父亲' },
+	{ id: '4b78f55d', name: '唐治芳', birthYear: 1960, gender: '女', generation: '父亲', fatherId: 'f24a95c7' },
+	{ id: 'a7bcc3b5', name: '邓能春', birthYear: 1960, gender: '男', generation: '父亲' },
+	{ id: '208fb133', name: '唐伟', birthYear: 1990, gender: '男', generation: '自己', fatherId: '4661dd7b', motherId: 'd4498bfb' },
+	{ id: 'ab86880b', name: '唐永洪', birthYear: 1990, gender: '男', generation: '自己', fatherId: '4661dd7b', motherId: 'd4498bfb' },
+	{ id: 'de8e2fc8', name: '唐雪', birthYear: 1990, gender: '女', generation: '自己', fatherId: '1be537aa', motherId: 'e6d9c5b3' },
+	{ id: '08a22bcd', name: '唐波', birthYear: 1990, gender: '男', generation: '自己', fatherId: '1be537aa', motherId: 'e6d9c5b3' },
+	{ id: '1ebd23c6', name: '邓静', birthYear: 1990, gender: '女', generation: '自己', fatherId: 'a7bcc3b5', motherId: '4b78f55d' },
+	{ id: '4c3f6e87', name: '邓正悦', birthYear: 1990, gender: '女', generation: '自己', fatherId: 'a7bcc3b5', motherId: '4b78f55d' },
+	{ id: 'a74c5ba0', name: '张红', birthYear: 1990, gender: '女', generation: '自己' }, // 唐波的配偶
+	{ id: '95672071', name: '郭思琪', birthYear: 1990, gender: '女', generation: '自己' }, // 唐永洪的配偶
+	{ id: '6fb43781', name: '彭先生', birthYear: 1990, gender: '男', generation: '自己' }, // 邓静的配偶
+	{ id: 'a1fa7080', name: '唐世宁', birthYear: 2020, gender: '女', generation: '子女', fatherId: 'ab86880b', motherId: '95672071' },
+	{ id: '74ab3d7a', name: '唐瑾瑜', birthYear: 2020, gender: '女', generation: '子女', fatherId: '08a22bcd', motherId: 'a74c5ba0' },
+	{ id: '6365ca6a', name: '唐世严', birthYear: 2020, gender: '男', generation: '子女', fatherId: '08a22bcd', motherId: 'a74c5ba0' },
+	{ id: '4736c2bb', name: '彭瑾瑜', birthYear: 2020, gender: '女', generation: '子女', fatherId: '6fb43781', motherId: '1ebd23c6' }
+];
+
+const map = {};
+members.forEach(m => (map[m.id] = m));
+
+// 识别家族关系
+const families = {};
+const familyKeyOfChild = {};
+members.forEach(child => {
+	const fatherId = child.fatherId && map[child.fatherId] ? child.fatherId : '';
+	const motherId = child.motherId && map[child.motherId] ? child.motherId : '';
+	if (!fatherId && !motherId) return;
+	const key = `${fatherId}|${motherId}`;
+	familyKeyOfChild[child.id] = key;
+	if (!families[key]) families[key] = { fatherId: fatherId || null, motherId: motherId || null, children: [] };
+	families[key].children.push(child.id);
+});
+
+console.log('=== 家族单元 ===');
+Object.keys(families).forEach(key => {
+	const fam = families[key];
+	const fatherName = fam.fatherId ? map[fam.fatherId].name : '(无)';
+	const motherName = fam.motherId ? map[fam.motherId].name : '(无)';
+	const childrenNames = fam.children.map(id => map[id].name).join(', ');
+	console.log(`${fatherName} + ${motherName} → ${childrenNames}`);
+});
+
+// 第二代(自己)的成员
+const gen2 = members.filter(m => m.generation === '自己');
+
+// 识别配偶关系
+const memberSet = new Set(gen2.map(m => m.id));
+const spouseOf = {};
+Object.keys(families).forEach(key => {
+	const fam = families[key];
+	if (!fam || !fam.fatherId || !fam.motherId) return;
+	if (memberSet.has(fam.fatherId) && memberSet.has(fam.motherId)) {
+		spouseOf[fam.fatherId] = fam.motherId;
+		spouseOf[fam.motherId] = fam.fatherId;
+	}
+});
+
+console.log('\n=== 配偶关系 ===');
+Object.keys(spouseOf).forEach(id => {
+	const spouseId = spouseOf[id];
+	if (id < spouseId) {
+		// 只打印一次
+		console.log(`${map[id].name} + ${map[spouseId].name}`);
+	}
+});
+
+// 分离血缘成员和配偶
+const bloodMembers = [];
+const spouseMembers = new Set();
+
+gen2.forEach(m => {
+	const hasParents = (m.fatherId && map[m.fatherId]) || (m.motherId && map[m.motherId]);
+	const isSpouseOnly = !hasParents && spouseOf[m.id]; // 没有父母但有配偶
+
+	if (isSpouseOnly) {
+		spouseMembers.add(m.id);
+	} else {
+		bloodMembers.push(m);
+	}
+});
+
+console.log('\n=== 血缘成员 ===');
+console.log(bloodMembers.map(m => m.name).join(', '));
+
+console.log('\n=== 仅配偶身份的成员 ===');
+console.log([...spouseMembers].map(id => map[id].name).join(', '));
+
+// 按血缘分组
+const siblingGroups = {};
+bloodMembers.forEach(m => {
+	const key = familyKeyOfChild[m.id] || `solo:${m.id}`;
+	if (!siblingGroups[key]) siblingGroups[key] = [];
+	siblingGroups[key].push(m);
+});
+
+console.log('\n=== 血缘兄弟姐妹分组 ===');
+Object.keys(siblingGroups).forEach(key => {
+	const group = siblingGroups[key];
+	const names = group.map(m => m.name).join(', ');
+	console.log(`${key}: ${names}`);
+});
+
+// 构建家庭单元
+function compareByBirthName(a, b) {
+	if (a.birthYear !== b.birthYear) return b.birthYear - a.birthYear;
+	return a.name.localeCompare(b.name, 'zh-Hans-CN');
+}
+
+const familyUnits = [];
+const globalProcessed = new Set();
+
+Object.keys(siblingGroups).forEach(groupKey => {
+	const siblings = siblingGroups[groupKey].slice().sort(compareByBirthName);
+
+	const unitMembers = [];
+	siblings.forEach(person => {
+		if (globalProcessed.has(person.id)) return;
+		globalProcessed.add(person.id);
+
+		const spouseId = spouseOf[person.id];
+		if (spouseId && memberSet.has(spouseId)) {
+			globalProcessed.add(spouseId);
+			const spouse = map[spouseId];
+			if (person.gender === '男') {
+				unitMembers.push(person, spouse);
+			} else {
+				unitMembers.push(spouse, person);
+			}
+		} else {
+			unitMembers.push(person);
+		}
+	});
+
+	if (unitMembers.length > 0) {
+		familyUnits.push({ groupKey, members: unitMembers });
+	}
+});
+
+console.log('\n=== 家庭单元构建结果 ===');
+familyUnits.forEach((unit, idx) => {
+	const names = unit.members.map(m => m.name).join(', ');
+	console.log(`单元 ${idx + 1} (${unit.groupKey}): ${names}`);
+});
+
+// 计算上一行的顺序
+const gen1 = members.filter(m => m.generation === '父亲');
+const prevOrderIndex = {};
+gen1.forEach((m, idx) => {
+	prevOrderIndex[m.id] = idx;
+});
+
+// 排序家庭单元
+function getUnitWeight(unit) {
+	const key = unit.groupKey;
+	if (key.startsWith('solo:')) {
+		const member = unit.members.find(m => !spouseMembers.has(m.id));
+		if (member) {
+			const spouseId = spouseOf[member.id];
+			if (spouseId && prevOrderIndex[spouseId] !== undefined) {
+				return { parentIdx: prevOrderIndex[spouseId], birthYear: member.birthYear };
+			}
+		}
+		return { parentIdx: 9999, birthYear: unit.members[0].birthYear };
+	}
+
+	const parts = key.split('|');
+	const fId = parts[0] || null;
+	const mId = parts[1] || null;
+	const indices = [];
+	if (fId && prevOrderIndex[fId] !== undefined) indices.push(prevOrderIndex[fId]);
+	if (mId && prevOrderIndex[mId] !== undefined) indices.push(prevOrderIndex[mId]);
+	const parentIdx = indices.length > 0 ? Math.min(...indices) : 9999;
+
+	const bloodMembersInUnit = unit.members.filter(m => !spouseMembers.has(m.id));
+	const birthYear = bloodMembersInUnit.length > 0 ? Math.max(...bloodMembersInUnit.map(m => m.birthYear)) : Math.max(...unit.members.map(m => m.birthYear));
+
+	return { parentIdx, birthYear };
+}
+
+familyUnits.sort((a, b) => {
+	const wa = getUnitWeight(a);
+	const wb = getUnitWeight(b);
+
+	if (wa.parentIdx !== wb.parentIdx) return wa.parentIdx - wb.parentIdx;
+	if (wa.birthYear !== wb.birthYear) return wb.birthYear - wa.birthYear;
+
+	return 0;
+});
+
+console.log('\n=== 排序后的家庭单元 ===');
+familyUnits.forEach((unit, idx) => {
+	const weight = getUnitWeight(unit);
+	const names = unit.members.map(m => m.name).join(', ');
+	console.log(`单元 ${idx + 1} (parentIdx=${weight.parentIdx}, birthYear=${weight.birthYear}): ${names}`);
+});
+
+console.log('\n=== 最终排序结果(第二代)===');
+const result = [];
+familyUnits.forEach(unit => {
+	result.push(...unit.members);
+});
+console.log(result.map(m => m.name).join(' → '));
diff --git a/test/index.spec.ts b/test/index.spec.ts
index 52cfa0d..72d7fe8 100644
--- a/test/index.spec.ts
+++ b/test/index.spec.ts
@@ -1,41 +1,82 @@
-import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
+import { SELF } from 'cloudflare:test';
 import { describe, it, expect } from 'vitest';
-import worker from '../src';
 
-describe('Hello World user worker', () => {
-	describe('request for /message', () => {
-		it('/ responds with "Hello, World!" (unit style)', async () => {
-			const request = new Request('http://example.com/message');
-			// Create an empty context to pass to `worker.fetch()`.
-			const ctx = createExecutionContext();
-			const response = await worker.fetch(request, env, ctx);
-			// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
-			await waitOnExecutionContext(ctx);
-			expect(await response.text()).toMatchInlineSnapshot(`"Hello, World!"`);
+describe('tangfamily API', () => {
+	describe('POST /api/login', () => {
+		it('rejects wrong visitor password', async () => {
+			const res = await SELF.fetch('http://example.com/api/login', {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json' },
+				body: JSON.stringify({ password: 'wrongpass', isAdmin: false }),
+			});
+			expect(res.status).toBe(401);
+			const data = await res.json() as { success: boolean };
+			expect(data.success).toBe(false);
 		});
 
-		it('responds with "Hello, World!" (integration style)', async () => {
-			const request = new Request('http://example.com/message');
-			const response = await SELF.fetch(request);
-			expect(await response.text()).toMatchInlineSnapshot(`"Hello, World!"`);
+		it('accepts correct visitor password', async () => {
+			const res = await SELF.fetch('http://example.com/api/login', {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json' },
+				body: JSON.stringify({ password: 'tangfamily', isAdmin: false }),
+			});
+			expect(res.status).toBe(200);
+			const data = await res.json() as { success: boolean; token: string };
+			expect(data.success).toBe(true);
+			expect(typeof data.token).toBe('string');
+		});
+
+		it('rejects wrong admin password', async () => {
+			const res = await SELF.fetch('http://example.com/api/login', {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json' },
+				body: JSON.stringify({ password: 'wrongpass', isAdmin: true }),
+			});
+			expect(res.status).toBe(401);
+		});
+
+		it('accepts correct admin password', async () => {
+			const res = await SELF.fetch('http://example.com/api/login', {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json' },
+				body: JSON.stringify({ password: 'shumengya5201314', isAdmin: true }),
+			});
+			expect(res.status).toBe(200);
+			const data = await res.json() as { success: boolean; isAdmin: boolean };
+			expect(data.success).toBe(true);
+			expect(data.isAdmin).toBe(true);
 		});
 	});
 
-	describe('request for /random', () => {
-		it('/ responds with a random UUID (unit style)', async () => {
-			const request = new Request('http://example.com/random');
-			// Create an empty context to pass to `worker.fetch()`.
-			const ctx = createExecutionContext();
-			const response = await worker.fetch(request, env, ctx);
-			// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
-			await waitOnExecutionContext(ctx);
-			expect(await response.text()).toMatch(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/);
+	describe('GET /api/members', () => {
+		it('returns 401 without token', async () => {
+			const res = await SELF.fetch('http://example.com/api/members');
+			expect(res.status).toBe(401);
 		});
 
-		it('responds with a random UUID (integration style)', async () => {
-			const request = new Request('http://example.com/random');
-			const response = await SELF.fetch(request);
-			expect(await response.text()).toMatch(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/);
+		it('returns member list with valid token', async () => {
+			// login first
+			const loginRes = await SELF.fetch('http://example.com/api/login', {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json' },
+				body: JSON.stringify({ password: 'tangfamily', isAdmin: false }),
+			});
+			const { token } = await loginRes.json() as { token: string };
+
+			const res = await SELF.fetch('http://example.com/api/members', {
+				headers: { Authorization: `Bearer ${token}` },
+			});
+			expect(res.status).toBe(200);
+			const data = await res.json();
+			expect(Array.isArray(data)).toBe(true);
+		});
+	});
+
+	describe('unknown routes', () => {
+		it('returns 404 for unknown API route', async () => {
+			const res = await SELF.fetch('http://example.com/api/unknown');
+			expect(res.status).toBe(404);
 		});
 	});
 });
+
diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts
index 4e989f6..4a0aa46 100644
--- a/worker-configuration.d.ts
+++ b/worker-configuration.d.ts
@@ -1,11 +1,12 @@
 /* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: b739a9c19cff1463949c4db47674ed86)
+// Generated by Wrangler by running `wrangler types` (hash: ffe47106a350f18a15c69d61ba9b3bae)
 // Runtime types generated with workerd@1.20260217.0 2026-02-17 global_fetch_strictly_public,nodejs_compat
 declare namespace Cloudflare {
 	interface GlobalProps {
 		mainModule: typeof import("./src/index");
 	}
 	interface Env {
+		FAMILY_DATA: KVNamespace;
 	}
 }
 interface Env extends Cloudflare.Env {}
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 524f808..4bdfe84 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -17,28 +17,11 @@
 	},
 	"observability": {
 		"enabled": true
-	}
-	/**
-	 * Smart Placement
-	 * https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
-	 */
-	// "placement": {  "mode": "smart" }
-	/**
-	 * Bindings
-	 * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
-	 * databases, object storage, AI inference, real-time communication and more.
-	 * https://developers.cloudflare.com/workers/runtime-apis/bindings/
-	 */
-	/**
-	 * Environment Variables
-	 * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
-	 * Note: Use secrets to store sensitive data.
-	 * https://developers.cloudflare.com/workers/configuration/secrets/
-	 */
-	// "vars": {  "MY_VARIABLE": "production_value" }
-	/**
-	 * Service Bindings (communicate between multiple Workers)
-	 * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
-	 */
-	// "services": [  {   "binding": "MY_SERVICE",   "service": "my-service"  } ]
+	},
+	"kv_namespaces": [
+		{
+			"binding": "FAMILY_DATA",
+			"id": "07e25767617e469aa297cdf79ceeb010"
+		}
+	]
 }
\ No newline at end of file