fix: 添加skills

This commit is contained in:
imeepos
2026-01-29 15:13:27 +08:00
parent b716198d4b
commit 6279752f23
16 changed files with 5462 additions and 27 deletions

View File

@@ -0,0 +1,340 @@
---
name: repo-core-di
description: "@repo/core 依赖注入框架使用指南。当需要创建注入器、注册提供者、使用装饰器(@Injectable, @Inject, @Optional等)、处理生命周期(OnInit, OnDestroy)、或解决循环依赖时使用此技能。适用于:(1) 创建和配置注入器层次结构 (2) 注册各类提供者(Value/Class/Factory/Lazy) (3) 使用参数装饰器控制注入行为 (4) 实现服务生命周期管理 (5) 使用 InjectionToken 和 ForwardRef"
---
# @repo/core 依赖注入框架
## 核心概念
@repo/core 提供企业级依赖注入框架,支持层次化注入器、多种提供者类型、生命周期管理。
### 注入器层次结构
```
Root (根注入器)
└── Platform (平台注入器)
└── Application (应用注入器)
└── Feature (特性注入器)
```
## 快速开始
### 创建注入器
```typescript
import { EnvironmentInjector, Injectable, InjectionToken } from '@repo/core'
// 定义类型安全的令牌
const API_URL = new InjectionToken<string>('API_URL')
// 创建根注入器
const rootInjector = EnvironmentInjector.createRootInjector([
{ provide: API_URL, useValue: 'https://api.example.com' }
])
// 创建应用注入器(继承根注入器)
const appInjector = EnvironmentInjector.createApplicationInjector([
UserService,
{ provide: LoggerService, useClass: LoggerService }
])
// 初始化(执行所有 @OnInit
await appInjector.init()
// 获取服务
const userService = appInjector.get(UserService)
```
### 使用 @Injectable 装饰器
```typescript
import { Injectable } from '@repo/core'
// providedIn: 'root' - 单例,自动注册到根注入器
@Injectable({ providedIn: 'root' })
class ConfigService {
apiUrl = 'https://api.example.com'
}
// providedIn: 'auto' - 不自动注册到根注入器,可在任何注入器中被按需解析
// 推荐用于请求级服务
@Injectable({ providedIn: 'auto' })
class RequestScopedService {
// 每次请求创建新实例
}
// 使用工厂函数
@Injectable({
providedIn: 'root',
useFactory: (config: ConfigService) => new ApiClient(config.apiUrl),
deps: [ConfigService]
})
class ApiClient {
constructor(public baseUrl: string) {}
}
```
## 提供者类型
### 值提供者 (ValueProvider)
```typescript
const API_URL = new InjectionToken<string>('API_URL')
const CONFIG_TOKEN = new InjectionToken<AppConfig>('CONFIG_TOKEN')
{ provide: API_URL, useValue: 'https://api.example.com' }
{ provide: CONFIG_TOKEN, useValue: { debug: true, timeout: 5000 } }
```
### 类提供者 (ClassProvider)
```typescript
{ provide: UserService, useClass: UserService }
{ provide: Logger, useClass: ConsoleLogger } // 接口映射到实现
```
### 工厂提供者 (FactoryProvider)
```typescript
{
provide: DatabaseConnection,
useFactory: (config: ConfigService) => {
return new DatabaseConnection(config.dbUrl)
},
deps: [ConfigService]
}
```
### 别名提供者 (ExistingProvider)
```typescript
const LOGGER_TOKEN = new InjectionToken<Logger>('Logger')
{ provide: LOGGER_TOKEN, useExisting: LoggerService }
```
### 延迟提供者 (LazyProvider)
```typescript
const LAZY_CONFIG = new InjectionToken<AppConfig>('LAZY_CONFIG')
// 延迟类 - 首次访问时才实例化
{ provide: HeavyService, useLazyClass: HeavyService }
// 延迟工厂
{ provide: LAZY_CONFIG, useLazyFactory: () => loadConfig(), deps: [] }
```
### 多值提供者 (Multi Provider)
```typescript
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
// 获取时返回数组
const interceptors = injector.get(HTTP_INTERCEPTORS) // [AuthInterceptor, LoggingInterceptor]
```
## 参数装饰器
### @Inject - 显式指定注入令牌
```typescript
const API_URL = new InjectionToken<string>('API_URL')
class UserController {
constructor(
@Inject(UserService) private userService: UserService,
@Inject(API_URL) private apiUrl: string
) {}
}
```
### @Optional - 可选注入
```typescript
class NotificationService {
constructor(
@Optional(EmailService) private email?: EmailService // 不存在时为 null
) {}
}
```
### @Self - 仅在当前注入器查找
```typescript
class ChildComponent {
constructor(
@Self(LocalService) private local: LocalService // 不查找父注入器
) {}
}
```
### @SkipSelf - 跳过当前注入器
```typescript
class ChildService {
constructor(
@SkipSelf(ParentService) private parent: ParentService // 从父注入器开始查找
) {}
}
```
### @Host - 在宿主注入器查找
```typescript
class DirectiveService {
constructor(
@Host(HostService) private host: HostService
) {}
}
```
## 生命周期管理
### @OnInit 装饰器
```typescript
import { Injectable, OnInit } from '@repo/core'
@Injectable({ providedIn: 'root' })
class DatabaseService implements OnInit {
private connection: Connection
@OnInit()
async onInit() {
this.connection = await this.connect()
console.log('Database connected')
}
private async connect() {
// 连接逻辑
}
}
```
### OnDestroy 接口
```typescript
import { Injectable, OnDestroy } from '@repo/core'
@Injectable({ providedIn: 'root' })
class CacheService implements OnDestroy {
async onDestroy() {
await this.flush()
console.log('Cache flushed')
}
}
// 销毁注入器时自动调用
await injector.destroy()
```
### APP_INITIALIZER - 应用初始化器
```typescript
import { APP_INITIALIZER, InjectionToken } from '@repo/core'
const appInjector = EnvironmentInjector.createApplicationInjector([
{
provide: APP_INITIALIZER,
useValue: {
provide: new InjectionToken('DB_INIT'),
deps: [ConfigService],
init: async () => {
await initDatabase()
}
},
multi: true
},
{
provide: APP_INITIALIZER,
useValue: {
provide: new InjectionToken('CACHE_INIT'),
deps: [new InjectionToken('DB_INIT')], // 依赖 DB_INIT
init: async () => {
await initCache()
}
},
multi: true
}
])
// 按依赖顺序执行初始化器
await appInjector.init()
```
## InjectionToken
```typescript
import { InjectionToken } from '@repo/core'
// 创建类型安全的令牌
const API_URL = new InjectionToken<string>('API_URL')
const CONFIG = new InjectionToken<AppConfig>('CONFIG')
const LOGGER_LEVEL = new InjectionToken<LoggerLevel>('LOGGER_LEVEL')
// 注册
const injector = EnvironmentInjector.createRootInjector([
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: CONFIG, useValue: { debug: true } }
])
// 获取(类型安全)
const url: string = injector.get(API_URL)
const config: AppConfig = injector.get(CONFIG)
```
## ForwardRef - 解决循环依赖
```typescript
import { Injectable, Inject, forwardRef } from '@repo/core'
@Injectable({ providedIn: 'root' })
class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB
) {}
}
@Injectable({ providedIn: 'root' })
class ServiceB {
constructor(
@Inject(forwardRef(() => ServiceA)) private serviceA: ServiceA
) {}
}
```
## 便捷工厂函数
```typescript
import {
createRootInjector,
createPlatformInjector,
createApplicationInjector,
createFeatureInjector
} from '@repo/core'
// 简化创建
const root = createRootInjector([...providers])
const platform = createPlatformInjector([...providers])
const app = createApplicationInjector([...providers])
const feature = createFeatureInjector([...providers], parentInjector)
```
## 最佳实践
1. **使用 providedIn: 'root'** - 大多数服务应该是单例
2. **避免循环依赖** - 使用 ForwardRef 仅作为最后手段
3. **使用 InjectionToken** - 为非类类型创建类型安全令牌
4. **实现 OnDestroy** - 清理资源(连接、订阅等)
5. **使用 APP_INITIALIZER** - 管理复杂的初始化顺序
6. **延迟加载** - 对重型服务使用 useLazyClass/useLazyFactory
## 参考文档
- 详细 API 参考:见 [references/api.md](references/api.md)
- 提供者类型详解:见 [references/providers.md](references/providers.md)
- 请求级注入器:见 [references/request-scoped-injector.md](references/request-scoped-injector.md) - HTTP 请求隔离、CURRENT_USER 注入、Controller 生命周期
- 错误处理系统:见 [references/errors.md](references/errors.md) - 预定义错误类、ErrorFactory、ErrorSerializer

View File

@@ -0,0 +1,228 @@
# @repo/core 依赖注入 API 参考
## EnvironmentInjector
核心注入器类,管理依赖的注册和解析。
### 静态方法
```typescript
// 创建根注入器 - 最顶层,无父注入器
static createRootInjector(providers: Provider[]): EnvironmentInjector
// 创建平台注入器 - 继承根注入器
static createPlatformInjector(providers: Provider[]): EnvironmentInjector
// 创建应用注入器 - 继承平台注入器
static createApplicationInjector(providers: Provider[]): EnvironmentInjector
// 创建特性注入器 - 需要指定父注入器
static createFeatureInjector(
providers: Provider[],
parent: Injector
): EnvironmentInjector
// 创建带自动提供者解析的注入器
static createWithAutoProviders(
providers: Provider[],
parent: Injector | null,
scope: InjectorScope
): EnvironmentInjector
```
### 实例方法
```typescript
// 获取服务实例
get<T>(token: InjectionTokenType<T>, defaultValue?: T): T
// 动态注册提供者
set(providers: Provider[]): void
// 初始化 - 执行所有 @OnInit 和 APP_INITIALIZER
async init(): Promise<void>
// 销毁 - 执行所有 OnDestroy
async destroy(): Promise<void>
```
## InjectionToken<T>
类型安全的注入令牌。
```typescript
class InjectionToken<T> {
constructor(description: string)
toString(): string
}
// 使用示例
const API_URL = new InjectionToken<string>('API_URL')
const CONFIG = new InjectionToken<AppConfig>('CONFIG')
```
## HostAttributeToken<T>
宿主属性令牌,用于获取宿主元素属性。
```typescript
class HostAttributeToken<T> {
constructor(attributeName: string, defaultValue?: T)
}
// 使用示例
const dataAttr = new HostAttributeToken<string>('data-value', 'default')
```
## 装饰器
### @Injectable(options?)
标记类为可注入服务。
```typescript
interface InjectableOptions {
providedIn?: 'root' | 'platform' | 'application' | 'feature' | 'auto' | null
useFactory?: (...args: any[]) => any
deps?: any[]
}
@Injectable({ providedIn: 'root' })
class MyService {}
```
### @Inject(token)
显式指定注入令牌。
```typescript
constructor(@Inject(TOKEN) value: string) {}
```
### @Optional(token)
可选注入,不存在时返回 null。
```typescript
constructor(@Optional(OptionalService) service?: OptionalService) {}
```
### @Self(token)
仅在当前注入器查找。
```typescript
constructor(@Self(LocalService) service: LocalService) {}
```
### @SkipSelf(token)
跳过当前注入器,从父注入器开始查找。
```typescript
constructor(@SkipSelf(ParentService) service: ParentService) {}
```
### @Host(token)
在宿主注入器中查找。
```typescript
constructor(@Host(HostService) service: HostService) {}
```
### @OnInit()
标记初始化方法。
```typescript
@OnInit()
async onInit() {
await this.initialize()
}
```
## 生命周期接口
### OnInit
```typescript
interface OnInit {
onInit(): Promise<void>
}
```
### OnDestroy
```typescript
interface OnDestroy {
onDestroy(): Promise<void> | void
}
```
## ForwardRef
解决循环依赖。
```typescript
// 创建前向引用
function forwardRef<T>(fn: () => T): ForwardRef<T>
// 检查是否为前向引用
function isForwardRef(ref: any): boolean
// 解析前向引用
function resolveForwardRef<T>(ref: T | ForwardRef<T>): T
```
## 内部标志位
用于性能优化的位标志。
```typescript
enum InternalInjectFlags {
Default = 0,
Optional = 1 << 0, // 1
SkipSelf = 1 << 1, // 2
Self = 1 << 2, // 4
Host = 1 << 3, // 8
}
// 工具函数
function combineInjectFlags(...flags: InternalInjectFlags[]): number
function hasFlag(flags: number, flag: InternalInjectFlags): boolean
function convertInjectOptionsToFlags(options: InjectOptions): number
```
## 类型定义
### InjectionTokenType
```typescript
type InjectionTokenType<T> =
| InjectionToken<T>
| HostAttributeToken<T>
| Type<T>
| AbstractType<T>
| StringToken<T>
| SymbolToken<T>
| Function
| ForwardRef<InjectionTokenType<T>>
```
### InjectOptions
```typescript
interface InjectOptions {
optional?: boolean
skipSelf?: boolean
self?: boolean
host?: boolean
}
```
### InjectorScope
```typescript
type InjectorScope = 'root' | 'platform' | 'application' | 'feature' | 'auto'
```

View File

@@ -0,0 +1,371 @@
---
name: repo-core-controller
description: "@repo/core HTTP 控制器装饰器使用指南。当需要创建 REST API 端点、处理 HTTP 请求参数、实现权限控制、或使用 SSE 流式响应时使用此技能。适用于:(1) 使用 @Controller 和 HTTP 方法装饰器(@Get, @Post, @Put, @Delete, @Patch) (2) 使用参数装饰器(@Body, @Query, @Param, @Header) (3) 实现权限和会话验证(@RequirePermissions, @RequireSession) (4) 创建 SSE 端点(@Sse) (5) 添加 API 文档(@ApiDescription)"
---
# @repo/core HTTP 控制器
## 核心概念
@repo/core 提供声明式 HTTP 控制器装饰器,支持路由定义、参数绑定、权限控制和 SSE 流式响应。
## 快速开始
### 创建控制器
```typescript
import { Controller, Get, Post, Body, Param, Injectable } from '@repo/core'
import { z } from 'zod'
@Controller('/api/users')
class UserController {
constructor(private userService: UserService) {}
@Get('/list')
async getUsers() {
return this.userService.findAll()
}
@Get('/:id')
async getUser(@Param('id') id: string) {
return this.userService.findById(id)
}
@Post('/create', z.object({
name: z.string(),
email: z.string().email()
}))
async createUser(@Body() data: { name: string; email: string }) {
return this.userService.create(data)
}
}
```
## HTTP 方法装饰器
### @Get(path)
```typescript
@Get('/users')
async getUsers() { }
@Get('/users/:id')
async getUser(@Param('id') id: string) { }
@Get('/search')
async search(@Query('q') query: string) { }
```
### @Post(path, schema?, contentType?)
```typescript
// 基本用法
@Post('/users')
async createUser(@Body() data: CreateUserDto) { }
// 带 Zod 验证
@Post('/users', z.object({
name: z.string().min(1),
email: z.string().email()
}))
async createUser(@Body() data: { name: string; email: string }) { }
// 指定 Content-Type
@Post('/upload', schema, 'multipart/form-data')
async upload(@Body() data: FormData) { }
```
### @Put(path, schema?)
```typescript
@Put('/users/:id', z.object({
name: z.string().optional(),
email: z.string().email().optional()
}))
async updateUser(
@Param('id') id: string,
@Body() data: Partial<User>
) { }
```
### @Delete(path)
```typescript
@Delete('/users/:id')
async deleteUser(@Param('id') id: string) { }
```
### @Patch(path, schema?)
```typescript
@Patch('/users/:id/status', z.object({
status: z.enum(['active', 'inactive'])
}))
async updateStatus(
@Param('id') id: string,
@Body('status') status: string
) { }
```
### @Sse(path) - Server-Sent Events
```typescript
@Sse('/events')
async *streamEvents() {
while (true) {
yield { data: { timestamp: Date.now() } }
await sleep(1000)
}
}
@Sse('/notifications/:userId')
async *userNotifications(@Param('userId') userId: string) {
const stream = this.notificationService.subscribe(userId)
for await (const notification of stream) {
yield { event: 'notification', data: notification }
}
}
```
## 参数装饰器
### @Body(key?, schema?)
```typescript
// 获取整个请求体
@Post('/users')
async create(@Body() data: CreateUserDto) { }
// 获取特定字段
@Post('/users')
async create(@Body('name') name: string) { }
// 带验证
@Post('/users')
async create(@Body('email', z.string().email()) email: string) { }
```
### @Query(key?, schema?)
```typescript
// 获取查询参数
@Get('/search')
async search(@Query('q') query: string) { }
// 带默认值和验证
@Get('/list')
async list(
@Query('page', z.coerce.number().default(1)) page: number,
@Query('limit', z.coerce.number().default(10)) limit: number
) { }
```
### @Param(key)
```typescript
@Get('/users/:id')
async getUser(@Param('id') id: string) { }
@Get('/projects/:projectId/tasks/:taskId')
async getTask(
@Param('projectId') projectId: string,
@Param('taskId') taskId: string
) { }
```
### @Header(key)
```typescript
@Get('/protected')
async protected(@Header('authorization') auth: string) { }
@Post('/webhook')
async webhook(
@Header('x-signature') signature: string,
@Body() payload: any
) { }
```
### @Session()
```typescript
@Get('/profile')
async getProfile(@Session() session: UserSession) {
return this.userService.findById(session.userId)
}
```
### @Req() / @Res()
```typescript
@Get('/download')
async download(@Req() req: Request, @Res() res: Response) {
// 直接访问原始请求/响应对象
res.setHeader('Content-Disposition', 'attachment')
return fileStream
}
```
### @Headers()
```typescript
@Post('/webhook')
async webhook(@Headers() headers: Record<string, string>) {
const signature = headers['x-signature']
// ...
}
```
## 权限控制
### @RequireSession()
```typescript
@Controller('/api/account')
class AccountController {
@Get('/profile')
@RequireSession()
async getProfile(@Session() session: UserSession) {
return this.userService.findById(session.userId)
}
}
```
### @RequirePermissions(permissions, mode?)
```typescript
// 需要单个权限
@Delete('/users/:id')
@RequirePermissions({ user: ['delete'] })
async deleteUser(@Param('id') id: string) { }
// 需要多个权限 (AND 模式 - 全部满足)
@Put('/projects/:id')
@RequirePermissions({ project: ['read', 'update'] }, 'AND')
async updateProject(@Param('id') id: string) { }
// OR 模式 - 满足任一即可
@Get('/reports')
@RequirePermissions({ report: ['read'], admin: ['access'] }, 'OR')
async getReports() { }
```
### 权限资源类型
```typescript
// 可用的权限资源和操作
const permissions = {
activity: ['create', 'read', 'update', 'delete'],
project: ['create', 'read', 'list', 'update', 'delete', 'run'],
template: ['create', 'read', 'list', 'update', 'delete', 'run'],
user: ['read', 'update', 'delete'],
admin: ['access', 'manage'],
report: ['read', 'export']
}
```
## API 文档
### @ApiDescription(description, tags?)
```typescript
@Controller('/api/users')
class UserController {
@Get('/list')
@ApiDescription('获取用户列表', ['Users', 'Admin'])
async getUsers() { }
@Post('/create')
@ApiDescription('创建新用户', ['Users'])
async createUser(@Body() data: CreateUserDto) { }
}
```
## 完整示例
```typescript
import {
Controller, Get, Post, Put, Delete, Sse,
Body, Query, Param, Header, Session,
RequireSession, RequirePermissions, ApiDescription,
Injectable
} from '@repo/core'
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['user', 'admin']).default('user')
})
const UpdateUserSchema = CreateUserSchema.partial()
@Controller('/api/users')
class UserController {
constructor(
private userService: UserService,
private notificationService: NotificationService
) {}
@Get('/list')
@RequireSession()
@ApiDescription('获取用户列表', ['Users'])
async list(
@Query('page', z.coerce.number().default(1)) page: number,
@Query('limit', z.coerce.number().default(20)) limit: number,
@Query('search') search?: string
) {
return this.userService.findAll({ page, limit, search })
}
@Get('/:id')
@RequireSession()
@ApiDescription('获取用户详情', ['Users'])
async getById(@Param('id') id: string) {
return this.userService.findById(id)
}
@Post('/create', CreateUserSchema)
@RequirePermissions({ user: ['create'] })
@ApiDescription('创建用户', ['Users', 'Admin'])
async create(@Body() data: z.infer<typeof CreateUserSchema>) {
return this.userService.create(data)
}
@Put('/:id', UpdateUserSchema)
@RequirePermissions({ user: ['update'] })
@ApiDescription('更新用户', ['Users', 'Admin'])
async update(
@Param('id') id: string,
@Body() data: z.infer<typeof UpdateUserSchema>
) {
return this.userService.update(id, data)
}
@Delete('/:id')
@RequirePermissions({ user: ['delete'] })
@ApiDescription('删除用户', ['Users', 'Admin'])
async delete(@Param('id') id: string) {
return this.userService.delete(id)
}
@Sse('/notifications')
@RequireSession()
@ApiDescription('用户通知流', ['Users', 'Realtime'])
async *notifications(@Session() session: UserSession) {
const stream = this.notificationService.subscribe(session.userId)
for await (const notification of stream) {
yield { event: 'notification', data: notification }
}
}
}
```
## 最佳实践
1. **使用 Zod 验证** - 始终为 POST/PUT/PATCH 请求定义 schema
2. **分层权限** - 使用 @RequireSession@RequirePermissions 组合
3. **API 文档** - 为所有端点添加 @ApiDescription
4. **参数验证** - 使用 @Query@Body 的 schema 参数
5. **SSE 清理** - 确保 SSE 生成器正确处理客户端断开
6. **控制器单一职责** - 每个控制器处理一个资源类型

View File

@@ -0,0 +1,450 @@
---
name: repo-core-errors
description: "@repo/core 错误处理系统使用指南。当需要创建自定义错误、处理 HTTP 错误响应、序列化错误信息、或使用错误工厂时使用此技能。适用于:(1) 使用预定义错误类(ValidationError, NotFoundError, UnauthorizedError等) (2) 创建业务逻辑错误 (3) 使用 ErrorFactory 快速创建错误 (4) 序列化错误用于 API 响应 (5) 实现全局错误处理"
---
# @repo/core 错误处理系统
## 核心概念
@repo/core 提供完整的错误处理系统,包括预定义错误类、错误工厂、错误序列化和全局错误处理器。
## 错误类层次结构
```
Error (原生)
└── AppError (基础应用错误)
├── NoRetryError (不可重试错误)
├── HTTP 错误
│ ├── ValidationError (400)
│ ├── UnauthorizedError (401)
│ ├── ForbiddenError (403)
│ ├── NotFoundError (404)
│ ├── ConflictError (409)
│ ├── UnprocessableEntityError (422)
│ ├── RateLimitError (429)
│ ├── InternalError (500)
│ └── ServiceUnavailableError (503)
└── 业务错误
├── DatabaseError
├── ExternalServiceError
├── ConfigurationError
└── BusinessRuleError
```
## 快速开始
### 抛出错误
```typescript
import {
ValidationError,
NotFoundError,
UnauthorizedError,
errorFactory // 注意:使用小写的实例
} from '@repo/core'
// 直接使用错误类
throw new NotFoundError('User', '123') // "User with identifier '123' not found"
// 使用错误工厂实例
throw errorFactory.notFound('User', '123')
throw errorFactory.validation('Invalid email format', { email: 'Invalid format' })
```
### 处理错误
```typescript
import { ErrorSerializer, ErrorHandler } from '@repo/core'
try {
await userService.findById(id)
} catch (error) {
// 序列化错误用于 API 响应
const serialized = ErrorSerializer.serialize(error)
return Response.json(serialized, { status: serialized.statusCode })
}
```
## HTTP 错误类
### ValidationError (400)
```typescript
// 基本用法
throw new ValidationError('Invalid input')
// 带字段错误信息
throw new ValidationError('Validation failed', {
email: 'Invalid email format',
age: 'Must be a positive number'
})
// 使用工厂
throw errorFactory.validation('Email is required', { email: 'Required field' })
```
### UnauthorizedError (401)
```typescript
// 未认证(使用默认消息)
throw new UnauthorizedError() // "Authentication required"
// 自定义消息
throw new UnauthorizedError('Token expired')
// 使用工厂
throw errorFactory.unauthorized('Invalid credentials')
```
### ForbiddenError (403)
```typescript
// 无权限(使用默认消息)
throw new ForbiddenError() // "Access forbidden"
// 自定义消息
throw new ForbiddenError('Cannot access this resource')
// 使用工厂
throw errorFactory.forbidden('Insufficient permissions')
```
### NotFoundError (404)
```typescript
// 基本用法 - 只有资源类型
throw new NotFoundError('User') // "User not found"
// 带资源标识符
throw new NotFoundError('User', '123') // "User with identifier '123' not found"
// 使用工厂 - 推荐
throw errorFactory.notFound('User', '123')
```
### ConflictError (409)
```typescript
// 资源冲突
throw new ConflictError('Email already exists')
// 带冲突详情
throw new ConflictError('Duplicate entry', {
field: 'email',
value: 'user@example.com'
})
// 使用工厂
throw errorFactory.conflict('Email already registered', { email: 'user@example.com' })
```
### UnprocessableEntityError (422)
```typescript
// 业务规则验证失败
throw new UnprocessableEntityError('Cannot process request', {
reason: 'Insufficient balance'
})
// 使用工厂
throw errorFactory.unprocessable('Cannot process request', { reason: 'Insufficient balance' })
```
### RateLimitError (429)
```typescript
// 速率限制(使用默认消息)
throw new RateLimitError() // "Rate limit exceeded"
// 自定义消息和重试时间
throw new RateLimitError('Too many requests', 60) // 60秒后重试
// 使用工厂
throw errorFactory.rateLimit('Too many requests', 60)
```
### InternalError (500)
```typescript
// 内部错误
throw new InternalError('Unexpected error occurred')
// 包装原始错误
throw new InternalError('Database operation failed', originalError)
// 使用工厂
throw errorFactory.internal('Something went wrong', originalError)
```
### ServiceUnavailableError (503)
```typescript
// 服务不可用(使用默认消息)
throw new ServiceUnavailableError() // "Service temporarily unavailable"
// 自定义消息和重试时间
throw new ServiceUnavailableError('Service temporarily unavailable', 300)
// 使用工厂
throw errorFactory.unavailable('Service down for maintenance', 300)
```
## 业务错误类
### DatabaseError
```typescript
import { DatabaseError } from '@repo/core'
// 基本用法
throw new DatabaseError('Connection failed')
// 带操作信息
throw new DatabaseError('Query timeout', 'SELECT')
// 带原始错误
throw new DatabaseError('Connection failed', 'connect', originalError)
// 使用工厂
throw errorFactory.database('Query timeout', 'SELECT', originalError)
```
### ExternalServiceError
```typescript
import { ExternalServiceError } from '@repo/core'
// 基本用法
throw new ExternalServiceError('stripe', 'Payment failed')
// 带原始错误
throw new ExternalServiceError('stripe', 'Payment gateway error', gatewayError)
// 使用工厂
throw errorFactory.externalService('Stripe', 'Payment failed', originalError)
```
### ConfigurationError
```typescript
import { ConfigurationError } from '@repo/core'
// 基本用法
throw new ConfigurationError('Missing required configuration')
// 带配置键
throw new ConfigurationError('Missing required configuration', 'DATABASE_URL')
// 使用工厂
throw errorFactory.configuration('DATABASE_URL is not set', 'DATABASE_URL')
```
### BusinessRuleError
```typescript
import { BusinessRuleError } from '@repo/core'
// 基本用法
throw new BusinessRuleError('Cannot delete active subscription')
// 带规则标识
throw new BusinessRuleError('Cannot delete active subscription', 'SUBSCRIPTION_ACTIVE')
// 带额外元数据
throw new BusinessRuleError('Insufficient balance', 'BALANCE_CHECK', {
required: 100,
available: 50
})
// 使用工厂
throw errorFactory.businessRule('Insufficient balance for withdrawal', 'BALANCE_CHECK', { amount: 100 })
```
## errorFactory
错误工厂是一个 `@Injectable()` 服务实例,提供便捷的错误创建方法。
```typescript
import { errorFactory } from '@repo/core'
// HTTP 错误
errorFactory.validation(message, fields?)
errorFactory.unauthorized(message?)
errorFactory.forbidden(message?)
errorFactory.notFound(resource, identifier?)
errorFactory.conflict(message, meta?)
errorFactory.unprocessable(message, meta?)
errorFactory.rateLimit(message?, retryAfter?)
errorFactory.internal(message, originalError?)
errorFactory.unavailable(message?, retryAfter?)
// 业务错误
errorFactory.database(message, operation?, originalError?)
errorFactory.externalService(service, message, originalError?)
errorFactory.configuration(message, configKey?)
errorFactory.businessRule(message, rule?, meta?)
```
> **注意**: `errorFactory` 是预创建的实例(小写),可以直接导入使用。
> 如果需要依赖注入,可以注入 `ErrorFactory` 类。
## ErrorSerializer
错误序列化器用于将错误转换为 API 响应格式。
```typescript
import { ErrorSerializer } from '@repo/core'
// 序列化错误
const serialized = ErrorSerializer.serialize(error)
// {
// message: 'User not found',
// name: 'NotFoundError',
// type: 'NOT_FOUND',
// statusCode: 404,
// response: { userId: '123' }
// }
// 包含堆栈信息(开发环境)
const withStack = ErrorSerializer.serialize(error, true)
// 提取最深层错误
const deepest = ErrorSerializer.extractDeepestError(wrappedError)
// 获取完整错误描述
const description = ErrorSerializer.getFullDescription(error)
```
### SerializedError 结构
```typescript
interface SerializedError {
message: string
name: string
type?: string
statusCode?: number
response?: any
stack?: string
cause?: SerializedError
originalError?: any
}
```
## ErrorHandler
全局错误处理器服务。
```typescript
import { ErrorHandler, Injectable } from '@repo/core'
@Injectable({ providedIn: 'root' })
class MyErrorHandler extends ErrorHandler {
handle(error: unknown) {
// 记录错误
console.error('Error occurred:', error)
// 发送到监控服务
this.reportToMonitoring(error)
// 调用父类处理
super.handle(error)
}
private reportToMonitoring(error: unknown) {
// Sentry, DataDog 等
}
}
// 注册自定义处理器
const injector = EnvironmentInjector.createRootInjector([
{ provide: ErrorHandler, useClass: MyErrorHandler }
])
```
## 完整示例
```typescript
import {
Controller, Get, Post, Body, Param,
Injectable,
NotFoundError, ValidationError, ForbiddenError,
errorFactory, ErrorSerializer
} from '@repo/core'
@Controller('/api/orders')
class OrderController {
constructor(
private orderService: OrderService,
private userService: UserService
) {}
@Get('/:id')
async getOrder(@Param('id') id: string) {
const order = await this.orderService.findById(id)
if (!order) {
throw errorFactory.notFound('Order', id)
}
return order
}
@Post('/create')
async createOrder(@Body() data: CreateOrderDto) {
// 验证
if (!data.items?.length) {
throw new ValidationError('Order must have at least one item', {
items: 'At least one item required'
})
}
// 业务规则检查
const user = await this.userService.findById(data.userId)
if (!user) {
throw errorFactory.notFound('User', data.userId)
}
if (user.status === 'suspended') {
throw new ForbiddenError('Suspended users cannot create orders')
}
// 检查库存
for (const item of data.items) {
const available = await this.checkInventory(item.productId)
if (available < item.quantity) {
throw errorFactory.businessRule(
`Insufficient inventory for product ${item.productId}`,
'INVENTORY_CHECK'
)
}
}
return this.orderService.create(data)
}
}
// 全局错误处理中间件
async function errorMiddleware(ctx: Context, next: Next) {
try {
await next()
} catch (error) {
const serialized = ErrorSerializer.serialize(
error,
process.env.NODE_ENV === 'development'
)
ctx.status = serialized.statusCode || 500
ctx.body = serialized
}
}
```
## 最佳实践
1. **使用具体错误类** - 不要只用通用 Error使用语义化错误类
2. **提供上下文** - 错误消息应包含足够的调试信息
3. **使用 errorFactory** - 保持错误创建的一致性
4. **序列化响应** - 使用 ErrorSerializer 确保 API 响应格式统一
5. **区分环境** - 生产环境不暴露堆栈信息
6. **错误链** - 使用 originalError 参数保留原始错误上下文
7. **自定义处理器** - 集成监控和日志系统

