Compare commits
2 Commits
02954f56db
...
afd707d15f
| Author | SHA1 | Date |
|---|---|---|
|
|
afd707d15f | |
|
|
9e1dca25f2 |
|
|
@ -0,0 +1 @@
|
||||||
|
export { useAuthStore } from './useAuthStore';
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { authApi, AdminInfo } from '../infrastructure/auth.api';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
admin: AdminInfo | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
checkAuth: () => Promise<boolean>;
|
||||||
|
hasPermission: (permission: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
admin: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
login: async (username: string, password: string) => {
|
||||||
|
const data = await authApi.login(username, password);
|
||||||
|
localStorage.setItem('admin_token', data.token);
|
||||||
|
set({
|
||||||
|
admin: data.admin,
|
||||||
|
token: data.token,
|
||||||
|
isAuthenticated: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('admin_token');
|
||||||
|
set({
|
||||||
|
admin: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkAuth: async () => {
|
||||||
|
const token = localStorage.getItem('admin_token');
|
||||||
|
if (!token) {
|
||||||
|
set({ isAuthenticated: false, admin: null, token: null });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const admin = await authApi.verify();
|
||||||
|
set({
|
||||||
|
admin,
|
||||||
|
token,
|
||||||
|
isAuthenticated: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('admin_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ isAuthenticated: false, admin: null, token: null });
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission: (permission: string) => {
|
||||||
|
const { admin } = get();
|
||||||
|
if (!admin) return false;
|
||||||
|
|
||||||
|
const permissions = admin.permissions || [];
|
||||||
|
|
||||||
|
if (permissions.includes('*')) return true;
|
||||||
|
if (permissions.includes(permission)) return true;
|
||||||
|
|
||||||
|
const [resource] = permission.split(':');
|
||||||
|
if (permissions.includes(`${resource}:*`)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'auth-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
admin: state.admin,
|
||||||
|
token: state.token,
|
||||||
|
isAuthenticated: state.isAuthenticated,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface AdminInfo {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
admin: AdminInfo;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
login: async (username: string, password: string): Promise<LoginResponse> => {
|
||||||
|
const response = await api.post('/admin/login', { username, password });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
verify: async (): Promise<AdminInfo> => {
|
||||||
|
const response = await api.get('/admin/verify');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { authApi } from './auth.api';
|
||||||
|
export type { AdminInfo, LoginResponse } from './auth.api';
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Form, Input, Button, Card, message } from 'antd';
|
import { Form, Input, Button, Card, message } from 'antd';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { useAuth } from '../../../../shared/hooks/useAuth';
|
import { useAuthStore } from '../../application';
|
||||||
|
|
||||||
interface LoginFormValues {
|
interface LoginFormValues {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
@ -12,7 +12,7 @@ interface LoginFormValues {
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const login = useAuth((state) => state.login);
|
const login = useAuthStore((state) => state.login);
|
||||||
|
|
||||||
const onFinish = async (values: LoginFormValues) => {
|
const onFinish = async (values: LoginFormValues) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export {
|
||||||
|
useEvolutionStatistics,
|
||||||
|
useSystemHealth,
|
||||||
|
EVOLUTION_STATS_KEY,
|
||||||
|
SYSTEM_HEALTH_KEY,
|
||||||
|
} from './useDashboard';
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { dashboardApi } from '../infrastructure/dashboard.api';
|
||||||
|
|
||||||
|
export const EVOLUTION_STATS_KEY = 'evolution-stats';
|
||||||
|
export const SYSTEM_HEALTH_KEY = 'system-health';
|
||||||
|
|
||||||
|
export function useEvolutionStatistics() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EVOLUTION_STATS_KEY],
|
||||||
|
queryFn: () => dashboardApi.getEvolutionStatistics(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSystemHealth() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [SYSTEM_HEALTH_KEY],
|
||||||
|
queryFn: () => dashboardApi.getSystemHealth(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface EvolutionStatistics {
|
||||||
|
totalExperiences: number;
|
||||||
|
activeExperiences: number;
|
||||||
|
pendingExperiences: number;
|
||||||
|
approvedExperiences: number;
|
||||||
|
topExperienceTypes: { type: string; count: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthMetric {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
threshold: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthReport {
|
||||||
|
overall: string;
|
||||||
|
metrics: HealthMetric[];
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardApi = {
|
||||||
|
getEvolutionStatistics: async (): Promise<EvolutionStatistics> => {
|
||||||
|
const response = await api.get('/evolution/statistics');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSystemHealth: async (): Promise<HealthReport> => {
|
||||||
|
const response = await api.get('/evolution/health');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { dashboardApi } from './dashboard.api';
|
||||||
|
export type { EvolutionStatistics, HealthMetric, HealthReport } from './dashboard.api';
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Card, Row, Col, Statistic, Tag, Progress, List, Typography } from 'antd';
|
import { Card, Row, Col, Statistic, Tag, Progress, List, Typography } from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
|
@ -20,7 +19,8 @@ import {
|
||||||
Pie,
|
Pie,
|
||||||
Cell,
|
Cell,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import api from '../../../../shared/utils/api';
|
import { useEvolutionStatistics, useSystemHealth } from '../../application';
|
||||||
|
import type { HealthMetric } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -45,21 +45,8 @@ const mockCategoryData = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { data: evolutionStats } = useQuery({
|
const { data: evolutionStats } = useEvolutionStatistics();
|
||||||
queryKey: ['evolution-stats'],
|
const { data: healthReport } = useSystemHealth();
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/evolution/statistics');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: healthReport } = useQuery({
|
|
||||||
queryKey: ['system-health'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/evolution/health');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const getHealthColor = (status: string) => {
|
const getHealthColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -207,7 +194,7 @@ export function DashboardPage() {
|
||||||
>
|
>
|
||||||
<List
|
<List
|
||||||
dataSource={healthReport?.metrics || []}
|
dataSource={healthReport?.metrics || []}
|
||||||
renderItem={(item: { name: string; value: number; threshold: number; status: string }) => (
|
renderItem={(item: HealthMetric) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between mb-1">
|
||||||
|
|
@ -232,11 +219,11 @@ export function DashboardPage() {
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{healthReport?.recommendations?.length > 0 && (
|
{(healthReport?.recommendations?.length ?? 0) > 0 && (
|
||||||
<div className="mt-4 p-3 bg-yellow-50 rounded">
|
<div className="mt-4 p-3 bg-yellow-50 rounded">
|
||||||
<Text type="warning">建议:</Text>
|
<Text type="warning">建议:</Text>
|
||||||
<ul className="mt-2 ml-4 text-sm text-gray-600">
|
<ul className="mt-2 ml-4 text-sm text-gray-600">
|
||||||
{healthReport.recommendations.map((rec: string, i: number) => (
|
{healthReport?.recommendations?.map((rec: string, i: number) => (
|
||||||
<li key={i}>{rec}</li>
|
<li key={i}>{rec}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export {
|
||||||
|
usePendingExperiences,
|
||||||
|
useExperienceStatistics,
|
||||||
|
useApproveExperience,
|
||||||
|
useRejectExperience,
|
||||||
|
useRunEvolution,
|
||||||
|
EXPERIENCE_QUERY_KEY,
|
||||||
|
EXPERIENCE_STATS_KEY,
|
||||||
|
} from './useExperience';
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { experienceApi } from '../infrastructure/experience.api';
|
||||||
|
|
||||||
|
export const EXPERIENCE_QUERY_KEY = 'pending-experiences';
|
||||||
|
export const EXPERIENCE_STATS_KEY = 'experience-stats';
|
||||||
|
|
||||||
|
export function usePendingExperiences(type?: string, enabled: boolean = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EXPERIENCE_QUERY_KEY, type],
|
||||||
|
queryFn: () => experienceApi.getPendingExperiences(type),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExperienceStatistics() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [EXPERIENCE_STATS_KEY],
|
||||||
|
queryFn: () => experienceApi.getStatistics(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApproveExperience() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, adminId }: { id: string; adminId: string }) =>
|
||||||
|
experienceApi.approveExperience(id, adminId),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('经验已批准');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRejectExperience() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, adminId }: { id: string; adminId: string }) =>
|
||||||
|
experienceApi.rejectExperience(id, adminId),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('经验已拒绝');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRunEvolution() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params?: { hoursBack?: number; limit?: number }) =>
|
||||||
|
experienceApi.runEvolution(params?.hoursBack, params?.limit),
|
||||||
|
onSuccess: (result) => {
|
||||||
|
message.success(
|
||||||
|
`进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验`
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface Experience {
|
||||||
|
id: string;
|
||||||
|
experienceType: string;
|
||||||
|
content: string;
|
||||||
|
scenario: string;
|
||||||
|
confidence: number;
|
||||||
|
relatedCategory: string;
|
||||||
|
sourceConversationIds: string[];
|
||||||
|
verificationStatus: string;
|
||||||
|
usageCount: number;
|
||||||
|
positiveCount: number;
|
||||||
|
negativeCount: number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceListResponse {
|
||||||
|
items: Experience[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceStatistics {
|
||||||
|
total: number;
|
||||||
|
byStatus: Record<string, number>;
|
||||||
|
byType: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const experienceApi = {
|
||||||
|
getPendingExperiences: async (type?: string): Promise<ExperienceListResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (type) params.append('type', type);
|
||||||
|
const response = await api.get(`/memory/experience/pending?${params}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatistics: async (): Promise<ExperienceStatistics> => {
|
||||||
|
const response = await api.get('/memory/experience/statistics');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
approveExperience: async (id: string, adminId: string): Promise<void> => {
|
||||||
|
await api.post(`/memory/experience/${id}/approve`, { adminId });
|
||||||
|
},
|
||||||
|
|
||||||
|
rejectExperience: async (id: string, adminId: string): Promise<void> => {
|
||||||
|
await api.post(`/memory/experience/${id}/reject`, { adminId });
|
||||||
|
},
|
||||||
|
|
||||||
|
runEvolution: async (hoursBack: number = 24, limit: number = 50): Promise<{
|
||||||
|
conversationsAnalyzed: number;
|
||||||
|
experiencesExtracted: number;
|
||||||
|
}> => {
|
||||||
|
const response = await api.post('/evolution/run', { hoursBack, limit });
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { experienceApi } from './experience.api';
|
||||||
|
export type { Experience, ExperienceListResponse, ExperienceStatistics } from './experience.api';
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -8,7 +7,6 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
Space,
|
Space,
|
||||||
Modal,
|
Modal,
|
||||||
message,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
Statistic,
|
Statistic,
|
||||||
|
|
@ -21,8 +19,15 @@ import {
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import api from '../../../../shared/utils/api';
|
import { useAuthStore } from '../../../auth/application';
|
||||||
import { useAuth } from '../../../../shared/hooks/useAuth';
|
import {
|
||||||
|
usePendingExperiences,
|
||||||
|
useExperienceStatistics,
|
||||||
|
useApproveExperience,
|
||||||
|
useRejectExperience,
|
||||||
|
useRunEvolution,
|
||||||
|
} from '../../application';
|
||||||
|
import type { Experience } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
|
@ -37,86 +42,39 @@ const EXPERIENCE_TYPES = [
|
||||||
{ value: 'OBJECTION_HANDLING', label: '异议处理' },
|
{ value: 'OBJECTION_HANDLING', label: '异议处理' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Experience {
|
|
||||||
id: string;
|
|
||||||
experienceType: string;
|
|
||||||
content: string;
|
|
||||||
scenario: string;
|
|
||||||
confidence: number;
|
|
||||||
relatedCategory: string;
|
|
||||||
sourceConversationIds: string[];
|
|
||||||
verificationStatus: string;
|
|
||||||
usageCount: number;
|
|
||||||
positiveCount: number;
|
|
||||||
negativeCount: number;
|
|
||||||
isActive: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExperiencePage() {
|
export function ExperiencePage() {
|
||||||
const [activeTab, setActiveTab] = useState('pending');
|
const [activeTab, setActiveTab] = useState('pending');
|
||||||
const [typeFilter, setTypeFilter] = useState<string>();
|
const [typeFilter, setTypeFilter] = useState<string>();
|
||||||
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const admin = useAuthStore((state) => state.admin);
|
||||||
const admin = useAuth((state) => state.admin);
|
|
||||||
|
|
||||||
const { data: pendingData, isLoading: pendingLoading } = useQuery({
|
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences(
|
||||||
queryKey: ['pending-experiences', typeFilter],
|
typeFilter,
|
||||||
queryFn: async () => {
|
activeTab === 'pending'
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (typeFilter) params.append('type', typeFilter);
|
|
||||||
const response = await api.get(`/memory/experience/pending?${params}`);
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
enabled: activeTab === 'pending',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: stats } = useQuery({
|
|
||||||
queryKey: ['experience-stats'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await api.get('/memory/experience/statistics');
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const approveMutation = useMutation({
|
|
||||||
mutationFn: (id: string) =>
|
|
||||||
api.post(`/memory/experience/${id}/approve`, { adminId: admin?.id }),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('经验已批准');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const rejectMutation = useMutation({
|
|
||||||
mutationFn: (id: string) =>
|
|
||||||
api.post(`/memory/experience/${id}/reject`, { adminId: admin?.id }),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('经验已拒绝');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const runEvolutionMutation = useMutation({
|
|
||||||
mutationFn: () => api.post('/evolution/run', { hoursBack: 24, limit: 50 }),
|
|
||||||
onSuccess: (response) => {
|
|
||||||
const result = response.data.data;
|
|
||||||
message.success(
|
|
||||||
`进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验`
|
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
const { data: stats } = useExperienceStatistics();
|
||||||
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
const approveMutation = useApproveExperience();
|
||||||
},
|
const rejectMutation = useRejectExperience();
|
||||||
});
|
const runEvolutionMutation = useRunEvolution();
|
||||||
|
|
||||||
const handleView = (exp: Experience) => {
|
const handleView = (exp: Experience) => {
|
||||||
setSelectedExperience(exp);
|
setSelectedExperience(exp);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApprove = (id: string) => {
|
||||||
|
if (admin?.id) {
|
||||||
|
approveMutation.mutate({ id, adminId: admin.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = (id: string) => {
|
||||||
|
if (admin?.id) {
|
||||||
|
rejectMutation.mutate({ id, adminId: admin.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
const getTypeLabel = (type: string) => {
|
||||||
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
||||||
};
|
};
|
||||||
|
|
@ -202,14 +160,14 @@ export function ExperiencePage() {
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CheckOutlined />}
|
icon={<CheckOutlined />}
|
||||||
className="text-green-600"
|
className="text-green-600"
|
||||||
onClick={() => approveMutation.mutate(record.id)}
|
onClick={() => handleApprove(record.id)}
|
||||||
loading={approveMutation.isPending}
|
loading={approveMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CloseOutlined />}
|
icon={<CloseOutlined />}
|
||||||
danger
|
danger
|
||||||
onClick={() => rejectMutation.mutate(record.id)}
|
onClick={() => handleReject(record.id)}
|
||||||
loading={rejectMutation.isPending}
|
loading={rejectMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -226,7 +184,7 @@ export function ExperiencePage() {
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
onClick={() => runEvolutionMutation.mutate()}
|
onClick={() => runEvolutionMutation.mutate({})}
|
||||||
loading={runEvolutionMutation.isPending}
|
loading={runEvolutionMutation.isPending}
|
||||||
>
|
>
|
||||||
运行进化任务
|
运行进化任务
|
||||||
|
|
@ -318,7 +276,7 @@ export function ExperiencePage() {
|
||||||
key="reject"
|
key="reject"
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
rejectMutation.mutate(selectedExperience.id);
|
handleReject(selectedExperience.id);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -328,7 +286,7 @@ export function ExperiencePage() {
|
||||||
key="approve"
|
key="approve"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
approveMutation.mutate(selectedExperience.id);
|
handleApprove(selectedExperience.id);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export {
|
||||||
|
useKnowledgeArticles,
|
||||||
|
useCreateArticle,
|
||||||
|
useUpdateArticle,
|
||||||
|
useDeleteArticle,
|
||||||
|
usePublishArticle,
|
||||||
|
useUnpublishArticle,
|
||||||
|
KNOWLEDGE_QUERY_KEY,
|
||||||
|
} from './useKnowledge';
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { knowledgeApi, CreateArticleParams, UpdateArticleParams } from '../infrastructure/knowledge.api';
|
||||||
|
|
||||||
|
export const KNOWLEDGE_QUERY_KEY = 'knowledge-articles';
|
||||||
|
|
||||||
|
export function useKnowledgeArticles(category?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [KNOWLEDGE_QUERY_KEY, category],
|
||||||
|
queryFn: () => knowledgeApi.getArticles(category),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: CreateArticleParams) => knowledgeApi.createArticle(params),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章创建成功');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: UpdateArticleParams) => knowledgeApi.updateArticle(params),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章更新成功');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.deleteArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已删除');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublishArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.publishArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已发布');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnpublishArticle() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => knowledgeApi.unpublishArticle(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('文章已取消发布');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [KNOWLEDGE_QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { knowledgeApi } from './knowledge.api';
|
||||||
|
export type { Article, ArticleListResponse, CreateArticleParams, UpdateArticleParams } from './knowledge.api';
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import api from '../../../shared/utils/api';
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
summary: string;
|
||||||
|
category: string;
|
||||||
|
tags: string[];
|
||||||
|
source: string;
|
||||||
|
isPublished: boolean;
|
||||||
|
citationCount: number;
|
||||||
|
qualityScore: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleListResponse {
|
||||||
|
items: Article[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateArticleParams {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateArticleParams extends Partial<CreateArticleParams> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const knowledgeApi = {
|
||||||
|
getArticles: async (category?: string): Promise<ArticleListResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (category) params.append('category', category);
|
||||||
|
const response = await api.get(`/knowledge/articles?${params}`);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createArticle: async (params: CreateArticleParams): Promise<Article> => {
|
||||||
|
const response = await api.post('/knowledge/articles', params);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateArticle: async ({ id, ...params }: UpdateArticleParams): Promise<Article> => {
|
||||||
|
const response = await api.put(`/knowledge/articles/${id}`, params);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/knowledge/articles/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
publishArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.post(`/knowledge/articles/${id}/publish`);
|
||||||
|
},
|
||||||
|
|
||||||
|
unpublishArticle: async (id: string): Promise<void> => {
|
||||||
|
await api.post(`/knowledge/articles/${id}/unpublish`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -10,7 +9,6 @@ import {
|
||||||
Space,
|
Space,
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
message,
|
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Typography,
|
Typography,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
|
@ -24,7 +22,15 @@ import {
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import api from '../../../../shared/utils/api';
|
import {
|
||||||
|
useKnowledgeArticles,
|
||||||
|
useCreateArticle,
|
||||||
|
useUpdateArticle,
|
||||||
|
useDeleteArticle,
|
||||||
|
usePublishArticle,
|
||||||
|
useUnpublishArticle,
|
||||||
|
} from '../../application';
|
||||||
|
import type { Article, CreateArticleParams } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -39,20 +45,6 @@ const CATEGORIES = [
|
||||||
{ value: 'GENERAL', label: '通用知识' },
|
{ value: 'GENERAL', label: '通用知识' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Article {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
summary: string;
|
|
||||||
category: string;
|
|
||||||
tags: string[];
|
|
||||||
source: string;
|
|
||||||
isPublished: boolean;
|
|
||||||
citationCount: number;
|
|
||||||
qualityScore: number;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KnowledgePage() {
|
export function KnowledgePage() {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = useState<string>();
|
const [categoryFilter, setCategoryFilter] = useState<string>();
|
||||||
|
|
@ -60,64 +52,13 @@ export function KnowledgePage() {
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useKnowledgeArticles(categoryFilter);
|
||||||
queryKey: ['knowledge-articles', categoryFilter],
|
const createMutation = useCreateArticle();
|
||||||
queryFn: async () => {
|
const updateMutation = useUpdateArticle();
|
||||||
const params = new URLSearchParams();
|
const deleteMutation = useDeleteArticle();
|
||||||
if (categoryFilter) params.append('category', categoryFilter);
|
const publishMutation = usePublishArticle();
|
||||||
const response = await api.get(`/knowledge/articles?${params}`);
|
const unpublishMutation = useUnpublishArticle();
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMutation = useMutation({
|
|
||||||
mutationFn: (values: Partial<Article>) =>
|
|
||||||
api.post('/knowledge/articles', values),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章创建成功');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
setIsModalOpen(false);
|
|
||||||
form.resetFields();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
|
||||||
mutationFn: ({ id, ...values }: { id: string } & Partial<Article>) =>
|
|
||||||
api.put(`/knowledge/articles/${id}`, values),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章更新成功');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
setIsModalOpen(false);
|
|
||||||
form.resetFields();
|
|
||||||
setSelectedArticle(null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.delete(`/knowledge/articles/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已删除');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const publishMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/publish`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已发布');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const unpublishMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => api.post(`/knowledge/articles/${id}/unpublish`),
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('文章已取消发布');
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['knowledge-articles'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleEdit = (article: Article) => {
|
const handleEdit = (article: Article) => {
|
||||||
setSelectedArticle(article);
|
setSelectedArticle(article);
|
||||||
|
|
@ -135,11 +76,25 @@ export function KnowledgePage() {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (values: Partial<Article>) => {
|
const handleSubmit = (values: CreateArticleParams) => {
|
||||||
if (selectedArticle) {
|
if (selectedArticle) {
|
||||||
updateMutation.mutate({ id: selectedArticle.id, ...values });
|
updateMutation.mutate(
|
||||||
|
{ id: selectedArticle.id, ...values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
form.resetFields();
|
||||||
|
setSelectedArticle(null);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
form.resetFields();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,3 @@
|
||||||
import { create } from 'zustand';
|
// Re-export from auth feature for backward compatibility
|
||||||
import { persist } from 'zustand/middleware';
|
export { useAuthStore as useAuth } from '../../features/auth/application';
|
||||||
import api from '../utils/api';
|
export type { AdminInfo } from '../../features/auth/infrastructure';
|
||||||
|
|
||||||
interface AdminInfo {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthState {
|
|
||||||
admin: AdminInfo | null;
|
|
||||||
token: string | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
login: (username: string, password: string) => Promise<void>;
|
|
||||||
logout: () => void;
|
|
||||||
checkAuth: () => Promise<boolean>;
|
|
||||||
hasPermission: (permission: string) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAuth = create<AuthState>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
admin: null,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
|
|
||||||
login: async (username: string, password: string) => {
|
|
||||||
const response = await api.post('/admin/login', { username, password });
|
|
||||||
const { data } = response.data;
|
|
||||||
|
|
||||||
localStorage.setItem('admin_token', data.token);
|
|
||||||
|
|
||||||
set({
|
|
||||||
admin: data.admin,
|
|
||||||
token: data.token,
|
|
||||||
isAuthenticated: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
logout: () => {
|
|
||||||
localStorage.removeItem('admin_token');
|
|
||||||
set({
|
|
||||||
admin: null,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAuth: async () => {
|
|
||||||
const token = localStorage.getItem('admin_token');
|
|
||||||
if (!token) {
|
|
||||||
set({ isAuthenticated: false, admin: null, token: null });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.get('/admin/verify');
|
|
||||||
if (response.data.success) {
|
|
||||||
set({
|
|
||||||
admin: response.data.data,
|
|
||||||
token,
|
|
||||||
isAuthenticated: true,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem('admin_token');
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ isAuthenticated: false, admin: null, token: null });
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasPermission: (permission: string) => {
|
|
||||||
const { admin } = get();
|
|
||||||
if (!admin) return false;
|
|
||||||
|
|
||||||
const permissions = admin.permissions || [];
|
|
||||||
|
|
||||||
// 超管拥有所有权限
|
|
||||||
if (permissions.includes('*')) return true;
|
|
||||||
|
|
||||||
// 完全匹配
|
|
||||||
if (permissions.includes(permission)) return true;
|
|
||||||
|
|
||||||
// 通配符匹配
|
|
||||||
const [resource] = permission.split(':');
|
|
||||||
if (permissions.includes(`${resource}:*`)) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'auth-storage',
|
|
||||||
partialize: (state) => ({
|
|
||||||
admin: state.admin,
|
|
||||||
token: state.token,
|
|
||||||
isAuthenticated: state.isAuthenticated,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,8 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { IsOptional, IsString, IsNotEmpty } from 'class-validator';
|
import { ConversationService } from '../../application/services/conversation.service';
|
||||||
import { ConversationService } from './conversation.service';
|
import { CreateConversationDto, SendMessageDto } from '../../application/dtos/conversation.dto';
|
||||||
|
|
||||||
class CreateConversationDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SendMessageDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller('conversations')
|
@Controller('conversations')
|
||||||
export class ConversationController {
|
export class ConversationController {
|
||||||
|
|
@ -8,17 +8,7 @@ import {
|
||||||
MessageBody,
|
MessageBody,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { ConversationService } from './conversation.service';
|
import { ConversationService, FileAttachment } from '../../application/services/conversation.service';
|
||||||
|
|
||||||
interface FileAttachment {
|
|
||||||
id: string;
|
|
||||||
originalName: string;
|
|
||||||
mimeType: string;
|
|
||||||
type: 'image' | 'document' | 'audio' | 'video' | 'other';
|
|
||||||
size: number;
|
|
||||||
downloadUrl?: string;
|
|
||||||
thumbnailUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SendMessagePayload {
|
interface SendMessagePayload {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './conversation.controller';
|
||||||
|
export * from './conversation.gateway';
|
||||||
|
export * from './internal.controller';
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Controller, Get, Query, Param } from '@nestjs/common';
|
import { Controller, Get, Query, Param } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, MoreThan, LessThan } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { ConversationEntity } from '../domain/entities/conversation.entity';
|
import { ConversationORM } from '../../infrastructure/database/postgres/entities/conversation.orm';
|
||||||
import { MessageEntity } from '../domain/entities/message.entity';
|
import { MessageORM } from '../../infrastructure/database/postgres/entities/message.orm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内部 API - 供其他微服务调用
|
* 内部 API - 供其他微服务调用
|
||||||
|
|
@ -11,10 +11,10 @@ import { MessageEntity } from '../domain/entities/message.entity';
|
||||||
@Controller('internal/conversations')
|
@Controller('internal/conversations')
|
||||||
export class InternalConversationController {
|
export class InternalConversationController {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ConversationEntity)
|
@InjectRepository(ConversationORM)
|
||||||
private conversationRepo: Repository<ConversationEntity>,
|
private conversationRepo: Repository<ConversationORM>,
|
||||||
@InjectRepository(MessageEntity)
|
@InjectRepository(MessageORM)
|
||||||
private messageRepo: Repository<MessageEntity>,
|
private messageRepo: Repository<MessageORM>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, MoreThan, LessThan } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { ConversationORM } from './entities/conversation.orm';
|
import { ConversationORM } from '../../../infrastructure/database/postgres/entities/conversation.orm';
|
||||||
import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface';
|
import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface';
|
||||||
import {
|
import {
|
||||||
ConversationEntity,
|
ConversationEntity,
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './conversation-postgres.repository';
|
||||||
|
export * from './message-postgres.repository';
|
||||||
|
export * from './token-usage-postgres.repository';
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { MessageORM } from './entities/message.orm';
|
import { MessageORM } from '../../../infrastructure/database/postgres/entities/message.orm';
|
||||||
import { IMessageRepository } from '../../../domain/repositories/message.repository.interface';
|
import { IMessageRepository } from '../../../domain/repositories/message.repository.interface';
|
||||||
import { MessageEntity } from '../../../domain/entities/message.entity';
|
import { MessageEntity } from '../../../domain/entities/message.entity';
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { TokenUsageORM } from './entities/token-usage.orm';
|
import { TokenUsageORM } from '../../../infrastructure/database/postgres/entities/token-usage.orm';
|
||||||
import { ITokenUsageRepository } from '../../../domain/repositories/token-usage.repository.interface';
|
import { ITokenUsageRepository } from '../../../domain/repositories/token-usage.repository.interface';
|
||||||
import { TokenUsageEntity } from '../../../domain/entities/token-usage.entity';
|
import { TokenUsageEntity } from '../../../domain/entities/token-usage.entity';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateConversationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileAttachmentDto {
|
||||||
|
id: string;
|
||||||
|
originalName: string;
|
||||||
|
mimeType: string;
|
||||||
|
type: 'image' | 'document' | 'audio' | 'video' | 'other';
|
||||||
|
size: number;
|
||||||
|
downloadUrl?: string;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => FileAttachmentDto)
|
||||||
|
attachments?: FileAttachmentDto[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './conversation.dto';
|
||||||
|
|
@ -3,27 +3,27 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
import {
|
import {
|
||||||
ConversationEntity,
|
ConversationEntity,
|
||||||
ConversationStatus,
|
ConversationStatus,
|
||||||
} from '../domain/entities/conversation.entity';
|
} from '../../domain/entities/conversation.entity';
|
||||||
import {
|
import {
|
||||||
MessageEntity,
|
MessageEntity,
|
||||||
MessageRole,
|
MessageRole,
|
||||||
MessageType,
|
MessageType,
|
||||||
} from '../domain/entities/message.entity';
|
} from '../../domain/entities/message.entity';
|
||||||
import {
|
import {
|
||||||
IConversationRepository,
|
IConversationRepository,
|
||||||
CONVERSATION_REPOSITORY,
|
CONVERSATION_REPOSITORY,
|
||||||
} from '../domain/repositories/conversation.repository.interface';
|
} from '../../domain/repositories/conversation.repository.interface';
|
||||||
import {
|
import {
|
||||||
IMessageRepository,
|
IMessageRepository,
|
||||||
MESSAGE_REPOSITORY,
|
MESSAGE_REPOSITORY,
|
||||||
} from '../domain/repositories/message.repository.interface';
|
} from '../../domain/repositories/message.repository.interface';
|
||||||
import {
|
import {
|
||||||
ClaudeAgentServiceV2,
|
ClaudeAgentServiceV2,
|
||||||
ConversationContext,
|
ConversationContext,
|
||||||
StreamChunk,
|
StreamChunk,
|
||||||
} from '../infrastructure/claude/claude-agent-v2.service';
|
} from '../../infrastructure/claude/claude-agent-v2.service';
|
||||||
|
|
||||||
export interface CreateConversationDto {
|
export interface CreateConversationParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ export interface FileAttachment {
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendMessageDto {
|
export interface SendMessageParams {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -58,11 +58,11 @@ export class ConversationService {
|
||||||
/**
|
/**
|
||||||
* Create a new conversation
|
* Create a new conversation
|
||||||
*/
|
*/
|
||||||
async createConversation(dto: CreateConversationDto): Promise<ConversationEntity> {
|
async createConversation(params: CreateConversationParams): Promise<ConversationEntity> {
|
||||||
const conversation = ConversationEntity.create({
|
const conversation = ConversationEntity.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
userId: dto.userId,
|
userId: params.userId,
|
||||||
title: dto.title || '新对话',
|
title: params.title || '新对话',
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.conversationRepo.save(conversation);
|
return this.conversationRepo.save(conversation);
|
||||||
|
|
@ -107,34 +107,34 @@ export class ConversationService {
|
||||||
/**
|
/**
|
||||||
* Send a message and get streaming response
|
* Send a message and get streaming response
|
||||||
*/
|
*/
|
||||||
async *sendMessage(dto: SendMessageDto): AsyncGenerator<StreamChunk> {
|
async *sendMessage(params: SendMessageParams): AsyncGenerator<StreamChunk> {
|
||||||
// Verify conversation exists and belongs to user
|
// Verify conversation exists and belongs to user
|
||||||
const conversation = await this.getConversation(dto.conversationId, dto.userId);
|
const conversation = await this.getConversation(params.conversationId, params.userId);
|
||||||
|
|
||||||
if (!conversation.isActive()) {
|
if (!conversation.isActive()) {
|
||||||
throw new Error('Conversation is not active');
|
throw new Error('Conversation is not active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save user message with attachments if present
|
// Save user message with attachments if present
|
||||||
const hasAttachments = dto.attachments && dto.attachments.length > 0;
|
const hasAttachments = params.attachments && params.attachments.length > 0;
|
||||||
const userMessage = MessageEntity.create({
|
const userMessage = MessageEntity.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
conversationId: dto.conversationId,
|
conversationId: params.conversationId,
|
||||||
role: MessageRole.USER,
|
role: MessageRole.USER,
|
||||||
type: hasAttachments ? MessageType.TEXT_WITH_ATTACHMENTS : MessageType.TEXT,
|
type: hasAttachments ? MessageType.TEXT_WITH_ATTACHMENTS : MessageType.TEXT,
|
||||||
content: dto.content,
|
content: params.content,
|
||||||
metadata: hasAttachments ? { attachments: dto.attachments } : undefined,
|
metadata: hasAttachments ? { attachments: params.attachments } : undefined,
|
||||||
});
|
});
|
||||||
await this.messageRepo.save(userMessage);
|
await this.messageRepo.save(userMessage);
|
||||||
|
|
||||||
// Get previous messages for context
|
// Get previous messages for context
|
||||||
const previousMessages = await this.messageRepo.findByConversationId(dto.conversationId);
|
const previousMessages = await this.messageRepo.findByConversationId(params.conversationId);
|
||||||
const recentMessages = previousMessages.slice(-20); // Last 20 messages for context
|
const recentMessages = previousMessages.slice(-20); // Last 20 messages for context
|
||||||
|
|
||||||
// Build context with support for multimodal messages and consulting state (V2)
|
// Build context with support for multimodal messages and consulting state (V2)
|
||||||
const context: ConversationContext = {
|
const context: ConversationContext = {
|
||||||
userId: dto.userId,
|
userId: params.userId,
|
||||||
conversationId: dto.conversationId,
|
conversationId: params.conversationId,
|
||||||
previousMessages: recentMessages.map((m) => {
|
previousMessages: recentMessages.map((m) => {
|
||||||
const msg: { role: 'user' | 'assistant'; content: string; attachments?: FileAttachment[] } = {
|
const msg: { role: 'user' | 'assistant'; content: string; attachments?: FileAttachment[] } = {
|
||||||
role: m.role as 'user' | 'assistant',
|
role: m.role as 'user' | 'assistant',
|
||||||
|
|
@ -158,9 +158,9 @@ export class ConversationService {
|
||||||
|
|
||||||
// Stream response from Claude (with attachments for multimodal support)
|
// Stream response from Claude (with attachments for multimodal support)
|
||||||
for await (const chunk of this.claudeAgentService.sendMessage(
|
for await (const chunk of this.claudeAgentService.sendMessage(
|
||||||
dto.content,
|
params.content,
|
||||||
context,
|
context,
|
||||||
dto.attachments,
|
params.attachments,
|
||||||
)) {
|
)) {
|
||||||
if (chunk.type === 'text' && chunk.content) {
|
if (chunk.type === 'text' && chunk.content) {
|
||||||
fullResponse += chunk.content;
|
fullResponse += chunk.content;
|
||||||
|
|
@ -194,7 +194,7 @@ export class ConversationService {
|
||||||
// Save assistant response
|
// Save assistant response
|
||||||
const assistantMessage = MessageEntity.create({
|
const assistantMessage = MessageEntity.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
conversationId: dto.conversationId,
|
conversationId: params.conversationId,
|
||||||
role: MessageRole.ASSISTANT,
|
role: MessageRole.ASSISTANT,
|
||||||
type: MessageType.TEXT,
|
type: MessageType.TEXT,
|
||||||
content: fullResponse,
|
content: fullResponse,
|
||||||
|
|
@ -204,7 +204,7 @@ export class ConversationService {
|
||||||
|
|
||||||
// Update conversation title if first message
|
// Update conversation title if first message
|
||||||
if (conversation.messageCount === 0) {
|
if (conversation.messageCount === 0) {
|
||||||
const title = await this.generateTitle(dto.content);
|
const title = await this.generateTitle(params.content);
|
||||||
conversation.title = title;
|
conversation.title = title;
|
||||||
await this.conversationRepo.update(conversation);
|
await this.conversationRepo.update(conversation);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './conversation.service';
|
||||||
|
|
@ -3,16 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ConversationORM } from '../infrastructure/database/postgres/entities/conversation.orm';
|
import { ConversationORM } from '../infrastructure/database/postgres/entities/conversation.orm';
|
||||||
import { MessageORM } from '../infrastructure/database/postgres/entities/message.orm';
|
import { MessageORM } from '../infrastructure/database/postgres/entities/message.orm';
|
||||||
import { TokenUsageORM } from '../infrastructure/database/postgres/entities/token-usage.orm';
|
import { TokenUsageORM } from '../infrastructure/database/postgres/entities/token-usage.orm';
|
||||||
import { ConversationPostgresRepository } from '../infrastructure/database/postgres/conversation-postgres.repository';
|
import { ConversationPostgresRepository } from '../adapters/outbound/persistence/conversation-postgres.repository';
|
||||||
import { MessagePostgresRepository } from '../infrastructure/database/postgres/message-postgres.repository';
|
import { MessagePostgresRepository } from '../adapters/outbound/persistence/message-postgres.repository';
|
||||||
import { TokenUsagePostgresRepository } from '../infrastructure/database/postgres/token-usage-postgres.repository';
|
import { TokenUsagePostgresRepository } from '../adapters/outbound/persistence/token-usage-postgres.repository';
|
||||||
import { CONVERSATION_REPOSITORY } from '../domain/repositories/conversation.repository.interface';
|
import { CONVERSATION_REPOSITORY } from '../domain/repositories/conversation.repository.interface';
|
||||||
import { MESSAGE_REPOSITORY } from '../domain/repositories/message.repository.interface';
|
import { MESSAGE_REPOSITORY } from '../domain/repositories/message.repository.interface';
|
||||||
import { TOKEN_USAGE_REPOSITORY } from '../domain/repositories/token-usage.repository.interface';
|
import { TOKEN_USAGE_REPOSITORY } from '../domain/repositories/token-usage.repository.interface';
|
||||||
import { ConversationService } from './conversation.service';
|
import { ConversationService } from '../application/services/conversation.service';
|
||||||
import { ConversationController } from './conversation.controller';
|
import { ConversationController } from '../adapters/inbound/conversation.controller';
|
||||||
import { InternalConversationController } from './internal.controller';
|
import { InternalConversationController } from '../adapters/inbound/internal.controller';
|
||||||
import { ConversationGateway } from './conversation.gateway';
|
import { ConversationGateway } from '../adapters/inbound/conversation.gateway';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([ConversationORM, MessageORM, TokenUsageORM])],
|
imports: [TypeOrmModule.forFeature([ConversationORM, MessageORM, TokenUsageORM])],
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ClaudeAgentServiceV2 } from './claude-agent-v2.service';
|
||||||
import { ImmigrationToolsService } from './tools/immigration-tools.service';
|
import { ImmigrationToolsService } from './tools/immigration-tools.service';
|
||||||
import { TokenUsageService } from './token-usage.service';
|
import { TokenUsageService } from './token-usage.service';
|
||||||
import { StrategyEngineService } from './strategy/strategy-engine.service';
|
import { StrategyEngineService } from './strategy/strategy-engine.service';
|
||||||
import { TokenUsageEntity } from '../../domain/entities/token-usage.entity';
|
import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm';
|
||||||
import { KnowledgeModule } from '../knowledge/knowledge.module';
|
import { KnowledgeModule } from '../knowledge/knowledge.module';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
|
|
@ -14,7 +14,7 @@ import { KnowledgeModule } from '../knowledge/knowledge.module';
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
KnowledgeModule,
|
KnowledgeModule,
|
||||||
TypeOrmModule.forFeature([TokenUsageEntity]),
|
TypeOrmModule.forFeature([TokenUsageORM]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ClaudeAgentService,
|
ClaudeAgentService,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, Between, MoreThanOrEqual } from 'typeorm';
|
import { Repository, Between, MoreThanOrEqual } from 'typeorm';
|
||||||
import { TokenUsageEntity } from '../../domain/entities/token-usage.entity';
|
import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude API 定价 (截至 2024年)
|
* Claude API 定价 (截至 2024年)
|
||||||
|
|
@ -65,8 +65,8 @@ export interface UsageStats {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenUsageService {
|
export class TokenUsageService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(TokenUsageEntity)
|
@InjectRepository(TokenUsageORM)
|
||||||
private tokenUsageRepository: Repository<TokenUsageEntity>,
|
private tokenUsageRepository: Repository<TokenUsageORM>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,7 +95,7 @@ export class TokenUsageService {
|
||||||
/**
|
/**
|
||||||
* 记录一次 API 调用的 token 使用量
|
* 记录一次 API 调用的 token 使用量
|
||||||
*/
|
*/
|
||||||
async recordUsage(input: TokenUsageInput): Promise<TokenUsageEntity> {
|
async recordUsage(input: TokenUsageInput): Promise<TokenUsageORM> {
|
||||||
const cacheCreationTokens = input.cacheCreationTokens || 0;
|
const cacheCreationTokens = input.cacheCreationTokens || 0;
|
||||||
const cacheReadTokens = input.cacheReadTokens || 0;
|
const cacheReadTokens = input.cacheReadTokens || 0;
|
||||||
const totalTokens = input.inputTokens + input.outputTokens;
|
const totalTokens = input.inputTokens + input.outputTokens;
|
||||||
|
|
@ -203,7 +203,7 @@ export class TokenUsageService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按日期分组
|
// 按日期分组
|
||||||
const byDate = new Map<string, TokenUsageEntity[]>();
|
const byDate = new Map<string, TokenUsageORM[]>();
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const date = record.createdAt.toISOString().split('T')[0];
|
const date = record.createdAt.toISOString().split('T')[0];
|
||||||
if (!byDate.has(date)) {
|
if (!byDate.has(date)) {
|
||||||
|
|
@ -256,7 +256,7 @@ export class TokenUsageService {
|
||||||
/**
|
/**
|
||||||
* 计算统计数据
|
* 计算统计数据
|
||||||
*/
|
*/
|
||||||
private calculateStats(records: TokenUsageEntity[]): UsageStats {
|
private calculateStats(records: TokenUsageORM[]): UsageStats {
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
return {
|
return {
|
||||||
totalRequests: 0,
|
totalRequests: 0,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
export * from './entities';
|
export * from './entities';
|
||||||
export * from './conversation-postgres.repository';
|
|
||||||
export * from './message-postgres.repository';
|
|
||||||
export * from './token-usage-postgres.repository';
|
|
||||||
|
|
|
||||||
|
|
@ -12,42 +12,16 @@ import {
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AdminService, AdminRole, LoginResult } from './admin.service';
|
import { AdminService } from '../../application/services/admin.service';
|
||||||
|
import { AdminRole } from '../../domain/value-objects/admin-role.enum';
|
||||||
// ========== DTOs ==========
|
import {
|
||||||
|
LoginDto,
|
||||||
class LoginDto {
|
CreateAdminDto,
|
||||||
username: string;
|
UpdateAdminDto,
|
||||||
password: string;
|
ChangePasswordDto,
|
||||||
}
|
ResetPasswordDto,
|
||||||
|
LoginResult,
|
||||||
class CreateAdminDto {
|
} from '../../application/dtos/admin.dto';
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
role: AdminRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UpdateAdminDto {
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
role?: AdminRole;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChangePasswordDto {
|
|
||||||
oldPassword: string;
|
|
||||||
newPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResetPasswordDto {
|
|
||||||
newPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Controller ==========
|
|
||||||
|
|
||||||
@Controller('admin')
|
@Controller('admin')
|
||||||
export class AdminController {
|
export class AdminController {
|
||||||
|
|
@ -3,21 +3,11 @@ import {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Body,
|
Body,
|
||||||
Query,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { EvolutionService } from './evolution.service';
|
import { EvolutionService } from '../../application/services/evolution.service';
|
||||||
|
import { RunEvolutionTaskDto } from '../../application/dtos/evolution.dto';
|
||||||
// ========== DTOs ==========
|
|
||||||
|
|
||||||
class RunEvolutionTaskDto {
|
|
||||||
hoursBack?: number;
|
|
||||||
limit?: number;
|
|
||||||
minMessageCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Controller ==========
|
|
||||||
|
|
||||||
@Controller('evolution')
|
@Controller('evolution')
|
||||||
export class EvolutionController {
|
export class EvolutionController {
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './evolution.controller';
|
||||||
|
export * from './admin.controller';
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './conversation.client';
|
||||||
|
export * from './knowledge.client';
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { IAdminRepository } from '../../../domain/repositories/admin.repository.interface';
|
||||||
|
import { AdminEntity } from '../../../domain/entities/admin.entity';
|
||||||
|
import { AdminRole } from '../../../domain/value-objects/admin-role.enum';
|
||||||
|
import { AdminORM } from '../../../infrastructure/database/postgres/entities/admin.orm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminPostgresRepository implements IAdminRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AdminORM)
|
||||||
|
private adminRepo: Repository<AdminORM>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async save(admin: AdminEntity): Promise<void> {
|
||||||
|
const orm = this.toORM(admin);
|
||||||
|
await this.adminRepo.save(orm);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<AdminEntity | null> {
|
||||||
|
const orm = await this.adminRepo.findOne({ where: { id } });
|
||||||
|
return orm ? this.toEntity(orm) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUsername(username: string): Promise<AdminEntity | null> {
|
||||||
|
const orm = await this.adminRepo.findOne({ where: { username } });
|
||||||
|
return orm ? this.toEntity(orm) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(options?: {
|
||||||
|
role?: AdminRole;
|
||||||
|
isActive?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): Promise<AdminEntity[]> {
|
||||||
|
const query = this.adminRepo.createQueryBuilder('admin');
|
||||||
|
|
||||||
|
if (options?.role) {
|
||||||
|
query.andWhere('admin.role = :role', { role: options.role });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.isActive !== undefined) {
|
||||||
|
query.andWhere('admin.isActive = :active', { active: options.isActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
query.orderBy('admin.createdAt', 'DESC');
|
||||||
|
|
||||||
|
if (options?.limit) {
|
||||||
|
query.take(options.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.offset) {
|
||||||
|
query.skip(options.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orms = await query.getMany();
|
||||||
|
return orms.map(orm => this.toEntity(orm));
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(options?: {
|
||||||
|
role?: AdminRole;
|
||||||
|
isActive?: boolean;
|
||||||
|
}): Promise<number> {
|
||||||
|
const query = this.adminRepo.createQueryBuilder('admin');
|
||||||
|
|
||||||
|
if (options?.role) {
|
||||||
|
query.andWhere('admin.role = :role', { role: options.role });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.isActive !== undefined) {
|
||||||
|
query.andWhere('admin.isActive = :active', { active: options.isActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(admin: AdminEntity): Promise<void> {
|
||||||
|
const orm = this.toORM(admin);
|
||||||
|
await this.adminRepo.save(orm);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.adminRepo.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toORM(entity: AdminEntity): AdminORM {
|
||||||
|
const orm = new AdminORM();
|
||||||
|
orm.id = entity.id;
|
||||||
|
orm.username = entity.username;
|
||||||
|
orm.passwordHash = entity.passwordHash;
|
||||||
|
orm.name = entity.name;
|
||||||
|
orm.email = entity.email;
|
||||||
|
orm.phone = entity.phone;
|
||||||
|
orm.role = entity.role;
|
||||||
|
orm.permissions = entity.permissions;
|
||||||
|
orm.avatar = entity.avatar;
|
||||||
|
orm.lastLoginAt = entity.lastLoginAt;
|
||||||
|
orm.lastLoginIp = entity.lastLoginIp;
|
||||||
|
orm.isActive = entity.isActive;
|
||||||
|
orm.createdAt = entity.createdAt;
|
||||||
|
orm.updatedAt = entity.updatedAt;
|
||||||
|
return orm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toEntity(orm: AdminORM): AdminEntity {
|
||||||
|
return AdminEntity.fromPersistence({
|
||||||
|
id: orm.id,
|
||||||
|
username: orm.username,
|
||||||
|
passwordHash: orm.passwordHash,
|
||||||
|
name: orm.name,
|
||||||
|
email: orm.email,
|
||||||
|
phone: orm.phone,
|
||||||
|
role: orm.role as AdminRole,
|
||||||
|
permissions: orm.permissions,
|
||||||
|
avatar: orm.avatar,
|
||||||
|
lastLoginAt: orm.lastLoginAt,
|
||||||
|
lastLoginIp: orm.lastLoginIp,
|
||||||
|
isActive: orm.isActive,
|
||||||
|
createdAt: orm.createdAt,
|
||||||
|
updatedAt: orm.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './admin-postgres.repository';
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from '../adapters/inbound/admin.controller';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from '../application/services/admin.service';
|
||||||
import { AdminORM } from '../infrastructure/database/entities/admin.orm';
|
import { AdminPostgresRepository } from '../adapters/outbound/persistence/admin-postgres.repository';
|
||||||
|
import { AdminORM } from '../infrastructure/database/postgres/entities/admin.orm';
|
||||||
|
import { ADMIN_REPOSITORY } from '../domain/repositories/admin.repository.interface';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([AdminORM]),
|
TypeOrmModule.forFeature([AdminORM]),
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService],
|
providers: [
|
||||||
|
AdminService,
|
||||||
|
{
|
||||||
|
provide: ADMIN_REPOSITORY,
|
||||||
|
useClass: AdminPostgresRepository,
|
||||||
|
},
|
||||||
|
],
|
||||||
exports: [AdminService],
|
exports: [AdminService],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { AdminRole } from '../../domain/value-objects/admin-role.enum';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateAdminDto {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
role: AdminRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateAdminDto {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
role?: AdminRole;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChangePasswordDto {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
|
admin: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
token: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class RunEvolutionTaskDto {
|
||||||
|
hoursBack?: number;
|
||||||
|
limit?: number;
|
||||||
|
minMessageCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvolutionTaskResult {
|
||||||
|
taskId: string;
|
||||||
|
status: 'success' | 'partial' | 'failed';
|
||||||
|
conversationsAnalyzed: number;
|
||||||
|
experiencesExtracted: number;
|
||||||
|
knowledgeGapsFound: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './evolution.dto';
|
||||||
|
export * from './admin.dto';
|
||||||
|
|
@ -1,68 +1,11 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AdminORM } from '../infrastructure/database/entities/admin.orm';
|
import { AdminEntity } from '../../domain/entities/admin.entity';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { AdminRole, ROLE_PERMISSIONS } from '../../domain/value-objects/admin-role.enum';
|
||||||
|
import { IAdminRepository, ADMIN_REPOSITORY } from '../../domain/repositories/admin.repository.interface';
|
||||||
/**
|
import { LoginResult } from '../dtos/admin.dto';
|
||||||
* 管理员角色
|
|
||||||
*/
|
|
||||||
export enum AdminRole {
|
|
||||||
SUPER_ADMIN = 'SUPER_ADMIN',
|
|
||||||
ADMIN = 'ADMIN',
|
|
||||||
OPERATOR = 'OPERATOR',
|
|
||||||
VIEWER = 'VIEWER',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 角色权限映射
|
|
||||||
*/
|
|
||||||
const ROLE_PERMISSIONS: Record<AdminRole, string[]> = {
|
|
||||||
[AdminRole.SUPER_ADMIN]: ['*'],
|
|
||||||
[AdminRole.ADMIN]: [
|
|
||||||
'knowledge:*',
|
|
||||||
'experience:*',
|
|
||||||
'user:read',
|
|
||||||
'conversation:read',
|
|
||||||
'statistics:*',
|
|
||||||
'admin:read',
|
|
||||||
],
|
|
||||||
[AdminRole.OPERATOR]: [
|
|
||||||
'knowledge:read',
|
|
||||||
'knowledge:create',
|
|
||||||
'knowledge:update',
|
|
||||||
'experience:read',
|
|
||||||
'experience:approve',
|
|
||||||
'user:read',
|
|
||||||
'conversation:read',
|
|
||||||
'statistics:read',
|
|
||||||
],
|
|
||||||
[AdminRole.VIEWER]: [
|
|
||||||
'knowledge:read',
|
|
||||||
'experience:read',
|
|
||||||
'user:read',
|
|
||||||
'conversation:read',
|
|
||||||
'statistics:read',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录结果
|
|
||||||
*/
|
|
||||||
export interface LoginResult {
|
|
||||||
admin: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
permissions: string[];
|
|
||||||
};
|
|
||||||
token: string;
|
|
||||||
expiresIn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员服务
|
* 管理员服务
|
||||||
|
|
@ -73,8 +16,8 @@ export class AdminService {
|
||||||
private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时
|
private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AdminORM)
|
@Inject(ADMIN_REPOSITORY)
|
||||||
private adminRepo: Repository<AdminORM>,
|
private adminRepo: IAdminRepository,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.jwtSecret = this.configService.get('JWT_SECRET') || 'iconsulting-secret-key';
|
this.jwtSecret = this.configService.get('JWT_SECRET') || 'iconsulting-secret-key';
|
||||||
|
|
@ -84,7 +27,7 @@ export class AdminService {
|
||||||
* 管理员登录
|
* 管理员登录
|
||||||
*/
|
*/
|
||||||
async login(username: string, password: string, ip?: string): Promise<LoginResult> {
|
async login(username: string, password: string, ip?: string): Promise<LoginResult> {
|
||||||
const admin = await this.adminRepo.findOne({ where: { username } });
|
const admin = await this.adminRepo.findByUsername(username);
|
||||||
|
|
||||||
if (!admin || !admin.isActive) {
|
if (!admin || !admin.isActive) {
|
||||||
throw new Error('用户名或密码错误');
|
throw new Error('用户名或密码错误');
|
||||||
|
|
@ -96,9 +39,8 @@ export class AdminService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新登录信息
|
// 更新登录信息
|
||||||
admin.lastLoginAt = new Date();
|
admin.recordLogin(ip);
|
||||||
admin.lastLoginIp = ip;
|
await this.adminRepo.update(admin);
|
||||||
await this.adminRepo.save(admin);
|
|
||||||
|
|
||||||
// 生成Token
|
// 生成Token
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
|
|
@ -112,7 +54,7 @@ export class AdminService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取权限
|
// 获取权限
|
||||||
const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
|
const permissions = admin.getPermissions();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
admin: {
|
admin: {
|
||||||
|
|
@ -146,12 +88,12 @@ export class AdminService {
|
||||||
role: string;
|
role: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const admin = await this.adminRepo.findOne({ where: { id: decoded.sub } });
|
const admin = await this.adminRepo.findById(decoded.sub);
|
||||||
if (!admin || !admin.isActive) {
|
if (!admin || !admin.isActive) {
|
||||||
return { valid: false };
|
return { valid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
|
const permissions = admin.getPermissions();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
|
|
@ -177,11 +119,9 @@ export class AdminService {
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
role: AdminRole;
|
role: AdminRole;
|
||||||
}): Promise<AdminORM> {
|
}): Promise<AdminEntity> {
|
||||||
// 检查用户名是否存在
|
// 检查用户名是否存在
|
||||||
const existing = await this.adminRepo.findOne({
|
const existing = await this.adminRepo.findByUsername(params.username);
|
||||||
where: { username: params.username },
|
|
||||||
});
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new Error('用户名已存在');
|
throw new Error('用户名已存在');
|
||||||
}
|
}
|
||||||
|
|
@ -189,16 +129,13 @@ export class AdminService {
|
||||||
// 加密密码
|
// 加密密码
|
||||||
const passwordHash = await bcrypt.hash(params.password, 10);
|
const passwordHash = await bcrypt.hash(params.password, 10);
|
||||||
|
|
||||||
const admin = this.adminRepo.create({
|
const admin = AdminEntity.create({
|
||||||
id: uuidv4(),
|
|
||||||
username: params.username,
|
username: params.username,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
email: params.email,
|
email: params.email,
|
||||||
phone: params.phone,
|
phone: params.phone,
|
||||||
role: params.role,
|
role: params.role,
|
||||||
permissions: ROLE_PERMISSIONS[params.role],
|
|
||||||
isActive: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.adminRepo.save(admin);
|
await this.adminRepo.save(admin);
|
||||||
|
|
@ -215,28 +152,24 @@ export class AdminService {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
items: AdminORM[];
|
items: AdminEntity[];
|
||||||
total: number;
|
total: number;
|
||||||
}> {
|
}> {
|
||||||
const page = options?.page || 1;
|
const page = options?.page || 1;
|
||||||
const pageSize = options?.pageSize || 20;
|
const pageSize = options?.pageSize || 20;
|
||||||
|
|
||||||
const query = this.adminRepo.createQueryBuilder('admin');
|
const [items, total] = await Promise.all([
|
||||||
|
this.adminRepo.findAll({
|
||||||
if (options?.role) {
|
role: options?.role,
|
||||||
query.andWhere('admin.role = :role', { role: options.role });
|
isActive: options?.isActive,
|
||||||
}
|
limit: pageSize,
|
||||||
|
offset: (page - 1) * pageSize,
|
||||||
if (options?.isActive !== undefined) {
|
}),
|
||||||
query.andWhere('admin.isActive = :active', { active: options.isActive });
|
this.adminRepo.count({
|
||||||
}
|
role: options?.role,
|
||||||
|
isActive: options?.isActive,
|
||||||
query.orderBy('admin.createdAt', 'DESC');
|
}),
|
||||||
|
]);
|
||||||
const [items, total] = await query
|
|
||||||
.skip((page - 1) * pageSize)
|
|
||||||
.take(pageSize)
|
|
||||||
.getManyAndCount();
|
|
||||||
|
|
||||||
return { items, total };
|
return { items, total };
|
||||||
}
|
}
|
||||||
|
|
@ -253,8 +186,8 @@ export class AdminService {
|
||||||
role?: AdminRole;
|
role?: AdminRole;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<AdminORM> {
|
): Promise<AdminEntity> {
|
||||||
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
const admin = await this.adminRepo.findById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new Error('管理员不存在');
|
throw new Error('管理员不存在');
|
||||||
}
|
}
|
||||||
|
|
@ -262,13 +195,10 @@ export class AdminService {
|
||||||
if (params.name) admin.name = params.name;
|
if (params.name) admin.name = params.name;
|
||||||
if (params.email !== undefined) admin.email = params.email;
|
if (params.email !== undefined) admin.email = params.email;
|
||||||
if (params.phone !== undefined) admin.phone = params.phone;
|
if (params.phone !== undefined) admin.phone = params.phone;
|
||||||
if (params.role) {
|
if (params.role) admin.updateRole(params.role);
|
||||||
admin.role = params.role;
|
|
||||||
admin.permissions = ROLE_PERMISSIONS[params.role];
|
|
||||||
}
|
|
||||||
if (params.isActive !== undefined) admin.isActive = params.isActive;
|
if (params.isActive !== undefined) admin.isActive = params.isActive;
|
||||||
|
|
||||||
await this.adminRepo.save(admin);
|
await this.adminRepo.update(admin);
|
||||||
|
|
||||||
return admin;
|
return admin;
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +211,7 @@ export class AdminService {
|
||||||
oldPassword: string,
|
oldPassword: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
const admin = await this.adminRepo.findById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new Error('管理员不存在');
|
throw new Error('管理员不存在');
|
||||||
}
|
}
|
||||||
|
|
@ -292,20 +222,20 @@ export class AdminService {
|
||||||
}
|
}
|
||||||
|
|
||||||
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
||||||
await this.adminRepo.save(admin);
|
await this.adminRepo.update(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置密码(超管功能)
|
* 重置密码(超管功能)
|
||||||
*/
|
*/
|
||||||
async resetPassword(adminId: string, newPassword: string): Promise<void> {
|
async resetPassword(adminId: string, newPassword: string): Promise<void> {
|
||||||
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
const admin = await this.adminRepo.findById(adminId);
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new Error('管理员不存在');
|
throw new Error('管理员不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
admin.passwordHash = await bcrypt.hash(newPassword, 10);
|
||||||
await this.adminRepo.save(admin);
|
await this.adminRepo.update(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -323,22 +253,11 @@ export class AdminService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通配符匹配 (如 knowledge:* 匹配 knowledge:read)
|
// 通配符匹配 (如 knowledge:* 匹配 knowledge:read)
|
||||||
const [resource, action] = requiredPermission.split(':');
|
const [resource] = requiredPermission.split(':');
|
||||||
if (adminPermissions.includes(`${resource}:*`)) {
|
if (adminPermissions.includes(`${resource}:*`)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取管理员权限列表
|
|
||||||
*/
|
|
||||||
private getPermissions(role: AdminRole, customPermissions?: string[]): string[] {
|
|
||||||
const rolePermissions = ROLE_PERMISSIONS[role] || [];
|
|
||||||
if (customPermissions && customPermissions.length > 0) {
|
|
||||||
return [...new Set([...rolePermissions, ...customPermissions])];
|
|
||||||
}
|
|
||||||
return rolePermissions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,9 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service';
|
import { ExperienceExtractorService } from '../../infrastructure/claude/experience-extractor.service';
|
||||||
import { ConversationClient } from '../infrastructure/clients/conversation.client';
|
import { ConversationClient } from '../../adapters/outbound/clients/conversation.client';
|
||||||
import { KnowledgeClient } from '../infrastructure/clients/knowledge.client';
|
import { KnowledgeClient } from '../../adapters/outbound/clients/knowledge.client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { EvolutionTaskResult } from '../dtos/evolution.dto';
|
||||||
/**
|
|
||||||
* 进化任务结果
|
|
||||||
*/
|
|
||||||
export interface EvolutionTaskResult {
|
|
||||||
taskId: string;
|
|
||||||
status: 'success' | 'partial' | 'failed';
|
|
||||||
conversationsAnalyzed: number;
|
|
||||||
experiencesExtracted: number;
|
|
||||||
knowledgeGapsFound: number;
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 进化服务
|
* 进化服务
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './evolution.service';
|
||||||
|
export * from './admin.service';
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { AdminRole, ROLE_PERMISSIONS } from '../value-objects/admin-role.enum';
|
||||||
|
|
||||||
|
export interface AdminProps {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
passwordHash: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
role: AdminRole;
|
||||||
|
permissions: string[];
|
||||||
|
avatar?: string;
|
||||||
|
lastLoginAt?: Date;
|
||||||
|
lastLoginIp?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminEntity {
|
||||||
|
private constructor(private props: AdminProps) {}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
get id(): string { return this.props.id; }
|
||||||
|
get username(): string { return this.props.username; }
|
||||||
|
get passwordHash(): string { return this.props.passwordHash; }
|
||||||
|
get name(): string { return this.props.name; }
|
||||||
|
get email(): string | undefined { return this.props.email; }
|
||||||
|
get phone(): string | undefined { return this.props.phone; }
|
||||||
|
get role(): AdminRole { return this.props.role; }
|
||||||
|
get permissions(): string[] { return this.props.permissions; }
|
||||||
|
get avatar(): string | undefined { return this.props.avatar; }
|
||||||
|
get lastLoginAt(): Date | undefined { return this.props.lastLoginAt; }
|
||||||
|
get lastLoginIp(): string | undefined { return this.props.lastLoginIp; }
|
||||||
|
get isActive(): boolean { return this.props.isActive; }
|
||||||
|
get createdAt(): Date { return this.props.createdAt; }
|
||||||
|
get updatedAt(): Date { return this.props.updatedAt; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
set name(value: string) { this.props.name = value; this.props.updatedAt = new Date(); }
|
||||||
|
set email(value: string | undefined) { this.props.email = value; this.props.updatedAt = new Date(); }
|
||||||
|
set phone(value: string | undefined) { this.props.phone = value; this.props.updatedAt = new Date(); }
|
||||||
|
set isActive(value: boolean) { this.props.isActive = value; this.props.updatedAt = new Date(); }
|
||||||
|
set passwordHash(value: string) { this.props.passwordHash = value; this.props.updatedAt = new Date(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新管理员
|
||||||
|
*/
|
||||||
|
static create(params: {
|
||||||
|
username: string;
|
||||||
|
passwordHash: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
role: AdminRole;
|
||||||
|
}): AdminEntity {
|
||||||
|
const now = new Date();
|
||||||
|
return new AdminEntity({
|
||||||
|
id: uuidv4(),
|
||||||
|
username: params.username,
|
||||||
|
passwordHash: params.passwordHash,
|
||||||
|
name: params.name,
|
||||||
|
email: params.email,
|
||||||
|
phone: params.phone,
|
||||||
|
role: params.role,
|
||||||
|
permissions: ROLE_PERMISSIONS[params.role],
|
||||||
|
isActive: true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从持久化数据恢复实体
|
||||||
|
*/
|
||||||
|
static fromPersistence(props: AdminProps): AdminEntity {
|
||||||
|
return new AdminEntity(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
*/
|
||||||
|
updateRole(role: AdminRole): void {
|
||||||
|
this.props.role = role;
|
||||||
|
this.props.permissions = ROLE_PERMISSIONS[role];
|
||||||
|
this.props.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录
|
||||||
|
*/
|
||||||
|
recordLogin(ip?: string): void {
|
||||||
|
this.props.lastLoginAt = new Date();
|
||||||
|
this.props.lastLoginIp = ip;
|
||||||
|
this.props.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整权限列表
|
||||||
|
*/
|
||||||
|
getPermissions(customPermissions?: string[]): string[] {
|
||||||
|
const rolePermissions = ROLE_PERMISSIONS[this.props.role] || [];
|
||||||
|
if (customPermissions && customPermissions.length > 0) {
|
||||||
|
return [...new Set([...rolePermissions, ...customPermissions])];
|
||||||
|
}
|
||||||
|
return rolePermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有指定权限
|
||||||
|
*/
|
||||||
|
hasPermission(requiredPermission: string): boolean {
|
||||||
|
// 超管拥有所有权限
|
||||||
|
if (this.props.permissions.includes('*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完全匹配
|
||||||
|
if (this.props.permissions.includes(requiredPermission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通配符匹配 (如 knowledge:* 匹配 knowledge:read)
|
||||||
|
const [resource] = requiredPermission.split(':');
|
||||||
|
if (this.props.permissions.includes(`${resource}:*`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { AdminEntity } from '../entities/admin.entity';
|
||||||
|
import { AdminRole } from '../value-objects/admin-role.enum';
|
||||||
|
|
||||||
|
export interface IAdminRepository {
|
||||||
|
save(admin: AdminEntity): Promise<void>;
|
||||||
|
findById(id: string): Promise<AdminEntity | null>;
|
||||||
|
findByUsername(username: string): Promise<AdminEntity | null>;
|
||||||
|
findAll(options?: {
|
||||||
|
role?: AdminRole;
|
||||||
|
isActive?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): Promise<AdminEntity[]>;
|
||||||
|
count(options?: {
|
||||||
|
role?: AdminRole;
|
||||||
|
isActive?: boolean;
|
||||||
|
}): Promise<number>;
|
||||||
|
update(admin: AdminEntity): Promise<void>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_REPOSITORY = Symbol('IAdminRepository');
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './admin.repository.interface';
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* 管理员角色
|
||||||
|
*/
|
||||||
|
export enum AdminRole {
|
||||||
|
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
OPERATOR = 'OPERATOR',
|
||||||
|
VIEWER = 'VIEWER',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色权限映射
|
||||||
|
*/
|
||||||
|
export const ROLE_PERMISSIONS: Record<AdminRole, string[]> = {
|
||||||
|
[AdminRole.SUPER_ADMIN]: ['*'],
|
||||||
|
[AdminRole.ADMIN]: [
|
||||||
|
'knowledge:*',
|
||||||
|
'experience:*',
|
||||||
|
'user:read',
|
||||||
|
'conversation:read',
|
||||||
|
'statistics:*',
|
||||||
|
'admin:read',
|
||||||
|
],
|
||||||
|
[AdminRole.OPERATOR]: [
|
||||||
|
'knowledge:read',
|
||||||
|
'knowledge:create',
|
||||||
|
'knowledge:update',
|
||||||
|
'experience:read',
|
||||||
|
'experience:approve',
|
||||||
|
'user:read',
|
||||||
|
'conversation:read',
|
||||||
|
'statistics:read',
|
||||||
|
],
|
||||||
|
[AdminRole.VIEWER]: [
|
||||||
|
'knowledge:read',
|
||||||
|
'experience:read',
|
||||||
|
'user:read',
|
||||||
|
'conversation:read',
|
||||||
|
'statistics:read',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { EvolutionController } from './evolution.controller';
|
import { EvolutionController } from '../adapters/inbound/evolution.controller';
|
||||||
import { EvolutionService } from './evolution.service';
|
import { EvolutionService } from '../application/services/evolution.service';
|
||||||
import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service';
|
import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service';
|
||||||
import { ConversationClient } from '../infrastructure/clients/conversation.client';
|
import { ConversationClient } from '../adapters/outbound/clients/conversation.client';
|
||||||
import { KnowledgeClient } from '../infrastructure/clients/knowledge.client';
|
import { KnowledgeClient } from '../adapters/outbound/clients/knowledge.client';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ export class AdminORM {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
@Column({ length: 255, nullable: true })
|
||||||
email: string;
|
email?: string;
|
||||||
|
|
||||||
@Column({ length: 20, nullable: true })
|
@Column({ length: 20, nullable: true })
|
||||||
phone: string;
|
phone?: string;
|
||||||
|
|
||||||
@Column({ length: 20, default: 'OPERATOR' })
|
@Column({ length: 20, default: 'OPERATOR' })
|
||||||
role: string;
|
role: string;
|
||||||
|
|
@ -33,10 +33,10 @@ export class AdminORM {
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
|
||||||
@Column({ length: 500, nullable: true })
|
@Column({ length: 500, nullable: true })
|
||||||
avatar: string;
|
avatar?: string;
|
||||||
|
|
||||||
@Column({ name: 'last_login_at', nullable: true })
|
@Column({ name: 'last_login_at', nullable: true })
|
||||||
lastLoginAt: Date;
|
lastLoginAt?: Date;
|
||||||
|
|
||||||
@Column({ name: 'last_login_ip', length: 50, nullable: true })
|
@Column({ name: 'last_login_ip', length: 50, nullable: true })
|
||||||
lastLoginIp?: string;
|
lastLoginIp?: string;
|
||||||
|
|
@ -14,13 +14,13 @@ import {
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { FileService } from './file.service';
|
import { FileService } from '../../application/services/file.service';
|
||||||
import {
|
import {
|
||||||
UploadFileDto,
|
UploadFileDto,
|
||||||
PresignedUrlDto,
|
PresignedUrlDto,
|
||||||
FileResponseDto,
|
FileResponseDto,
|
||||||
PresignedUrlResponseDto,
|
PresignedUrlResponseDto,
|
||||||
} from './dto/upload-file.dto';
|
} from '../../application/dtos/upload-file.dto';
|
||||||
|
|
||||||
@Controller('files')
|
@Controller('files')
|
||||||
export class FileController {
|
export class FileController {
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './file.controller';
|
||||||
|
|
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { IFileRepository } from '../../../domain/repositories/file.repository.interface';
|
import { IFileRepository } from '../../../domain/repositories/file.repository.interface';
|
||||||
import { FileEntity, FileStatus } from '../../../domain/entities/file.entity';
|
import { FileEntity, FileStatus } from '../../../domain/entities/file.entity';
|
||||||
import { FileORM } from './entities/file.orm';
|
import { FileORM } from '../../../infrastructure/database/postgres/entities/file.orm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilePostgresRepository implements IFileRepository {
|
export class FilePostgresRepository implements IFileRepository {
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './file-postgres.repository';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './minio-storage.adapter';
|
||||||
|
|
@ -3,8 +3,8 @@ import { ConfigService } from '@nestjs/config';
|
||||||
import * as Minio from 'minio';
|
import * as Minio from 'minio';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MinioService implements OnModuleInit {
|
export class MinioStorageAdapter implements OnModuleInit {
|
||||||
private readonly logger = new Logger(MinioService.name);
|
private readonly logger = new Logger(MinioStorageAdapter.name);
|
||||||
private client: Minio.Client;
|
private client: Minio.Client;
|
||||||
private bucketName: string;
|
private bucketName: string;
|
||||||
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { FileModule } from './file/file.module';
|
import { FileModule } from './file/file.module';
|
||||||
import { MinioModule } from './minio/minio.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -33,9 +32,6 @@ import { MinioModule } from './minio/minio.module';
|
||||||
// Health check
|
// Health check
|
||||||
HealthModule,
|
HealthModule,
|
||||||
|
|
||||||
// MinIO storage
|
|
||||||
MinioModule,
|
|
||||||
|
|
||||||
// 功能模块
|
// 功能模块
|
||||||
FileModule,
|
FileModule,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './upload-file.dto';
|
||||||
|
|
@ -7,14 +7,14 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as mimeTypes from 'mime-types';
|
import * as mimeTypes from 'mime-types';
|
||||||
import { FileEntity, FileType, FileStatus } from '../domain/entities/file.entity';
|
import { FileEntity, FileType, FileStatus } from '../../domain/entities/file.entity';
|
||||||
import { IFileRepository, FILE_REPOSITORY } from '../domain/repositories/file.repository.interface';
|
import { IFileRepository, FILE_REPOSITORY } from '../../domain/repositories/file.repository.interface';
|
||||||
import { MinioService } from '../minio/minio.service';
|
import { MinioStorageAdapter } from '../../adapters/outbound/storage/minio-storage.adapter';
|
||||||
import {
|
import {
|
||||||
FileResponseDto,
|
FileResponseDto,
|
||||||
PresignedUrlDto,
|
PresignedUrlDto,
|
||||||
PresignedUrlResponseDto,
|
PresignedUrlResponseDto,
|
||||||
} from './dto/upload-file.dto';
|
} from '../dtos/upload-file.dto';
|
||||||
|
|
||||||
// 允许的文件类型
|
// 允许的文件类型
|
||||||
const ALLOWED_IMAGE_TYPES = [
|
const ALLOWED_IMAGE_TYPES = [
|
||||||
|
|
@ -45,7 +45,7 @@ export class FileService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(FILE_REPOSITORY)
|
@Inject(FILE_REPOSITORY)
|
||||||
private readonly fileRepo: IFileRepository,
|
private readonly fileRepo: IFileRepository,
|
||||||
private readonly minioService: MinioService,
|
private readonly storageAdapter: MinioStorageAdapter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -87,7 +87,7 @@ export class FileService {
|
||||||
|
|
||||||
// 获取预签名 URL (有效期 1 小时)
|
// 获取预签名 URL (有效期 1 小时)
|
||||||
const expiresIn = 3600;
|
const expiresIn = 3600;
|
||||||
const uploadUrl = await this.minioService.getPresignedPutUrl(
|
const uploadUrl = await this.storageAdapter.getPresignedPutUrl(
|
||||||
objectName,
|
objectName,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
);
|
);
|
||||||
|
|
@ -162,7 +162,7 @@ export class FileService {
|
||||||
const objectName = `uploads/${fileType}s/${datePath}/${userId}/${fileId}.${extension}`;
|
const objectName = `uploads/${fileType}s/${datePath}/${userId}/${fileId}.${extension}`;
|
||||||
|
|
||||||
// 上传到 MinIO
|
// 上传到 MinIO
|
||||||
await this.minioService.uploadFile(objectName, buffer, mimetype, {
|
await this.storageAdapter.uploadFile(objectName, buffer, mimetype, {
|
||||||
'x-amz-meta-original-name': encodeURIComponent(originalname),
|
'x-amz-meta-original-name': encodeURIComponent(originalname),
|
||||||
'x-amz-meta-user-id': userId,
|
'x-amz-meta-user-id': userId,
|
||||||
});
|
});
|
||||||
|
|
@ -218,7 +218,7 @@ export class FileService {
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.minioService.getPresignedUrl(file.storagePath, 3600);
|
return this.storageAdapter.getPresignedUrl(file.storagePath, 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -287,13 +287,13 @@ export class FileService {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file.isReady()) {
|
if (file.isReady()) {
|
||||||
dto.downloadUrl = await this.minioService.getPresignedUrl(
|
dto.downloadUrl = await this.storageAdapter.getPresignedUrl(
|
||||||
file.storagePath,
|
file.storagePath,
|
||||||
3600,
|
3600,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (file.thumbnailPath) {
|
if (file.thumbnailPath) {
|
||||||
dto.thumbnailUrl = await this.minioService.getPresignedUrl(
|
dto.thumbnailUrl = await this.storageAdapter.getPresignedUrl(
|
||||||
file.thumbnailPath,
|
file.thumbnailPath,
|
||||||
3600,
|
3600,
|
||||||
);
|
);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './file.service';
|
||||||
|
|
@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { MulterModule } from '@nestjs/platform-express';
|
import { MulterModule } from '@nestjs/platform-express';
|
||||||
import { FileORM } from '../infrastructure/database/postgres/entities/file.orm';
|
import { FileORM } from '../infrastructure/database/postgres/entities/file.orm';
|
||||||
import { FilePostgresRepository } from '../infrastructure/database/postgres/file-postgres.repository';
|
import { FilePostgresRepository } from '../adapters/outbound/persistence/file-postgres.repository';
|
||||||
import { FILE_REPOSITORY } from '../domain/repositories/file.repository.interface';
|
import { FILE_REPOSITORY } from '../domain/repositories/file.repository.interface';
|
||||||
import { FileController } from './file.controller';
|
import { FileController } from '../adapters/inbound/file.controller';
|
||||||
import { FileService } from './file.service';
|
import { FileService } from '../application/services/file.service';
|
||||||
|
import { MinioStorageAdapter } from '../adapters/outbound/storage/minio-storage.adapter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -19,6 +20,7 @@ import { FileService } from './file.service';
|
||||||
controllers: [FileController],
|
controllers: [FileController],
|
||||||
providers: [
|
providers: [
|
||||||
FileService,
|
FileService,
|
||||||
|
MinioStorageAdapter,
|
||||||
{
|
{
|
||||||
provide: FILE_REPOSITORY,
|
provide: FILE_REPOSITORY,
|
||||||
useClass: FilePostgresRepository,
|
useClass: FilePostgresRepository,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { Module, Global } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { MinioService } from './minio.service';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
imports: [ConfigModule],
|
|
||||||
providers: [MinioService],
|
|
||||||
exports: [MinioService],
|
|
||||||
})
|
|
||||||
export class MinioModule {}
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './knowledge.controller';
|
||||||
|
export * from './memory.controller';
|
||||||
|
export * from './internal-memory.controller';
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
||||||
import { MemoryService } from './memory.service';
|
import { MemoryService } from '../../application/services/memory.service';
|
||||||
import { ExperienceType } from '../domain/entities/system-experience.entity';
|
import { ExperienceType } from '../../domain/entities/system-experience.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内部 API 控制器
|
* 内部 API 控制器
|
||||||
|
|
@ -10,56 +10,17 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { KnowledgeService } from './knowledge.service';
|
import { KnowledgeService } from '../../application/services/knowledge.service';
|
||||||
import { RAGService } from '../application/services/rag.service';
|
import { RAGService } from '../../application/services/rag.service';
|
||||||
import { KnowledgeSource } from '../domain/entities/knowledge-article.entity';
|
import { KnowledgeSource } from '../../domain/entities/knowledge-article.entity';
|
||||||
|
import {
|
||||||
// ========== DTOs ==========
|
CreateArticleDto,
|
||||||
|
UpdateArticleDto,
|
||||||
class CreateArticleDto {
|
SearchArticlesDto,
|
||||||
title: string;
|
RetrieveKnowledgeDto,
|
||||||
content: string;
|
ImportArticlesDto,
|
||||||
category: string;
|
FeedbackDto,
|
||||||
tags?: string[];
|
} from '../../application/dtos/knowledge.dto';
|
||||||
sourceUrl?: string;
|
|
||||||
autoPublish?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UpdateArticleDto {
|
|
||||||
title?: string;
|
|
||||||
content?: string;
|
|
||||||
category?: string;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchArticlesDto {
|
|
||||||
query: string;
|
|
||||||
category?: string;
|
|
||||||
useVector?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RetrieveKnowledgeDto {
|
|
||||||
query: string;
|
|
||||||
userId?: string;
|
|
||||||
category?: string;
|
|
||||||
includeMemories?: boolean;
|
|
||||||
includeExperiences?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImportArticlesDto {
|
|
||||||
articles: Array<{
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
category: string;
|
|
||||||
tags?: string[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeedbackDto {
|
|
||||||
helpful: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Controller ==========
|
|
||||||
|
|
||||||
@Controller('knowledge')
|
@Controller('knowledge')
|
||||||
export class KnowledgeController {
|
export class KnowledgeController {
|
||||||
|
|
@ -10,41 +10,15 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { MemoryService } from './memory.service';
|
import { MemoryService } from '../../application/services/memory.service';
|
||||||
import { MemoryType } from '../domain/entities/user-memory.entity';
|
import { MemoryType } from '../../domain/entities/user-memory.entity';
|
||||||
import { ExperienceType } from '../domain/entities/system-experience.entity';
|
import { ExperienceType } from '../../domain/entities/system-experience.entity';
|
||||||
|
import {
|
||||||
// ========== DTOs ==========
|
SaveMemoryDto,
|
||||||
|
SearchMemoryDto,
|
||||||
class SaveMemoryDto {
|
ExtractExperienceDto,
|
||||||
userId: string;
|
MemoryFeedbackDto,
|
||||||
memoryType: MemoryType;
|
} from '../../application/dtos/memory.dto';
|
||||||
content: string;
|
|
||||||
importance?: number;
|
|
||||||
sourceConversationId?: string;
|
|
||||||
relatedCategory?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchMemoryDto {
|
|
||||||
userId: string;
|
|
||||||
query: string;
|
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtractExperienceDto {
|
|
||||||
experienceType: ExperienceType;
|
|
||||||
content: string;
|
|
||||||
scenario: string;
|
|
||||||
relatedCategory?: string;
|
|
||||||
sourceConversationId: string;
|
|
||||||
confidence?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeedbackDto {
|
|
||||||
positive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Controller ==========
|
|
||||||
|
|
||||||
@Controller('memory')
|
@Controller('memory')
|
||||||
export class MemoryController {
|
export class MemoryController {
|
||||||
|
|
@ -263,7 +237,7 @@ export class MemoryController {
|
||||||
@Post('experience/:id/feedback')
|
@Post('experience/:id/feedback')
|
||||||
async recordExperienceFeedback(
|
async recordExperienceFeedback(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: FeedbackDto,
|
@Body() dto: MemoryFeedbackDto,
|
||||||
) {
|
) {
|
||||||
await this.memoryService.recordExperienceFeedback(id, dto.positive);
|
await this.memoryService.recordExperienceFeedback(id, dto.positive);
|
||||||
return {
|
return {
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './knowledge-postgres.repository';
|
||||||
|
export * from './memory-postgres.repository';
|
||||||
|
|
@ -4,8 +4,8 @@ import { Repository, ILike } from 'typeorm';
|
||||||
import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface';
|
import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface';
|
||||||
import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity';
|
import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity';
|
||||||
import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity';
|
import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity';
|
||||||
import { KnowledgeArticleORM } from './entities/knowledge-article.orm';
|
import { KnowledgeArticleORM } from '../../../infrastructure/database/postgres/entities/knowledge-article.orm';
|
||||||
import { KnowledgeChunkORM } from './entities/knowledge-chunk.orm';
|
import { KnowledgeChunkORM } from '../../../infrastructure/database/postgres/entities/knowledge-chunk.orm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
||||||
|
|
@ -11,8 +11,8 @@ import {
|
||||||
ExperienceType,
|
ExperienceType,
|
||||||
VerificationStatus,
|
VerificationStatus,
|
||||||
} from '../../../domain/entities/system-experience.entity';
|
} from '../../../domain/entities/system-experience.entity';
|
||||||
import { UserMemoryORM } from './entities/user-memory.orm';
|
import { UserMemoryORM } from '../../../infrastructure/database/postgres/entities/user-memory.orm';
|
||||||
import { SystemExperienceORM } from './entities/system-experience.orm';
|
import { SystemExperienceORM } from '../../../infrastructure/database/postgres/entities/system-experience.orm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './knowledge.dto';
|
||||||
|
export * from './memory.dto';
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { KnowledgeSource } from '../../domain/entities/knowledge-article.entity';
|
||||||
|
|
||||||
|
export class CreateArticleDto {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
tags?: string[];
|
||||||
|
sourceUrl?: string;
|
||||||
|
autoPublish?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateArticleDto {
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
category?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchArticlesDto {
|
||||||
|
query: string;
|
||||||
|
category?: string;
|
||||||
|
useVector?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RetrieveKnowledgeDto {
|
||||||
|
query: string;
|
||||||
|
userId?: string;
|
||||||
|
category?: string;
|
||||||
|
includeMemories?: boolean;
|
||||||
|
includeExperiences?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImportArticlesDto {
|
||||||
|
articles: Array<{
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
tags?: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeedbackDto {
|
||||||
|
helpful: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { MemoryType } from '../../domain/entities/user-memory.entity';
|
||||||
|
import { ExperienceType } from '../../domain/entities/system-experience.entity';
|
||||||
|
|
||||||
|
export class SaveMemoryDto {
|
||||||
|
userId: string;
|
||||||
|
memoryType: MemoryType;
|
||||||
|
content: string;
|
||||||
|
importance?: number;
|
||||||
|
sourceConversationId?: string;
|
||||||
|
relatedCategory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchMemoryDto {
|
||||||
|
userId: string;
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtractExperienceDto {
|
||||||
|
experienceType: ExperienceType;
|
||||||
|
content: string;
|
||||||
|
scenario: string;
|
||||||
|
relatedCategory?: string;
|
||||||
|
sourceConversationId: string;
|
||||||
|
confidence?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemoryFeedbackDto {
|
||||||
|
positive: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './knowledge.service';
|
||||||
|
export * from './memory.service';
|
||||||
|
export * from './rag.service';
|
||||||
|
export * from './chunking.service';
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
import { EmbeddingService } from '../../infrastructure/embedding/embedding.service';
|
||||||
import { ChunkingService } from '../application/services/chunking.service';
|
import { ChunkingService } from './chunking.service';
|
||||||
import {
|
import {
|
||||||
IKnowledgeRepository,
|
IKnowledgeRepository,
|
||||||
KNOWLEDGE_REPOSITORY,
|
KNOWLEDGE_REPOSITORY,
|
||||||
} from '../domain/repositories/knowledge.repository.interface';
|
} from '../../domain/repositories/knowledge.repository.interface';
|
||||||
import {
|
import {
|
||||||
KnowledgeArticleEntity,
|
KnowledgeArticleEntity,
|
||||||
KnowledgeSource,
|
KnowledgeSource,
|
||||||
} from '../domain/entities/knowledge-article.entity';
|
} from '../../domain/entities/knowledge-article.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识管理服务
|
* 知识管理服务
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
import { EmbeddingService } from '../../infrastructure/embedding/embedding.service';
|
||||||
import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service';
|
import { Neo4jService } from '../../infrastructure/database/neo4j/neo4j.service';
|
||||||
import {
|
import {
|
||||||
IUserMemoryRepository,
|
IUserMemoryRepository,
|
||||||
ISystemExperienceRepository,
|
ISystemExperienceRepository,
|
||||||
USER_MEMORY_REPOSITORY,
|
USER_MEMORY_REPOSITORY,
|
||||||
SYSTEM_EXPERIENCE_REPOSITORY,
|
SYSTEM_EXPERIENCE_REPOSITORY,
|
||||||
} from '../domain/repositories/memory.repository.interface';
|
} from '../../domain/repositories/memory.repository.interface';
|
||||||
import { UserMemoryEntity, MemoryType } from '../domain/entities/user-memory.entity';
|
import { UserMemoryEntity, MemoryType } from '../../domain/entities/user-memory.entity';
|
||||||
import {
|
import {
|
||||||
SystemExperienceEntity,
|
SystemExperienceEntity,
|
||||||
ExperienceType,
|
ExperienceType,
|
||||||
VerificationStatus,
|
VerificationStatus,
|
||||||
} from '../domain/entities/system-experience.entity';
|
} from '../../domain/entities/system-experience.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记忆管理服务
|
* 记忆管理服务
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { KnowledgeController } from './knowledge.controller';
|
import { KnowledgeController } from '../adapters/inbound/knowledge.controller';
|
||||||
import { KnowledgeService } from './knowledge.service';
|
import { KnowledgeService } from '../application/services/knowledge.service';
|
||||||
import { RAGService } from '../application/services/rag.service';
|
import { RAGService } from '../application/services/rag.service';
|
||||||
import { ChunkingService } from '../application/services/chunking.service';
|
import { ChunkingService } from '../application/services/chunking.service';
|
||||||
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
||||||
import { KnowledgePostgresRepository } from '../infrastructure/database/postgres/knowledge-postgres.repository';
|
import { KnowledgePostgresRepository } from '../adapters/outbound/persistence/knowledge-postgres.repository';
|
||||||
import {
|
import {
|
||||||
UserMemoryPostgresRepository,
|
UserMemoryPostgresRepository,
|
||||||
SystemExperiencePostgresRepository,
|
SystemExperiencePostgresRepository,
|
||||||
} from '../infrastructure/database/postgres/memory-postgres.repository';
|
} from '../adapters/outbound/persistence/memory-postgres.repository';
|
||||||
import { KnowledgeArticleORM } from '../infrastructure/database/postgres/entities/knowledge-article.orm';
|
import { KnowledgeArticleORM } from '../infrastructure/database/postgres/entities/knowledge-article.orm';
|
||||||
import { KnowledgeChunkORM } from '../infrastructure/database/postgres/entities/knowledge-chunk.orm';
|
import { KnowledgeChunkORM } from '../infrastructure/database/postgres/entities/knowledge-chunk.orm';
|
||||||
import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm';
|
import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm';
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { MemoryController } from './memory.controller';
|
import { MemoryController } from '../adapters/inbound/memory.controller';
|
||||||
import { InternalMemoryController } from './internal.controller';
|
import { InternalMemoryController } from '../adapters/inbound/internal-memory.controller';
|
||||||
import { MemoryService } from './memory.service';
|
import { MemoryService } from '../application/services/memory.service';
|
||||||
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
import { EmbeddingService } from '../infrastructure/embedding/embedding.service';
|
||||||
import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service';
|
import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service';
|
||||||
import {
|
import {
|
||||||
UserMemoryPostgresRepository,
|
UserMemoryPostgresRepository,
|
||||||
SystemExperiencePostgresRepository,
|
SystemExperiencePostgresRepository,
|
||||||
} from '../infrastructure/database/postgres/memory-postgres.repository';
|
} from '../adapters/outbound/persistence/memory-postgres.repository';
|
||||||
import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm';
|
import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm';
|
||||||
import { SystemExperienceORM } from '../infrastructure/database/postgres/entities/system-experience.orm';
|
import { SystemExperienceORM } from '../infrastructure/database/postgres/entities/system-experience.orm';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './order.controller';
|
||||||
|
export * from './payment.controller';
|
||||||
|
|
@ -8,14 +8,8 @@ import {
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { OrderService } from './order.service';
|
import { OrderService } from '../../application/services/order.service';
|
||||||
import { ServiceType } from '../domain/entities/order.entity';
|
import { CreateOrderDto } from '../../application/dtos/order.dto';
|
||||||
|
|
||||||
class CreateOrderDto {
|
|
||||||
serviceType: ServiceType;
|
|
||||||
serviceCategory?: string;
|
|
||||||
conversationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller('orders')
|
@Controller('orders')
|
||||||
export class OrderController {
|
export class OrderController {
|
||||||
|
|
@ -10,13 +10,9 @@ import {
|
||||||
Req,
|
Req,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { PaymentService } from './payment.service';
|
import { PaymentService } from '../../application/services/payment.service';
|
||||||
import { PaymentMethod } from '../domain/entities/payment.entity';
|
import { CreatePaymentDto } from '../../application/dtos/payment.dto';
|
||||||
|
import { PaymentMethod } from '../../domain/entities/payment.entity';
|
||||||
class CreatePaymentDto {
|
|
||||||
orderId: string;
|
|
||||||
method: PaymentMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Controller('payments')
|
@Controller('payments')
|
||||||
export class PaymentController {
|
export class PaymentController {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { OrderEntity } from '../../domain/entities/order.entity';
|
import { OrderEntity } from '../../../domain/entities/order.entity';
|
||||||
|
|
||||||
export interface AlipayPaymentResult {
|
export interface AlipayPaymentResult {
|
||||||
qrCodeUrl: string;
|
qrCodeUrl: string;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './alipay.adapter';
|
||||||
|
export * from './wechat-pay.adapter';
|
||||||
|
export * from './stripe.adapter';
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OrderEntity } from '../../domain/entities/order.entity';
|
import { OrderEntity } from '../../../domain/entities/order.entity';
|
||||||
|
|
||||||
export interface StripePaymentResult {
|
export interface StripePaymentResult {
|
||||||
paymentUrl: string;
|
paymentUrl: string;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { OrderEntity } from '../../domain/entities/order.entity';
|
import { OrderEntity } from '../../../domain/entities/order.entity';
|
||||||
|
|
||||||
export interface WechatPaymentResult {
|
export interface WechatPaymentResult {
|
||||||
qrCodeUrl: string;
|
qrCodeUrl: string;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './order-postgres.repository';
|
||||||
|
export * from './payment-postgres.repository';
|
||||||
|
|
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { IOrderRepository } from '../../../domain/repositories/order.repository.interface';
|
import { IOrderRepository } from '../../../domain/repositories/order.repository.interface';
|
||||||
import { OrderEntity } from '../../../domain/entities/order.entity';
|
import { OrderEntity } from '../../../domain/entities/order.entity';
|
||||||
import { OrderORM } from './entities/order.orm';
|
import { OrderORM } from '../../../infrastructure/database/postgres/entities/order.orm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderPostgresRepository implements IOrderRepository {
|
export class OrderPostgresRepository implements IOrderRepository {
|
||||||
|
|
@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { IPaymentRepository } from '../../../domain/repositories/payment.repository.interface';
|
import { IPaymentRepository } from '../../../domain/repositories/payment.repository.interface';
|
||||||
import { PaymentEntity, PaymentStatus } from '../../../domain/entities/payment.entity';
|
import { PaymentEntity, PaymentStatus } from '../../../domain/entities/payment.entity';
|
||||||
import { PaymentORM } from './entities/payment.orm';
|
import { PaymentORM } from '../../../infrastructure/database/postgres/entities/payment.orm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentPostgresRepository implements IPaymentRepository {
|
export class PaymentPostgresRepository implements IPaymentRepository {
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './order.dto';
|
||||||
|
export * from './payment.dto';
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { IsString, IsNotEmpty, IsOptional, IsEnum } from 'class-validator';
|
||||||
|
import { ServiceType } from '../../domain/entities/order.entity';
|
||||||
|
|
||||||
|
export class CreateOrderDto {
|
||||||
|
@IsEnum(ServiceType)
|
||||||
|
@IsNotEmpty()
|
||||||
|
serviceType: ServiceType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
serviceCategory?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
conversationId?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { IsString, IsNotEmpty, IsEnum } from 'class-validator';
|
||||||
|
import { PaymentMethod } from '../../domain/entities/payment.entity';
|
||||||
|
|
||||||
|
export class CreatePaymentDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
orderId: string;
|
||||||
|
|
||||||
|
@IsEnum(PaymentMethod)
|
||||||
|
@IsNotEmpty()
|
||||||
|
method: PaymentMethod;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './order.service';
|
||||||
|
export * from './payment.service';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue