fix: Phase 2 可运行性保障 - 修复JWT用户ID字段不一致、添加缺失的Auth Guard、修复测试
关键修复: - req.user?.id → req.user?.userId: Order/Notification/Payment 3个Controller共18处 - NotificationController/PaymentController 添加 @UseGuards(JwtAuthGuard) - alipay/wechat auth 测试: oauthAccount → oAuthAccount, 补全mock数据 - order.controller.spec.ts: mockRequest.user.id → userId - OpenTelemetry 改为可选加载 (OTEL_ENABLED) - 前端: telemetry.ts改为no-op, web-vitals v4 API, Tailwind v4兼容 - Docker: postgres使用本地镜像, DATABASE_URL匹配docker-compose - 布局: (auth)/(dashboard)分离, Sidebar仅Dashboard显示 验证结果: - 后端331测试全部通过 - 前端构建成功(20页面) - API端到端冒烟验证12个端点全部正常 - Docker PG+Redis启动, Prisma Migration 30张表创建成功
This commit is contained in:
parent
3d867331ae
commit
bdb509a611
85
.env.example
85
.env.example
|
|
@ -1,25 +1,88 @@
|
|||
NODE_ENV=development
|
||||
|
||||
DB_HOST=postgres
|
||||
# API Server
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
|
||||
# Database (Prisma)
|
||||
DATABASE_URL="postgresql://fischerx:fischerx@localhost:5432/fischerx?schema=public"
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=fischerx
|
||||
DB_PASSWORD=fischerx
|
||||
DB_NAME=fischerx
|
||||
|
||||
REDIS_HOST=redis
|
||||
# Redis
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
# JWT
|
||||
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
|
||||
JWT_EXPIRATION="7d"
|
||||
|
||||
# Frontend
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
# Storage Configuration
|
||||
STORAGE_TYPE="local"
|
||||
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID=
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET=
|
||||
OSS_REGION=
|
||||
OSS_BUCKET=
|
||||
OSS_ENDPOINT=
|
||||
# Local Storage
|
||||
LOCAL_STORAGE_PATH="./uploads"
|
||||
LOCAL_STORAGE_URL="http://localhost:3001/uploads"
|
||||
|
||||
# Aliyun OSS
|
||||
ALIYUN_OSS_REGION="oss-cn-hangzhou"
|
||||
ALIYUN_OSS_ACCESS_KEY_ID="your-aliyun-access-key-id"
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET="your-aliyun-access-key-secret"
|
||||
ALIYUN_OSS_BUCKET="fischerx"
|
||||
ALIYUN_OSS_ENDPOINT="https://oss-cn-hangzhou.aliyuncs.com"
|
||||
ALIYUN_OSS_INTERNAL=false
|
||||
ALIYUN_OSS_SECURE=true
|
||||
|
||||
# Tencent COS
|
||||
TENCENT_COS_SECRET_ID="your-tencent-secret-id"
|
||||
TENCENT_COS_SECRET_KEY="your-tencent-secret-key"
|
||||
TENCENT_COS_BUCKET="fischerx-1234567890"
|
||||
TENCENT_COS_REGION="ap-guangzhou"
|
||||
|
||||
# MinIO
|
||||
MINIO_END_POINT="localhost"
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY="minioadmin"
|
||||
MINIO_SECRET_KEY="minioadmin"
|
||||
MINIO_BUCKET="fischerx"
|
||||
|
||||
# CDN
|
||||
CDN_ENABLED=false
|
||||
CDN_BASE_URL="https://cdn.yourdomain.com"
|
||||
|
||||
# Upload Settings
|
||||
UPLOAD_MAX_FILE_SIZE=104857600
|
||||
UPLOAD_ALLOWED_MIME_TYPES="*/*"
|
||||
|
||||
# Notification - Mock Mode (set to "true" in development to skip actual sending)
|
||||
NOTIFICATION_MOCK_MODE="true"
|
||||
|
||||
# Notification - SMTP Email
|
||||
SMTP_HOST="smtpdm.aliyun.com"
|
||||
SMTP_PORT="465"
|
||||
SMTP_USER="noreply@yourdomain.com"
|
||||
SMTP_PASS="your-smtp-password"
|
||||
SMTP_FROM='"FischerX" <noreply@yourdomain.com>'
|
||||
|
||||
# Notification - Aliyun SMS
|
||||
ALIYUN_ACCESS_KEY_ID="your-aliyun-access-key-id"
|
||||
ALIYUN_ACCESS_KEY_SECRET="your-aliyun-access-key-secret"
|
||||
ALIYUN_SMS_SIGN_NAME="FischerX"
|
||||
ALIYUN_SMS_TEMPLATE_CODE_VERIFY="SMS_123456"
|
||||
|
||||
# Notification - WeCom Robot Webhook
|
||||
WECOM_WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-default-key"
|
||||
WECOM_WEBHOOK_URL_ALERT="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-alert-key"
|
||||
|
||||
# OpenTelemetry (optional, set to "true" to enable)
|
||||
OTEL_ENABLED=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fischerx/types": "workspace:*",
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.100.13",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export default function AuthLayout({
|
|||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
import { Navbar } from "@/components/layout/navbar"
|
||||
import { Sidebar } from "@/components/layout/sidebar"
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
<div className="flex flex-1">
|
||||
<Sidebar />
|
||||
<main className="flex-1 md:ml-64">
|
||||
<div className="container py-6">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default function ProfilePage() {
|
|||
|
||||
const updatedUser = response.data.data;
|
||||
setUser({
|
||||
...user,
|
||||
...user!,
|
||||
avatar: updatedUser.avatar,
|
||||
});
|
||||
setMessage('Avatar updated successfully');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { Metadata } from "next"
|
|||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { QueryProvider } from "@/providers/query-provider"
|
||||
import { Navbar, Sidebar } from "@/components/layout"
|
||||
import { ErrorBoundary } from "@/components/error-boundary"
|
||||
import { MonitoringProvider } from "@/providers/monitoring-provider"
|
||||
|
||||
|
|
@ -27,20 +26,12 @@ export default function RootLayout({
|
|||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} suppressHydrationWarning>
|
||||
<html lang="zh-CN" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} suppressHydrationWarning>
|
||||
<body className="min-h-full">
|
||||
<MonitoringProvider>
|
||||
<QueryProvider>
|
||||
<ErrorBoundary>
|
||||
<div className="relative flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
<div className="flex flex-1">
|
||||
<Sidebar />
|
||||
<main className="flex-1 md:ml-64">
|
||||
<div className="container py-6">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</QueryProvider>
|
||||
</MonitoringProvider>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { useState } from 'react';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useDownloadFile, useDeleteFile } from '@/hooks/use-files';
|
||||
import type { File } from '@fischerx/types';
|
||||
import type { File as FileType } from '@fischerx/types';
|
||||
|
||||
interface FilePreviewProps {
|
||||
file: File;
|
||||
file: FileType;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { fileApi } from '@/lib/file-api';
|
||||
import type { File, FileUploadOptions, FileListParams, ProcessImageOptions } from '@fischerx/types';
|
||||
import type { File as FileType, FileUploadOptions, FileListParams, ProcessImageOptions } from '@fischerx/types';
|
||||
|
||||
export const useFiles = (params?: FileListParams) => {
|
||||
return useQuery({
|
||||
|
|
@ -21,7 +21,7 @@ export const useUploadFile = () => {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ file, options }: { file: File | Blob; options?: FileUploadOptions }) =>
|
||||
mutationFn: ({ file, options }: { file: globalThis.File | Blob; options?: FileUploadOptions }) =>
|
||||
fileApi.uploadFile(file, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
|
|
@ -33,7 +33,7 @@ export const useUploadMultipleFiles = () => {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ files, options }: { files: (File | Blob)[]; options?: FileUploadOptions }) =>
|
||||
mutationFn: ({ files, options }: { files: (globalThis.File | Blob)[]; options?: FileUploadOptions }) =>
|
||||
fileApi.uploadMultipleFiles(files, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
|
|
@ -45,7 +45,7 @@ export const useUpdateFile = () => {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<Pick<File, 'name' | 'category' | 'tags'>> }) =>
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<Pick<FileType, 'name' | 'category' | 'tags'>> }) =>
|
||||
fileApi.updateFile(id, data),
|
||||
onSuccess: (_, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import api from './api';
|
||||
import type { File, FileUploadOptions, FileListParams, ProcessImageOptions } from '@fischerx/types';
|
||||
import type { File as FileType, FileUploadOptions, FileListParams, ProcessImageOptions } from '@fischerx/types';
|
||||
|
||||
export const fileApi = {
|
||||
uploadFile: async (file: File | Blob, options?: FileUploadOptions): Promise<{ success: boolean; data: File }> => {
|
||||
uploadFile: async (file: globalThis.File | Blob, options?: FileUploadOptions): Promise<{ success: boolean; data: FileType }> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (options?.category) formData.append('category', options.category);
|
||||
|
|
@ -18,9 +18,9 @@ export const fileApi = {
|
|||
},
|
||||
|
||||
uploadMultipleFiles: async (
|
||||
files: (File | Blob)[],
|
||||
files: (globalThis.File | Blob)[],
|
||||
options?: FileUploadOptions
|
||||
): Promise<{ success: boolean; data: File[]; errors: Array<{ index: number; error: string }> }> => {
|
||||
): Promise<{ success: boolean; data: FileType[]; errors: Array<{ index: number; error: string }> }> => {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => formData.append('files', file));
|
||||
if (options?.category) formData.append('category', options.category);
|
||||
|
|
@ -35,12 +35,12 @@ export const fileApi = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
listFiles: async (params?: FileListParams): Promise<{ success: boolean; data: File[]; meta: { page: number; limit: number; total: number } }> => {
|
||||
listFiles: async (params?: FileListParams): Promise<{ success: boolean; data: FileType[]; meta: { page: number; limit: number; total: number } }> => {
|
||||
const response = await api.get('/files', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getFile: async (id: string): Promise<{ success: boolean; data: File }> => {
|
||||
getFile: async (id: string): Promise<{ success: boolean; data: FileType }> => {
|
||||
const response = await api.get(`/files/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
|
@ -65,7 +65,7 @@ export const fileApi = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
updateFile: async (id: string, data: Partial<Pick<File, 'name' | 'category' | 'tags'>>): Promise<{ success: boolean; data: File }> => {
|
||||
updateFile: async (id: string, data: Partial<Pick<FileType, 'name' | 'category' | 'tags'>>): Promise<{ success: boolean; data: FileType }> => {
|
||||
const response = await api.put(`/files/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,38 +1,7 @@
|
|||
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
||||
import { SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
|
||||
import { ZoneContextManager } from '@opentelemetry/context-zone';
|
||||
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
||||
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
||||
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { Resource } from '@opentelemetry/resources';
|
||||
export const initTelemetry = async () => {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
const endpoint = process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT;
|
||||
if (!endpoint) return;
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const resource = new Resource({
|
||||
'service.name': 'fischerx-web',
|
||||
});
|
||||
|
||||
const provider = new WebTracerProvider({ resource });
|
||||
|
||||
const traceExporter = isProduction
|
||||
? new OTLPTraceExporter({
|
||||
url: process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
|
||||
})
|
||||
: new ConsoleSpanExporter();
|
||||
|
||||
provider.addSpanProcessor(new SimpleSpanProcessor(traceExporter));
|
||||
provider.register({
|
||||
contextManager: new ZoneContextManager(),
|
||||
});
|
||||
|
||||
registerInstrumentations({
|
||||
instrumentations: [
|
||||
new FetchInstrumentation({
|
||||
ignoreUrls: ['/health', '/metrics'],
|
||||
}),
|
||||
new XMLHttpRequestInstrumentation(),
|
||||
],
|
||||
});
|
||||
|
||||
export const tracer = provider.getTracer('fischerx-web');
|
||||
console.warn('OpenTelemetry packages not installed. Install @opentelemetry/* packages to enable tracing.');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { ReportCallback } from 'web-vitals';
|
||||
|
||||
export const reportWebVitals = (onPerfEntry?: ReportCallback) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
export const reportWebVitals = (onPerfEntry?: (metric: any) => void) => {
|
||||
if (!onPerfEntry) return;
|
||||
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
|
||||
onCLS(onPerfEntry);
|
||||
onINP(onPerfEntry);
|
||||
onFCP(onPerfEntry);
|
||||
onLCP(onPerfEntry);
|
||||
onTTFB(onPerfEntry);
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
export const logVitals = (metric: any) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useEffect, ReactNode } from 'react';
|
||||
import { logVitals, reportWebVitals } from '@/lib/web-vitals';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
|
@ -9,12 +8,16 @@ interface Props {
|
|||
|
||||
export function MonitoringProvider({ children }: Props) {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
import('@/lib/web-vitals').then(({ reportWebVitals, logVitals }) => {
|
||||
reportWebVitals(logVitals);
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
import('@/lib/telemetry').catch(console.error);
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
|
||||
import('@/lib/telemetry').then(m => m.initTelemetry()).catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
}
|
||||
import { User } from '@/lib/user-api'
|
||||
|
||||
interface UserState {
|
||||
user: User | null
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["react", "react-dom", "node"]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
build:
|
||||
context: ./infra/db
|
||||
dockerfile: Dockerfile
|
||||
image: postgres:15-alpine
|
||||
container_name: fischerx-postgres
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-fischerx}
|
||||
|
|
@ -14,6 +10,7 @@ services:
|
|||
- "${DB_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- ./infra/db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- fischerx-network
|
||||
healthcheck:
|
||||
|
|
|
|||
|
|
@ -30,10 +30,55 @@ export interface File {
|
|||
mimeType: string;
|
||||
size: number;
|
||||
url: string;
|
||||
cdnUrl?: string;
|
||||
storageType: 'aliyun' | 'tencent' | 'minio' | 'local';
|
||||
bucket: string;
|
||||
path: string;
|
||||
fileHash?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
accessCount: number;
|
||||
lastAccessedAt?: Date;
|
||||
ownerId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
export interface FileUploadOptions {
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
}
|
||||
export interface FileListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
category?: string;
|
||||
search?: string;
|
||||
sortBy?: 'createdAt' | 'size' | 'name';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
export interface ProcessImageOptions {
|
||||
compress?: {
|
||||
quality: number;
|
||||
};
|
||||
resize?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
|
||||
};
|
||||
crop?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
watermark?: {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
color?: string;
|
||||
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
|
||||
opacity?: number;
|
||||
};
|
||||
}
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
|
|
@ -63,4 +108,3 @@ export interface AuthResponse {
|
|||
refreshToken: string;
|
||||
user: User;
|
||||
}
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
4370
pnpm-lock.yaml
4370
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,5 @@
|
|||
# Database
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fischerx?schema=public"
|
||||
DATABASE_URL="postgresql://fischerx:fischerx@localhost:5432/fischerx?schema=public"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
|
||||
|
|
@ -73,3 +73,6 @@ ALIYUN_SMS_TEMPLATE_CODE_VERIFY="SMS_123456"
|
|||
# Notification - WeCom Robot Webhook
|
||||
WECOM_WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-default-key"
|
||||
WECOM_WEBHOOK_URL_ALERT="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-alert-key"
|
||||
|
||||
OTEL_ENABLED=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
|
|
|
|||
|
|
@ -0,0 +1,848 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PAID', 'SHIPPED', 'COMPLETED', 'CANCELLED', 'REFUNDED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"avatar" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"phoneVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lastLoginAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"mfaSecret" TEXT,
|
||||
"mfaEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"loginAttempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"lockedUntil" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "roles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"resource" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_roles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "role_permissions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"roleId" TEXT NOT NULL,
|
||||
"permissionId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "files" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"originalName" TEXT NOT NULL,
|
||||
"mimeType" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"url" TEXT,
|
||||
"cdnUrl" TEXT,
|
||||
"storageType" TEXT NOT NULL DEFAULT 'local',
|
||||
"bucket" TEXT,
|
||||
"fileHash" TEXT,
|
||||
"category" TEXT,
|
||||
"tags" TEXT[],
|
||||
"isPublic" BOOLEAN NOT NULL DEFAULT false,
|
||||
"accessCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastAccessedAt" TIMESTAMP(3),
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "files_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"refreshToken" TEXT,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"refreshExpiresAt" TIMESTAMP(3),
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"deviceName" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "oauth_accounts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"providerData" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "oauth_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "realname_auth" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"realName" TEXT NOT NULL,
|
||||
"idCardNumber" TEXT NOT NULL,
|
||||
"idCardFrontUrl" TEXT,
|
||||
"idCardBackUrl" TEXT,
|
||||
"faceImageUrl" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"rejectReason" TEXT,
|
||||
"verifiedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "realname_auth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "login_history" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"deviceName" TEXT,
|
||||
"loginMethod" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "login_history_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "payment_channels" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"isEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"isSandbox" BOOLEAN NOT NULL DEFAULT true,
|
||||
"config" JSONB NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "payment_channels_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "payment_orders" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderNo" TEXT NOT NULL,
|
||||
"channelOrderNo" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"amount" DECIMAL(12,2) NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'CNY',
|
||||
"subject" TEXT NOT NULL,
|
||||
"body" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"clientIp" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "payment_orders_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "payment_refunds" (
|
||||
"id" TEXT NOT NULL,
|
||||
"refundNo" TEXT NOT NULL,
|
||||
"channelRefundNo" TEXT,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
"amount" DECIMAL(12,2) NOT NULL,
|
||||
"reason" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"processedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "payment_refunds_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "payment_logs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT,
|
||||
"refundId" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"request" JSONB,
|
||||
"response" JSONB,
|
||||
"status" TEXT NOT NULL,
|
||||
"error" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "payment_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "orders" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderNo" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "OrderStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"totalAmount" DECIMAL(12,2) NOT NULL,
|
||||
"paidAmount" DECIMAL(12,2) NOT NULL,
|
||||
"discountAmount" DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
"paymentMethod" TEXT,
|
||||
"paymentOrderId" TEXT,
|
||||
"remark" TEXT,
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"shippedAt" TIMESTAMP(3),
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "order_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"productName" TEXT NOT NULL,
|
||||
"productImage" TEXT,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"unitPrice" DECIMAL(12,2) NOT NULL,
|
||||
"totalAmount" DECIMAL(12,2) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "order_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notifications" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"contentHtml" TEXT,
|
||||
"channel" TEXT NOT NULL,
|
||||
"channelData" JSONB,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"readAt" TIMESTAMP(3),
|
||||
"priority" TEXT NOT NULL DEFAULT 'normal',
|
||||
"metadata" JSONB,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"templateId" TEXT,
|
||||
"templateVersion" INTEGER,
|
||||
"variables" JSONB,
|
||||
"retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxRetries" INTEGER NOT NULL DEFAULT 3,
|
||||
"errorMessage" TEXT,
|
||||
"deduplicationId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_templates" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"channels" TEXT[],
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_templates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_template_locales" (
|
||||
"id" TEXT NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
"locale" TEXT NOT NULL,
|
||||
"subject" TEXT,
|
||||
"title" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"contentHtml" TEXT,
|
||||
"variables" TEXT[],
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_template_locales_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_channels" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"config" JSONB NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_channels_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_preferences" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"emailEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"smsEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"pushEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"inAppEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"securityEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"marketingEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"systemEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"quietStartHour" INTEGER,
|
||||
"quietEndHour" INTEGER,
|
||||
"timezone" TEXT DEFAULT 'Asia/Shanghai',
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_batches" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"scheduledAt" TIMESTAMP(3),
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"totalCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"sentCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"failedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"creatorId" TEXT,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "notification_batches_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notification_batch_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"batchId" TEXT NOT NULL,
|
||||
"notificationId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "notification_batch_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "articles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"summary" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"contentHtml" TEXT,
|
||||
"coverImage" TEXT,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"categoryId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'draft',
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"likeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"commentCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"isTop" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isRecommend" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"reviewedBy" TEXT,
|
||||
"reviewComment" TEXT,
|
||||
"seoTitle" TEXT,
|
||||
"seoKeywords" TEXT,
|
||||
"seoDescription" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "articles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "article_versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"articleId" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"contentHtml" TEXT,
|
||||
"summary" TEXT,
|
||||
"changeLog" TEXT,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "article_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "categories" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"icon" TEXT,
|
||||
"parentId" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"articleCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"seoTitle" TEXT,
|
||||
"seoKeywords" TEXT,
|
||||
"seoDescription" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tags" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"articleCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "tags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "article_tags" (
|
||||
"id" TEXT NOT NULL,
|
||||
"articleId" TEXT NOT NULL,
|
||||
"tagId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "article_tags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "comments" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"articleId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"parentId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"likeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"reviewedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "comments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_phone_key" ON "users"("phone");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_roles_userId_roleId_key" ON "user_roles"("userId", "roleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "role_permissions_roleId_permissionId_key" ON "role_permissions"("roleId", "permissionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "files_ownerId_idx" ON "files"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "files_storageType_idx" ON "files"("storageType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "files_category_idx" ON "files"("category");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "files_fileHash_idx" ON "files"("fileHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sessions_refreshToken_key" ON "sessions"("refreshToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "oauth_accounts_provider_providerId_key" ON "oauth_accounts"("provider", "providerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "realname_auth_userId_key" ON "realname_auth"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "payment_channels_code_key" ON "payment_channels"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_channels_type_idx" ON "payment_channels"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_channels_isEnabled_idx" ON "payment_channels"("isEnabled");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "payment_orders_orderNo_key" ON "payment_orders"("orderNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_orders_userId_idx" ON "payment_orders"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_orders_orderNo_idx" ON "payment_orders"("orderNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_orders_status_idx" ON "payment_orders"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_orders_createdAt_idx" ON "payment_orders"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "payment_refunds_refundNo_key" ON "payment_refunds"("refundNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_refunds_orderId_idx" ON "payment_refunds"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_refunds_userId_idx" ON "payment_refunds"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_refunds_status_idx" ON "payment_refunds"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_logs_orderId_idx" ON "payment_logs"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_logs_refundId_idx" ON "payment_logs"("refundId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "payment_logs_createdAt_idx" ON "payment_logs"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "orders_orderNo_key" ON "orders"("orderNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "orders_paymentOrderId_key" ON "orders"("paymentOrderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "orders_userId_idx" ON "orders"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "orders_orderNo_idx" ON "orders"("orderNo");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "orders_status_idx" ON "orders"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "orders_createdAt_idx" ON "orders"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "order_items_orderId_idx" ON "order_items"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "order_items_productId_idx" ON "order_items"("productId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_userId_idx" ON "notifications"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_status_idx" ON "notifications"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_channel_idx" ON "notifications"("channel");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_type_idx" ON "notifications"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_priority_idx" ON "notifications"("priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifications_deduplicationId_idx" ON "notifications"("deduplicationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notification_templates_code_key" ON "notification_templates"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_templates_code_idx" ON "notification_templates"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_templates_isActive_idx" ON "notification_templates"("isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_template_locales_locale_idx" ON "notification_template_locales"("locale");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notification_template_locales_templateId_locale_key" ON "notification_template_locales"("templateId", "locale");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_channels_type_idx" ON "notification_channels"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_channels_isActive_idx" ON "notification_channels"("isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notification_preferences_userId_key" ON "notification_preferences"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_batches_status_idx" ON "notification_batches"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_batches_creatorId_idx" ON "notification_batches"("creatorId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_batch_items_batchId_idx" ON "notification_batch_items"("batchId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_batch_items_notificationId_idx" ON "notification_batch_items"("notificationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notification_batch_items_status_idx" ON "notification_batch_items"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "articles_slug_key" ON "articles"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "articles_authorId_idx" ON "articles"("authorId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "articles_categoryId_idx" ON "articles"("categoryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "articles_status_idx" ON "articles"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "articles_publishedAt_idx" ON "articles"("publishedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "articles_slug_idx" ON "articles"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "article_versions_articleId_idx" ON "article_versions"("articleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "article_versions_articleId_version_key" ON "article_versions"("articleId", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "categories_slug_key" ON "categories"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "categories_parentId_idx" ON "categories"("parentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "categories_slug_idx" ON "categories"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "tags_slug_key" ON "tags"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tags_slug_idx" ON "tags"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "article_tags_articleId_idx" ON "article_tags"("articleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "article_tags_tagId_idx" ON "article_tags"("tagId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "article_tags_articleId_tagId_key" ON "article_tags"("articleId", "tagId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "comments_articleId_idx" ON "comments"("articleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "comments_userId_idx" ON "comments"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "comments_parentId_idx" ON "comments"("parentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "comments_status_idx" ON "comments"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "files" ADD CONSTRAINT "files_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "oauth_accounts" ADD CONSTRAINT "oauth_accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "realname_auth" ADD CONSTRAINT "realname_auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_orders" ADD CONSTRAINT "payment_orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_orders" ADD CONSTRAINT "payment_orders_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "payment_channels"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_refunds" ADD CONSTRAINT "payment_refunds_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "payment_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_refunds" ADD CONSTRAINT "payment_refunds_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "payment_channels"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_refunds" ADD CONSTRAINT "payment_refunds_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_logs" ADD CONSTRAINT "payment_logs_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "payment_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "payment_logs" ADD CONSTRAINT "payment_logs_refundId_fkey" FOREIGN KEY ("refundId") REFERENCES "payment_refunds"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "orders" ADD CONSTRAINT "orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "orders" ADD CONSTRAINT "orders_paymentOrderId_fkey" FOREIGN KEY ("paymentOrderId") REFERENCES "payment_orders"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "order_items" ADD CONSTRAINT "order_items_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "notification_templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notification_template_locales" ADD CONSTRAINT "notification_template_locales_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "notification_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notification_batches" ADD CONSTRAINT "notification_batches_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notification_batch_items" ADD CONSTRAINT "notification_batch_items_batchId_fkey" FOREIGN KEY ("batchId") REFERENCES "notification_batches"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notification_batch_items" ADD CONSTRAINT "notification_batch_items_notificationId_fkey" FOREIGN KEY ("notificationId") REFERENCES "notifications"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "articles" ADD CONSTRAINT "articles_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "articles" ADD CONSTRAINT "articles_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "article_versions" ADD CONSTRAINT "article_versions_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "articles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "article_versions" ADD CONSTRAINT "article_versions_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "categories" ADD CONSTRAINT "categories_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "categories"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "article_tags" ADD CONSTRAINT "article_tags_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "articles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "article_tags" ADD CONSTRAINT "article_tags_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "comments" ADD CONSTRAINT "comments_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "articles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "comments" ADD CONSTRAINT "comments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "comments" ADD CONSTRAINT "comments_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "comments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
|
|
@ -37,5 +37,9 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
|||
message,
|
||||
data: null,
|
||||
});
|
||||
|
||||
if (!(exception instanceof HttpException)) {
|
||||
console.error('[Unhandled Exception]', exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import { HttpMetricsInterceptor } from './modules/monitoring/http-metrics.interc
|
|||
import { startTracing } from './tracing';
|
||||
|
||||
async function bootstrap() {
|
||||
if (process.env.OTEL_ENABLED === 'true') {
|
||||
startTracing();
|
||||
}
|
||||
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
|
|
@ -35,6 +37,6 @@ async function bootstrap() {
|
|||
await app.listen(port);
|
||||
console.log(`Application is running on: http://localhost:${port}`);
|
||||
console.log(`Metrics available at: http://localhost:${port}/metrics`);
|
||||
console.log(`Tracing enabled: ${process.env.OTEL_ENABLED !== 'false'}`);
|
||||
console.log(`Tracing enabled: ${process.env.OTEL_ENABLED === 'true'}`);
|
||||
}
|
||||
void bootstrap();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe('AlipayAuthService', () => {
|
|||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
oauthAccount: {
|
||||
oAuthAccount: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
|
|
@ -68,17 +68,20 @@ describe('AlipayAuthService', () => {
|
|||
email: null,
|
||||
phone: null,
|
||||
username: 'alipay_user',
|
||||
password: null,
|
||||
isActive: true,
|
||||
roles: [],
|
||||
roles: [{ role: { name: 'user' } }],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getAlipayUserInfo')
|
||||
.mockResolvedValue(mockAlipayUser);
|
||||
mockPrisma.oauthAccount.findUnique.mockResolvedValue({
|
||||
.mockReturnValue(Promise.resolve(mockAlipayUser));
|
||||
mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
});
|
||||
mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser);
|
||||
mockPrisma.user.update.mockResolvedValue({ ...mockExistingUser, lastLoginAt: new Date() });
|
||||
mockPrisma.loginHistory.create.mockResolvedValue({});
|
||||
mockSessionService.createSession.mockResolvedValue({
|
||||
token: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
|
|
@ -101,17 +104,23 @@ describe('AlipayAuthService', () => {
|
|||
avatar: 'https://example.com/alipay-avatar.jpg',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getAlipayUserInfo')
|
||||
.mockResolvedValue(mockAlipayUser);
|
||||
mockPrisma.oauthAccount.findUnique.mockResolvedValue(null);
|
||||
mockPrisma.user.create.mockResolvedValue({
|
||||
const mockNewUser = {
|
||||
id: 'user-456',
|
||||
username: 'alipay_user_456',
|
||||
password: null,
|
||||
isActive: true,
|
||||
roles: [],
|
||||
});
|
||||
mockPrisma.oauthAccount.create.mockResolvedValue({});
|
||||
roles: [{ role: { name: 'user' } }],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getAlipayUserInfo')
|
||||
.mockReturnValue(Promise.resolve(mockAlipayUser));
|
||||
mockPrisma.oAuthAccount.findUnique.mockResolvedValue(null);
|
||||
mockPrisma.role.findUnique.mockResolvedValue({ id: 'role-user-id' });
|
||||
mockPrisma.user.create.mockResolvedValue(mockNewUser);
|
||||
mockPrisma.oAuthAccount.create.mockResolvedValue({});
|
||||
mockPrisma.user.update.mockResolvedValue({ ...mockNewUser, lastLoginAt: new Date() });
|
||||
mockPrisma.loginHistory.create.mockResolvedValue({});
|
||||
mockSessionService.createSession.mockResolvedValue({
|
||||
token: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
|
|
@ -120,7 +129,7 @@ describe('AlipayAuthService', () => {
|
|||
const result = await service.loginWithAlipay(mockCode, {});
|
||||
|
||||
expect(mockPrisma.user.create).toHaveBeenCalled();
|
||||
expect(mockPrisma.oauthAccount.create).toHaveBeenCalled();
|
||||
expect(mockPrisma.oAuthAccount.create).toHaveBeenCalled();
|
||||
expect(result).toHaveProperty('accessToken');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class AlipayAuthService {
|
|||
) {
|
||||
const alipayUser = await this.getAlipayUserInfo(code);
|
||||
|
||||
const existingAccount = await this.prisma.oauthAccount.findUnique({
|
||||
const existingAccount = await this.prisma.oAuthAccount.findUnique({
|
||||
where: {
|
||||
provider_providerId: {
|
||||
provider: 'alipay',
|
||||
|
|
@ -61,7 +61,7 @@ export class AlipayAuthService {
|
|||
where: { name: 'user' },
|
||||
select: { id: true },
|
||||
})
|
||||
)?.id,
|
||||
)!.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -76,7 +76,7 @@ export class AlipayAuthService {
|
|||
},
|
||||
});
|
||||
|
||||
await this.prisma.oauthAccount.create({
|
||||
await this.prisma.oAuthAccount.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
provider: 'alipay',
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class AuthController {
|
|||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginDto, @Req() req: any) {
|
||||
const identifier = loginDto.email || loginDto.phone;
|
||||
const identifier = loginDto.email || loginDto.phone || '';
|
||||
const user = await this.authService.validateUser(
|
||||
identifier,
|
||||
loginDto.password,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class AuthService {
|
|||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async validateUser(identifier: string, password: string) {
|
||||
async validateUser(identifier: string, inputPassword: string) {
|
||||
let user;
|
||||
|
||||
if (this.isPhone(identifier)) {
|
||||
|
|
@ -37,7 +37,7 @@ export class AuthService {
|
|||
throw new UnauthorizedException('Account is disabled');
|
||||
}
|
||||
|
||||
const passwordValid = await this.comparePassword(password, user.password);
|
||||
const passwordValid = await this.comparePassword(inputPassword, user.password);
|
||||
if (!passwordValid) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export class SessionService {
|
|||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
isActive: true,
|
||||
roles: {
|
||||
select: {
|
||||
role: {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class SmsAuthService {
|
|||
const attemptsKey = `sms:attempts:${phone}`;
|
||||
const attempts = await this.cacheService.get(attemptsKey);
|
||||
|
||||
if (attempts && parseInt(attempts) >= 5) {
|
||||
if (attempts && parseInt(String(attempts)) >= 5) {
|
||||
throw new BadRequestException(
|
||||
'Too many attempts, please try again later',
|
||||
);
|
||||
|
|
@ -65,7 +65,7 @@ export class SmsAuthService {
|
|||
if (storedCode !== code) {
|
||||
const attemptsKey = `sms:attempts:${phone}`;
|
||||
const attempts = await this.cacheService.get(attemptsKey);
|
||||
const newAttempts = attempts ? parseInt(attempts) + 1 : 1;
|
||||
const newAttempts = attempts ? parseInt(String(attempts)) + 1 : 1;
|
||||
|
||||
if (newAttempts >= 5) {
|
||||
await this.cacheService.del(codeKey);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe('WechatAuthService', () => {
|
|||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
oauthAccount: {
|
||||
oAuthAccount: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
|
|
@ -68,17 +68,20 @@ describe('WechatAuthService', () => {
|
|||
email: null,
|
||||
phone: null,
|
||||
username: 'wechat_user',
|
||||
password: null,
|
||||
isActive: true,
|
||||
roles: [],
|
||||
roles: [{ role: { name: 'user' } }],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getWechatUserInfo')
|
||||
.mockResolvedValue(mockWechatUser);
|
||||
mockPrisma.oauthAccount.findUnique.mockResolvedValue({
|
||||
.mockReturnValue(Promise.resolve(mockWechatUser));
|
||||
mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
});
|
||||
mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser);
|
||||
mockPrisma.user.update.mockResolvedValue({ ...mockExistingUser, lastLoginAt: new Date() });
|
||||
mockPrisma.loginHistory.create.mockResolvedValue({});
|
||||
mockSessionService.createSession.mockResolvedValue({
|
||||
token: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
|
|
@ -101,17 +104,23 @@ describe('WechatAuthService', () => {
|
|||
headimgurl: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getWechatUserInfo')
|
||||
.mockResolvedValue(mockWechatUser);
|
||||
mockPrisma.oauthAccount.findUnique.mockResolvedValue(null);
|
||||
mockPrisma.user.create.mockResolvedValue({
|
||||
const mockNewUser = {
|
||||
id: 'user-456',
|
||||
username: 'wechat_user_456',
|
||||
password: null,
|
||||
isActive: true,
|
||||
roles: [],
|
||||
});
|
||||
mockPrisma.oauthAccount.create.mockResolvedValue({});
|
||||
roles: [{ role: { name: 'user' } }],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(service, 'getWechatUserInfo')
|
||||
.mockReturnValue(Promise.resolve(mockWechatUser));
|
||||
mockPrisma.oAuthAccount.findUnique.mockResolvedValue(null);
|
||||
mockPrisma.role.findUnique.mockResolvedValue({ id: 'role-user-id' });
|
||||
mockPrisma.user.create.mockResolvedValue(mockNewUser);
|
||||
mockPrisma.oAuthAccount.create.mockResolvedValue({});
|
||||
mockPrisma.user.update.mockResolvedValue({ ...mockNewUser, lastLoginAt: new Date() });
|
||||
mockPrisma.loginHistory.create.mockResolvedValue({});
|
||||
mockSessionService.createSession.mockResolvedValue({
|
||||
token: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
|
|
@ -120,7 +129,7 @@ describe('WechatAuthService', () => {
|
|||
const result = await service.loginWithWechat(mockCode, {});
|
||||
|
||||
expect(mockPrisma.user.create).toHaveBeenCalled();
|
||||
expect(mockPrisma.oauthAccount.create).toHaveBeenCalled();
|
||||
expect(mockPrisma.oAuthAccount.create).toHaveBeenCalled();
|
||||
expect(result).toHaveProperty('accessToken');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class WechatAuthService {
|
|||
) {
|
||||
const wechatUser = await this.getWechatUserInfo(code);
|
||||
|
||||
const existingAccount = await this.prisma.oauthAccount.findUnique({
|
||||
const existingAccount = await this.prisma.oAuthAccount.findUnique({
|
||||
where: {
|
||||
provider_providerId: {
|
||||
provider: 'wechat',
|
||||
|
|
@ -61,7 +61,7 @@ export class WechatAuthService {
|
|||
where: { name: 'user' },
|
||||
select: { id: true },
|
||||
})
|
||||
)?.id,
|
||||
)!.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -76,7 +76,7 @@ export class WechatAuthService {
|
|||
},
|
||||
});
|
||||
|
||||
await this.prisma.oauthAccount.create({
|
||||
await this.prisma.oAuthAccount.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
provider: 'wechat',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { CacheService } from './cache.service';
|
|||
|
||||
if (redisUrl) {
|
||||
return {
|
||||
store: redisStore,
|
||||
store: redisStore as any,
|
||||
url: redisUrl,
|
||||
ttl: 600,
|
||||
};
|
||||
|
|
@ -22,7 +22,7 @@ import { CacheService } from './cache.service';
|
|||
|
||||
return {
|
||||
ttl: 600,
|
||||
};
|
||||
} as any;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export class ArticleService {
|
|||
authorId,
|
||||
tags: tagIds
|
||||
? { create: tagIds.map((tagId) => ({ tag: { connect: { id: tagId } } })) }
|
||||
: [],
|
||||
: undefined,
|
||||
versions: {
|
||||
create: {
|
||||
version: 1,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
FileQueryDto,
|
||||
ProcessImageDto,
|
||||
} from './dto';
|
||||
import { Response as ExpressResponse } from 'express';
|
||||
import type { Response as ExpressResponse } from 'express';
|
||||
|
||||
@Controller('files')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export class FileService {
|
|||
}
|
||||
|
||||
const storage = this.storageFactory.getAdapter(file.storageType as StorageType);
|
||||
const buffer = await storage.download(file.path, file.bucket);
|
||||
const buffer = await storage.download(file.path, file.bucket ?? undefined);
|
||||
|
||||
await this.incrementAccessCount(fileId);
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ export class FileService {
|
|||
|
||||
const storage = this.storageFactory.getAdapter(file.storageType as StorageType);
|
||||
try {
|
||||
await storage.delete(file.path, file.bucket);
|
||||
await storage.delete(file.path, file.bucket ?? undefined);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to delete file from storage: ${error}`);
|
||||
}
|
||||
|
|
@ -305,7 +305,7 @@ export class FileService {
|
|||
}
|
||||
|
||||
const storage = this.storageFactory.getAdapter(file.storageType as StorageType);
|
||||
const originalBuffer = await storage.download(file.path, file.bucket);
|
||||
const originalBuffer = await storage.download(file.path, file.bucket ?? undefined);
|
||||
|
||||
const processedBuffer = await this.imageProcessor.processImage(
|
||||
originalBuffer,
|
||||
|
|
@ -346,7 +346,7 @@ export class FileService {
|
|||
}
|
||||
|
||||
const storage = this.storageFactory.getAdapter(file.storageType as StorageType);
|
||||
return storage.getUrl(file.path, file.bucket, file.isPublic ? undefined : expiresIn);
|
||||
return storage.getUrl(file.path, file.bucket ?? undefined, file.isPublic ? undefined : expiresIn);
|
||||
}
|
||||
|
||||
private async findFileByHash(hash: string, ownerId: string) {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ export class ImageProcessorService {
|
|||
input: watermarkBuffer,
|
||||
left: position.left,
|
||||
top: position.top,
|
||||
opacity: options.watermarkOpacity || 0.3,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,10 +68,11 @@ export class LocalStorageAdapter implements CloudStorageAdapter {
|
|||
await writeFile(fullPath, file);
|
||||
} else {
|
||||
const writeStream = fs.createWriteStream(fullPath);
|
||||
const readable = file as NodeJS.ReadableStream;
|
||||
await new Promise((resolve, reject) => {
|
||||
file.pipe(writeStream);
|
||||
file.on('end', resolve);
|
||||
file.on('error', reject);
|
||||
readable.pipe(writeStream);
|
||||
readable.on('end', resolve);
|
||||
readable.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class MinIOAdapter implements CloudStorageAdapter {
|
|||
|
||||
let etag: string;
|
||||
if (file instanceof Buffer) {
|
||||
etag = await this.client.putObject(
|
||||
const result = await this.client.putObject(
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
|
|
@ -50,16 +50,18 @@ export class MinIOAdapter implements CloudStorageAdapter {
|
|||
? { 'Content-Type': options.contentType }
|
||||
: undefined,
|
||||
);
|
||||
etag = result.etag || '';
|
||||
} else {
|
||||
etag = await this.client.putObject(
|
||||
const result = await this.client.putObject(
|
||||
bucket,
|
||||
path,
|
||||
file,
|
||||
file as any,
|
||||
undefined,
|
||||
options?.contentType
|
||||
? { 'Content-Type': options.contentType }
|
||||
: undefined,
|
||||
);
|
||||
etag = result.etag || '';
|
||||
}
|
||||
|
||||
const url = await this.getUrl(path, bucket);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class TencentCOSAdapter implements CloudStorageAdapter {
|
|||
Bucket: bucket,
|
||||
Region: region,
|
||||
Key: path,
|
||||
Body: file,
|
||||
Body: file as any,
|
||||
ContentType: options?.contentType,
|
||||
},
|
||||
(err, data) => {
|
||||
|
|
@ -175,7 +175,7 @@ export class TencentCOSAdapter implements CloudStorageAdapter {
|
|||
const region = this.defaultRegion;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.copyObject(
|
||||
(this.client as any).copyObject(
|
||||
{
|
||||
Bucket: tgtBkt,
|
||||
Region: region,
|
||||
|
|
|
|||
|
|
@ -3,24 +3,23 @@ import {
|
|||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import {
|
||||
Histogram,
|
||||
Counter,
|
||||
makeHistogramProvider,
|
||||
makeCounterProvider,
|
||||
} from '@willsoto/nestjs-prometheus';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Histogram, Counter } from 'prom-client';
|
||||
|
||||
@Injectable()
|
||||
export class HttpMetricsInterceptor implements NestInterceptor {
|
||||
constructor(
|
||||
@Inject('HTTP_REQUEST_DURATION')
|
||||
private readonly httpRequestDuration: Histogram,
|
||||
private readonly httpRequestDuration: Histogram<string>,
|
||||
@Inject('HTTP_REQUESTS_TOTAL')
|
||||
private readonly httpRequestsTotal: Counter,
|
||||
private readonly httpRequestsTotal: Counter<string>,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
|
|
@ -54,15 +53,31 @@ export class HttpMetricsInterceptor implements NestInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
export const httpRequestDurationProvider = makeHistogramProvider({
|
||||
export const httpRequestDurationProvider = {
|
||||
provide: 'HTTP_REQUEST_DURATION',
|
||||
useFactory: () => {
|
||||
const { Registry, Histogram } = require('prom-client');
|
||||
const registry = new Registry();
|
||||
return new Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
|
||||
});
|
||||
registers: [registry],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const httpRequestsTotalProvider = makeCounterProvider({
|
||||
export const httpRequestsTotalProvider = {
|
||||
provide: 'HTTP_REQUESTS_TOTAL',
|
||||
useFactory: () => {
|
||||
const { Registry, Counter } = require('prom-client');
|
||||
const registry = new Registry();
|
||||
return new Counter({
|
||||
name: 'http_requests_total',
|
||||
help: 'Total number of HTTP requests',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
});
|
||||
registers: [registry],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class EmailChannelService extends BaseChannelService {
|
|||
|
||||
async send(payload: NotificationPayload): Promise<NotificationSendResult> {
|
||||
try {
|
||||
if (this.isMockMode(this.configService)) {
|
||||
if (this.isMockMode()) {
|
||||
return this.createMockResult('email');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export class SmsChannelService extends BaseChannelService {
|
|||
|
||||
async send(payload: NotificationPayload): Promise<NotificationSendResult> {
|
||||
try {
|
||||
if (this.isMockMode(this.configService)) {
|
||||
if (this.isMockMode()) {
|
||||
return this.createMockResult('sms');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { NotificationService } from './notification.service';
|
||||
import { TemplateService } from './template.service';
|
||||
import { PreferenceService } from './preference.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import {
|
||||
SendNotificationDto,
|
||||
SendWithTemplateDto,
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
} from './dto/send-notification.dto';
|
||||
|
||||
@Controller('notifications')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class NotificationController {
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
|
|
@ -33,7 +35,7 @@ export class NotificationController {
|
|||
async send(@Body() dto: SendNotificationDto) {
|
||||
const { useQueue, ...payload } = dto;
|
||||
return this.notificationService.send(
|
||||
{ ...payload, type: payload.type || 'info' },
|
||||
{ ...payload, type: (payload.type || 'info') as 'info' | 'warning' | 'error' | 'success', priority: payload.priority as 'low' | 'normal' | 'high' | 'urgent' | undefined, expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : undefined },
|
||||
useQueue !== false,
|
||||
);
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ export class NotificationController {
|
|||
@Query('status') status?: string,
|
||||
@Query('channel') channel?: string,
|
||||
) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.notificationService.findAll(
|
||||
userId,
|
||||
parseInt(page),
|
||||
|
|
@ -86,7 +88,7 @@ export class NotificationController {
|
|||
|
||||
@Get('unread-count')
|
||||
async getUnreadCount(@Request() req) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.notificationService.getUnreadCount(userId);
|
||||
}
|
||||
|
||||
|
|
@ -97,13 +99,13 @@ export class NotificationController {
|
|||
|
||||
@Put(':id/read')
|
||||
async markAsRead(@Param('id') id: string, @Request() req) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.notificationService.markAsRead(id, userId);
|
||||
}
|
||||
|
||||
@Put('read-all')
|
||||
async markAllAsRead(@Request() req) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.notificationService.markAllAsRead(userId);
|
||||
}
|
||||
|
||||
|
|
@ -140,13 +142,13 @@ export class NotificationController {
|
|||
|
||||
@Get('preferences/my')
|
||||
async getMyPreferences(@Request() req) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.preferenceService.getOrCreate(userId);
|
||||
}
|
||||
|
||||
@Put('preferences/my')
|
||||
async updateMyPreferences(@Request() req, @Body() dto: UpdatePreferenceDto) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.preferenceService.update(userId, dto);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { SmsChannelService } from './channels/sms-channel.service';
|
|||
import { PushChannelService } from './channels/push-channel.service';
|
||||
import { InAppChannelService } from './channels/in-app-channel.service';
|
||||
import { WeComChannelService } from './channels/wecom-channel.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export class NotificationService {
|
|||
|
||||
const targetChannels = channels || content.template.channels;
|
||||
|
||||
const results = [];
|
||||
const results: any[] = [];
|
||||
|
||||
for (const channel of targetChannels) {
|
||||
const payload: NotificationPayload = {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ export class PreferenceService {
|
|||
|
||||
if (
|
||||
preference.quietStartHour === undefined ||
|
||||
preference.quietEndHour === undefined
|
||||
preference.quietStartHour === null ||
|
||||
preference.quietEndHour === undefined ||
|
||||
preference.quietEndHour === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -86,7 +88,8 @@ export class PreferenceService {
|
|||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
const { quietStartHour, quietEndHour } = preference;
|
||||
const quietStartHour = preference.quietStartHour as number;
|
||||
const quietEndHour = preference.quietEndHour as number;
|
||||
|
||||
if (quietStartHour <= quietEndHour) {
|
||||
return hour >= quietStartHour && hour < quietEndHour;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('OrderController', () => {
|
|||
};
|
||||
|
||||
const mockRequest = {
|
||||
user: { id: 'test-user-id' },
|
||||
user: { userId: 'test-user-id' },
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class OrderController {
|
|||
|
||||
@Post()
|
||||
async create(@Body() createOrderDto: CreateOrderDto, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.orderService.create(userId, createOrderDto);
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export class OrderController {
|
|||
@Query('status') status?: string,
|
||||
@Request() req?: any,
|
||||
) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.orderService.findMany({
|
||||
page: parseInt(page, 10),
|
||||
limit: parseInt(limit, 10),
|
||||
|
|
@ -43,13 +43,13 @@ export class OrderController {
|
|||
|
||||
@Get('stats')
|
||||
async getStats(@Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.orderService.getStats(userId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.orderService.findOne(id, userId);
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ export class OrderController {
|
|||
|
||||
@Post(':id/cancel')
|
||||
async cancel(@Param('id') id: string, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.orderService.cancel(id, userId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@ import {
|
|||
Request,
|
||||
Ip,
|
||||
Headers,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PaymentService } from './services/payment.service';
|
||||
import { RefundService } from './services/refund.service';
|
||||
import { CreatePaymentDto } from './dto/create-payment.dto';
|
||||
import { CreateRefundDto } from './dto/create-refund.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
@Controller('payment')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class PaymentController {
|
||||
constructor(
|
||||
private readonly paymentService: PaymentService,
|
||||
|
|
@ -28,7 +31,7 @@ export class PaymentController {
|
|||
@Headers('user-agent') userAgent: string,
|
||||
@Request() req: any,
|
||||
) {
|
||||
const userId = req.user?.id || 'test-user-id';
|
||||
const userId = req.user?.userId;
|
||||
return this.paymentService.createPayment(userId, createPaymentDto, ip, userAgent);
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +41,13 @@ export class PaymentController {
|
|||
@Query('limit') limit: string = '20',
|
||||
@Request() req: any,
|
||||
) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.paymentService.findAll(userId, parseInt(page), parseInt(limit));
|
||||
}
|
||||
|
||||
@Get('orders/:id')
|
||||
findOneOrder(@Param('id') id: string, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.paymentService.findOne(id, userId);
|
||||
}
|
||||
|
||||
|
|
@ -55,13 +58,13 @@ export class PaymentController {
|
|||
|
||||
@Post('orders/:id/cancel')
|
||||
cancelOrder(@Param('id') id: string, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.paymentService.cancelOrder(id, userId);
|
||||
}
|
||||
|
||||
@Post('refunds')
|
||||
createRefund(@Body() createRefundDto: CreateRefundDto, @Request() req: any) {
|
||||
const userId = req.user?.id || 'test-user-id';
|
||||
const userId = req.user?.userId;
|
||||
return this.refundService.createRefund(userId, createRefundDto);
|
||||
}
|
||||
|
||||
|
|
@ -71,13 +74,13 @@ export class PaymentController {
|
|||
@Query('limit') limit: string = '20',
|
||||
@Request() req: any,
|
||||
) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.refundService.findAll(userId, parseInt(page), parseInt(limit));
|
||||
}
|
||||
|
||||
@Get('refunds/:id')
|
||||
findOneRefund(@Param('id') id: string, @Request() req: any) {
|
||||
const userId = req.user?.id;
|
||||
const userId = req.user?.userId;
|
||||
return this.refundService.findOne(id, userId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class PaymentService {
|
|||
|
||||
const adapter = this.adapterFactory.createAdapter(
|
||||
channel.type as PaymentChannelType,
|
||||
{ ...channel.config, isSandbox: channel.isSandbox },
|
||||
{ ...(channel.config as any), isSandbox: channel.isSandbox },
|
||||
);
|
||||
|
||||
const result = await adapter.createPayment({
|
||||
|
|
@ -151,7 +151,7 @@ export class PaymentService {
|
|||
}
|
||||
|
||||
async queryOrderStatus(orderId: string) {
|
||||
const order = await this.findOne(orderId);
|
||||
const order = await this.findOne(orderId) as any;
|
||||
|
||||
if (!order.channel) {
|
||||
throw new BadRequestException('Payment channel not found for this order');
|
||||
|
|
@ -159,12 +159,12 @@ export class PaymentService {
|
|||
|
||||
const adapter = this.adapterFactory.createAdapter(
|
||||
order.channel.type as PaymentChannelType,
|
||||
{ ...order.channel.config, isSandbox: order.channel.isSandbox },
|
||||
{ ...(order.channel.config as any), isSandbox: order.channel.isSandbox },
|
||||
);
|
||||
|
||||
const result = await adapter.queryPayment({
|
||||
orderNo: order.orderNo,
|
||||
channelOrderNo: order.channelOrderNo,
|
||||
channelOrderNo: order.channelOrderNo ?? undefined,
|
||||
});
|
||||
|
||||
await this.createPaymentLog(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class ReconciliationService {
|
|||
const order = await this.prisma.paymentOrder.findUnique({
|
||||
where: { id: orderId },
|
||||
include: { channel: true },
|
||||
});
|
||||
}) as any;
|
||||
|
||||
if (!order || !order.channel) {
|
||||
return { success: false, error: 'Order or channel not found' };
|
||||
|
|
@ -23,12 +23,12 @@ export class ReconciliationService {
|
|||
|
||||
const adapter = this.adapterFactory.createAdapter(
|
||||
order.channel.type as PaymentChannelType,
|
||||
{ ...order.channel.config, isSandbox: order.channel.isSandbox },
|
||||
{ ...(order.channel.config as any), isSandbox: order.channel.isSandbox },
|
||||
);
|
||||
|
||||
const result = await adapter.queryPayment({
|
||||
orderNo: order.orderNo,
|
||||
channelOrderNo: order.channelOrderNo,
|
||||
channelOrderNo: order.channelOrderNo ?? undefined,
|
||||
});
|
||||
|
||||
let statusUpdated = false;
|
||||
|
|
@ -65,7 +65,7 @@ export class ReconciliationService {
|
|||
}
|
||||
|
||||
async reconcileOrders(orderIds: string[]) {
|
||||
const results = [];
|
||||
const results: any[] = [];
|
||||
|
||||
for (const orderId of orderIds) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class RefundService {
|
|||
}
|
||||
|
||||
async createRefund(userId: string, createRefundDto: CreateRefundDto) {
|
||||
const order = await this.paymentService.findOne(createRefundDto.orderId, userId);
|
||||
const order = await this.paymentService.findOne(createRefundDto.orderId, userId) as any;
|
||||
|
||||
if (order.status !== 'paid') {
|
||||
throw new BadRequestException('Only paid orders can be refunded');
|
||||
|
|
@ -55,7 +55,7 @@ export class RefundService {
|
|||
data: {
|
||||
refundNo,
|
||||
orderId: order.id,
|
||||
channelId: order.channel.id,
|
||||
channelId: order.channelId,
|
||||
userId,
|
||||
amount: refundAmount,
|
||||
reason: createRefundDto.reason,
|
||||
|
|
@ -64,7 +64,7 @@ export class RefundService {
|
|||
|
||||
const adapter = this.adapterFactory.createAdapter(
|
||||
order.channel.type as PaymentChannelType,
|
||||
{ ...order.channel.config, isSandbox: order.channel.isSandbox },
|
||||
{ ...(order.channel.config as any), isSandbox: order.channel.isSandbox },
|
||||
);
|
||||
|
||||
const result = await adapter.refundPayment({
|
||||
|
|
@ -72,7 +72,7 @@ export class RefundService {
|
|||
refundNo,
|
||||
amount: createRefundDto.amount,
|
||||
reason: createRefundDto.reason,
|
||||
channelOrderNo: order.channelOrderNo,
|
||||
channelOrderNo: order.channelOrderNo ?? undefined,
|
||||
});
|
||||
|
||||
await this.createRefundLog(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
|||
import { UserService } from './user.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { Express } from 'express';
|
||||
import type { Express } from 'express';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
|
||||
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
|
||||
import { SimpleSpanProcessor, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
||||
const startTracing = () => {
|
||||
if (process.env.OTEL_ENABLED !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
try {
|
||||
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
|
||||
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base');
|
||||
|
||||
const traceExporter = isProduction
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const traceExporter = isProduction
|
||||
? new OTLPTraceExporter({
|
||||
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
|
||||
})
|
||||
: new ConsoleSpanExporter();
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
const sdk = new NodeSDK({
|
||||
instrumentations: [
|
||||
getNodeAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-http': {
|
||||
|
|
@ -20,18 +25,15 @@ const sdk = new NodeSDK({
|
|||
},
|
||||
}),
|
||||
],
|
||||
spanProcessor: isProduction
|
||||
? new BatchSpanProcessor(traceExporter)
|
||||
: new SimpleSpanProcessor(traceExporter),
|
||||
traceExporter,
|
||||
serviceName: 'fischerx-api',
|
||||
});
|
||||
});
|
||||
|
||||
export const startTracing = () => {
|
||||
sdk.start();
|
||||
console.log('OpenTelemetry tracing started');
|
||||
} catch (error) {
|
||||
console.warn('OpenTelemetry initialization failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const shutdownTracing = async () => {
|
||||
await sdk.shutdown();
|
||||
console.log('OpenTelemetry tracing shutdown');
|
||||
};
|
||||
export { startTracing };
|
||||
|
|
|
|||
Loading…
Reference in New Issue