View File

@@ -0,0 +1,291 @@
# @repo/core 提供者类型详解
## 提供者类型概览
| 类型 | 用途 | 示例 |
|------|------|------|
| ValueProvider | 提供静态值 | 配置、常量 |
| ClassProvider | 提供类实例 | 服务类 |
| FactoryProvider | 通过工厂函数创建 | 需要动态配置的服务 |
| ExistingProvider | 别名映射 | 接口到实现的映射 |
| ConstructorProvider | 类本身作为令牌 | 简化注册 |
| LazyClassProvider | 延迟实例化类 | 重型服务 |
| LazyFactoryProvider | 延迟调用工厂 | 按需加载配置 |
## ValueProvider
直接提供值,不进行实例化。
```typescript
interface ValueProvider<T> {
provide: InjectionTokenType<T>
useValue: T
multi?: boolean
}
// 示例 - 始终使用 InjectionToken 确保类型安全
const API_URL = new InjectionToken<string>('API_URL')
const CONFIG_TOKEN = new InjectionToken<AppConfig>('CONFIG_TOKEN')
const VERSION = new InjectionToken<string>('VERSION')
{ provide: API_URL, useValue: 'https://api.example.com' }
{ provide: CONFIG_TOKEN, useValue: { debug: true, timeout: 5000 } }
{ provide: VERSION, useValue: '1.0.0' }
```
**适用场景**
- 配置值
- 常量
- 已创建的实例
- 简单对象
## ClassProvider
通过构造函数创建实例。
```typescript
interface ClassProvider<T> {
provide: InjectionTokenType<T>
useClass: Type<T>
multi?: boolean
}
// 示例
{ provide: UserService, useClass: UserService }
{ provide: Logger, useClass: ConsoleLogger }
{ provide: IAuthService, useClass: JwtAuthService }
```
**适用场景**
- 服务类
- 接口到实现的映射
- 替换默认实现
## FactoryProvider
通过工厂函数创建实例。
```typescript
interface FactoryProvider<T> {
provide: InjectionTokenType<T>
useFactory: (...deps: any[]) => T
deps?: InjectionTokenType<any>[]
multi?: boolean
}
// 示例
{
provide: DatabaseConnection,
useFactory: (config: ConfigService) => {
return new DatabaseConnection({
host: config.dbHost,
port: config.dbPort,
database: config.dbName
})
},
deps: [ConfigService]
}
// 异步工厂
{
provide: RemoteConfig,
useFactory: async (http: HttpClient) => {
const config = await http.get('/config')
return new RemoteConfig(config)
},
deps: [HttpClient]
}
```
**适用场景**
- 需要复杂初始化逻辑
- 依赖运行时配置
- 条件创建实例
- 异步初始化
## ExistingProvider
创建到另一个令牌的别名。
```typescript
interface ExistingProvider<T> {
provide: InjectionTokenType<T>
useExisting: InjectionTokenType<T>
multi?: boolean
}
// 示例 - 使用 InjectionToken 确保类型安全
const LOGGER_TOKEN = new InjectionToken<Logger>('Logger')
{ provide: LOGGER_TOKEN, useExisting: LoggerService }
{ provide: AbstractLogger, useExisting: ConsoleLogger }
```
**适用场景**
- 为同一服务提供多个令牌
- 向后兼容
- 接口别名
## ConstructorProvider
类本身作为令牌和实现。
```typescript
interface ConstructorProvider<T> {
provide: Type<T>
multi?: boolean
}
// 示例
{ provide: UserService }
// 等价于
{ provide: UserService, useClass: UserService }
```
**适用场景**
- 简化注册
- 类名即令牌
## LazyClassProvider
延迟实例化,首次访问时才创建。
```typescript
interface LazyClassProvider<T> {
provide: InjectionTokenType<T>
useLazyClass: Type<T>
multi?: boolean
}
// 示例
{ provide: HeavyAnalyticsService, useLazyClass: HeavyAnalyticsService }
{ provide: ReportGenerator, useLazyClass: ReportGenerator }
```
**适用场景**
- 重型服务(大量初始化开销)
- 可能不会使用的服务
- 优化启动时间
## LazyFactoryProvider
延迟调用工厂函数。
```typescript
interface LazyFactoryProvider<T> {
provide: InjectionTokenType<T>
useLazyFactory: (...deps: any[]) => T
deps?: InjectionTokenType<any>[]
multi?: boolean
}
// 示例 - 使用 InjectionToken 确保类型安全
const LAZY_CONFIG = new InjectionToken<AppConfig>('LAZY_CONFIG')
{
provide: LAZY_CONFIG,
useLazyFactory: async () => {
const response = await fetch('/api/config')
return response.json()
},
deps: []
}
```
**适用场景**
- 按需加载配置
- 延迟网络请求
- 条件初始化
## Multi Provider
允许多个提供者注册到同一令牌。
```typescript
// 注册多个拦截器
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
// 获取时返回数组
const interceptors = injector.get(HTTP_INTERCEPTORS)
// [AuthInterceptor实例, LoggingInterceptor实例, ErrorInterceptor实例]
```
**适用场景**
- 插件系统
- 中间件/拦截器
- 事件处理器
- 验证器集合
## 提供者选择决策树
```
需要提供什么?
├── 静态值/配置 → ValueProvider
├── 类实例
│ ├── 简单实例化 → ClassProvider 或 ConstructorProvider
│ ├── 需要复杂初始化 → FactoryProvider
│ ├── 延迟加载 → LazyClassProvider
│ └── 别名/映射 → ExistingProvider
├── 动态值
│ ├── 同步创建 → FactoryProvider
│ └── 延迟创建 → LazyFactoryProvider
└── 多个实现 → 任意类型 + multi: true
```
## 常见模式
### 环境配置
```typescript
import { InjectionToken } from '@repo/core'
// 定义类型安全的令牌
const ENV = new InjectionToken<string>('ENV')
const API_URL = new InjectionToken<string>('API_URL')
const providers = [
{ provide: ENV, useValue: process.env.NODE_ENV },
{
provide: API_URL,
useFactory: (env: string) => {
return env === 'production'
? 'https://api.prod.com'
: 'https://api.dev.com'
},
deps: [ENV]
}
]
// 获取时类型安全
const apiUrl: string = injector.get(API_URL)
```
### 条件提供者
```typescript
const providers = [
process.env.USE_MOCK
? { provide: UserService, useClass: MockUserService }
: { provide: UserService, useClass: RealUserService }
]
```
### 工厂链
```typescript
const providers = [
{ provide: ConfigService, useClass: ConfigService },
{
provide: DatabaseService,
useFactory: (config: ConfigService) => new DatabaseService(config),
deps: [ConfigService]
},
{
provide: UserRepository,
useFactory: (db: DatabaseService) => new UserRepository(db),
deps: [DatabaseService]
}
]
```

View File

