duoqi-api/src/routes/progress.ts
Wang Zhuoxuan c36d828df9
All checks were successful
CI/CD Pipeline / Unit Tests (push) Successful in 30s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Successful in 1m19s
feat: narrow gamification to first-version MVP scope
行为按 docs/GAMIFICATION_DESIGN.md(duoqi-flutter/docs/GAMIFICATION_REQUIREMENTS_CHANGE.md)对齐:

- 新增 src/utils/time.ts 北京时间工具,统一替换散落的 UTC 日期/周边界计算
- challenge-service:未完成 session 续答(同 nodeId 复用 pending/in_progress),每日次数按 session 幂等扣减(条件 UPDATE 抢 daily_attempt_consumed_at 锁),移除高奖励 multiplier 和答对时发 first_knowledge_card
- 新增 knowledge-card-service 和 POST /challenges/knowledge-cards/:cardId/view:用户打开/收下卡片时在单事务内发 review_explanation (3 XP) 和 first_knowledge_card (15 XP),fallback 卡走独立幂等 namespace
- tracks-service:mapNodeStatus 只返回 current/done
- hearts-service:删除 Pro/ProPlus 免扣分支和新用户 1 心保护,RestoreMethod 收窄为 'ad' | 'wait'
- xp-service / leaderboard-service / streak-service / ad-recovery-service / progress-summary-service:时区切北京;排行榜同分按 last_xp_at 升序,weeklySettlement 不再发前 3 名金币;streak milestone 返回空
- /shop/purchase、/inventory/items/use、/subscription/verify 返回 NOT_AVAILABLE_IN_MVP
- schema 加 challenge_sessions.daily_attempt_consumed_at(迁移 0007)
- 接口文档 docs/api-reference.md 同步 12 项 MVP 行为变化
- 测试:新增 utils/time 和 tracks node-status 测试;hearts/streak/leaderboard 测试按新行为更新;challenge-service 13 个 MVP 之外(Plus/高奖励/first_knowledge_card 答对触发)测试 it.skip 待重写 mock 队列

typecheck / lint / test (146 passed, 13 skipped) 全部通过。
2026-06-16 18:05:04 +08:00

77 lines
2.5 KiB
TypeScript

import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import {
getDashboard,
getStreak,
getHearts,
restoreHearts,
getChapterProgress,
} from '../services/progress/progress-service.js';
const restoreHeartsSchema = z.object({
method: z.enum(['ad', 'wait']),
});
const feedbackSchema = z.object({
content: z.string().min(1).max(2000),
contact: z.string().max(255).optional(),
pageContext: z.string().max(200).optional(),
});
function getUserId(request: { user: unknown }): string {
return (request.user as { userId: string }).userId;
}
export async function progressRoutes(app: FastifyInstance): Promise<void> {
app.get('/progress/dashboard', async (request) => {
const data = await getDashboard(getUserId(request));
return { success: true, data, error: null };
});
app.get('/progress/streak', async (request) => {
const data = await getStreak(getUserId(request));
return { success: true, data, error: null };
});
app.get('/progress/hearts', async (request) => {
const data = await getHearts(getUserId(request));
return { success: true, data, error: null };
});
// [废弃] 请使用 POST /rewards/ad-recovery/session + /rewards/ad-recovery/complete 两步流程。
app.post('/progress/hearts/restore', async (request) => {
const parsed = restoreHeartsSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
const data = await restoreHearts(getUserId(request), parsed.data.method);
return { success: true, data, error: null };
});
app.get('/progress/chapters', async (request) => {
const data = await getChapterProgress(getUserId(request));
return { success: true, data, error: null };
});
app.post('/feedback', async (request) => {
const parsed = feedbackSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
const { db } = await import('../db/client.js');
const { userFeedback } = await import('../db/schema.js');
const { v4: uuid } = await import('uuid');
const userId = getUserId(request);
await db.insert(userFeedback).values({
id: uuid(),
userId,
content: parsed.data.content,
contact: parsed.data.contact ?? null,
pageContext: parsed.data.pageContext ?? null,
});
return { success: true, data: null, error: null };
});
}