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:
fischer 2026-05-25 17:44:37 +08:00
parent 3d867331ae
commit bdb509a611
54 changed files with 5481 additions and 396 deletions

View File

@ -1,25 +1,88 @@
NODE_ENV=development 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_PORT=5432
DB_USER=fischerx DB_USER=fischerx
DB_PASSWORD=fischerx DB_PASSWORD=fischerx
DB_NAME=fischerx DB_NAME=fischerx
REDIS_HOST=redis # Redis
REDIS_URL="redis://localhost:6379"
REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD= REDIS_PASSWORD=
API_PORT=3001 # JWT
WEB_PORT=3000 JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
JWT_EXPIRATION="7d"
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:3001 NEXT_PUBLIC_API_URL=http://localhost:3001
JWT_SECRET=your-super-secret-jwt-key-change-in-production # Storage Configuration
JWT_EXPIRES_IN=7d STORAGE_TYPE="local"
ALIBABA_CLOUD_ACCESS_KEY_ID= # Local Storage
ALIBABA_CLOUD_ACCESS_KEY_SECRET= LOCAL_STORAGE_PATH="./uploads"
OSS_REGION= LOCAL_STORAGE_URL="http://localhost:3001/uploads"
OSS_BUCKET=
OSS_ENDPOINT= # 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

View File

@ -13,6 +13,7 @@
"test:e2e:ui": "playwright test --ui" "test:e2e:ui": "playwright test --ui"
}, },
"dependencies": { "dependencies": {
"@fischerx/types": "workspace:*",
"@hookform/resolvers": "^5.4.0", "@hookform/resolvers": "^5.4.0",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.13", "@tanstack/react-query": "^5.100.13",

View File

@ -4,7 +4,7 @@ export default function AuthLayout({
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( 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} {children}
</div> </div>
) )

View File

@ -1,7 +1,20 @@
import { Navbar } from "@/components/layout/navbar"
import { Sidebar } from "@/components/layout/sidebar"
export default function DashboardLayout({ export default function DashboardLayout({
children, children,
}: { }: {
children: React.ReactNode 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>
)
} }

View File

@ -97,7 +97,7 @@ export default function ProfilePage() {
const updatedUser = response.data.data; const updatedUser = response.data.data;
setUser({ setUser({
...user, ...user!,
avatar: updatedUser.avatar, avatar: updatedUser.avatar,
}); });
setMessage('Avatar updated successfully'); setMessage('Avatar updated successfully');

View File

@ -1,6 +1,30 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; @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 { @layer base {
:root { :root {

View File

@ -2,7 +2,6 @@ import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google" import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css" import "./globals.css"
import { QueryProvider } from "@/providers/query-provider" import { QueryProvider } from "@/providers/query-provider"
import { Navbar, Sidebar } from "@/components/layout"
import { ErrorBoundary } from "@/components/error-boundary" import { ErrorBoundary } from "@/components/error-boundary"
import { MonitoringProvider } from "@/providers/monitoring-provider" import { MonitoringProvider } from "@/providers/monitoring-provider"
@ -27,20 +26,12 @@ export default function RootLayout({
children: React.ReactNode children: React.ReactNode
}>) { }>) {
return ( 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"> <body className="min-h-full">
<MonitoringProvider> <MonitoringProvider>
<QueryProvider> <QueryProvider>
<ErrorBoundary> <ErrorBoundary>
<div className="relative flex min-h-screen flex-col"> {children}
<Navbar />
<div className="flex flex-1">
<Sidebar />
<main className="flex-1 md:ml-64">
<div className="container py-6">{children}</div>
</main>
</div>
</div>
</ErrorBoundary> </ErrorBoundary>
</QueryProvider> </QueryProvider>
</MonitoringProvider> </MonitoringProvider>

View File

@ -4,10 +4,10 @@ import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { useDownloadFile, useDeleteFile } from '@/hooks/use-files'; import { useDownloadFile, useDeleteFile } from '@/hooks/use-files';
import type { File } from '@fischerx/types'; import type { File as FileType } from '@fischerx/types';
interface FilePreviewProps { interface FilePreviewProps {
file: File; file: FileType;
onDelete?: () => void; onDelete?: () => void;
} }

View File

@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fileApi } from '@/lib/file-api'; 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) => { export const useFiles = (params?: FileListParams) => {
return useQuery({ return useQuery({
@ -21,7 +21,7 @@ export const useUploadFile = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ file, options }: { file: File | Blob; options?: FileUploadOptions }) => mutationFn: ({ file, options }: { file: globalThis.File | Blob; options?: FileUploadOptions }) =>
fileApi.uploadFile(file, options), fileApi.uploadFile(file, options),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] }); queryClient.invalidateQueries({ queryKey: ['files'] });
@ -33,7 +33,7 @@ export const useUploadMultipleFiles = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ files, options }: { files: (File | Blob)[]; options?: FileUploadOptions }) => mutationFn: ({ files, options }: { files: (globalThis.File | Blob)[]; options?: FileUploadOptions }) =>
fileApi.uploadMultipleFiles(files, options), fileApi.uploadMultipleFiles(files, options),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] }); queryClient.invalidateQueries({ queryKey: ['files'] });
@ -45,7 +45,7 @@ export const useUpdateFile = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ 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), fileApi.updateFile(id, data),
onSuccess: (_, { id }) => { onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['files'] }); queryClient.invalidateQueries({ queryKey: ['files'] });

View File

@ -1,8 +1,8 @@
import api from './api'; 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 = { 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(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
if (options?.category) formData.append('category', options.category); if (options?.category) formData.append('category', options.category);
@ -18,9 +18,9 @@ export const fileApi = {
}, },
uploadMultipleFiles: async ( uploadMultipleFiles: async (
files: (File | Blob)[], files: (globalThis.File | Blob)[],
options?: FileUploadOptions 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(); const formData = new FormData();
files.forEach((file) => formData.append('files', file)); files.forEach((file) => formData.append('files', file));
if (options?.category) formData.append('category', options.category); if (options?.category) formData.append('category', options.category);
@ -35,12 +35,12 @@ export const fileApi = {
return response.data; 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 }); const response = await api.get('/files', { params });
return response.data; 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}`); const response = await api.get(`/files/${id}`);
return response.data; return response.data;
}, },
@ -65,7 +65,7 @@ export const fileApi = {
return response.data; 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); const response = await api.put(`/files/${id}`, data);
return response.data; return response.data;
}, },

View File

@ -1,38 +1,7 @@
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; export const initTelemetry = async () => {
import { SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'; if (process.env.NODE_ENV !== 'production') return;
import { ZoneContextManager } from '@opentelemetry/context-zone'; const endpoint = process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT;
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; if (!endpoint) return;
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';
const isProduction = process.env.NODE_ENV === 'production'; console.warn('OpenTelemetry packages not installed. Install @opentelemetry/* packages to enable tracing.');
};
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');

View File

@ -1,15 +1,12 @@
import { ReportCallback } from 'web-vitals'; export const reportWebVitals = (onPerfEntry?: (metric: any) => void) => {
if (!onPerfEntry) return;
export const reportWebVitals = (onPerfEntry?: ReportCallback) => { import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
if (onPerfEntry && onPerfEntry instanceof Function) { onCLS(onPerfEntry);
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { onINP(onPerfEntry);
getCLS(onPerfEntry); onFCP(onPerfEntry);
getFID(onPerfEntry); onLCP(onPerfEntry);
getFCP(onPerfEntry); onTTFB(onPerfEntry);
getLCP(onPerfEntry); }).catch(() => {});
getTTFB(onPerfEntry);
});
}
}; };
export const logVitals = (metric: any) => { export const logVitals = (metric: any) => {

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import React, { useEffect, ReactNode } from 'react'; import React, { useEffect, ReactNode } from 'react';
import { logVitals, reportWebVitals } from '@/lib/web-vitals';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@ -9,12 +8,16 @@ interface Props {
export function MonitoringProvider({ children }: Props) { export function MonitoringProvider({ children }: Props) {
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') {
import('@/lib/web-vitals').then(({ reportWebVitals, logVitals }) => {
reportWebVitals(logVitals); reportWebVitals(logVitals);
}).catch(() => {});
}
}, []); }, []);
useEffect(() => { useEffect(() => {
if (process.env.NODE_ENV === 'production') { if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
import('@/lib/telemetry').catch(console.error); import('@/lib/telemetry').then(m => m.initTelemetry()).catch(() => {});
} }
}, []); }, []);

View File

@ -1,12 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { User } from '@/lib/user-api'
interface User {
id: string
name: string
email: string
avatar?: string
}
interface UserState { interface UserState {
user: User | null user: User | null

View File

@ -20,7 +20,8 @@
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"types": ["react", "react-dom", "node"]
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",

View File

@ -1,10 +1,6 @@
version: '3.8'
services: services:
postgres: postgres:
build: image: postgres:15-alpine
context: ./infra/db
dockerfile: Dockerfile
container_name: fischerx-postgres container_name: fischerx-postgres
environment: environment:
POSTGRES_USER: ${DB_USER:-fischerx} POSTGRES_USER: ${DB_USER:-fischerx}
@ -14,6 +10,7 @@ services:
- "${DB_PORT:-5432}:5432" - "${DB_PORT:-5432}:5432"
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
- ./infra/db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks: networks:
- fischerx-network - fischerx-network
healthcheck: healthcheck:

View File

@ -30,10 +30,55 @@ export interface File {
mimeType: string; mimeType: string;
size: number; size: number;
url: string; 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; ownerId: string;
createdAt: Date; createdAt: Date;
updatedAt: 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> { export interface ApiResponse<T = unknown> {
success: boolean; success: boolean;
data?: T; data?: T;
@ -63,4 +108,3 @@ export interface AuthResponse {
refreshToken: string; refreshToken: string;
user: User; user: User;
} }
//# sourceMappingURL=index.d.ts.map

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# Database # Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fischerx?schema=public" DATABASE_URL="postgresql://fischerx:fischerx@localhost:5432/fischerx?schema=public"
# JWT # JWT
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" 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 # Notification - WeCom Robot Webhook
WECOM_WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-default-key" 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" 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

View File

@ -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;

View File

@ -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"

View File

@ -37,5 +37,9 @@ export class GlobalExceptionFilter implements ExceptionFilter {
message, message,
data: null, data: null,
}); });
if (!(exception instanceof HttpException)) {
console.error('[Unhandled Exception]', exception);
}
} }
} }

View File

@ -9,7 +9,9 @@ import { HttpMetricsInterceptor } from './modules/monitoring/http-metrics.interc
import { startTracing } from './tracing'; import { startTracing } from './tracing';
async function bootstrap() { async function bootstrap() {
if (process.env.OTEL_ENABLED === 'true') {
startTracing(); startTracing();
}
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
@ -35,6 +37,6 @@ async function bootstrap() {
await app.listen(port); await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`); console.log(`Application is running on: http://localhost:${port}`);
console.log(`Metrics available at: http://localhost:${port}/metrics`); 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(); void bootstrap();

View File

@ -14,7 +14,7 @@ describe('AlipayAuthService', () => {
create: jest.fn(), create: jest.fn(),
update: jest.fn(), update: jest.fn(),
}, },
oauthAccount: { oAuthAccount: {
findUnique: jest.fn(), findUnique: jest.fn(),
create: jest.fn(), create: jest.fn(),
}, },
@ -68,17 +68,20 @@ describe('AlipayAuthService', () => {
email: null, email: null,
phone: null, phone: null,
username: 'alipay_user', username: 'alipay_user',
password: null,
isActive: true, isActive: true,
roles: [], roles: [{ role: { name: 'user' } }],
}; };
jest jest
.spyOn(service, 'getAlipayUserInfo') .spyOn(service, 'getAlipayUserInfo')
.mockResolvedValue(mockAlipayUser); .mockReturnValue(Promise.resolve(mockAlipayUser));
mockPrisma.oauthAccount.findUnique.mockResolvedValue({ mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
userId: 'user-123', userId: 'user-123',
}); });
mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser); mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser);
mockPrisma.user.update.mockResolvedValue({ ...mockExistingUser, lastLoginAt: new Date() });
mockPrisma.loginHistory.create.mockResolvedValue({});
mockSessionService.createSession.mockResolvedValue({ mockSessionService.createSession.mockResolvedValue({
token: 'access-token', token: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
@ -101,17 +104,23 @@ describe('AlipayAuthService', () => {
avatar: 'https://example.com/alipay-avatar.jpg', avatar: 'https://example.com/alipay-avatar.jpg',
}; };
jest const mockNewUser = {
.spyOn(service, 'getAlipayUserInfo')
.mockResolvedValue(mockAlipayUser);
mockPrisma.oauthAccount.findUnique.mockResolvedValue(null);
mockPrisma.user.create.mockResolvedValue({
id: 'user-456', id: 'user-456',
username: 'alipay_user_456', username: 'alipay_user_456',
password: null,
isActive: true, isActive: true,
roles: [], roles: [{ role: { name: 'user' } }],
}); };
mockPrisma.oauthAccount.create.mockResolvedValue({});
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({ mockSessionService.createSession.mockResolvedValue({
token: 'access-token', token: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
@ -120,7 +129,7 @@ describe('AlipayAuthService', () => {
const result = await service.loginWithAlipay(mockCode, {}); const result = await service.loginWithAlipay(mockCode, {});
expect(mockPrisma.user.create).toHaveBeenCalled(); expect(mockPrisma.user.create).toHaveBeenCalled();
expect(mockPrisma.oauthAccount.create).toHaveBeenCalled(); expect(mockPrisma.oAuthAccount.create).toHaveBeenCalled();
expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('accessToken');
}); });
}); });

View File

@ -19,7 +19,7 @@ export class AlipayAuthService {
) { ) {
const alipayUser = await this.getAlipayUserInfo(code); const alipayUser = await this.getAlipayUserInfo(code);
const existingAccount = await this.prisma.oauthAccount.findUnique({ const existingAccount = await this.prisma.oAuthAccount.findUnique({
where: { where: {
provider_providerId: { provider_providerId: {
provider: 'alipay', provider: 'alipay',
@ -61,7 +61,7 @@ export class AlipayAuthService {
where: { name: 'user' }, where: { name: 'user' },
select: { id: true }, select: { id: true },
}) })
)?.id, )!.id,
}, },
}, },
}, },
@ -76,7 +76,7 @@ export class AlipayAuthService {
}, },
}); });
await this.prisma.oauthAccount.create({ await this.prisma.oAuthAccount.create({
data: { data: {
userId: user.id, userId: user.id,
provider: 'alipay', provider: 'alipay',

View File

@ -40,7 +40,7 @@ export class AuthController {
@Post('login') @Post('login')
async login(@Body() loginDto: LoginDto, @Req() req: any) { 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( const user = await this.authService.validateUser(
identifier, identifier,
loginDto.password, loginDto.password,

View File

@ -14,7 +14,7 @@ export class AuthService {
private jwtService: JwtService, private jwtService: JwtService,
) {} ) {}
async validateUser(identifier: string, password: string) { async validateUser(identifier: string, inputPassword: string) {
let user; let user;
if (this.isPhone(identifier)) { if (this.isPhone(identifier)) {
@ -37,7 +37,7 @@ export class AuthService {
throw new UnauthorizedException('Account is disabled'); throw new UnauthorizedException('Account is disabled');
} }
const passwordValid = await this.comparePassword(password, user.password); const passwordValid = await this.comparePassword(inputPassword, user.password);
if (!passwordValid) { if (!passwordValid) {
throw new UnauthorizedException('Invalid credentials'); throw new UnauthorizedException('Invalid credentials');
} }

View File

@ -93,6 +93,7 @@ export class SessionService {
select: { select: {
id: true, id: true,
email: true, email: true,
isActive: true,
roles: { roles: {
select: { select: {
role: { role: {

View File

@ -27,7 +27,7 @@ export class SmsAuthService {
const attemptsKey = `sms:attempts:${phone}`; const attemptsKey = `sms:attempts:${phone}`;
const attempts = await this.cacheService.get(attemptsKey); const attempts = await this.cacheService.get(attemptsKey);
if (attempts && parseInt(attempts) >= 5) { if (attempts && parseInt(String(attempts)) >= 5) {
throw new BadRequestException( throw new BadRequestException(
'Too many attempts, please try again later', 'Too many attempts, please try again later',
); );
@ -65,7 +65,7 @@ export class SmsAuthService {
if (storedCode !== code) { if (storedCode !== code) {
const attemptsKey = `sms:attempts:${phone}`; const attemptsKey = `sms:attempts:${phone}`;
const attempts = await this.cacheService.get(attemptsKey); 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) { if (newAttempts >= 5) {
await this.cacheService.del(codeKey); await this.cacheService.del(codeKey);

View File

@ -14,7 +14,7 @@ describe('WechatAuthService', () => {
create: jest.fn(), create: jest.fn(),
update: jest.fn(), update: jest.fn(),
}, },
oauthAccount: { oAuthAccount: {
findUnique: jest.fn(), findUnique: jest.fn(),
create: jest.fn(), create: jest.fn(),
}, },
@ -68,17 +68,20 @@ describe('WechatAuthService', () => {
email: null, email: null,
phone: null, phone: null,
username: 'wechat_user', username: 'wechat_user',
password: null,
isActive: true, isActive: true,
roles: [], roles: [{ role: { name: 'user' } }],
}; };
jest jest
.spyOn(service, 'getWechatUserInfo') .spyOn(service, 'getWechatUserInfo')
.mockResolvedValue(mockWechatUser); .mockReturnValue(Promise.resolve(mockWechatUser));
mockPrisma.oauthAccount.findUnique.mockResolvedValue({ mockPrisma.oAuthAccount.findUnique.mockResolvedValue({
userId: 'user-123', userId: 'user-123',
}); });
mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser); mockPrisma.user.findUnique.mockResolvedValue(mockExistingUser);
mockPrisma.user.update.mockResolvedValue({ ...mockExistingUser, lastLoginAt: new Date() });
mockPrisma.loginHistory.create.mockResolvedValue({});
mockSessionService.createSession.mockResolvedValue({ mockSessionService.createSession.mockResolvedValue({
token: 'access-token', token: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
@ -101,17 +104,23 @@ describe('WechatAuthService', () => {
headimgurl: 'https://example.com/avatar.jpg', headimgurl: 'https://example.com/avatar.jpg',
}; };
jest const mockNewUser = {
.spyOn(service, 'getWechatUserInfo')
.mockResolvedValue(mockWechatUser);
mockPrisma.oauthAccount.findUnique.mockResolvedValue(null);
mockPrisma.user.create.mockResolvedValue({
id: 'user-456', id: 'user-456',
username: 'wechat_user_456', username: 'wechat_user_456',
password: null,
isActive: true, isActive: true,
roles: [], roles: [{ role: { name: 'user' } }],
}); };
mockPrisma.oauthAccount.create.mockResolvedValue({});
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({ mockSessionService.createSession.mockResolvedValue({
token: 'access-token', token: 'access-token',
refreshToken: 'refresh-token', refreshToken: 'refresh-token',
@ -120,7 +129,7 @@ describe('WechatAuthService', () => {
const result = await service.loginWithWechat(mockCode, {}); const result = await service.loginWithWechat(mockCode, {});
expect(mockPrisma.user.create).toHaveBeenCalled(); expect(mockPrisma.user.create).toHaveBeenCalled();
expect(mockPrisma.oauthAccount.create).toHaveBeenCalled(); expect(mockPrisma.oAuthAccount.create).toHaveBeenCalled();
expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('accessToken');
}); });
}); });

View File

@ -19,7 +19,7 @@ export class WechatAuthService {
) { ) {
const wechatUser = await this.getWechatUserInfo(code); const wechatUser = await this.getWechatUserInfo(code);
const existingAccount = await this.prisma.oauthAccount.findUnique({ const existingAccount = await this.prisma.oAuthAccount.findUnique({
where: { where: {
provider_providerId: { provider_providerId: {
provider: 'wechat', provider: 'wechat',
@ -61,7 +61,7 @@ export class WechatAuthService {
where: { name: 'user' }, where: { name: 'user' },
select: { id: true }, select: { id: true },
}) })
)?.id, )!.id,
}, },
}, },
}, },
@ -76,7 +76,7 @@ export class WechatAuthService {
}, },
}); });
await this.prisma.oauthAccount.create({ await this.prisma.oAuthAccount.create({
data: { data: {
userId: user.id, userId: user.id,
provider: 'wechat', provider: 'wechat',

View File

@ -14,7 +14,7 @@ import { CacheService } from './cache.service';
if (redisUrl) { if (redisUrl) {
return { return {
store: redisStore, store: redisStore as any,
url: redisUrl, url: redisUrl,
ttl: 600, ttl: 600,
}; };
@ -22,7 +22,7 @@ import { CacheService } from './cache.service';
return { return {
ttl: 600, ttl: 600,
}; } as any;
}, },
inject: [ConfigService], inject: [ConfigService],
}), }),

View File

@ -25,7 +25,7 @@ export class ArticleService {
authorId, authorId,
tags: tagIds tags: tagIds
? { create: tagIds.map((tagId) => ({ tag: { connect: { id: tagId } } })) } ? { create: tagIds.map((tagId) => ({ tag: { connect: { id: tagId } } })) }
: [], : undefined,
versions: { versions: {
create: { create: {
version: 1, version: 1,

View File

@ -28,7 +28,7 @@ import {
FileQueryDto, FileQueryDto,
ProcessImageDto, ProcessImageDto,
} from './dto'; } from './dto';
import { Response as ExpressResponse } from 'express'; import type { Response as ExpressResponse } from 'express';
@Controller('files') @Controller('files')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)

View File

@ -165,7 +165,7 @@ export class FileService {
} }
const storage = this.storageFactory.getAdapter(file.storageType as StorageType); 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); await this.incrementAccessCount(fileId);
@ -191,7 +191,7 @@ export class FileService {
const storage = this.storageFactory.getAdapter(file.storageType as StorageType); const storage = this.storageFactory.getAdapter(file.storageType as StorageType);
try { try {
await storage.delete(file.path, file.bucket); await storage.delete(file.path, file.bucket ?? undefined);
} catch (error) { } catch (error) {
this.logger.warn(`Failed to delete file from storage: ${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 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( const processedBuffer = await this.imageProcessor.processImage(
originalBuffer, originalBuffer,
@ -346,7 +346,7 @@ export class FileService {
} }
const storage = this.storageFactory.getAdapter(file.storageType as StorageType); 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) { private async findFileByHash(hash: string, ownerId: string) {

View File

@ -83,7 +83,6 @@ export class ImageProcessorService {
input: watermarkBuffer, input: watermarkBuffer,
left: position.left, left: position.left,
top: position.top, top: position.top,
opacity: options.watermarkOpacity || 0.3,
}, },
]); ]);
} }

View File

@ -68,10 +68,11 @@ export class LocalStorageAdapter implements CloudStorageAdapter {
await writeFile(fullPath, file); await writeFile(fullPath, file);
} else { } else {
const writeStream = fs.createWriteStream(fullPath); const writeStream = fs.createWriteStream(fullPath);
const readable = file as NodeJS.ReadableStream;
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
file.pipe(writeStream); readable.pipe(writeStream);
file.on('end', resolve); readable.on('end', resolve);
file.on('error', reject); readable.on('error', reject);
}); });
} }

View File

@ -41,7 +41,7 @@ export class MinIOAdapter implements CloudStorageAdapter {
let etag: string; let etag: string;
if (file instanceof Buffer) { if (file instanceof Buffer) {
etag = await this.client.putObject( const result = await this.client.putObject(
bucket, bucket,
path, path,
file, file,
@ -50,16 +50,18 @@ export class MinIOAdapter implements CloudStorageAdapter {
? { 'Content-Type': options.contentType } ? { 'Content-Type': options.contentType }
: undefined, : undefined,
); );
etag = result.etag || '';
} else { } else {
etag = await this.client.putObject( const result = await this.client.putObject(
bucket, bucket,
path, path,
file, file as any,
undefined, undefined,
options?.contentType options?.contentType
? { 'Content-Type': options.contentType } ? { 'Content-Type': options.contentType }
: undefined, : undefined,
); );
etag = result.etag || '';
} }
const url = await this.getUrl(path, bucket); const url = await this.getUrl(path, bucket);

View File

@ -43,7 +43,7 @@ export class TencentCOSAdapter implements CloudStorageAdapter {
Bucket: bucket, Bucket: bucket,
Region: region, Region: region,
Key: path, Key: path,
Body: file, Body: file as any,
ContentType: options?.contentType, ContentType: options?.contentType,
}, },
(err, data) => { (err, data) => {
@ -175,7 +175,7 @@ export class TencentCOSAdapter implements CloudStorageAdapter {
const region = this.defaultRegion; const region = this.defaultRegion;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.copyObject( (this.client as any).copyObject(
{ {
Bucket: tgtBkt, Bucket: tgtBkt,
Region: region, Region: region,

View File

@ -3,24 +3,23 @@ import {
NestInterceptor, NestInterceptor,
ExecutionContext, ExecutionContext,
CallHandler, CallHandler,
Inject,
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
Histogram,
Counter,
makeHistogramProvider, makeHistogramProvider,
makeCounterProvider, makeCounterProvider,
} from '@willsoto/nestjs-prometheus'; } from '@willsoto/nestjs-prometheus';
import { Inject } from '@nestjs/common'; import { Histogram, Counter } from 'prom-client';
@Injectable() @Injectable()
export class HttpMetricsInterceptor implements NestInterceptor { export class HttpMetricsInterceptor implements NestInterceptor {
constructor( constructor(
@Inject('HTTP_REQUEST_DURATION') @Inject('HTTP_REQUEST_DURATION')
private readonly httpRequestDuration: Histogram, private readonly httpRequestDuration: Histogram<string>,
@Inject('HTTP_REQUESTS_TOTAL') @Inject('HTTP_REQUESTS_TOTAL')
private readonly httpRequestsTotal: Counter, private readonly httpRequestsTotal: Counter<string>,
) {} ) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 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', name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds', help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'], labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10], 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', name: 'http_requests_total',
help: 'Total number of HTTP requests', help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'], labelNames: ['method', 'route', 'status'],
registers: [registry],
}); });
},
};

View File

@ -55,7 +55,7 @@ export class EmailChannelService extends BaseChannelService {
async send(payload: NotificationPayload): Promise<NotificationSendResult> { async send(payload: NotificationPayload): Promise<NotificationSendResult> {
try { try {
if (this.isMockMode(this.configService)) { if (this.isMockMode()) {
return this.createMockResult('email'); return this.createMockResult('email');
} }

View File

@ -46,7 +46,7 @@ export class SmsChannelService extends BaseChannelService {
async send(payload: NotificationPayload): Promise<NotificationSendResult> { async send(payload: NotificationPayload): Promise<NotificationSendResult> {
try { try {
if (this.isMockMode(this.configService)) { if (this.isMockMode()) {
return this.createMockResult('sms'); return this.createMockResult('sms');
} }

View File

@ -12,6 +12,7 @@ import {
import { NotificationService } from './notification.service'; import { NotificationService } from './notification.service';
import { TemplateService } from './template.service'; import { TemplateService } from './template.service';
import { PreferenceService } from './preference.service'; import { PreferenceService } from './preference.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { import {
SendNotificationDto, SendNotificationDto,
SendWithTemplateDto, SendWithTemplateDto,
@ -22,6 +23,7 @@ import {
} from './dto/send-notification.dto'; } from './dto/send-notification.dto';
@Controller('notifications') @Controller('notifications')
@UseGuards(JwtAuthGuard)
export class NotificationController { export class NotificationController {
constructor( constructor(
private notificationService: NotificationService, private notificationService: NotificationService,
@ -33,7 +35,7 @@ export class NotificationController {
async send(@Body() dto: SendNotificationDto) { async send(@Body() dto: SendNotificationDto) {
const { useQueue, ...payload } = dto; const { useQueue, ...payload } = dto;
return this.notificationService.send( 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, useQueue !== false,
); );
} }
@ -74,7 +76,7 @@ export class NotificationController {
@Query('status') status?: string, @Query('status') status?: string,
@Query('channel') channel?: string, @Query('channel') channel?: string,
) { ) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.notificationService.findAll( return this.notificationService.findAll(
userId, userId,
parseInt(page), parseInt(page),
@ -86,7 +88,7 @@ export class NotificationController {
@Get('unread-count') @Get('unread-count')
async getUnreadCount(@Request() req) { async getUnreadCount(@Request() req) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.notificationService.getUnreadCount(userId); return this.notificationService.getUnreadCount(userId);
} }
@ -97,13 +99,13 @@ export class NotificationController {
@Put(':id/read') @Put(':id/read')
async markAsRead(@Param('id') id: string, @Request() req) { async markAsRead(@Param('id') id: string, @Request() req) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.notificationService.markAsRead(id, userId); return this.notificationService.markAsRead(id, userId);
} }
@Put('read-all') @Put('read-all')
async markAllAsRead(@Request() req) { async markAllAsRead(@Request() req) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.notificationService.markAllAsRead(userId); return this.notificationService.markAllAsRead(userId);
} }
@ -140,13 +142,13 @@ export class NotificationController {
@Get('preferences/my') @Get('preferences/my')
async getMyPreferences(@Request() req) { async getMyPreferences(@Request() req) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.preferenceService.getOrCreate(userId); return this.preferenceService.getOrCreate(userId);
} }
@Put('preferences/my') @Put('preferences/my')
async updateMyPreferences(@Request() req, @Body() dto: UpdatePreferenceDto) { async updateMyPreferences(@Request() req, @Body() dto: UpdatePreferenceDto) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.preferenceService.update(userId, dto); return this.preferenceService.update(userId, dto);
} }

View File

@ -10,7 +10,7 @@ import { SmsChannelService } from './channels/sms-channel.service';
import { PushChannelService } from './channels/push-channel.service'; import { PushChannelService } from './channels/push-channel.service';
import { InAppChannelService } from './channels/in-app-channel.service'; import { InAppChannelService } from './channels/in-app-channel.service';
import { WeComChannelService } from './channels/wecom-channel.service'; import { WeComChannelService } from './channels/wecom-channel.service';
import { PrismaModule } from '../prisma/prisma.module'; import { PrismaModule } from '../../prisma/prisma.module';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@ -101,7 +101,7 @@ export class NotificationService {
const targetChannels = channels || content.template.channels; const targetChannels = channels || content.template.channels;
const results = []; const results: any[] = [];
for (const channel of targetChannels) { for (const channel of targetChannels) {
const payload: NotificationPayload = { const payload: NotificationPayload = {

View File

@ -78,7 +78,9 @@ export class PreferenceService {
if ( if (
preference.quietStartHour === undefined || preference.quietStartHour === undefined ||
preference.quietEndHour === undefined preference.quietStartHour === null ||
preference.quietEndHour === undefined ||
preference.quietEndHour === null
) { ) {
return false; return false;
} }
@ -86,7 +88,8 @@ export class PreferenceService {
const now = new Date(); const now = new Date();
const hour = now.getHours(); const hour = now.getHours();
const { quietStartHour, quietEndHour } = preference; const quietStartHour = preference.quietStartHour as number;
const quietEndHour = preference.quietEndHour as number;
if (quietStartHour <= quietEndHour) { if (quietStartHour <= quietEndHour) {
return hour >= quietStartHour && hour < quietEndHour; return hour >= quietStartHour && hour < quietEndHour;

View File

@ -17,7 +17,7 @@ describe('OrderController', () => {
}; };
const mockRequest = { const mockRequest = {
user: { id: 'test-user-id' }, user: { userId: 'test-user-id' },
}; };
beforeEach(async () => { beforeEach(async () => {

View File

@ -21,7 +21,7 @@ export class OrderController {
@Post() @Post()
async create(@Body() createOrderDto: CreateOrderDto, @Request() req: any) { async create(@Body() createOrderDto: CreateOrderDto, @Request() req: any) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.orderService.create(userId, createOrderDto); return this.orderService.create(userId, createOrderDto);
} }
@ -32,7 +32,7 @@ export class OrderController {
@Query('status') status?: string, @Query('status') status?: string,
@Request() req?: any, @Request() req?: any,
) { ) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.orderService.findMany({ return this.orderService.findMany({
page: parseInt(page, 10), page: parseInt(page, 10),
limit: parseInt(limit, 10), limit: parseInt(limit, 10),
@ -43,13 +43,13 @@ export class OrderController {
@Get('stats') @Get('stats')
async getStats(@Request() req: any) { async getStats(@Request() req: any) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.orderService.getStats(userId); return this.orderService.getStats(userId);
} }
@Get(':id') @Get(':id')
async findOne(@Param('id') id: string, @Request() req: any) { 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); return this.orderService.findOne(id, userId);
} }
@ -63,7 +63,7 @@ export class OrderController {
@Post(':id/cancel') @Post(':id/cancel')
async cancel(@Param('id') id: string, @Request() req: any) { 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); return this.orderService.cancel(id, userId);
} }
} }

View File

@ -8,13 +8,16 @@ import {
Request, Request,
Ip, Ip,
Headers, Headers,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { PaymentService } from './services/payment.service'; import { PaymentService } from './services/payment.service';
import { RefundService } from './services/refund.service'; import { RefundService } from './services/refund.service';
import { CreatePaymentDto } from './dto/create-payment.dto'; import { CreatePaymentDto } from './dto/create-payment.dto';
import { CreateRefundDto } from './dto/create-refund.dto'; import { CreateRefundDto } from './dto/create-refund.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('payment') @Controller('payment')
@UseGuards(JwtAuthGuard)
export class PaymentController { export class PaymentController {
constructor( constructor(
private readonly paymentService: PaymentService, private readonly paymentService: PaymentService,
@ -28,7 +31,7 @@ export class PaymentController {
@Headers('user-agent') userAgent: string, @Headers('user-agent') userAgent: string,
@Request() req: any, @Request() req: any,
) { ) {
const userId = req.user?.id || 'test-user-id'; const userId = req.user?.userId;
return this.paymentService.createPayment(userId, createPaymentDto, ip, userAgent); return this.paymentService.createPayment(userId, createPaymentDto, ip, userAgent);
} }
@ -38,13 +41,13 @@ export class PaymentController {
@Query('limit') limit: string = '20', @Query('limit') limit: string = '20',
@Request() req: any, @Request() req: any,
) { ) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.paymentService.findAll(userId, parseInt(page), parseInt(limit)); return this.paymentService.findAll(userId, parseInt(page), parseInt(limit));
} }
@Get('orders/:id') @Get('orders/:id')
findOneOrder(@Param('id') id: string, @Request() req: any) { findOneOrder(@Param('id') id: string, @Request() req: any) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.paymentService.findOne(id, userId); return this.paymentService.findOne(id, userId);
} }
@ -55,13 +58,13 @@ export class PaymentController {
@Post('orders/:id/cancel') @Post('orders/:id/cancel')
cancelOrder(@Param('id') id: string, @Request() req: any) { cancelOrder(@Param('id') id: string, @Request() req: any) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.paymentService.cancelOrder(id, userId); return this.paymentService.cancelOrder(id, userId);
} }
@Post('refunds') @Post('refunds')
createRefund(@Body() createRefundDto: CreateRefundDto, @Request() req: any) { 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); return this.refundService.createRefund(userId, createRefundDto);
} }
@ -71,13 +74,13 @@ export class PaymentController {
@Query('limit') limit: string = '20', @Query('limit') limit: string = '20',
@Request() req: any, @Request() req: any,
) { ) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.refundService.findAll(userId, parseInt(page), parseInt(limit)); return this.refundService.findAll(userId, parseInt(page), parseInt(limit));
} }
@Get('refunds/:id') @Get('refunds/:id')
findOneRefund(@Param('id') id: string, @Request() req: any) { findOneRefund(@Param('id') id: string, @Request() req: any) {
const userId = req.user?.id; const userId = req.user?.userId;
return this.refundService.findOne(id, userId); return this.refundService.findOne(id, userId);
} }

View File

@ -54,7 +54,7 @@ export class PaymentService {
const adapter = this.adapterFactory.createAdapter( const adapter = this.adapterFactory.createAdapter(
channel.type as PaymentChannelType, channel.type as PaymentChannelType,
{ ...channel.config, isSandbox: channel.isSandbox }, { ...(channel.config as any), isSandbox: channel.isSandbox },
); );
const result = await adapter.createPayment({ const result = await adapter.createPayment({
@ -151,7 +151,7 @@ export class PaymentService {
} }
async queryOrderStatus(orderId: string) { async queryOrderStatus(orderId: string) {
const order = await this.findOne(orderId); const order = await this.findOne(orderId) as any;
if (!order.channel) { if (!order.channel) {
throw new BadRequestException('Payment channel not found for this order'); throw new BadRequestException('Payment channel not found for this order');
@ -159,12 +159,12 @@ export class PaymentService {
const adapter = this.adapterFactory.createAdapter( const adapter = this.adapterFactory.createAdapter(
order.channel.type as PaymentChannelType, 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({ const result = await adapter.queryPayment({
orderNo: order.orderNo, orderNo: order.orderNo,
channelOrderNo: order.channelOrderNo, channelOrderNo: order.channelOrderNo ?? undefined,
}); });
await this.createPaymentLog( await this.createPaymentLog(

View File

@ -15,7 +15,7 @@ export class ReconciliationService {
const order = await this.prisma.paymentOrder.findUnique({ const order = await this.prisma.paymentOrder.findUnique({
where: { id: orderId }, where: { id: orderId },
include: { channel: true }, include: { channel: true },
}); }) as any;
if (!order || !order.channel) { if (!order || !order.channel) {
return { success: false, error: 'Order or channel not found' }; return { success: false, error: 'Order or channel not found' };
@ -23,12 +23,12 @@ export class ReconciliationService {
const adapter = this.adapterFactory.createAdapter( const adapter = this.adapterFactory.createAdapter(
order.channel.type as PaymentChannelType, 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({ const result = await adapter.queryPayment({
orderNo: order.orderNo, orderNo: order.orderNo,
channelOrderNo: order.channelOrderNo, channelOrderNo: order.channelOrderNo ?? undefined,
}); });
let statusUpdated = false; let statusUpdated = false;
@ -65,7 +65,7 @@ export class ReconciliationService {
} }
async reconcileOrders(orderIds: string[]) { async reconcileOrders(orderIds: string[]) {
const results = []; const results: any[] = [];
for (const orderId of orderIds) { for (const orderId of orderIds) {
try { try {

View File

@ -23,7 +23,7 @@ export class RefundService {
} }
async createRefund(userId: string, createRefundDto: CreateRefundDto) { 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') { if (order.status !== 'paid') {
throw new BadRequestException('Only paid orders can be refunded'); throw new BadRequestException('Only paid orders can be refunded');
@ -55,7 +55,7 @@ export class RefundService {
data: { data: {
refundNo, refundNo,
orderId: order.id, orderId: order.id,
channelId: order.channel.id, channelId: order.channelId,
userId, userId,
amount: refundAmount, amount: refundAmount,
reason: createRefundDto.reason, reason: createRefundDto.reason,
@ -64,7 +64,7 @@ export class RefundService {
const adapter = this.adapterFactory.createAdapter( const adapter = this.adapterFactory.createAdapter(
order.channel.type as PaymentChannelType, 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({ const result = await adapter.refundPayment({
@ -72,7 +72,7 @@ export class RefundService {
refundNo, refundNo,
amount: createRefundDto.amount, amount: createRefundDto.amount,
reason: createRefundDto.reason, reason: createRefundDto.reason,
channelOrderNo: order.channelOrderNo, channelOrderNo: order.channelOrderNo ?? undefined,
}); });
await this.createRefundLog( await this.createRefundLog(

View File

@ -16,7 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { Express } from 'express'; import type { Express } from 'express';
@Controller('users') @Controller('users')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)

View File

@ -1,8 +1,13 @@
import { NodeSDK } from '@opentelemetry/sdk-node'; const startTracing = () => {
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; if (process.env.OTEL_ENABLED !== 'true') {
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; return;
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'; }
import { SimpleSpanProcessor, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
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 isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
@ -20,18 +25,15 @@ const sdk = new NodeSDK({
}, },
}), }),
], ],
spanProcessor: isProduction traceExporter,
? new BatchSpanProcessor(traceExporter)
: new SimpleSpanProcessor(traceExporter),
serviceName: 'fischerx-api', serviceName: 'fischerx-api',
}); });
export const startTracing = () => {
sdk.start(); sdk.start();
console.log('OpenTelemetry tracing started'); console.log('OpenTelemetry tracing started');
} catch (error) {
console.warn('OpenTelemetry initialization failed:', error);
}
}; };
export const shutdownTracing = async () => { export { startTracing };
await sdk.shutdown();
console.log('OpenTelemetry tracing shutdown');
};