@@ -0,0 +1,364 @@
# 请求级注入器 (Request-Scoped Injector)
## 核心概念
在 HTTP 请求处理中,每个请求需要独立的依赖注入上下文,以便:
1. 隔离请求间的状态
2. 注入请求特定的数据(当前用户、请求上下文等)
3. 请求结束后自动清理资源
## 架构模式
### 完整注入器层次结构
```
Root Injector (scope: 'root', 全局单例)
├── 全局服务 (ConfigService, DatabaseService...)
└── Platform Injector (scope: 'platform', 全局单例, 可选)
└── Application Injector (scope: 'application', 可选)
└── Feature/Request Injector (scope: 'feature', 请求级)
├── 请求上下文 (BETTER_AUTH_CONTEXT)
├── 当前用户 (CURRENT_USER)
├── Feature Providers
└── Controller 实例
```
### 简化模式(常用)
```
Root Injector (应用级单例)
├── 全局服务 (ConfigService, DatabaseService...)
└── Request Injector (请求级,生命周期 = 请求)
├── 请求上下文 (BETTER_AUTH_CONTEXT)
├── 当前用户 (CURRENT_USER)
├── Feature Providers
└── Controller 实例
```
## 实现模式
### 1. 定义请求级令牌
```typescript
import { InjectionToken } from '@repo/core'
// 请求上下文令牌
const BETTER_AUTH_CONTEXT = new InjectionToken<AuthContext>('BETTER_AUTH_CONTEXT')
// 当前用户令牌
const CURRENT_USER = new InjectionToken<User | undefined>('CURRENT_USER')
// 请求对象令牌
const REQUEST = new InjectionToken<Request>('REQUEST')
// 响应对象令牌
const RESPONSE = new InjectionToken<Response>('RESPONSE')
```
### 2. 创建请求级注入器
```typescript
import { createInjector, root, FEATURE_PROVIDERS } from '@repo/core'
function createEndpointHandler(ControllerClass, methodName) {
return async (ctx: RequestContext) => {
// 获取全局注册的 feature providers
const providers = root.get(FEATURE_PROVIDERS, [])
// 创建请求级注入器
const reqInjector = createInjector([
// 注入请求上下文
{ provide: BETTER_AUTH_CONTEXT, useValue: ctx.context },
// 注入当前用户(延迟加载)
{
provide: CURRENT_USER,
useFactory: async () => {
const session = await getSessionFromCtx(ctx)
return session?.user
}
},
// 注入原始请求/响应
{ provide: REQUEST, useValue: ctx.request },
{ provide: RESPONSE, useValue: ctx.response },
// 展开 feature providers
...providers.flat(),
], root, 'feature') // parent = root, scope = 'feature'
// 从请求级注入器获取 Controller 实例
const instance = reqInjector.get(ControllerClass)
// 执行方法
const result = await instance[methodName](...args)
return result
}
}
```
### 3. 在服务中使用请求级依赖
```typescript
import { Injectable, Inject } from '@repo/core'
// 注意providedIn: 'auto' 表示不自动注册到根注入器
// 由请求级注入器在运行时创建实例
@Injectable({ providedIn: 'auto' })
class UserService {
constructor(
// CURRENT_USER 可能为 undefined使用 optional: true 避免注入失败
@Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined,
@Inject(BETTER_AUTH_CONTEXT) private authContext: AuthContext
) {}
async getCurrentUserProfile() {
if (!this.currentUser) {
throw new UnauthorizedError('Not logged in')
}
return this.userRepository.findById(this.currentUser.id)
}
}
```
> **重要**:
> - `providedIn: 'auto'` - 推荐用于请求级服务,可在任何注入器中被自动解析
> - `providedIn: null` - 不会被自动解析,必须手动注册
> - `providedIn: 'feature'` - 只能在 `scope: 'feature'` 的注入器中被解析
### 4. Controller 使用请求级服务
```typescript
@Injectable({ providedIn: 'auto' })
@Controller('/api/users')
class UserController {
constructor(
private userService: UserService, // 请求级实例
@Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined
) {}
@Get('/me')
@RequireSession()
async getProfile() {
return this.userService.getCurrentUserProfile()
}
}
```
## FEATURE_PROVIDERS 机制
用于在应用启动时注册需要在每个请求中可用的提供者。
```typescript
import { FEATURE_PROVIDERS, root } from '@repo/core'
// 注册 feature providers
root.set([
{
provide: FEATURE_PROVIDERS,
useValue: [
{ provide: UserRepository, useClass: UserRepository },
{ provide: OrderRepository, useClass: OrderRepository },
],
multi: true
}
])
// 在请求处理时,这些 providers 会被注入到请求级注入器
```
## 生命周期对比
| 作用域 | 生命周期 | 示例 |
|--------|----------|------|
| root | 应用启动 → 应用关闭 | ConfigService, DatabasePool |
| platform | 应用启动 → 应用关闭 | 跨应用共享服务 |
| application | 应用启动 → 应用关闭 | 应用级服务 |
| feature (请求级) | 请求开始 → 请求结束 | Controller, 请求上下文 |
### 注入器生命周期钩子
```typescript
// OnDestroy - 注入器销毁时调用
@Injectable({ providedIn: 'auto' })
class ResourceService {
private connection: Connection
async onDestroy() {
// 清理资源
await this.connection.close()
}
}
// 销毁注入器会自动调用所有实例的 onDestroy
await reqInjector.destroy()
```
> **注意**
> - 销毁后的注入器不能再获取服务,会抛出"注入器已销毁"错误
> - 重复销毁注入器是安全的(幂等操作)
## 最佳实践
### 1. 明确服务作用域
```typescript
// 全局单例 - 无状态或共享状态
@Injectable({ providedIn: 'root' })
class ConfigService { }
// 请求级 - 依赖请求上下文,使用 'auto' 不自动注册
@Injectable({ providedIn: 'auto' })
class RequestScopedService {
constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | undefined) { }
}
```
### 2. 避免在单例中注入请求级依赖
```typescript
// ⚠️ 问题:单例服务注入请求级依赖
// 如果使用 optional: true不会报错但 user 始终为 null
@Injectable({ providedIn: 'root' })
class ProblematicService {
constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | null) { }
// user 永远是 null因为根注入器没有 CURRENT_USER
}
// ❌ 错误:单例服务注入必需的请求级依赖会抛出错误
@Injectable({ providedIn: 'root' })
class BadService {
constructor(@Inject(CURRENT_USER) private user: User) { } // 抛出错误!
}
// ✅ 正确:使用 'auto' 作用域,让服务在请求级注入器中实例化
@Injectable({ providedIn: 'auto' })
class GoodService {
constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | undefined) { }
}
```
> **行为说明**
> - `providedIn: 'root'` + `optional: true` 的请求级依赖 → 依赖值为 `null`,不报错
> - `providedIn: 'root'` + 必需的请求级依赖 → 抛出错误
> - `providedIn: 'auto'` → 在请求级注入器中实例化,可正确获取请求级依赖
### 3. 使用工厂延迟加载
```typescript
// 延迟加载当前用户,避免不必要的数据库查询
{
provide: CURRENT_USER,
useFactory: async () => {
const session = await getSessionFromCtx(ctx)
return session?.user
}
}
```
### 4. 请求隔离保证
每个请求创建独立的注入器实例,确保:
- 请求 A 的用户数据不会泄露到请求 B
- 并发请求互不干扰
- 请求结束后资源自动释放
```typescript
// 请求 1
const reqInjector1 = createInjector([...], root, 'feature')
const user1 = reqInjector1.get(CURRENT_USER) // User A
// 请求 2 (并发)
const reqInjector2 = createInjector([...], root, 'feature')
const user2 = reqInjector2.get(CURRENT_USER) // User B
// user1 !== user2完全隔离
```
## 完整示例
```typescript
import {
Injectable, Inject, Controller, Get, Post, Body,
InjectionToken, createInjector, root, FEATURE_PROVIDERS,
RequireSession
} from '@repo/core'
// 1. 定义令牌
const AUTH_CONTEXT = new InjectionToken<AuthContext>('AUTH_CONTEXT')
const CURRENT_USER = new InjectionToken<User | undefined>('CURRENT_USER')
// 2. 请求级服务
@Injectable({ providedIn: 'auto' })
class OrderService {
constructor(
@Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined,
private orderRepository: OrderRepository
) {}
async getMyOrders() {
if (!this.currentUser) throw new UnauthorizedError()
return this.orderRepository.findByUserId(this.currentUser.id)
}
async createOrder(data: CreateOrderDto) {
if (!this.currentUser) throw new UnauthorizedError()
return this.orderRepository.create({
...data,
userId: this.currentUser.id
})
}
}
// 3. Controller
@Injectable({ providedIn: 'auto' })
@Controller('/api/orders')
class OrderController {
constructor(private orderService: OrderService) {}
@Get('/my')
@RequireSession()
async getMyOrders() {
return this.orderService.getMyOrders()
}
@Post('/create')
@RequireSession()
async createOrder(@Body() data: CreateOrderDto) {
return this.orderService.createOrder(data)
}
}
// 4. 请求处理器
function handleRequest(ControllerClass, methodName) {
return async (ctx) => {
const providers = root.get(FEATURE_PROVIDERS, [])
const reqInjector = createInjector([
{ provide: AUTH_CONTEXT, useValue: ctx.context },
{
provide: CURRENT_USER,
useFactory: async () => {
const session = await getSessionFromCtx(ctx)
return session?.user
}
},
...providers.flat(),
], root, 'feature')
const controller = reqInjector.get(ControllerClass)
const args = await injectParameters(ctx)
return controller[methodName](...args)
}
}
```

View File

@@ -0,0 +1,280 @@
---
name: repo-sdk
description: "@repo/sdk 使用指南。当需要调用 Loomart 平台 API、实现项目管理、模板运行、AI 生成、文件处理、社交互动等功能时使用此技能。适用于:(1) 服务端实现 Controller 接口 (2) 客户端调用 API (3) 理解接口组合使用场景 (4) 数据验证和类型定义"
---
# @repo/sdk 使用指南
## 架构概述
SDK 采用**三层分离架构**
```
@repo/sdk
├── controllers/ # 26 个业务控制器API 契约定义)
├── schemas/ # Zod 数据验证 Schema
├── types/ # TypeScript 类型定义
└── client/ # 客户端集成Better Auth 插件 + API 生成器)
```
**核心依赖**
- `@repo/core`:装饰器系统(@Controller, @Post, @Get, @Body, @Query 等)
- `zod`:数据验证
- `reflect-metadata`:元数据反射
## 推荐调用方式root.get (DI 风格)
**强烈推荐使用 `root.get` 方式**,保证完整的类型安全:
```typescript
import { root } from '@repo/core'
import { ProjectController, TemplateController, AigcController } from '@repo/sdk'
import type { CreateProjectInput, Project } from '@repo/sdk'
// 获取控制器(完整类型推导)
const projectCtrl = root.get(ProjectController)
const templateCtrl = root.get(TemplateController)
const aigcCtrl = root.get(AigcController)
// 调用方法(参数和返回值都有类型提示)
const projects = await projectCtrl.list({ page: 1, limit: 10 })
const project = await projectCtrl.create({ title: '新项目', content: {} })
```
### 其他调用方式
<details>
<summary>Better Auth 插件风格</summary>
```typescript
import { createAuthClient } from 'better-auth/client'
import { createSkerClientPlugin } from '@repo/sdk'
const auth = createAuthClient({
baseURL: 'http://localhost:3000',
plugins: [createSkerClientPlugin()]
})
const projects = await auth.sker.project.list({ page: 1, limit: 10 })
```
</details>
<details>
<summary>API 函数风格</summary>
```typescript
import { generateApiFunctions, client } from '@repo/sdk'
client.setConfig({
baseUrl: 'http://localhost:3000',
auth: async () => localStorage.getItem('token')
})
const apis = generateApiFunctions()
const projects = await apis.postLoomartProjectList({ page: 1, limit: 10 })
```
</details>
## 功能模块导航
| 模块 | 控制器 | 主要功能 |
|------|--------|----------|
| 项目管理 | ProjectController | 创建、列表、更新、删除、转移项目 |
| 模板管理 | TemplateController | 创建、列表、运行模板工作流 |
| 模板生成 | TemplateGenerationController | 生成记录管理 |
| 社交互动 | TemplateSocialController | 点赞、收藏、评论、分享 |
| AI 生成 | AigcController | 图像/视频生成任务 |
| 聊天 | ChatController | AI 对话 |
| 文件处理 | FileController | 上传、压缩、格式转换 |
| 分类标签 | CategoryController, TagController | 分类和标签管理 |
| 消息系统 | MessageController | 用户消息通知 |
| 公告 | AnnouncementController | 系统公告 |
| 权限 | PermissionController, RoleController | 权限和角色管理 |
| 支付 | AlipayController, AdminBalanceController | 支付和余额管理 |
## 核心组合使用场景
### 场景 1模板运行完整流程
```typescript
// 1. 获取模板详情
const template = await templateCtrl.get(templateId)
// 2. 运行模板(返回 generationId
const { generationId } = await templateCtrl.run({
templateId,
formData: { prompt: '生成一只猫' }
})
// 3. 轮询任务状态
let status = await aigcCtrl.getTaskStatus(taskId)
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
await sleep(2000)
status = await aigcCtrl.getTaskStatus(taskId)
}
// 4. 处理生成结果(可选:转换格式)
if (status.result_url) {
const webp = await fileCtrl.convertToWebp({
videoUrl: status.result_url,
quality: 80,
loop: true
})
}
```
### 场景 2模板社交互动
```typescript
// 1. 获取模板列表(带社交状态)
const { templates } = await templateCtrl.list({ page: 1, limit: 20 })
// 2. 批量检查点赞/收藏状态
const likedStatus = await socialCtrl.checkLikedBatch({
templateIds: templates.map(t => t.id)
})
// 3. 点赞操作
await socialCtrl.like({ templateId })
// 4. 获取评论
const comments = await socialCtrl.getComments({ templateId, page: 1 })
// 5. 发表评论
await socialCtrl.createComment({
templateId,
content: '很棒的模板!'
})
```
### 场景 3项目管理与标签
```typescript
// 1. 创建项目
const project = await projectCtrl.create({
title: '我的项目',
content: { /* 项目数据 */ }
})
// 2. 创建用户标签
const tag = await projectTagCtrl.create({ name: '收藏夹' })
// 3. 给项目添加标签
await projectTagCtrl.addToProject({
projectId: project.id,
tagId: tag.id
})
// 4. 按标签筛选项目
const { projects } = await projectCtrl.list({
page: 1,
tagId: tag.id
})
```
## 参考文档
- **控制器 API 详情**:查阅 [references/controllers.md](references/controllers.md)
- **组合使用场景**:查阅 [references/workflows.md](references/workflows.md)
- **客户端集成指南**:查阅 [references/client-usage.md](references/client-usage.md)
## 权限装饰器
```typescript
// 需要登录
@RequireSession()
// 需要特定权限AND 模式)
@RequirePermissions({ project: ['create', 'update'] })
// 需要任一权限OR 模式)
@RequirePermissions({ project: ['run'], template: ['run'] }, 'OR')
```
## 复用 Zod Schema 和类型(推荐)
**强烈推荐复用 SDK 导出的 Schema 和类型**,确保前后端数据一致性:
### 导入类型
```typescript
// 从 @repo/sdk 导入类型(推荐)
import type {
// 项目
Project, CreateProjectInput, ListProjectsInput, ListProjectsResult,
// 模板
TemplateDetail, RunTemplateInput, ListTemplatesInput,
// AI 生成
AigcModel, SubmitTaskBody, GetTaskStatusResult,
// 消息
Message, ListMessagesInput, UnreadCountResult
} from '@repo/sdk'
```
### 复用 Zod Schema 验证
```typescript
import {
CreateProjectSchema,
ListProjectsSchema,
RunTemplateSchema,
SubmitTaskBodySchema
} from '@repo/sdk'
// 验证用户输入
function validateInput(data: unknown) {
return CreateProjectSchema.parse(data) // 抛出错误或返回类型安全的数据
}
// 安全验证(不抛错)
function safeValidate(data: unknown) {
const result = CreateProjectSchema.safeParse(data)
if (!result.success) {
console.error('验证失败:', result.error.flatten())
return null
}
return result.data
}
```
### 服务端实现复用类型
```typescript
import { Injectable } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import type { Project, CreateProjectInput, ListProjectsInput, ListProjectsResult } from '@repo/sdk'
@Injectable()
export class ProjectControllerImpl implements ProjectController {
// 参数和返回值类型直接复用 SDK 导出的类型
async create(data: CreateProjectInput): Promise<Project> {
// 实现...
}
async list(body: ListProjectsInput): Promise<ListProjectsResult> {
// 实现...
}
}
```
### 前端表单复用 Schema
```typescript
import { CreateProjectSchema } from '@repo/sdk'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
type FormData = z.infer<typeof CreateProjectSchema>
function CreateProjectForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(CreateProjectSchema)
})
const onSubmit = async (data: FormData) => {
const projectCtrl = root.get(ProjectController)
await projectCtrl.create(data) // 类型完全匹配
}
}
```

