Compare commits
2 Commits
91c59c3ad9
...
ca33d4353f
| Author | SHA1 | Date | |
|---|---|---|---|
| ca33d4353f | |||
| ff57f5e0a5 |
156
AGENTS.md
156
AGENTS.md
@@ -1,22 +1,142 @@
|
|||||||
# AGENTS.md
|
# LEARN-LANGUAGES 知识库
|
||||||
|
|
||||||
```bash
|
**生成时间:** 2026-03-08
|
||||||
# 使用以下命令检查代码合法性
|
**提交:** 91c59c3
|
||||||
pnpm build
|
**分支:** dev
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
全栈语言学习平台,集成 AI 翻译、词典和 TTS。Next.js 16 App Router + PostgreSQL + better-auth + next-intl。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js 路由 (Server Components)
|
||||||
|
│ ├── (auth)/ # 认证页面: 登录、注册、个人资料
|
||||||
|
│ ├── (features)/ # 功能页面: 翻译、词典、字幕播放器
|
||||||
|
│ ├── folders/ # 文件夹管理
|
||||||
|
│ └── api/auth/ # better-auth catch-all
|
||||||
|
├── modules/ # 业务逻辑 (action-service-repository)
|
||||||
|
│ ├── auth/ # 认证 actions, services, repositories
|
||||||
|
│ ├── translator/ # 翻译模块
|
||||||
|
│ ├── dictionary/ # 词典模块
|
||||||
|
│ └── folder/ # 文件夹管理模块
|
||||||
|
├── design-system/ # 可复用 UI 基础组件 (CVA)
|
||||||
|
├── components/ # 业务组件
|
||||||
|
├── lib/ # 集成层 (db, auth, bigmodel AI)
|
||||||
|
├── hooks/ # 自定义 hooks (useAudioPlayer, useFileUpload)
|
||||||
|
├── utils/ # 纯工具函数 (cn, validate, json)
|
||||||
|
└── shared/ # 类型和常量
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 查找位置
|
||||||
|
|
||||||
- Next.js 16 使用 App Router
|
| 任务 | 位置 | 备注 |
|
||||||
- TypeScript 严格模式和 ES2023 目标
|
|------|------|------|
|
||||||
- better-auth 身份验证(邮箱/用户名/密码)
|
| 添加功能页面 | `src/app/(features)/` | 路由组,无 URL 前缀 |
|
||||||
- next-intl 国际化(支持:en-US, zh-CN, ja-JP, ko-KR, de-DE, fr-FR, it-IT, ug-CN)
|
| 添加认证页面 | `src/app/(auth)/` | 登录、注册、个人资料 |
|
||||||
- 阿里云千问 TTS (qwen3-tts-flash) 文本转语音
|
| 添加业务逻辑 | `src/modules/{name}/` | 遵循 action-service-repository |
|
||||||
- 使用 pnpm,而不是 npm 或 yarn
|
| 添加 AI 管道 | `src/lib/bigmodel/{name}/` | 多阶段 orchestrator |
|
||||||
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
| 添加 UI 组件 | `src/design-system/{category}/` | base, feedback, layout, overlay |
|
||||||
- 所有面向用户的文本都需要国际化
|
| 添加工具函数 | `src/utils/` | 纯函数 |
|
||||||
- **优先使用 Server Components**,只在需要交互时使用 Client Components
|
| 添加类型定义 | `src/shared/` | 业务类型 |
|
||||||
- **新功能应遵循 action-service-repository 架构**
|
| 数据库查询 | `src/modules/*/` | Repository 层 |
|
||||||
- Better-auth 处理会话管理 - 使用 authClient 适配器进行认证操作
|
| i18n 翻译 | `messages/*.json` | 8 种语言 |
|
||||||
- 组件尽量复用/src/design-system里的可复用组件与/src/components里的业务相关组件
|
|
||||||
- 不要创建index.ts
|
## 约定
|
||||||
- 每变更一个完整项目自动git commit,如需撤销就git reset
|
|
||||||
|
### 架构: Action-Service-Repository
|
||||||
|
每个模块 6 个文件:
|
||||||
|
```
|
||||||
|
{name}-action.ts # Server Actions, "use server"
|
||||||
|
{name}-action-dto.ts # Zod schemas, ActionInput*/ActionOutput*
|
||||||
|
{name}-service.ts # 业务逻辑, 跨模块调用
|
||||||
|
{name}-service-dto.ts # ServiceInput*/ServiceOutput*
|
||||||
|
{name}-repository.ts # Prisma 操作
|
||||||
|
{name}-repository-dto.ts # RepoInput*/RepoOutput*
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命名
|
||||||
|
- 类型: `{Layer}{Input|Output}{Feature}` → `ActionInputSignUp`
|
||||||
|
- 函数: `{layer}{Feature}` → `actionSignUp`, `serviceSignUp`
|
||||||
|
- 文件: `kebab-case` 带角色后缀
|
||||||
|
|
||||||
|
### Server/Client 划分
|
||||||
|
- **默认**: Server Components (无 "use client")
|
||||||
|
- **Client**: 仅在需要时 (useState, useEffect, 浏览器 API)
|
||||||
|
- **Actions**: 必须有 `"use server"`
|
||||||
|
|
||||||
|
### 导入风格
|
||||||
|
- 显式路径: `@/design-system/base/button` (无 barrel exports)
|
||||||
|
- 不创建 `index.ts` 文件
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
- Zod schemas 放在 `*-dto.ts`
|
||||||
|
- 使用 `validate()` from `@/utils/validate`
|
||||||
|
|
||||||
|
### 认证
|
||||||
|
```typescript
|
||||||
|
// 服务端
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
|
||||||
|
// 客户端
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
const { data } = authClient.useSession();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 受保护操作
|
||||||
|
```typescript
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session?.user?.id) return { success: false, message: "未授权" };
|
||||||
|
// 变更前检查所有权
|
||||||
|
```
|
||||||
|
|
||||||
|
## 反模式 (本项目)
|
||||||
|
|
||||||
|
- ❌ `index.ts` barrel exports
|
||||||
|
- ❌ `as any`, `@ts-ignore`, `@ts-expect-error`
|
||||||
|
- ❌ 用 API routes 做数据操作 (使用 Server Actions)
|
||||||
|
- ❌ Server Component 可行时用 Client Component
|
||||||
|
- ❌ npm 或 yarn (使用 pnpm)
|
||||||
|
- ❌ 生产代码中使用 `console.log`
|
||||||
|
|
||||||
|
## 独特风格
|
||||||
|
|
||||||
|
### 设计系统分类
|
||||||
|
- `base/` — 原子组件: button, input, card, checkbox, radio, switch, select, textarea, range
|
||||||
|
- `feedback/` — 反馈: alert, progress, skeleton, toast
|
||||||
|
- `layout/` — 布局: container, grid, stack (VStack, HStack)
|
||||||
|
- `overlay/` — 覆盖层: modal
|
||||||
|
- `navigation/` — 导航: tabs
|
||||||
|
|
||||||
|
### AI 管道模式
|
||||||
|
`src/lib/bigmodel/` 中的多阶段 orchestrator:
|
||||||
|
```
|
||||||
|
{name}/
|
||||||
|
├── orchestrator.ts # 协调各阶段
|
||||||
|
├── types.ts # 共享接口
|
||||||
|
└── stage{n}-{name}.ts # 各阶段实现
|
||||||
|
```
|
||||||
|
|
||||||
|
### 废弃函数
|
||||||
|
`translator-action.ts` 中的 `genIPA()` 和 `genLanguage()` — 保留用于 text-speaker 兼容
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev # 开发服务器 (HTTPS)
|
||||||
|
pnpm build # 生产构建 (验证代码)
|
||||||
|
pnpm lint # ESLint
|
||||||
|
pnpm prisma studio # 数据库 GUI
|
||||||
|
```
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- Tailwind CSS v4 (无 tailwind.config.ts)
|
||||||
|
- React Compiler 已启用
|
||||||
|
- i18n: 8 种语言 (en-US, zh-CN, ja-JP, ko-KR, de-DE, fr-FR, it-IT, ug-CN)
|
||||||
|
- TTS: 阿里云千问 (qwen3-tts-flash)
|
||||||
|
- 数据库: PostgreSQL via Prisma (生成在 `generated/prisma/`)
|
||||||
|
- 未配置测试基础设施
|
||||||
|
|||||||
@@ -29,19 +29,19 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors"
|
className="flex justify-between items-center group py-4 px-5 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`/folders/${folder.id}`);
|
router.push(`/folders/${folder.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0 text-primary-500">
|
||||||
<Fd className="text-gray-600" size="md" />
|
<Fd size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium text-gray-900">{folder.name}</h3>
|
<h3 className="font-semibold text-gray-900 truncate">{folder.name}</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
{t("folderInfo", {
|
{t("folderInfo", {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
@@ -51,7 +51,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1 ml-4">
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -69,7 +69,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FolderPen size={16} />
|
<FolderPen size={18} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
@@ -89,9 +89,9 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
|
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={18} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<ChevronRight size={18} className="text-gray-400" />
|
<ChevronRight size={20} className="text-gray-400 ml-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -156,7 +156,7 @@ export function FoldersClient({ userId }: { userId: string; }) {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full border-dashed"
|
className="w-full border-dashed"
|
||||||
>
|
>
|
||||||
<FolderPlus size={18} />
|
<FolderPlus size={20} />
|
||||||
<span>{loading ? t("creating") : t("newFolder")}</span>
|
<span>{loading ? t("creating") : t("newFolder")}</span>
|
||||||
</LightButton>
|
</LightButton>
|
||||||
|
|
||||||
@@ -167,14 +167,13 @@ export function FoldersClient({ userId }: { userId: string; }) {
|
|||||||
// 空状态
|
// 空状态
|
||||||
<div className="text-center py-12 text-gray-400">
|
<div className="text-center py-12 text-gray-400">
|
||||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
<FolderPlus size="md" className="text-gray-400" />
|
<FolderPlus size={24} className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 文件夹卡片列表
|
// 文件夹卡片列表
|
||||||
<div className="rounded-md border border-gray-200 overflow-hidden">
|
folders
|
||||||
{folders
|
|
||||||
.toSorted((a, b) => a.id - b.id)
|
.toSorted((a, b) => a.id - b.id)
|
||||||
.map((folder) => (
|
.map((folder) => (
|
||||||
<FolderCard
|
<FolderCard
|
||||||
@@ -182,8 +181,7 @@ export function FoldersClient({ userId }: { userId: string; }) {
|
|||||||
folder={folder}
|
folder={folder}
|
||||||
refresh={updateFolders}
|
refresh={updateFolders}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardList>
|
</CardList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface CardListProps {
|
|||||||
export function CardList({ children, className = "" }: CardListProps) {
|
export function CardList({ children, className = "" }: CardListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`max-h-96 overflow-y-auto rounded-lg border-2 border-gray-200 ${className}`}>
|
<div className={`max-h-96 overflow-y-auto rounded-lg border-2 border-gray-200 ${className}`}>
|
||||||
<VStack gap={0}>
|
<VStack gap={0} align="stretch">
|
||||||
{children}
|
{children}
|
||||||
</VStack>
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
83
src/design-system/AGENTS.md
Normal file
83
src/design-system/AGENTS.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 设计系统指南
|
||||||
|
|
||||||
|
**生成时间:** 2026-03-08
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
基于 CVA 的可复用 UI 组件库,与业务组件分离。
|
||||||
|
|
||||||
|
## 组件分类
|
||||||
|
|
||||||
|
| 类别 | 路径 | 组件 |
|
||||||
|
|------|------|------|
|
||||||
|
| 基础 | `base/` | button, input, card, checkbox, radio, switch, select, textarea, range |
|
||||||
|
| 反馈 | `feedback/` | alert, progress, skeleton, toast |
|
||||||
|
| 布局 | `layout/` | container, grid, stack (VStack, HStack) |
|
||||||
|
| 覆盖层 | `overlay/` | modal |
|
||||||
|
| 导航 | `navigation/` | tabs |
|
||||||
|
|
||||||
|
## CVA 模式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
const buttonVariants = cva("base-styles", {
|
||||||
|
variants: {
|
||||||
|
variant: { primary: "...", secondary: "...", ghost: "..." },
|
||||||
|
size: { sm: "...", md: "...", lg: "..." },
|
||||||
|
},
|
||||||
|
defaultVariants: { variant: "secondary", size: "md" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件模板
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
"use client";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import { cn } from "@/design-system/lib/utils";
|
||||||
|
|
||||||
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, leftIcon, rightIcon, children, ...props }, ref) => (
|
||||||
|
<button ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props}>
|
||||||
|
{leftIcon && <span className="mr-2">{leftIcon}</span>}
|
||||||
|
{children}
|
||||||
|
{rightIcon && <span className="ml-2">{rightIcon}</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
export const PrimaryButton = (props: Omit<ButtonProps, "variant">) => <Button variant="primary" {...props} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 复合组件
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card variant="bordered"><CardHeader><CardTitle>标题</CardTitle></CardHeader><CardBody>内容</CardBody></Card>
|
||||||
|
<Modal open={isOpen}><Modal.Header><Modal.Title>标题</Modal.Title></Modal.Header><Modal.Body>内容</Modal.Body></Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 导入方式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 显式导入
|
||||||
|
import { Button } from "@/design-system/base/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardBody } from "@/design-system/base/card";
|
||||||
|
// ❌ 不要创建 barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cn } from "@/design-system/lib/utils";
|
||||||
|
<div className={cn("base", condition && "conditional", className)} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新组件
|
||||||
|
|
||||||
|
1. 确定类别目录
|
||||||
|
2. 创建 `{name}.tsx`,使用 CVA 定义变体
|
||||||
|
3. 添加 `"use client"` + `forwardRef` + `displayName`
|
||||||
|
4. 导出组件、变体类型、快捷组件
|
||||||
90
src/lib/bigmodel/AGENTS.md
Normal file
90
src/lib/bigmodel/AGENTS.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# AI 管道架构指南
|
||||||
|
|
||||||
|
**生成时间:** 2026-03-08
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
AI 处理采用多阶段管道路径,由 orchestrator 协调各 stage 执行。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
{name}/
|
||||||
|
├── orchestrator.ts # 协调器:编排所有阶段
|
||||||
|
├── types.ts # 共享接口定义
|
||||||
|
├── stage1-{name}.ts # 阶段 1
|
||||||
|
├── stage2-{name}.ts # 阶段 2
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 现有管道
|
||||||
|
|
||||||
|
| 管道 | 阶段数 | 用途 |
|
||||||
|
|------|--------|------|
|
||||||
|
| dictionary | 4 | 词典查询(输入分析→语义映射→标准形式→词条生成)|
|
||||||
|
| translator | 1 | 翻译处理 |
|
||||||
|
|
||||||
|
## 阶段命名约定
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 函数命名:动词+名词
|
||||||
|
async function analyzeInput(text: string): Promise<InputAnalysisResult>
|
||||||
|
async function determineSemanticMapping(...): Promise<SemanticMappingResult>
|
||||||
|
async function generateStandardForm(...): Promise<StandardFormResult>
|
||||||
|
async function generateEntries(...): Promise<EntriesResult>
|
||||||
|
|
||||||
|
// 结果接口命名:动词+名词+Result
|
||||||
|
interface InputAnalysisResult {
|
||||||
|
language: string;
|
||||||
|
normalizedText: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Orchestrator 模板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// orchestrator.ts
|
||||||
|
import { analyzeInput } from "./stage1-inputAnalysis";
|
||||||
|
import { determineSemanticMapping } from "./stage2-semanticMapping";
|
||||||
|
import { generateStandardForm } from "./stage3-standardForm";
|
||||||
|
import { generateEntries } from "./stage4-entriesGeneration";
|
||||||
|
|
||||||
|
export async function orchestrateDictionaryLookup(text: string, queryLang: string, defLang: string) {
|
||||||
|
// 阶段 1:输入分析
|
||||||
|
const analysis = await analyzeInput(text, queryLang);
|
||||||
|
|
||||||
|
// 阶段 2:语义映射
|
||||||
|
const mapping = await determineSemanticMapping(analysis);
|
||||||
|
|
||||||
|
// 阶段 3:标准形式
|
||||||
|
const standard = await generateStandardForm(mapping, defLang);
|
||||||
|
|
||||||
|
// 阶段 4:词条生成
|
||||||
|
const entries = await generateEntries(standard);
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## AI 响应解析
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||||
|
|
||||||
|
// AI 返回的 JSON 可能包含 markdown 代码块,用此函数解析
|
||||||
|
const result = parseAIGeneratedJSON<ExpectedType>(aiResponseString);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新管道
|
||||||
|
|
||||||
|
1. 创建目录 `src/lib/bigmodel/{name}/`
|
||||||
|
2. 创建 `types.ts` 定义阶段间传递的接口
|
||||||
|
3. 创建 `stage{n}-{name}.ts` 实现各阶段
|
||||||
|
4. 创建 `orchestrator.ts` 编排调用顺序
|
||||||
|
5. 在 Service 层调用 orchestrator
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- `@/lib/bigmodel/zhipu.ts` — 智谱 AI 客户端
|
||||||
|
- `@/lib/bigmodel/tts.ts` — 阿里云千问 TTS
|
||||||
|
- `@/utils/json` — AI JSON 响应解析
|
||||||
88
src/modules/AGENTS.md
Normal file
88
src/modules/AGENTS.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 模块层架构指南
|
||||||
|
|
||||||
|
**生成时间:** 2026-03-08
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
业务模块采用 Action-Service-Repository 三层架构,每模块 6 个文件。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
{name}/
|
||||||
|
├── {name}-action.ts # Server Actions
|
||||||
|
├── {name}-action-dto.ts # Zod 验证 + 类型
|
||||||
|
├── {name}-service.ts # 业务逻辑
|
||||||
|
├── {name}-service-dto.ts # Service 类型
|
||||||
|
├── {name}-repository.ts # 数据库操作
|
||||||
|
└── {name}-repository-dto.ts # Repository 类型
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件职责
|
||||||
|
|
||||||
|
| 层级 | 文件 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| Action | `*-action.ts` | 表单处理、Zod 验证、重定向、返回 ActionOutput |
|
||||||
|
| Service | `*-service.ts` | 业务逻辑、跨模块调用、调用 Repository |
|
||||||
|
| Repository | `*-repository.ts` | Prisma 查询、纯数据访问 |
|
||||||
|
|
||||||
|
## 命名约定
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 类型命名
|
||||||
|
type ActionInputSignUp = { ... };
|
||||||
|
type ActionOutputSignUp = { ... };
|
||||||
|
type ServiceInputSignUp = { ... };
|
||||||
|
type RepoOutputUser = { ... };
|
||||||
|
|
||||||
|
// 函数命名
|
||||||
|
async function actionSignUp(input: ActionInputSignUp): Promise<ActionOutputSignUp>
|
||||||
|
async function serviceSignUp(input: ServiceInputSignUp): Promise<ServiceOutputSignUp>
|
||||||
|
async function repoFindUserByUsername(username: string): Promise<RepoOutputUser | null>
|
||||||
|
|
||||||
|
// 验证函数
|
||||||
|
function validateActionInputSignUp(input: unknown): ActionInputSignUp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action 模板
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { validate } from "@/utils/validate";
|
||||||
|
import { ActionInputSignUp, ActionOutputSignUp, schemaActionInputSignUp } from "./auth-action-dto";
|
||||||
|
import { serviceSignUp } from "./auth-service";
|
||||||
|
|
||||||
|
export async function actionSignUp(input: unknown): Promise<ActionOutputSignUp> {
|
||||||
|
const validated = validate(schemaActionInputSignUp, input);
|
||||||
|
if (!validated.success) return { success: false, message: validated.message };
|
||||||
|
|
||||||
|
return serviceSignUp(validated.data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 受保护操作
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 Action 中检查会话
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return { success: false, message: "未授权" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变更前检查所有权
|
||||||
|
const isOwner = await checkOwnership(resourceId, session.user.id);
|
||||||
|
if (!isOwner) {
|
||||||
|
return { success: false, message: "无权限" };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有 action 文件必须有 `"use server"` 指令
|
||||||
|
- DTO 文件只放 Zod schema 和类型定义
|
||||||
|
- Repository 层不处理业务逻辑,只做数据访问
|
||||||
|
- 跨模块调用通过 Service 层
|
||||||
Reference in New Issue
Block a user