View File

@@ -0,0 +1,619 @@
# 客户端使用指南
本文档详细介绍 @repo/sdk 的客户端调用方式及其配置方法。
## 目录
1. [推荐方式root.get (DI 风格)](#1-推荐方式rootget-di-风格)
2. [复用 Schema 和类型](#2-复用-schema-和类型)
3. [Better Auth 插件风格](#3-better-auth-插件风格)
4. [API 函数风格](#4-api-函数风格)
5. [服务端实现](#5-服务端实现)
6. [错误处理](#6-错误处理)
---
## 1. 推荐方式root.get (DI 风格)
**强烈推荐使用此方式**,保证完整的类型安全和最佳开发体验。
### 基本用法
```typescript
import { root } from '@repo/core'
import {
ProjectController,
TemplateController,
TemplateSocialController,
AigcController,
FileController
} from '@repo/sdk'
import type { CreateProjectInput, Project, ListProjectsResult } from '@repo/sdk'
// 获取控制器实例(完整类型推导)
const projectCtrl = root.get(ProjectController)
const templateCtrl = root.get(TemplateController)
const socialCtrl = root.get(TemplateSocialController)
const aigcCtrl = root.get(AigcController)
const fileCtrl = root.get(FileController)
// 调用方法(参数和返回值都有完整类型提示)
const projects: ListProjectsResult = await projectCtrl.list({ page: 1, limit: 10 })
const project: Project = await projectCtrl.create({ title: '新项目', content: {} })
const template = await templateCtrl.get('template-id')
await socialCtrl.like({ templateId: 'xxx' })
```
### 优势
1. **完整类型安全**:参数和返回值都有精确的类型推导
2. **IDE 支持**:自动补全、跳转定义、重构支持
3. **可测试性**:可以轻松 mock Controller 进行单元测试
4. **前后端一致**:服务端和客户端使用相同的接口定义
5. **灵活性**:可以自定义代理实现(如添加缓存、日志等)
### 前置条件
需要先初始化客户端插件(注册 HTTP 代理):
```typescript
import { createAuthClient } from 'better-auth/client'
import { createSkerClientPlugin } from '@repo/sdk'
// 初始化(会自动将 Controller 替换为 HTTP 代理)
const auth = createAuthClient({
baseURL: 'http://localhost:3000',
plugins: [createSkerClientPlugin()]
})
```
### 自定义代理(高级)
```typescript
import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import type { ListProjectsInput, ListProjectsResult } from '@repo/sdk'
// 注册带缓存的自定义实现
root.set([{
provide: ProjectController,
useFactory: () => ({
list: async (input: ListProjectsInput): Promise<ListProjectsResult> => {
const cacheKey = `projects-${JSON.stringify(input)}`
const cached = cache.get(cacheKey)
if (cached) return cached
const result = await originalProxy.list(input)
cache.set(cacheKey, result)
return result
},
// ... 其他方法
})
}])
```
---
## 2. 复用 Schema 和类型
**强烈推荐复用 SDK 导出的 Zod Schema 和 TypeScript 类型**,确保前后端数据一致性。
### 导入类型(推荐)
```typescript
// 始终从 @repo/sdk 导入类型,不要自己定义
import type {
// 项目相关
Project,
CreateProjectInput,
ListProjectsInput,
ListProjectsResult,
UpdateProjectInput,
TransferProjectInput,
// 模板相关
TemplateDetail,
CreateTemplateInput,
RunTemplateInput,
ListTemplatesInput,
ListTemplatesResult,
// AI 生成
AigcModel,
SubmitTaskBody,
SubmitTaskResult,
GetTaskStatusResult,
// 社交
LikeTemplateInput,
CreateCommentInput,
TemplateComment,
GetCommentsResponse,
// 消息
Message,
ListMessagesInput,
UnreadCountResult,
// 文件
FileUploadResponse,
VideoCompressRequest,
ConvertToWebpRequest
} from '@repo/sdk'
```
### 复用 Zod Schema 验证
```typescript
import {
CreateProjectSchema,
ListProjectsSchema,
RunTemplateSchema,
SubmitTaskBodySchema,
CreateCommentSchema
} from '@repo/sdk'
// 验证用户输入(抛出错误)
function validateInput(data: unknown) {
return CreateProjectSchema.parse(data)
}
// 安全验证(不抛错,返回结果对象)
function safeValidate(data: unknown) {
const result = CreateProjectSchema.safeParse(data)
if (!result.success) {
console.error('验证失败:', result.error.flatten())
return null
}
return result.data // 类型安全的数据
}
// 部分验证(用于更新操作)
const PartialProjectSchema = CreateProjectSchema.partial()
function validatePartialUpdate(data: unknown) {
return PartialProjectSchema.parse(data)
}
```
### 前端表单集成
```typescript
import { CreateProjectSchema } from '@repo/sdk'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'
// 从 Schema 推导表单类型
type FormData = z.infer<typeof CreateProjectSchema>
function CreateProjectForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(CreateProjectSchema) // 复用 SDK Schema
})
const onSubmit = async (data: FormData) => {
const projectCtrl = root.get(ProjectController)
const project = await projectCtrl.create(data) // 类型完全匹配
console.log('创建成功:', project.id)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} />
{errors.title && <span>{errors.title.message}</span>}
{/* ... */}
</form>
)
}
```
### 服务端实现复用类型
```typescript
import { Injectable } from '@repo/core'
import { ProjectController } from '@repo/sdk'
// 直接复用 SDK 导出的类型
import type {
Project,
CreateProjectInput,
ListProjectsInput,
ListProjectsResult,
UpdateProjectInput
} from '@repo/sdk'
@Injectable()
export class ProjectControllerImpl implements ProjectController {
constructor(
private readonly db: DatabaseService,
private readonly auth: AuthService
) {}
// 参数和返回值类型直接复用 SDK 类型
async create(data: CreateProjectInput): Promise<Project> {
const userId = this.auth.getCurrentUserId()
return this.db.project.create({
data: { ...data, userId }
})
}
async list(body: ListProjectsInput): Promise<ListProjectsResult> {
// 实现...
}
async update(body: UpdateProjectInput): Promise<Project> {
// 实现...
}
}
```
---
## 3. Better Auth 插件风格
**适用场景**:已使用 Better Auth 进行身份认证的项目
### 安装和配置
```typescript
import { createAuthClient } from 'better-auth/client'
import { createSkerClientPlugin } from '@repo/sdk'
// 创建带 Sker 插件的 Auth 客户端
const auth = createAuthClient({
baseURL: 'http://localhost:3000',
plugins: [createSkerClientPlugin()]
})
export { auth }
```
### 调用方式
```typescript
// 项目管理
const projects = await auth.sker.project.list({ page: 1, limit: 10 })
const project = await auth.sker.project.create({ title: '新项目', content: {} })
const detail = await auth.sker.project.get('project-id')
// 模板管理
const templates = await auth.sker.template.list({ page: 1 })
const { generationId } = await auth.sker.template.run({ templateId: 'xxx', formData: {} })
// 社交功能
await auth.sker.templateSocial.like({ templateId: 'xxx' })
const comments = await auth.sker.templateSocial.getComments({ templateId: 'xxx' })
// AI 生成
const models = await auth.sker.aigc.getModels('image')
const { task_id } = await auth.sker.aigc.submitTask({ model_name: 'xxx', prompt: 'xxx' })
// 文件上传
const formData = new FormData()
formData.append('file', file)
const { data: fileUrl } = await auth.sker.file.uploadS3(formData)
```
### 插件工作原理
`createSkerClientPlugin()` 做了以下事情:
1. **注册 HTTP 代理**:将所有 Controller 替换为 HTTP 调用代理
2. **提供 Better Auth 风格 API**:通过 `auth.sker.xxx` 访问
3. **自动处理认证**:使用 Better Auth 的 session 进行认证
4. **生成路径映射**:自动从 Controller 元数据生成路由
```typescript
// 插件内部实现简化版
export function createSkerClientPlugin(): BetterAuthClientPlugin {
return {
id: 'sker',
getActions: ($fetch) => {
// 注册代理到 DI 容器
registerControllerProxies($fetch)
// 返回 Better Auth 风格的 actions
return buildBetterAuthActions(controllers, $fetch)
},
pathMethods: generatePathMethods(controllers)
}
}
```
---
## 4. API 函数风格
**推荐场景**:不使用 Better Auth、需要独立的 API 客户端
### 配置客户端
```typescript
import { client, generateApiFunctions } from '@repo/sdk'
// 配置基础 URL 和认证
client.setConfig({
baseUrl: 'http://localhost:3000',
auth: async () => {
// 返回认证 token
return localStorage.getItem('token') || ''
}
})
// 生成所有 API 函数
const apis = generateApiFunctions()
```
### 调用方式
函数名格式:`{httpMethod}{Path}`(驼峰命名)
```typescript
// POST /loomart/project/list -> postLoomartProjectList
const projects = await apis.postLoomartProjectList({ page: 1, limit: 10 })
// GET /loomart/template/get -> getLoomartTemplateGet
const template = await apis.getLoomartTemplateGet({ id: 'template-id' })
// POST /loomart/template/run -> postLoomartTemplateRun
const { generationId } = await apis.postLoomartTemplateRun({
templateId: 'xxx',
formData: {}
})
// GET /loomart/aigc/models -> getLoomartAigcModels
const models = await apis.getLoomartAigcModels({ category: 'image' })
// POST /loomart/file/upload-s3 -> postLoomartFileUploadS3
const formData = new FormData()
formData.append('file', file)
const result = await apis.postLoomartFileUploadS3(formData)
```
### 函数名映射规则
| 路由 | HTTP 方法 | 函数名 |
|------|-----------|--------|
| /loomart/project/create | POST | postLoomartProjectCreate |
| /loomart/project/list | POST | postLoomartProjectList |
| /loomart/project/get | GET | getLoomartProjectGet |
| /loomart/template/run | POST | postLoomartTemplateRun |
| /loomart/aigc/task/submit | POST | postLoomartAigcTaskSubmit |
| /loomart/aigc/task/status | GET | getLoomartAigcTaskStatus |
| /loomart/file/upload-s3 | POST | postLoomartFileUploadS3 |
### 动态认证
```typescript
client.setConfig({
baseUrl: 'http://localhost:3000',
auth: async (authContext) => {
// 可以根据上下文动态获取 token
if (authContext?.refreshToken) {
// 刷新 token
const newToken = await refreshAccessToken(authContext.refreshToken)
return newToken
}
return localStorage.getItem('token') || ''
}
})
```
---
## 5. 服务端实现
**场景**:在 Hono/Express 等后端框架中实现 Controller
### 注册控制器实现
```typescript
import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import { ProjectControllerImpl } from './impl/project.controller.impl'
// 注册实现类
root.set([
{
provide: ProjectController,
useClass: ProjectControllerImpl
}
])
```
### 实现类示例
```typescript
import { Injectable } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import type {
Project,
CreateProjectInput,
ListProjectsInput,
ListProjectsResult
} from '@repo/sdk'
@Injectable()
export class ProjectControllerImpl implements ProjectController {
constructor(
private readonly db: DatabaseService,
private readonly auth: AuthService
) {}
async create(data: CreateProjectInput): Promise<Project> {
const userId = this.auth.getCurrentUserId()
const project = await this.db.project.create({
data: {
...data,
userId
}
})
return project
}
async list(body: ListProjectsInput): Promise<ListProjectsResult> {
const userId = this.auth.getCurrentUserId()
const [projects, total] = await Promise.all([
this.db.project.findMany({
where: { userId, isDeleted: false },
skip: ((body.page || 1) - 1) * (body.limit || 20),
take: body.limit || 20,
orderBy: { [body.sortBy || 'createdAt']: body.sortOrder || 'desc' }
}),
this.db.project.count({ where: { userId, isDeleted: false } })
])
return {
projects,
total,
page: body.page || 1,
limit: body.limit || 20
}
}
async get(id: string): Promise<Project> {
const project = await this.db.project.findUnique({ where: { id } })
if (!project) throw new Error('Project not found')
return project
}
async update(body: UpdateProjectInput): Promise<Project> {
return this.db.project.update({
where: { id: body.id },
data: body
})
}
async delete(body: { id: string }): Promise<{ message: string }> {
await this.db.project.update({
where: { id: body.id },
data: { isDeleted: true }
})
return { message: 'Project deleted' }
}
async transfer(body: TransferProjectInput): Promise<TransferProjectResult> {
// 实现转移逻辑...
}
}
```
### 与 Hono 集成
```typescript
import { Hono } from 'hono'
import { root, CONTROLLES } from '@repo/core'
const app = new Hono()
// 自动注册所有控制器路由
const controllers = root.get(CONTROLLES, [])
for (const controller of controllers) {
// 使用 @repo/core 的路由注册逻辑
registerControllerRoutes(app, controller)
}
export default app
```
---
## 6. 错误处理
### 客户端错误处理
```typescript
import { ProjectController } from '@repo/sdk'
import { root } from '@repo/core'
const projectCtrl = root.get(ProjectController)
try {
const project = await projectCtrl.get('non-existent-id')
} catch (error) {
if (error.message.includes('API error')) {
// API 返回的错误
console.error('API 错误:', error.message)
} else {
// 网络或其他错误
console.error('请求失败:', error)
}
}
```
### 统一错误处理
```typescript
// 创建带错误处理的包装器
function withErrorHandling<T extends (...args: any[]) => Promise<any>>(fn: T): T {
return (async (...args: Parameters<T>) => {
try {
return await fn(...args)
} catch (error: any) {
// 统一错误处理
if (error.message.includes('401')) {
// 未认证,跳转登录
window.location.href = '/login'
return
}
if (error.message.includes('403')) {
// 无权限
throw new Error('您没有权限执行此操作')
}
if (error.message.includes('404')) {
throw new Error('资源不存在')
}
throw error
}
}) as T
}
// 使用
const safeList = withErrorHandling(projectCtrl.list.bind(projectCtrl))
const projects = await safeList({ page: 1 })
```
---
## 最佳实践总结
### 推荐做法
| 场景 | 推荐方式 |
|------|----------|
| 调用 API | `root.get(Controller)` - 完整类型安全 |
| 定义类型 | 从 `@repo/sdk` 导入,不要自己定义 |
| 验证数据 | 复用 SDK 导出的 Zod Schema |
| 表单验证 | 使用 `zodResolver(Schema)` |
| 服务端实现 | `implements Controller` + 复用类型 |
### 避免做法
```typescript
// ❌ 不要自己定义类型
interface MyProject {
id: string;
title: string;
}
// ✅ 从 SDK 导入类型
import type { Project } from '@repo/sdk'
// ❌ 不要自己写验证逻辑
if (!data.title || data.title.length < 1) {
throw new Error('标题不能为空')
}
// ✅ 复用 SDK Schema
import { CreateProjectSchema } from '@repo/sdk'
CreateProjectSchema.parse(data)
// ❌ 不要使用 any 类型
const projectCtrl: any = root.get(ProjectController)
// ✅ 让 TypeScript 自动推导
const projectCtrl = root.get(ProjectController) // 类型自动推导
```

View File

@@ -0,0 +1,691 @@
# 控制器 API 参考
## 目录
1. [项目管理](#项目管理)
2. [模板管理](#模板管理)
3. [模板生成记录](#模板生成记录)
4. [社交互动](#社交互动)
5. [AI 生成](#ai-生成)
6. [聊天](#聊天)
7. [文件处理](#文件处理)
8. [分类管理](#分类管理)
9. [标签管理](#标签管理)
10. [项目标签](#项目标签)
11. [消息系统](#消息系统)
12. [公告系统](#公告系统)
13. [权限管理](#权限管理)
14. [角色管理](#角色管理)
15. [支付相关](#支付相关)
---
## 项目管理
**控制器**: `ProjectController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /project/create | 创建新项目 | project:create |
| POST | /project/list | 获取用户项目列表 | project:list |
| GET | /project/get | 获取单个项目 | project:read |
| POST | /project/update | 更新项目信息 | project:update |
| POST | /project/delete | 删除项目 | project:delete |
| POST | /project/transfer | 转移/复制项目所有权 | project:update OR project:create |
### 类型定义
```typescript
// 创建项目
interface CreateProjectInput {
title: string;
titleEn?: string;
description?: string;
descriptionEn?: string;
content: any;
sourceTemplateId?: string;
}
// 列表查询
interface ListProjectsInput {
page?: number;
limit?: number;
tagId?: string;
search?: string;
sortBy?: 'createdAt' | 'updatedAt' | 'title';
sortOrder?: 'asc' | 'desc';
}
// 项目响应
interface Project {
id: string;
userId: string;
title: string;
titleEn: string;
description?: string;
descriptionEn?: string;
resultUrl?: string;
content: any;
sourceTemplateId?: string;
isDeleted: boolean;
createdAt: Date;
updatedAt: Date;
tags?: UserTagBasic[];
}
// 转移项目
interface TransferProjectInput {
projectId: string;
targetUserId: string;
mode: 'transfer' | 'copy';
}
```
---
## 模板管理
**控制器**: `TemplateController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /template/create | 创建新模板 | - |
| POST | /template/list | 获取模板列表 | - |
| GET | /template/get | 获取单个模板 | - |
| POST | /template/update | 更新模板 | - |
| POST | /template/delete | 删除模板 | - |
| POST | /template/run | 运行模板工作流 | - |
| POST | /template/rerun | 根据生成记录重新运行 | - |
### 类型定义
```typescript
// 模板详情
interface TemplateDetail {
id: string;
userId: string;
title: string;
titleEn: string;
description: string;
descriptionEn: string;
coverImageUrl: string;
previewUrl: string;
watermarkedPreviewUrl?: string;
webpPreviewUrl?: string;
webpHighPreviewUrl?: string;
content: any;
formSchema?: any;
sortOrder: number;
viewCount: number;
useCount: number;
likeCount: number;
favoriteCount: number;
shareCount: number;
commentCount: number;
costPrice?: number;
price?: number;
aspectRatio: string;
status: string;
isDeleted: boolean;
createdAt: Date;
updatedAt: Date;
isLiked?: boolean;
isFavorited?: boolean;
}
// 运行模板
interface RunTemplateInput {
templateId: string;
formData?: Record<string, any>;
projectId?: string;
}
// 列表查询
interface ListTemplatesInput {
page?: number;
limit?: number;
categoryId?: string;
tagId?: string;
status?: string;
search?: string;
sortBy?: 'sortOrder' | 'likeCount' | 'useCount' | 'viewCount' | 'createdAt';
sortOrder?: 'asc' | 'desc';
}
```
---
## 模板生成记录
**控制器**: `TemplateGenerationController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /template-generation/list | 获取生成记录列表 | template:run |
| GET | /template-generation/get | 获取单个记录 | template:run |
| POST | /template-generation/update | 更新记录 | template:run |
| POST | /template-generation/delete | 删除记录 | template:run |
| POST | /template-generation/batch-delete | 批量删除 | template:run |
### 类型定义
```typescript
interface TemplateGeneration {
id: string;
userId: string;
templateId: string;
projectId?: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
formData?: any;
resultUrl?: string;
errorMessage?: string;
createdAt: Date;
updatedAt: Date;
}
interface ListTemplateGenerationsInput {
page?: number;
limit?: number;
templateId?: string;
projectId?: string;
status?: string;
}
```
---
## 社交互动
**控制器**: `TemplateSocialController`
**路由前缀**: `/loomart/template`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /like | 点赞模板 | template:run |
| POST | /unlike | 取消点赞 | template:run |
| GET | /check-liked | 检查是否已点赞 | template:run |
| POST | /check-liked-batch | 批量检查点赞状态 | template:run |
| POST | /favorite | 收藏模板 | template:run |
| POST | /unfavorite | 取消收藏 | template:run |
| GET | /check-favorited | 检查是否已收藏 | template:run |
| POST | /check-favorited-batch | 批量检查收藏状态 | template:run |
| POST | /favorites | 获取用户收藏列表 | template:run |
| POST | /likes | 获取用户喜欢列表 | template:run |
| POST | /share | 转发模板 | template:run |
| POST | /comment/create | 创建评论 | template:run |
| POST | /comment/delete | 删除评论 | template:run |
| GET | /comments | 获取评论列表 | template:run |
| GET | /comment/replies | 获取评论回复 | template:run |
### 类型定义
```typescript
// 点赞/收藏
interface LikeTemplateInput { templateId: string; }
interface FavoriteTemplateInput { templateId: string; }
// 批量检查
interface CheckLikedBatchInput { templateIds: string[]; }
interface CheckLikedBatchResponse {
results: Record<string, boolean>;
}
// 评论
interface CreateCommentInput {
templateId: string;
content: string;
parentId?: string; // 回复评论时使用
}
interface TemplateComment {
id: string;
userId: string;
templateId: string;
content: string;
parentId?: string;
isDeleted: boolean;
createdAt: Date;
user?: { id: string; name: string; image?: string; };
replies?: TemplateComment[];
replyCount?: number;
}
// 获取评论
interface GetCommentsInput {
templateId: string;
page?: number;
limit?: number;
}
```
---
## AI 生成
**控制器**: `AigcController`
**路由前缀**: `/loomart/aigc`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| GET | /models | 获取支持的模型列表 | project:run OR template:run |
| POST | /task/submit | 提交图像/视频生成任务 | project:run OR template:run |
| GET | /task/status | 获取任务状态 | project:run OR template:run |
### 类型定义
```typescript
// 模型信息
interface AigcModel {
model_name: string;
description: string;
description_en: string;
supported_ar: string[]; // 支持的宽高比
supported_count: string[]; // 支持的生成数量
supported_duration: string[]; // 支持的时长(视频)
supported_resolution: string[];
mode: string; // 'image' | 'video'
model_provider: string;
tags: string[];
tags_en: string[];
provider: string;
display_name: string;
}
// 提交任务
interface SubmitTaskBody {
mode?: string;
model_name: string;
prompt: string;
aspect_ratio?: string;
webhook_flag?: boolean;
watermark?: boolean;
extra?: string;
img_url?: string;
img_list?: any[];
duration?: string;
resolution?: string;
}
// 任务状态
interface GetTaskStatusResult {
task_id: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
progress?: number;
result_url?: string;
error_message?: string;
}
```
---
## 聊天
**控制器**: `ChatController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /chat | 发送聊天消息 | project:run OR template:run |
| GET | /chat/models | 获取模型列表 | project:run OR template:run |
### 类型定义
```typescript
interface ChatRequest {
prompt: string;
model_name: string;
img_list?: string[];
video_url?: string[];
stream?: boolean;
tools?: string;
tool_choice?: string;
}
interface ChatResponse {
data: string;
status: boolean;
msg: string;
usage?: any;
}
```
---
## 文件处理
**控制器**: `FileController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 | 内容类型 |
|------|------|------|------|---------|
| POST | /file/upload-s3 | 上传文件到S3 | RequireSession | multipart/form-data |
| POST | /file/compress-video | 压缩视频 | project:run OR template:run | - |
| POST | /file/convert-to-ani | 转换为ANI格式 | project:run OR template:run | - |
| POST | /file/convert-to-webp | 转换为WebP格式 | project:run OR template:run | - |
| POST | /file/convert-to-watermark | 添加水印 | project:run OR template:run | - |
| POST | /file/upload-and-convert-ani | 上传并转换ANI | project:run OR template:run | multipart/form-data |
### 类型定义
```typescript
// 文件上传响应
interface FileUploadResponse {
status: boolean;
msg: string;
data: string; // 文件URL
}
// 视频压缩
interface VideoCompressRequest {
videoUrl: string;
quality: 'low' | 'medium' | 'high';
}
// 转换为 WebP
interface ConvertToWebpRequest {
videoUrl: string;
compressionLevel: number; // 0-6
quality: number; // 0-100
loop: boolean;
resolution: string;
fps: number;
mode?: string;
}
// 添加水印
interface ConvertToWatermarkRequest {
videoUrl: string;
watermarkUrl: string;
position?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'center';
}
```
---
## 分类管理
**控制器**: `CategoryController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /category/create | 创建分类 | category:create |
| POST | /category/list | 获取分类列表 | - |
| GET | /category/get | 获取单个分类 | - |
| GET | /category/get-by-name | 按名称获取分类 | - |
| GET | /category/get-by-id | 按ID获取分类及模板 | - |
| POST | /category/list-with-tags | 获取分类及标签列表 | - |
| POST | /category/update | 更新分类 | category:update |
| POST | /category/delete | 删除分类 | category:delete |
| POST | /category/batch-update-sort-order | 批量更新排序 | category:update |
| POST | /category/batch-update-tag-sort-order | 批量更新标签排序 | category:update |
| GET | /category/stats | 获取分类统计 | category:read |
### 类型定义
```typescript
interface Category {
id: string;
name: string;
nameEn: string;
description?: string;
descriptionEn?: string;
coverImageUrl?: string;
sortOrder: number;
isDeleted: boolean;
createdAt: Date;
updatedAt: Date;
tags?: Tag[];
templates?: TemplateDetail[];
}
interface CreateCategoryInput {
name: string;
nameEn?: string;
description?: string;
descriptionEn?: string;
coverImageUrl?: string;
sortOrder?: number;
}
```
---
## 标签管理
**控制器**: `TagController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /tags/create | 创建标签 | tag:create |
| POST | /tags/list | 获取标签列表 | tag:list |
| GET | /tags/get | 获取单个标签 | tag:read |
| GET | /tags/get-by-name | 按名称获取标签 | tag:read |
| GET | /tags/get-by-category-tag-id | 按CategoryTag ID获取 | - |
| POST | /tags/update | 更新标签 | tag:update |
| POST | /tags/delete | 删除标签 | tag:delete |
| POST | /tags/update-category-tag | 更新分类标签关联 | tag:update |
| POST | /tags/batch-update-sort-order | 批量更新排序 | tag:update |
### 类型定义
```typescript
interface Tag {
id: string;
name: string;
nameEn: string;
description?: string;
sortOrder: number;
isDeleted: boolean;
createdAt: Date;
updatedAt: Date;
}
interface CreateTagInput {
name: string;
nameEn?: string;
description?: string;
sortOrder?: number;
}
```
---
## 项目标签
**控制器**: `ProjectTagController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /project-tags/create | 创建用户标签 | - |
| GET | /project-tags/list | 获取标签列表 | - |
| POST | /project-tags/update | 更新标签 | - |
| POST | /project-tags/delete | 删除标签 | - |
| POST | /project-tags/add-to-project | 添加标签到项目 | - |
| POST | /project-tags/remove-from-project | 从项目移除标签 | - |
| POST | /project-tags/set-project-tags | 设置项目标签 | - |
### 类型定义
```typescript
interface UserTag {
id: string;
userId: string;
name: string;
color?: string;
sortOrder: number;
createdAt: Date;
updatedAt: Date;
}
interface CreateUserTagInput {
name: string;
color?: string;
}
interface SetProjectTagsInput {
projectId: string;
tagIds: string[];
}
```
---
## 消息系统
**控制器**: `MessageController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /messages/list | 获取消息列表 | RequireSession |
| GET | /messages/get | 获取单条消息 | RequireSession |
| POST | /messages/mark-read | 标记消息已读 | RequireSession |
| POST | /messages/batch-mark-read | 批量标记已读 | RequireSession |
| POST | /messages/delete | 删除消息 | RequireSession |
| GET | /messages/unread-count | 获取未读数量 | RequireSession |
### 类型定义
```typescript
interface Message {
id: string;
userId: string;
type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING';
title: string;
content: string;
data?: any;
link?: string;
priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
expiresAt?: Date;
isRead: boolean;
readAt?: Date;
isDeleted: boolean;
deletedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
interface ListMessagesInput {
page?: number;
limit?: number;
type?: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING';
isRead?: boolean;
}
interface UnreadCountResult {
total: number;
byType: {
SYSTEM: number;
ACTIVITY: number;
BILLING: number;
MARKETING: number;
};
}
```
---
## 公告系统
**控制器**: `AnnouncementController`
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| POST | /announcements/create | 创建公告 | announcement:create |
| POST | /announcements/list | 获取公告列表 | - |
| GET | /announcements/get | 获取单条公告 | - |
| POST | /announcements/update | 更新公告 | announcement:update |
| POST | /announcements/delete | 删除公告 | announcement:delete |
| POST | /announcements/mark-read | 标记已读 | RequireSession |
| GET | /announcements/unread-count | 获取未读数量 | RequireSession |
---
## 权限管理
**控制器**: `PermissionController`
**路由前缀**: `/loomart/admin/permissions`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| GET | /permissions/list | 获取权限列表 | user:permissions |
| POST | /permissions/create | 创建权限 | user:permissions |
| PATCH | /permissions/get | 更新权限 | user:permissions |
| DELETE | /permissions/delete | 删除权限 | user:permissions |
### 类型定义
```typescript
interface Permission {
id: string;
resource: string;
action: string;
description?: string;
createdAt: Date;
updatedAt: Date;
}
```
---
## 角色管理
**控制器**: `RoleController`
**路由前缀**: `/loomart/admin`
| 方法 | 路由 | 功能 | 权限 |
|------|------|------|------|
| GET | /roles/list | 获取角色列表 | role:list |
| POST | /roles/create | 创建角色 | role:create |
| PATCH | /roles/update | 更新角色基本信息 | role:update |
| PUT | /roles/update/permissions | 更新角色权限 | role:update |
| DELETE | /roles/delete | 删除角色 | role:delete |
### 类型定义
```typescript
interface RoleWithPermissions {
id: string;
name: string;
displayName: string;
description?: string;
isSystem: boolean;
scope: string;
organizationId?: string;
createdAt: Date;
updatedAt: Date;
permissions: Permission[];
}
```
---
## 支付相关
### AlipayController
**路由前缀**: `/loomart`
| 方法 | 路由 | 功能 |
|------|------|------|
| POST | /alipay/app-pay | 支付宝APP支付下单 |
| POST | /alipay/auth-info | 获取支付宝授权字符串 |
| POST | /alipay/webhook | 支付宝异步通知回调 |
| POST | /credits/pre-recharge | 积分预充值 |
### AdminBalanceController
**路由前缀**: `/loomart/admin-balance`
| 方法 | 路由 | 功能 |
|------|------|------|
| POST | /add-user-balance | 管理员给用户充值 |
| POST | /deduct-user-balance | 管理员扣除用户余额 |
| POST | /get-user-balance | 查询用户当前余额 |

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,8 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@repo/core": "1.0.3",
"@repo/sdk": "1.0.14",
"@repo/core": "1.0.4",
"@repo/sdk": "1.0.15",
"@shopify/flash-list": "^2.0.0",
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.5.3",
@@ -655,9 +655,9 @@
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
"@repo/core": ["@repo/core@1.0.3", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.3/core-1.0.3.tgz", {}, "sha512-lO7rk3hsrtoyewZu7cgwlFqjjhGBx+lw4wxkehfvTsbTWm/tKChq1t6SC+XXNJj/YVwnLp6AH8BOsvR4r1nxyg=="],
"@repo/core": ["@repo/core@1.0.4", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.4/core-1.0.4.tgz", {}, "sha512-uht306Xupvs637Ww4EPbCTHw9Gi0g0xbDZpnZHX0Eldw4xGfwAgW1SqC65qNxT2Wi2qkFeBvSBNdP6fB6CuxQg=="],
"@repo/sdk": ["@repo/sdk@1.0.14", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.14/sdk-1.0.14.tgz", { "dependencies": { "@repo/core": "1.0.3", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-J/j3kRL83X4TtGadjLUGX9ueiROStZ0hM+zCPkKtgk5bMUR8K98HefbWzt1PV8jKBp9n+heckyHWHcL3nKDPwA=="],
"@repo/sdk": ["@repo/sdk@1.0.15", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.15/sdk-1.0.15.tgz", { "dependencies": { "@repo/core": "1.0.4", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-QZtMwBQ/SM5m4qFMv63kCktdYhEzpzOrx1HWoQaOgLyV/vnyff5ktRNqmTVkHlPGGZmqlOjJHnpbqV6iQSeoYA=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],

View File

@@ -0,0 +1,373 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import { MessageCard, getMessageIcon, getMessageConfig, formatRelativeTime } from './MessageCard'
// Mock react-i18next
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { language: 'zh-CN' },
}),
}))
describe('MessageCard Utilities', () => {
describe('getMessageIcon', () => {
it('should return celebration icon for TEMPLATE_GENERATION_SUCCESS', () => {
expect(getMessageIcon('TEMPLATE_GENERATION_SUCCESS')).toBe('🎉')
})
it('should return error icon for TEMPLATE_GENERATION_FAILED', () => {
expect(getMessageIcon('TEMPLATE_GENERATION_FAILED')).toBe('❌')
})
it('should return thumbs up icon for TEMPLATE_LIKED', () => {
expect(getMessageIcon('TEMPLATE_LIKED')).toBe('👍')
})
it('should return star icon for TEMPLATE_FAVORITED', () => {
expect(getMessageIcon('TEMPLATE_FAVORITED')).toBe('⭐')
})
it('should return comment icon for TEMPLATE_COMMENTED', () => {
expect(getMessageIcon('TEMPLATE_COMMENTED')).toBe('💬')
})
it('should return comment icon for COMMENT_REPLIED', () => {
expect(getMessageIcon('COMMENT_REPLIED')).toBe('💬')
})
it('should return money icon for CREDITS_DEDUCTED', () => {
expect(getMessageIcon('CREDITS_DEDUCTED')).toBe('💰')
})
it('should return money icon for CREDITS_REFUNDED', () => {
expect(getMessageIcon('CREDITS_REFUNDED')).toBe('💰')
})
it('should return money icon for CREDITS_RECHARGED', () => {
expect(getMessageIcon('CREDITS_RECHARGED')).toBe('💰')
})
it('should return announcement icon for ANNOUNCEMENT', () => {
expect(getMessageIcon('ANNOUNCEMENT')).toBe('📢')
})
it('should return default icon for unknown subType', () => {
expect(getMessageIcon('UNKNOWN_TYPE')).toBe('📩')
})
it('should return default icon for undefined subType', () => {
expect(getMessageIcon(undefined)).toBe('📩')
})
})
describe('getMessageConfig', () => {
it('should return showPreview true for TEMPLATE_GENERATION_SUCCESS', () => {
const config = getMessageConfig('TEMPLATE_GENERATION_SUCCESS')
expect(config.showPreview).toBe(true)
expect(config.showAvatar).toBe(false)
})
it('should return showAvatar true for TEMPLATE_LIKED', () => {
const config = getMessageConfig('TEMPLATE_LIKED')
expect(config.showAvatar).toBe(true)
expect(config.showPreview).toBe(false)
})
it('should return showAvatar true for TEMPLATE_FAVORITED', () => {
const config = getMessageConfig('TEMPLATE_FAVORITED')
expect(config.showAvatar).toBe(true)
})
it('should return showAvatar true for TEMPLATE_COMMENTED', () => {
const config = getMessageConfig('TEMPLATE_COMMENTED')
expect(config.showAvatar).toBe(true)
})
it('should return showQuote true for COMMENT_REPLIED', () => {
const config = getMessageConfig('COMMENT_REPLIED')
expect(config.showQuote).toBe(true)
expect(config.showAvatar).toBe(true)
})
it('should return default config for unknown subType', () => {
const config = getMessageConfig('UNKNOWN_TYPE')
expect(config.showPreview).toBe(false)
expect(config.showAvatar).toBe(false)
expect(config.showQuote).toBe(false)
})
})
describe('formatRelativeTime', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2026-01-29T12:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
it('should return "刚刚" for time within 1 minute', () => {
const now = new Date()
const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000)
expect(formatRelativeTime(thirtySecondsAgo)).toBe('刚刚')
})
it('should return "X分钟前" for time within 1 hour', () => {
const now = new Date()
const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000)
expect(formatRelativeTime(tenMinutesAgo)).toBe('10分钟前')
})
it('should return "X小时前" for time within 24 hours', () => {
const now = new Date()
const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1000)
expect(formatRelativeTime(threeHoursAgo)).toBe('3小时前')
})
it('should return "昨天 HH:MM" for time within 48 hours', () => {
const now = new Date()
const yesterday = new Date(now.getTime() - 25 * 60 * 60 * 1000)
const result = formatRelativeTime(yesterday)
expect(result).toMatch(/^昨天 \d{2}:\d{2}$/)
})
it('should return formatted date for older times', () => {
const now = new Date()
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
const result = formatRelativeTime(threeDaysAgo)
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
})
it('should handle string date input', () => {
const result = formatRelativeTime('2026-01-29T11:50:00Z')
expect(result).toBe('10分钟前')
})
})
})
describe('MessageCard Component', () => {
const mockMessage = {
id: 'msg-123',
type: 'ACTIVITY' as const,
title: '收到新点赞',
content: '小明 赞了你的作品「风景照」',
data: {
subType: 'TEMPLATE_LIKED',
likerName: '小明',
likerAvatar: 'https://example.com/avatar.jpg',
templateId: 'tpl-456',
templateTitle: '风景照',
},
isRead: false,
createdAt: new Date('2026-01-29T11:50:00Z'),
}
const mockOnPress = jest.fn()
const mockOnDelete = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
jest.setSystemTime(new Date('2026-01-29T12:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
it('should render message title', () => {
const { getByText } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByText('收到新点赞')).toBeTruthy()
})
it('should render message content', () => {
const { getByText } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByText('小明 赞了你的作品「风景照」')).toBeTruthy()
})
it('should render relative time', () => {
const { getByText } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByText('10分钟前')).toBeTruthy()
})
it('should show unread indicator when isRead is false', () => {
const { getByTestId } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByTestId('unread-indicator')).toBeTruthy()
})
it('should not show unread indicator when isRead is true', () => {
const readMessage = { ...mockMessage, isRead: true }
const { queryByTestId } = render(
<MessageCard message={readMessage} onPress={mockOnPress} />
)
expect(queryByTestId('unread-indicator')).toBeNull()
})
it('should call onPress when card is pressed', () => {
const { getByTestId } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
fireEvent.press(getByTestId('message-card'))
expect(mockOnPress).toHaveBeenCalledWith(mockMessage)
})
it('should render message icon based on subType', () => {
const { getByText } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByText('👍')).toBeTruthy()
})
describe('Avatar Display', () => {
it('should show avatar for TEMPLATE_LIKED messages', () => {
const { getByTestId } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(getByTestId('user-avatar')).toBeTruthy()
})
it('should not show avatar for CREDITS_RECHARGED messages', () => {
const billingMessage = {
...mockMessage,
type: 'BILLING' as const,
data: { subType: 'CREDITS_RECHARGED', credits: 1000 },
}
const { queryByTestId } = render(
<MessageCard message={billingMessage} onPress={mockOnPress} />
)
expect(queryByTestId('user-avatar')).toBeNull()
})
})
describe('Preview Image Display', () => {
it('should show preview image for TEMPLATE_GENERATION_SUCCESS', () => {
const successMessage = {
...mockMessage,
type: 'SYSTEM' as const,
data: {
subType: 'TEMPLATE_GENERATION_SUCCESS',
webpPreviewUrl: 'https://example.com/preview.webp',
},
}
const { getByTestId } = render(
<MessageCard message={successMessage} onPress={mockOnPress} />
)
expect(getByTestId('preview-image')).toBeTruthy()
})
it('should not show preview image when webpPreviewUrl is missing', () => {
const successMessage = {
...mockMessage,
type: 'SYSTEM' as const,
data: { subType: 'TEMPLATE_GENERATION_SUCCESS' },
}
const { queryByTestId } = render(
<MessageCard message={successMessage} onPress={mockOnPress} />
)
expect(queryByTestId('preview-image')).toBeNull()
})
})
describe('Quote Display', () => {
it('should show quote for COMMENT_REPLIED messages', () => {
const replyMessage = {
...mockMessage,
data: {
subType: 'COMMENT_REPLIED',
originalComment: '太棒了!',
likerAvatar: 'https://example.com/avatar.jpg',
},
}
const { getByTestId, getByText } = render(
<MessageCard message={replyMessage} onPress={mockOnPress} />
)
expect(getByTestId('quote-container')).toBeTruthy()
expect(getByText('太棒了!')).toBeTruthy()
})
})
describe('Swipe to Delete', () => {
it('should call onDelete when delete action is triggered', () => {
const { getByTestId } = render(
<MessageCard
message={mockMessage}
onPress={mockOnPress}
onDelete={mockOnDelete}
/>
)
// Simulate swipe delete action
const deleteButton = getByTestId('delete-button')
fireEvent.press(deleteButton)
expect(mockOnDelete).toHaveBeenCalledWith(mockMessage.id)
})
it('should not render delete button when onDelete is not provided', () => {
const { queryByTestId } = render(
<MessageCard message={mockMessage} onPress={mockOnPress} />
)
expect(queryByTestId('delete-button')).toBeNull()
})
})
})
describe('MessageCard Types', () => {
it('should accept Message type with all required fields', () => {
const message = {
id: 'msg-123',
type: 'SYSTEM' as const,
title: 'Test Title',
content: 'Test Content',
isRead: false,
createdAt: new Date(),
}
expect(message.id).toBeDefined()
expect(message.type).toBeDefined()
expect(message.title).toBeDefined()
expect(message.content).toBeDefined()
expect(message.isRead).toBeDefined()
expect(message.createdAt).toBeDefined()
})
it('should accept Message type with optional data field', () => {
const message = {
id: 'msg-123',
type: 'ACTIVITY' as const,
title: 'Test Title',
content: 'Test Content',
data: {
subType: 'TEMPLATE_LIKED',
likerName: '小明',
},
isRead: false,
createdAt: new Date(),
}
expect(message.data).toBeDefined()
expect(message.data?.subType).toBe('TEMPLATE_LIKED')
})
it('should handle all message types', () => {
const types = ['SYSTEM', 'ACTIVITY', 'BILLING', 'MARKETING'] as const
types.forEach((type) => {
const message = {
id: 'msg-123',
type,
title: 'Test',
content: 'Test',
isRead: false,
createdAt: new Date(),
}
expect(message.type).toBe(type)
})
})
})

View File

@@ -0,0 +1,293 @@
import React from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
Image,
} from 'react-native'
export interface MessageData {
subType?: string
templateId?: string
templateTitle?: string
generationId?: string
resultUrl?: string[]
webpPreviewUrl?: string
likerId?: string
likerName?: string
likerAvatar?: string
commentId?: string
credits?: number
balanceAfter?: number
originalComment?: string
}
export interface Message {
id: string
type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING'
title: string
content: string
data?: MessageData
link?: string
priority?: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW'
isRead: boolean
readAt?: string
createdAt: Date | string
}
interface MessageCardProps {
message: Message
onPress: (message: Message) => void
onDelete?: (id: string) => void
}
const MESSAGE_TYPE_CONFIG: Record<string, {
icon: string
showPreview: boolean
showAvatar: boolean
showQuote: boolean
}> = {
TEMPLATE_GENERATION_SUCCESS: { icon: '🎉', showPreview: true, showAvatar: false, showQuote: false },
TEMPLATE_GENERATION_FAILED: { icon: '❌', showPreview: false, showAvatar: false, showQuote: false },
TEMPLATE_LIKED: { icon: '👍', showPreview: false, showAvatar: true, showQuote: false },
TEMPLATE_FAVORITED: { icon: '⭐', showPreview: false, showAvatar: true, showQuote: false },
TEMPLATE_COMMENTED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: false },
COMMENT_REPLIED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: true },
CREDITS_DEDUCTED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
CREDITS_REFUNDED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
CREDITS_RECHARGED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false },
ANNOUNCEMENT: { icon: '📢', showPreview: false, showAvatar: false, showQuote: false },
}
const DEFAULT_CONFIG = { icon: '📩', showPreview: false, showAvatar: false, showQuote: false }
export const getMessageIcon = (subType?: string): string => {
if (!subType) return DEFAULT_CONFIG.icon
return MESSAGE_TYPE_CONFIG[subType]?.icon ?? DEFAULT_CONFIG.icon
}
export const getMessageConfig = (subType?: string) => {
if (!subType) return DEFAULT_CONFIG
return MESSAGE_TYPE_CONFIG[subType] ?? DEFAULT_CONFIG
}
export const formatRelativeTime = (date: Date | string): string => {
const now = new Date()
const targetDate = typeof date === 'string' ? new Date(date) : date
const diffMs = now.getTime() - targetDate.getTime()
const diffMinutes = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMinutes < 1) {
return '刚刚'
}
if (diffMinutes < 60) {
return `${diffMinutes}分钟前`
}
if (diffHours < 24) {
return `${diffHours}小时前`
}
if (diffDays < 2) {
const hours = String(targetDate.getHours()).padStart(2, '0')
const minutes = String(targetDate.getMinutes()).padStart(2, '0')
return `昨天 ${hours}:${minutes}`
}
const year = targetDate.getFullYear()
const month = String(targetDate.getMonth() + 1).padStart(2, '0')
const day = String(targetDate.getDate()).padStart(2, '0')
const hours = String(targetDate.getHours()).padStart(2, '0')
const minutes = String(targetDate.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
export const MessageCard: React.FC<MessageCardProps> = ({
message,
onPress,
onDelete,
}) => {
const config = getMessageConfig(message.data?.subType)
const icon = getMessageIcon(message.data?.subType)
const showAvatar = config.showAvatar && message.data?.likerAvatar
const showPreview = config.showPreview && message.data?.webpPreviewUrl
const showQuote = config.showQuote && message.data?.originalComment
const handlePress = () => {
onPress(message)
}
const handleDelete = () => {
if (onDelete) {
onDelete(message.id)
}
}
return (
<Pressable
testID="message-card"
style={styles.container}
onPress={handlePress}
>
{/* Unread Indicator */}
{!message.isRead && (
<View testID="unread-indicator" style={styles.unreadIndicatorContainer}>
<View style={styles.unreadIndicator} />
</View>
)}
<View style={styles.contentWrapper}>
{/* Left: Icon or Avatar */}
<View style={styles.leftSection}>
{showAvatar ? (
<Image
testID="user-avatar"
source={{ uri: message.data?.likerAvatar }}
style={styles.avatar}
/>
) : (
<Text style={styles.icon}>{icon}</Text>
)}
</View>
{/* Middle: Content */}
<View style={styles.middleSection}>
<Text style={styles.title}>{message.title}</Text>
<Text style={styles.content} numberOfLines={2}>
{message.content}
</Text>
{/* Preview Image */}
{showPreview && (
<Image
testID="preview-image"
source={{ uri: message.data?.webpPreviewUrl }}
style={styles.previewImage}
/>
)}
{/* Quote */}
{showQuote && (
<View testID="quote-container" style={styles.quoteContainer}>
<Text style={styles.quoteText} numberOfLines={2}>
{message.data?.originalComment}
</Text>
</View>
)}
<Text style={styles.time}>
{formatRelativeTime(message.createdAt)}
</Text>
</View>
{/* Right: Delete Button (if onDelete provided) */}
{onDelete && (
<Pressable
testID="delete-button"
style={styles.deleteButton}
onPress={handleDelete}
>
<Text style={styles.deleteText}></Text>
</Pressable>
)}
</View>
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#16181B',
borderRadius: 16,
padding: 16,
marginBottom: 12,
marginHorizontal: 4,
position: 'relative',
},
unreadIndicatorContainer: {
position: 'absolute',
top: -4,
right: -2,
width: 16,
height: 16,
borderWidth: 4,
borderColor: '#090A0B',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
unreadIndicator: {
width: 6,
height: 6,
borderRadius: 4,
backgroundColor: '#00FF66',
},
contentWrapper: {
flexDirection: 'row',
alignItems: 'flex-start',
},
leftSection: {
marginRight: 12,
},
icon: {
fontSize: 24,
},
avatar: {
width: 40,
height: 40,
borderRadius: 20,
},
middleSection: {
flex: 1,
},
title: {
color: '#FFFFFF',
fontSize: 15,
fontWeight: '600',
marginBottom: 4,
},
content: {
color: '#ABABAB',
fontSize: 13,
marginBottom: 8,
lineHeight: 18,
},
previewImage: {
width: 80,
height: 80,
borderRadius: 8,
marginBottom: 8,
},
quoteContainer: {
backgroundColor: '#26292E',
borderRadius: 8,
padding: 8,
marginBottom: 8,
},
quoteText: {
color: '#8A8A8A',
fontSize: 12,
},
time: {
color: '#8A8A8A',
fontSize: 11,
},
deleteButton: {
backgroundColor: '#FF3B30',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
marginLeft: 8,
},
deleteText: {
color: '#FFFFFF',
fontSize: 12,
},
})
export default MessageCard

View File

@@ -0,0 +1,104 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import { SwipeToDelete } from './SwipeToDelete'
describe('SwipeToDelete Component', () => {
const mockOnDelete = jest.fn()
const mockChildren = <></>
beforeEach(() => {
jest.clearAllMocks()
})
it('should render children', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
expect(getByTestId('swipe-container')).toBeTruthy()
})
it('should render delete button', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
expect(getByTestId('swipe-delete-button')).toBeTruthy()
})
it('should call onDelete when delete button is pressed', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
fireEvent.press(getByTestId('swipe-delete-button'))
expect(mockOnDelete).toHaveBeenCalledTimes(1)
})
it('should render with custom delete text', () => {
const { getByText } = render(
<SwipeToDelete onDelete={mockOnDelete} deleteText="移除">
<></>
</SwipeToDelete>
)
expect(getByText('移除')).toBeTruthy()
})
it('should render default delete text when not provided', () => {
const { getByText } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
expect(getByText('删除')).toBeTruthy()
})
it('should be disabled when disabled prop is true', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete} disabled>
<></>
</SwipeToDelete>
)
fireEvent.press(getByTestId('swipe-delete-button'))
expect(mockOnDelete).not.toHaveBeenCalled()
})
it('should apply custom style to container', () => {
const customStyle = { marginTop: 10 }
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete} style={customStyle}>
<></>
</SwipeToDelete>
)
const container = getByTestId('swipe-container')
expect(container).toBeTruthy()
})
})
describe('SwipeToDelete Accessibility', () => {
const mockOnDelete = jest.fn()
it('should have accessible delete button', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
const deleteButton = getByTestId('swipe-delete-button')
expect(deleteButton.props.accessibilityRole).toBe('button')
})
it('should have accessibility label on delete button', () => {
const { getByTestId } = render(
<SwipeToDelete onDelete={mockOnDelete}>
<></>
</SwipeToDelete>
)
const deleteButton = getByTestId('swipe-delete-button')
expect(deleteButton.props.accessibilityLabel).toBe('删除')
})
})

View File

@@ -5,29 +5,51 @@ jest.mock('react-native-css-interop', () => ({
// Mock react-native BEFORE any other imports to avoid flow type issues
jest.mock('react-native', () => {
const React = require('react')
const mockReact = require('react')
// Helper to create mock components that render as actual React elements
const mockCreateMockComponent = (mockName) => {
const MockComponent = mockReact.forwardRef(({ children, ...mockProps }, mockRef) => {
return mockReact.createElement(mockName, { ...mockProps, ref: mockRef }, children)
})
MockComponent.displayName = mockName
return MockComponent
}
// Create Pressable with onPress support
const MockPressable = mockReact.forwardRef(({ children, onPress, ...mockProps }, mockRef) => {
return mockReact.createElement('Pressable', { ...mockProps, ref: mockRef, onPress, onClick: onPress }, children)
})
MockPressable.displayName = 'Pressable'
// Create TouchableOpacity with onPress support
const MockTouchableOpacity = mockReact.forwardRef(({ children, onPress, ...mockProps }, mockRef) => {
return mockReact.createElement('TouchableOpacity', { ...mockProps, ref: mockRef, onPress, onClick: onPress }, children)
})
MockTouchableOpacity.displayName = 'TouchableOpacity'
return {
RefreshControl: 'RefreshControl',
ScrollView: 'ScrollView',
FlatList: 'FlatList',
View: 'View',
Text: 'Text',
Image: 'Image',
Pressable: 'Pressable',
TouchableOpacity: 'TouchableOpacity',
TextInput: 'TextInput',
ActivityIndicator: 'ActivityIndicator',
RefreshControl: mockCreateMockComponent('RefreshControl'),
ScrollView: mockCreateMockComponent('ScrollView'),
FlatList: mockCreateMockComponent('FlatList'),
View: mockCreateMockComponent('View'),
Text: mockCreateMockComponent('Text'),
Image: mockCreateMockComponent('Image'),
Pressable: MockPressable,
TouchableOpacity: MockTouchableOpacity,
TextInput: mockCreateMockComponent('TextInput'),
ActivityIndicator: mockCreateMockComponent('ActivityIndicator'),
Platform: {
OS: 'web',
select: (obj) => obj.web || obj.default,
select: (mockObj) => mockObj.web || mockObj.default,
},
Animated: {
View: 'Animated.View',
Text: 'Animated.Text',
Image: 'Animated.Image',
View: mockCreateMockComponent('Animated.View'),
Text: mockCreateMockComponent('Animated.Text'),
Image: mockCreateMockComponent('Animated.Image'),
Value: class MockValue {
constructor(v) { this._value = v }
setValue(v) { this._value = v }
constructor(mockV) { this._value = mockV }
setValue(mockV) { this._value = mockV }
__getValue() { return this._value }
interpolate() { return this }
},
@@ -38,11 +60,10 @@ jest.mock('react-native', () => {
PanResponder: {
create: () => ({ panHandlers: {} }),
},
ActivityIndicator: 'ActivityIndicator',
StyleSheet: { create: (styles) => styles },
StyleSheet: { create: (mockStyles) => mockStyles },
Dimensions: { get: () => ({ width: 375, height: 812 }) },
StatusBar: 'StatusBar',
SafeAreaView: 'SafeAreaView',
StatusBar: mockCreateMockComponent('StatusBar'),
SafeAreaView: mockCreateMockComponent('SafeAreaView'),
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
NativeModules: {
DevMenu: {},

View File

@@ -31,8 +31,8 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@repo/core": "1.0.3",
"@repo/sdk": "1.0.14",
"@repo/core": "1.0.4",
"@repo/sdk": "1.0.15",
"@shopify/flash-list": "^2.0.0",
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.5.3",