Compare commits

..

No commits in common. "afd707d15f01f9a16a15ea1869bb0436b13079f6" and "02954f56db3663b9e17fb82a1ca4cfb98074f173" have entirely different histories.

119 changed files with 876 additions and 1400 deletions

View File

@ -1 +0,0 @@
export { useAuthStore } from './useAuthStore';

View File

@ -1,88 +0,0 @@
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,
}),
}
)
);

View File

@ -1,26 +0,0 @@
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;
},
};

View File

@ -1,2 +0,0 @@
export { authApi } from './auth.api';
export type { AdminInfo, LoginResponse } from './auth.api';

View File

@ -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 { useAuthStore } from '../../application'; import { useAuth } from '../../../../shared/hooks/useAuth';
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 = useAuthStore((state) => state.login); const login = useAuth((state) => state.login);
const onFinish = async (values: LoginFormValues) => { const onFinish = async (values: LoginFormValues) => {
setLoading(true); setLoading(true);

View File

@ -1,6 +0,0 @@
export {
useEvolutionStatistics,
useSystemHealth,
EVOLUTION_STATS_KEY,
SYSTEM_HEALTH_KEY,
} from './useDashboard';

View File

@ -1,19 +0,0 @@
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(),
});
}

View File

@ -1,34 +0,0 @@
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;
},
};

View File

@ -1,2 +0,0 @@
export { dashboardApi } from './dashboard.api';
export type { EvolutionStatistics, HealthMetric, HealthReport } from './dashboard.api';

View File

@ -1,3 +1,4 @@
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,
@ -19,8 +20,7 @@ import {
Pie, Pie,
Cell, Cell,
} from 'recharts'; } from 'recharts';
import { useEvolutionStatistics, useSystemHealth } from '../../application'; import api from '../../../../shared/utils/api';
import type { HealthMetric } from '../../infrastructure';
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -45,8 +45,21 @@ const mockCategoryData = [
]; ];
export function DashboardPage() { export function DashboardPage() {
const { data: evolutionStats } = useEvolutionStatistics(); const { data: evolutionStats } = useQuery({
const { data: healthReport } = useSystemHealth(); queryKey: ['evolution-stats'],
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) {
@ -194,7 +207,7 @@ export function DashboardPage() {
> >
<List <List
dataSource={healthReport?.metrics || []} dataSource={healthReport?.metrics || []}
renderItem={(item: HealthMetric) => ( renderItem={(item: { name: string; value: number; threshold: number; status: string }) => (
<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">
@ -219,11 +232,11 @@ export function DashboardPage() {
</List.Item> </List.Item>
)} )}
/> />
{(healthReport?.recommendations?.length ?? 0) > 0 && ( {healthReport?.recommendations?.length > 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>

View File

@ -1,9 +0,0 @@
export {
usePendingExperiences,
useExperienceStatistics,
useApproveExperience,
useRejectExperience,
useRunEvolution,
EXPERIENCE_QUERY_KEY,
EXPERIENCE_STATS_KEY,
} from './useExperience';

View File

@ -1,65 +0,0 @@
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] });
},
});
}

View File

@ -1,58 +0,0 @@
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;
},
};

View File

@ -1,2 +0,0 @@
export { experienceApi } from './experience.api';
export type { Experience, ExperienceListResponse, ExperienceStatistics } from './experience.api';

View File

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { import {
Card, Card,
Table, Table,
@ -7,6 +8,7 @@ import {
Tag, Tag,
Space, Space,
Modal, Modal,
message,
Tabs, Tabs,
Typography, Typography,
Statistic, Statistic,
@ -19,15 +21,8 @@ import {
EyeOutlined, EyeOutlined,
PlayCircleOutlined, PlayCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useAuthStore } from '../../../auth/application'; import api from '../../../../shared/utils/api';
import { import { useAuth } from '../../../../shared/hooks/useAuth';
usePendingExperiences,
useExperienceStatistics,
useApproveExperience,
useRejectExperience,
useRunEvolution,
} from '../../application';
import type { Experience } from '../../infrastructure';
const { Title, Text, Paragraph } = Typography; const { Title, Text, Paragraph } = Typography;
@ -42,39 +37,86 @@ 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 admin = useAuthStore((state) => state.admin); const queryClient = useQueryClient();
const admin = useAuth((state) => state.admin);
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences( const { data: pendingData, isLoading: pendingLoading } = useQuery({
typeFilter, queryKey: ['pending-experiences', typeFilter],
activeTab === 'pending' queryFn: async () => {
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}条经验`
); );
const { data: stats } = useExperienceStatistics(); queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
const approveMutation = useApproveExperience(); queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
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;
}; };
@ -160,14 +202,14 @@ export function ExperiencePage() {
type="text" type="text"
icon={<CheckOutlined />} icon={<CheckOutlined />}
className="text-green-600" className="text-green-600"
onClick={() => handleApprove(record.id)} onClick={() => approveMutation.mutate(record.id)}
loading={approveMutation.isPending} loading={approveMutation.isPending}
/> />
<Button <Button
type="text" type="text"
icon={<CloseOutlined />} icon={<CloseOutlined />}
danger danger
onClick={() => handleReject(record.id)} onClick={() => rejectMutation.mutate(record.id)}
loading={rejectMutation.isPending} loading={rejectMutation.isPending}
/> />
</> </>
@ -184,7 +226,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}
> >
@ -276,7 +318,7 @@ export function ExperiencePage() {
key="reject" key="reject"
danger danger
onClick={() => { onClick={() => {
handleReject(selectedExperience.id); rejectMutation.mutate(selectedExperience.id);
setIsModalOpen(false); setIsModalOpen(false);
}} }}
> >
@ -286,7 +328,7 @@ export function ExperiencePage() {
key="approve" key="approve"
type="primary" type="primary"
onClick={() => { onClick={() => {
handleApprove(selectedExperience.id); approveMutation.mutate(selectedExperience.id);
setIsModalOpen(false); setIsModalOpen(false);
}} }}
> >

View File

@ -1,9 +0,0 @@
export {
useKnowledgeArticles,
useCreateArticle,
useUpdateArticle,
useDeleteArticle,
usePublishArticle,
useUnpublishArticle,
KNOWLEDGE_QUERY_KEY,
} from './useKnowledge';

View File

@ -1,72 +0,0 @@
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] });
},
});
}

View File

@ -1,2 +0,0 @@
export { knowledgeApi } from './knowledge.api';
export type { Article, ArticleListResponse, CreateArticleParams, UpdateArticleParams } from './knowledge.api';

View File

@ -1,62 +0,0 @@
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`);
},
};

View File

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { import {
Card, Card,
Table, Table,
@ -9,6 +10,7 @@ import {
Space, Space,
Modal, Modal,
Form, Form,
message,
Popconfirm, Popconfirm,
Typography, Typography,
Drawer, Drawer,
@ -22,15 +24,7 @@ import {
CheckOutlined, CheckOutlined,
StopOutlined, StopOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import api from '../../../../shared/utils/api';
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;
@ -45,6 +39,20 @@ 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>();
@ -52,13 +60,64 @@ 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 } = useKnowledgeArticles(categoryFilter); const { data, isLoading } = useQuery({
const createMutation = useCreateArticle(); queryKey: ['knowledge-articles', categoryFilter],
const updateMutation = useUpdateArticle(); queryFn: async () => {
const deleteMutation = useDeleteArticle(); const params = new URLSearchParams();
const publishMutation = usePublishArticle(); if (categoryFilter) params.append('category', categoryFilter);
const unpublishMutation = useUnpublishArticle(); const response = await api.get(`/knowledge/articles?${params}`);
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);
@ -76,25 +135,11 @@ export function KnowledgePage() {
setIsDrawerOpen(true); setIsDrawerOpen(true);
}; };
const handleSubmit = (values: CreateArticleParams) => { const handleSubmit = (values: Partial<Article>) => {
if (selectedArticle) { if (selectedArticle) {
updateMutation.mutate( updateMutation.mutate({ id: selectedArticle.id, ...values });
{ id: selectedArticle.id, ...values },
{
onSuccess: () => {
setIsModalOpen(false);
form.resetFields();
setSelectedArticle(null);
},
}
);
} else { } else {
createMutation.mutate(values, { createMutation.mutate(values);
onSuccess: () => {
setIsModalOpen(false);
form.resetFields();
},
});
} }
}; };

View File

@ -1,3 +1,105 @@
// Re-export from auth feature for backward compatibility import { create } from 'zustand';
export { useAuthStore as useAuth } from '../../features/auth/application'; import { persist } from 'zustand/middleware';
export type { AdminInfo } from '../../features/auth/infrastructure'; import api from '../utils/api';
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,
}),
}
)
);

View File

@ -1,3 +0,0 @@
export * from './conversation.controller';
export * from './conversation.gateway';
export * from './internal.controller';

View File

@ -1,3 +0,0 @@
export * from './conversation-postgres.repository';
export * from './message-postgres.repository';
export * from './token-usage-postgres.repository';

View File

@ -1,30 +0,0 @@
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[];
}

View File

@ -1 +0,0 @@
export * from './conversation.dto';

View File

@ -1 +0,0 @@
export * from './conversation.service';

View File

@ -9,8 +9,20 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConversationService } from '../../application/services/conversation.service'; import { IsOptional, IsString, IsNotEmpty } from 'class-validator';
import { CreateConversationDto, SendMessageDto } from '../../application/dtos/conversation.dto'; import { ConversationService } from './conversation.service';
class CreateConversationDto {
@IsOptional()
@IsString()
title?: string;
}
class SendMessageDto {
@IsNotEmpty()
@IsString()
content: string;
}
@Controller('conversations') @Controller('conversations')
export class ConversationController { export class ConversationController {

View File

@ -8,7 +8,17 @@ import {
MessageBody, MessageBody,
} from '@nestjs/websockets'; } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { ConversationService, FileAttachment } from '../../application/services/conversation.service'; import { ConversationService } from './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;

View File

@ -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 '../adapters/outbound/persistence/conversation-postgres.repository'; import { ConversationPostgresRepository } from '../infrastructure/database/postgres/conversation-postgres.repository';
import { MessagePostgresRepository } from '../adapters/outbound/persistence/message-postgres.repository'; import { MessagePostgresRepository } from '../infrastructure/database/postgres/message-postgres.repository';
import { TokenUsagePostgresRepository } from '../adapters/outbound/persistence/token-usage-postgres.repository'; import { TokenUsagePostgresRepository } from '../infrastructure/database/postgres/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 '../application/services/conversation.service'; import { ConversationService } from './conversation.service';
import { ConversationController } from '../adapters/inbound/conversation.controller'; import { ConversationController } from './conversation.controller';
import { InternalConversationController } from '../adapters/inbound/internal.controller'; import { InternalConversationController } from './internal.controller';
import { ConversationGateway } from '../adapters/inbound/conversation.gateway'; import { ConversationGateway } from './conversation.gateway';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([ConversationORM, MessageORM, TokenUsageORM])], imports: [TypeOrmModule.forFeature([ConversationORM, MessageORM, TokenUsageORM])],

View File

@ -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 CreateConversationParams { export interface CreateConversationDto {
userId: string; userId: string;
title?: string; title?: string;
} }
@ -38,7 +38,7 @@ export interface FileAttachment {
thumbnailUrl?: string; thumbnailUrl?: string;
} }
export interface SendMessageParams { export interface SendMessageDto {
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(params: CreateConversationParams): Promise<ConversationEntity> { async createConversation(dto: CreateConversationDto): Promise<ConversationEntity> {
const conversation = ConversationEntity.create({ const conversation = ConversationEntity.create({
id: uuidv4(), id: uuidv4(),
userId: params.userId, userId: dto.userId,
title: params.title || '新对话', title: dto.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(params: SendMessageParams): AsyncGenerator<StreamChunk> { async *sendMessage(dto: SendMessageDto): AsyncGenerator<StreamChunk> {
// Verify conversation exists and belongs to user // Verify conversation exists and belongs to user
const conversation = await this.getConversation(params.conversationId, params.userId); const conversation = await this.getConversation(dto.conversationId, dto.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 = params.attachments && params.attachments.length > 0; const hasAttachments = dto.attachments && dto.attachments.length > 0;
const userMessage = MessageEntity.create({ const userMessage = MessageEntity.create({
id: uuidv4(), id: uuidv4(),
conversationId: params.conversationId, conversationId: dto.conversationId,
role: MessageRole.USER, role: MessageRole.USER,
type: hasAttachments ? MessageType.TEXT_WITH_ATTACHMENTS : MessageType.TEXT, type: hasAttachments ? MessageType.TEXT_WITH_ATTACHMENTS : MessageType.TEXT,
content: params.content, content: dto.content,
metadata: hasAttachments ? { attachments: params.attachments } : undefined, metadata: hasAttachments ? { attachments: dto.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(params.conversationId); const previousMessages = await this.messageRepo.findByConversationId(dto.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: params.userId, userId: dto.userId,
conversationId: params.conversationId, conversationId: dto.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(
params.content, dto.content,
context, context,
params.attachments, dto.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: params.conversationId, conversationId: dto.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(params.content); const title = await this.generateTitle(dto.content);
conversation.title = title; conversation.title = title;
await this.conversationRepo.update(conversation); await this.conversationRepo.update(conversation);
} }

View File

@ -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 } from 'typeorm'; import { Repository, MoreThan, LessThan } from 'typeorm';
import { ConversationORM } from '../../infrastructure/database/postgres/entities/conversation.orm'; import { ConversationEntity } from '../domain/entities/conversation.entity';
import { MessageORM } from '../../infrastructure/database/postgres/entities/message.orm'; import { MessageEntity } from '../domain/entities/message.entity';
/** /**
* API - * API -
@ -11,10 +11,10 @@ import { MessageORM } from '../../infrastructure/database/postgres/entities/mess
@Controller('internal/conversations') @Controller('internal/conversations')
export class InternalConversationController { export class InternalConversationController {
constructor( constructor(
@InjectRepository(ConversationORM) @InjectRepository(ConversationEntity)
private conversationRepo: Repository<ConversationORM>, private conversationRepo: Repository<ConversationEntity>,
@InjectRepository(MessageORM) @InjectRepository(MessageEntity)
private messageRepo: Repository<MessageORM>, private messageRepo: Repository<MessageEntity>,
) {} ) {}
/** /**

View File

@ -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 { TokenUsageORM } from '../database/postgres/entities/token-usage.orm'; import { TokenUsageEntity } from '../../domain/entities/token-usage.entity';
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([TokenUsageORM]), TypeOrmModule.forFeature([TokenUsageEntity]),
], ],
providers: [ providers: [
ClaudeAgentService, ClaudeAgentService,

View File

@ -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 { TokenUsageORM } from '../database/postgres/entities/token-usage.orm'; import { TokenUsageEntity } from '../../domain/entities/token-usage.entity';
/** /**
* Claude API ( 2024) * Claude API ( 2024)
@ -65,8 +65,8 @@ export interface UsageStats {
@Injectable() @Injectable()
export class TokenUsageService { export class TokenUsageService {
constructor( constructor(
@InjectRepository(TokenUsageORM) @InjectRepository(TokenUsageEntity)
private tokenUsageRepository: Repository<TokenUsageORM>, private tokenUsageRepository: Repository<TokenUsageEntity>,
) {} ) {}
/** /**
@ -95,7 +95,7 @@ export class TokenUsageService {
/** /**
* API token 使 * API token 使
*/ */
async recordUsage(input: TokenUsageInput): Promise<TokenUsageORM> { async recordUsage(input: TokenUsageInput): Promise<TokenUsageEntity> {
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, TokenUsageORM[]>(); const byDate = new Map<string, TokenUsageEntity[]>();
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: TokenUsageORM[]): UsageStats { private calculateStats(records: TokenUsageEntity[]): UsageStats {
if (records.length === 0) { if (records.length === 0) {
return { return {
totalRequests: 0, totalRequests: 0,

View File

@ -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, MoreThan, LessThan } from 'typeorm';
import { ConversationORM } from '../../../infrastructure/database/postgres/entities/conversation.orm'; import { ConversationORM } from './entities/conversation.orm';
import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface'; import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface';
import { import {
ConversationEntity, ConversationEntity,

View File

@ -1 +1,4 @@
export * from './entities'; export * from './entities';
export * from './conversation-postgres.repository';
export * from './message-postgres.repository';
export * from './token-usage-postgres.repository';

View File

@ -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 '../../../infrastructure/database/postgres/entities/message.orm'; import { MessageORM } from './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';

View File

@ -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 '../../../infrastructure/database/postgres/entities/token-usage.orm'; import { TokenUsageORM } from './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';

View File

@ -1,2 +0,0 @@
export * from './evolution.controller';
export * from './admin.controller';

View File

@ -1,2 +0,0 @@
export * from './conversation.client';
export * from './knowledge.client';

View File

@ -1,124 +0,0 @@
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,
});
}
}

View File

@ -1 +0,0 @@
export * from './admin-postgres.repository';

View File

@ -12,16 +12,42 @@ import {
UnauthorizedException, UnauthorizedException,
ForbiddenException, ForbiddenException,
} from '@nestjs/common'; } from '@nestjs/common';
import { AdminService } from '../../application/services/admin.service'; import { AdminService, AdminRole, LoginResult } from './admin.service';
import { AdminRole } from '../../domain/value-objects/admin-role.enum';
import { // ========== DTOs ==========
LoginDto,
CreateAdminDto, class LoginDto {
UpdateAdminDto, username: string;
ChangePasswordDto, password: string;
ResetPasswordDto, }
LoginResult,
} from '../../application/dtos/admin.dto'; class CreateAdminDto {
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 {

View File

@ -1,23 +1,15 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminController } from '../adapters/inbound/admin.controller'; import { AdminController } from './admin.controller';
import { AdminService } from '../application/services/admin.service'; import { AdminService } from './admin.service';
import { AdminPostgresRepository } from '../adapters/outbound/persistence/admin-postgres.repository'; import { AdminORM } from '../infrastructure/database/entities/admin.orm';
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: [ providers: [AdminService],
AdminService,
{
provide: ADMIN_REPOSITORY,
useClass: AdminPostgresRepository,
},
],
exports: [AdminService], exports: [AdminService],
}) })
export class AdminModule {} export class AdminModule {}

View File

@ -1,11 +1,68 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable } 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 { AdminEntity } from '../../domain/entities/admin.entity'; import { AdminORM } from '../infrastructure/database/entities/admin.orm';
import { AdminRole, ROLE_PERMISSIONS } from '../../domain/value-objects/admin-role.enum'; import { v4 as uuidv4 } from 'uuid';
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;
}
/** /**
* *
@ -16,8 +73,8 @@ export class AdminService {
private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时 private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时
constructor( constructor(
@Inject(ADMIN_REPOSITORY) @InjectRepository(AdminORM)
private adminRepo: IAdminRepository, private adminRepo: Repository<AdminORM>,
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';
@ -27,7 +84,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.findByUsername(username); const admin = await this.adminRepo.findOne({ where: { username } });
if (!admin || !admin.isActive) { if (!admin || !admin.isActive) {
throw new Error('用户名或密码错误'); throw new Error('用户名或密码错误');
@ -39,8 +96,9 @@ export class AdminService {
} }
// 更新登录信息 // 更新登录信息
admin.recordLogin(ip); admin.lastLoginAt = new Date();
await this.adminRepo.update(admin); admin.lastLoginIp = ip;
await this.adminRepo.save(admin);
// 生成Token // 生成Token
const token = jwt.sign( const token = jwt.sign(
@ -54,7 +112,7 @@ export class AdminService {
); );
// 获取权限 // 获取权限
const permissions = admin.getPermissions(); const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
return { return {
admin: { admin: {
@ -88,12 +146,12 @@ export class AdminService {
role: string; role: string;
}; };
const admin = await this.adminRepo.findById(decoded.sub); const admin = await this.adminRepo.findOne({ where: { id: decoded.sub } });
if (!admin || !admin.isActive) { if (!admin || !admin.isActive) {
return { valid: false }; return { valid: false };
} }
const permissions = admin.getPermissions(); const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions);
return { return {
valid: true, valid: true,
@ -119,9 +177,11 @@ export class AdminService {
email?: string; email?: string;
phone?: string; phone?: string;
role: AdminRole; role: AdminRole;
}): Promise<AdminEntity> { }): Promise<AdminORM> {
// 检查用户名是否存在 // 检查用户名是否存在
const existing = await this.adminRepo.findByUsername(params.username); const existing = await this.adminRepo.findOne({
where: { username: params.username },
});
if (existing) { if (existing) {
throw new Error('用户名已存在'); throw new Error('用户名已存在');
} }
@ -129,13 +189,16 @@ export class AdminService {
// 加密密码 // 加密密码
const passwordHash = await bcrypt.hash(params.password, 10); const passwordHash = await bcrypt.hash(params.password, 10);
const admin = AdminEntity.create({ const admin = this.adminRepo.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);
@ -152,24 +215,28 @@ export class AdminService {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
}): Promise<{ }): Promise<{
items: AdminEntity[]; items: AdminORM[];
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 [items, total] = await Promise.all([ const query = this.adminRepo.createQueryBuilder('admin');
this.adminRepo.findAll({
role: options?.role, if (options?.role) {
isActive: options?.isActive, query.andWhere('admin.role = :role', { role: options.role });
limit: pageSize, }
offset: (page - 1) * pageSize,
}), if (options?.isActive !== undefined) {
this.adminRepo.count({ query.andWhere('admin.isActive = :active', { active: options.isActive });
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 };
} }
@ -186,8 +253,8 @@ export class AdminService {
role?: AdminRole; role?: AdminRole;
isActive?: boolean; isActive?: boolean;
}, },
): Promise<AdminEntity> { ): Promise<AdminORM> {
const admin = await this.adminRepo.findById(adminId); const admin = await this.adminRepo.findOne({ where: { id: adminId } });
if (!admin) { if (!admin) {
throw new Error('管理员不存在'); throw new Error('管理员不存在');
} }
@ -195,10 +262,13 @@ 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) admin.updateRole(params.role); if (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.update(admin); await this.adminRepo.save(admin);
return admin; return admin;
} }
@ -211,7 +281,7 @@ export class AdminService {
oldPassword: string, oldPassword: string,
newPassword: string, newPassword: string,
): Promise<void> { ): Promise<void> {
const admin = await this.adminRepo.findById(adminId); const admin = await this.adminRepo.findOne({ where: { id: adminId } });
if (!admin) { if (!admin) {
throw new Error('管理员不存在'); throw new Error('管理员不存在');
} }
@ -222,20 +292,20 @@ export class AdminService {
} }
admin.passwordHash = await bcrypt.hash(newPassword, 10); admin.passwordHash = await bcrypt.hash(newPassword, 10);
await this.adminRepo.update(admin); await this.adminRepo.save(admin);
} }
/** /**
* *
*/ */
async resetPassword(adminId: string, newPassword: string): Promise<void> { async resetPassword(adminId: string, newPassword: string): Promise<void> {
const admin = await this.adminRepo.findById(adminId); const admin = await this.adminRepo.findOne({ where: { id: 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.update(admin); await this.adminRepo.save(admin);
} }
/** /**
@ -253,11 +323,22 @@ export class AdminService {
} }
// 通配符匹配 (如 knowledge:* 匹配 knowledge:read) // 通配符匹配 (如 knowledge:* 匹配 knowledge:read)
const [resource] = requiredPermission.split(':'); const [resource, action] = 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;
}
} }

View File

@ -1,44 +0,0 @@
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;
}

View File

@ -1,14 +0,0 @@
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[];
}

View File

@ -1,2 +0,0 @@
export * from './evolution.dto';
export * from './admin.dto';

View File

@ -1,2 +0,0 @@
export * from './evolution.service';
export * from './admin.service';

View File

@ -1,132 +0,0 @@
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;
}
}

View File

@ -1,22 +0,0 @@
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');

View File

@ -1 +0,0 @@
export * from './admin.repository.interface';

View File

@ -1,41 +0,0 @@
/**
*
*/
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',
],
};

View File

@ -3,11 +3,21 @@ import {
Get, Get,
Post, Post,
Body, Body,
Query,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { EvolutionService } from '../../application/services/evolution.service'; import { EvolutionService } from './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 {

View File

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { EvolutionController } from '../adapters/inbound/evolution.controller'; import { EvolutionController } from './evolution.controller';
import { EvolutionService } from '../application/services/evolution.service'; import { EvolutionService } from './evolution.service';
import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service';
import { ConversationClient } from '../adapters/outbound/clients/conversation.client'; import { ConversationClient } from '../infrastructure/clients/conversation.client';
import { KnowledgeClient } from '../adapters/outbound/clients/knowledge.client'; import { KnowledgeClient } from '../infrastructure/clients/knowledge.client';
@Module({ @Module({
imports: [], imports: [],

View File

@ -1,9 +1,20 @@
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 '../../adapters/outbound/clients/conversation.client'; import { ConversationClient } from '../infrastructure/clients/conversation.client';
import { KnowledgeClient } from '../../adapters/outbound/clients/knowledge.client'; import { KnowledgeClient } from '../infrastructure/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[];
}
/** /**
* *

View File

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

View File

@ -1 +0,0 @@
export * from './file.controller';

View File

@ -1 +0,0 @@
export * from './file-postgres.repository';

View File

@ -1 +0,0 @@
export * from './minio-storage.adapter';

View File

@ -3,6 +3,7 @@ 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: [
@ -32,6 +33,9 @@ import { FileModule } from './file/file.module';
// Health check // Health check
HealthModule, HealthModule,
// MinIO storage
MinioModule,
// 功能模块 // 功能模块
FileModule, FileModule,
], ],

View File

@ -1 +0,0 @@
export * from './upload-file.dto';

View File

@ -1 +0,0 @@
export * from './file.service';

View File

@ -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 '../../application/services/file.service'; import { FileService } from './file.service';
import { import {
UploadFileDto, UploadFileDto,
PresignedUrlDto, PresignedUrlDto,
FileResponseDto, FileResponseDto,
PresignedUrlResponseDto, PresignedUrlResponseDto,
} from '../../application/dtos/upload-file.dto'; } from './dto/upload-file.dto';
@Controller('files') @Controller('files')
export class FileController { export class FileController {

View File

@ -2,11 +2,10 @@ 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 '../adapters/outbound/persistence/file-postgres.repository'; import { FilePostgresRepository } from '../infrastructure/database/postgres/file-postgres.repository';
import { FILE_REPOSITORY } from '../domain/repositories/file.repository.interface'; import { FILE_REPOSITORY } from '../domain/repositories/file.repository.interface';
import { FileController } from '../adapters/inbound/file.controller'; import { FileController } from './file.controller';
import { FileService } from '../application/services/file.service'; import { FileService } from './file.service';
import { MinioStorageAdapter } from '../adapters/outbound/storage/minio-storage.adapter';
@Module({ @Module({
imports: [ imports: [
@ -20,7 +19,6 @@ import { MinioStorageAdapter } from '../adapters/outbound/storage/minio-storage.
controllers: [FileController], controllers: [FileController],
providers: [ providers: [
FileService, FileService,
MinioStorageAdapter,
{ {
provide: FILE_REPOSITORY, provide: FILE_REPOSITORY,
useClass: FilePostgresRepository, useClass: FilePostgresRepository,

View File

@ -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 { MinioStorageAdapter } from '../../adapters/outbound/storage/minio-storage.adapter'; import { MinioService } from '../minio/minio.service';
import { import {
FileResponseDto, FileResponseDto,
PresignedUrlDto, PresignedUrlDto,
PresignedUrlResponseDto, PresignedUrlResponseDto,
} from '../dtos/upload-file.dto'; } from './dto/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 storageAdapter: MinioStorageAdapter, private readonly minioService: MinioService,
) {} ) {}
/** /**
@ -87,7 +87,7 @@ export class FileService {
// 获取预签名 URL (有效期 1 小时) // 获取预签名 URL (有效期 1 小时)
const expiresIn = 3600; const expiresIn = 3600;
const uploadUrl = await this.storageAdapter.getPresignedPutUrl( const uploadUrl = await this.minioService.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.storageAdapter.uploadFile(objectName, buffer, mimetype, { await this.minioService.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.storageAdapter.getPresignedUrl(file.storagePath, 3600); return this.minioService.getPresignedUrl(file.storagePath, 3600);
} }
/** /**
@ -287,13 +287,13 @@ export class FileService {
}; };
if (file.isReady()) { if (file.isReady()) {
dto.downloadUrl = await this.storageAdapter.getPresignedUrl( dto.downloadUrl = await this.minioService.getPresignedUrl(
file.storagePath, file.storagePath,
3600, 3600,
); );
if (file.thumbnailPath) { if (file.thumbnailPath) {
dto.thumbnailUrl = await this.storageAdapter.getPresignedUrl( dto.thumbnailUrl = await this.minioService.getPresignedUrl(
file.thumbnailPath, file.thumbnailPath,
3600, 3600,
); );

View File

@ -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 '../../../infrastructure/database/postgres/entities/file.orm'; import { FileORM } from './entities/file.orm';
@Injectable() @Injectable()
export class FilePostgresRepository implements IFileRepository { export class FilePostgresRepository implements IFileRepository {

View File

@ -0,0 +1,11 @@
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 {}

View File

@ -3,8 +3,8 @@ import { ConfigService } from '@nestjs/config';
import * as Minio from 'minio'; import * as Minio from 'minio';
@Injectable() @Injectable()
export class MinioStorageAdapter implements OnModuleInit { export class MinioService implements OnModuleInit {
private readonly logger = new Logger(MinioStorageAdapter.name); private readonly logger = new Logger(MinioService.name);
private client: Minio.Client; private client: Minio.Client;
private bucketName: string; private bucketName: string;

View File

@ -1,3 +0,0 @@
export * from './knowledge.controller';
export * from './memory.controller';
export * from './internal-memory.controller';

View File

@ -1,2 +0,0 @@
export * from './knowledge-postgres.repository';
export * from './memory-postgres.repository';

View File

@ -1,2 +0,0 @@
export * from './knowledge.dto';
export * from './memory.dto';

View File

@ -1,44 +0,0 @@
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;
}

View File

@ -1,30 +0,0 @@
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;
}

View File

@ -1,4 +0,0 @@
export * from './knowledge.service';
export * from './memory.service';
export * from './rag.service';
export * from './chunking.service';

View File

@ -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 '../../../infrastructure/database/postgres/entities/knowledge-article.orm'; import { KnowledgeArticleORM } from './entities/knowledge-article.orm';
import { KnowledgeChunkORM } from '../../../infrastructure/database/postgres/entities/knowledge-chunk.orm'; import { KnowledgeChunkORM } from './entities/knowledge-chunk.orm';
@Injectable() @Injectable()
export class KnowledgePostgresRepository implements IKnowledgeRepository { export class KnowledgePostgresRepository implements IKnowledgeRepository {

View File

@ -11,8 +11,8 @@ import {
ExperienceType, ExperienceType,
VerificationStatus, VerificationStatus,
} from '../../../domain/entities/system-experience.entity'; } from '../../../domain/entities/system-experience.entity';
import { UserMemoryORM } from '../../../infrastructure/database/postgres/entities/user-memory.orm'; import { UserMemoryORM } from './entities/user-memory.orm';
import { SystemExperienceORM } from '../../../infrastructure/database/postgres/entities/system-experience.orm'; import { SystemExperienceORM } from './entities/system-experience.orm';
@Injectable() @Injectable()
export class UserMemoryPostgresRepository implements IUserMemoryRepository { export class UserMemoryPostgresRepository implements IUserMemoryRepository {

View File

@ -10,17 +10,56 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { KnowledgeService } from '../../application/services/knowledge.service'; import { KnowledgeService } from './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 {
CreateArticleDto, // ========== DTOs ==========
UpdateArticleDto,
SearchArticlesDto, class CreateArticleDto {
RetrieveKnowledgeDto, title: string;
ImportArticlesDto, content: string;
FeedbackDto, category: string;
} from '../../application/dtos/knowledge.dto'; tags?: string[];
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 {

View File

@ -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 '../adapters/inbound/knowledge.controller'; import { KnowledgeController } from './knowledge.controller';
import { KnowledgeService } from '../application/services/knowledge.service'; import { KnowledgeService } from './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 '../adapters/outbound/persistence/knowledge-postgres.repository'; import { KnowledgePostgresRepository } from '../infrastructure/database/postgres/knowledge-postgres.repository';
import { import {
UserMemoryPostgresRepository, UserMemoryPostgresRepository,
SystemExperiencePostgresRepository, SystemExperiencePostgresRepository,
} from '../adapters/outbound/persistence/memory-postgres.repository'; } from '../infrastructure/database/postgres/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';

View File

@ -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 './chunking.service'; import { ChunkingService } from '../application/services/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';
/** /**
* *

View File

@ -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 '../../application/services/memory.service'; import { MemoryService } from './memory.service';
import { ExperienceType } from '../../domain/entities/system-experience.entity'; import { ExperienceType } from '../domain/entities/system-experience.entity';
/** /**
* API * API

View File

@ -10,15 +10,41 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { MemoryService } from '../../application/services/memory.service'; import { MemoryService } from './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 {
SaveMemoryDto, // ========== DTOs ==========
SearchMemoryDto,
ExtractExperienceDto, class SaveMemoryDto {
MemoryFeedbackDto, userId: string;
} from '../../application/dtos/memory.dto'; memoryType: MemoryType;
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 {
@ -237,7 +263,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: MemoryFeedbackDto, @Body() dto: FeedbackDto,
) { ) {
await this.memoryService.recordExperienceFeedback(id, dto.positive); await this.memoryService.recordExperienceFeedback(id, dto.positive);
return { return {

View File

@ -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 '../adapters/inbound/memory.controller'; import { MemoryController } from './memory.controller';
import { InternalMemoryController } from '../adapters/inbound/internal-memory.controller'; import { InternalMemoryController } from './internal.controller';
import { MemoryService } from '../application/services/memory.service'; import { MemoryService } from './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 '../adapters/outbound/persistence/memory-postgres.repository'; } from '../infrastructure/database/postgres/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 {

View File

@ -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';
/** /**
* *

View File

@ -1,2 +0,0 @@
export * from './order.controller';
export * from './payment.controller';

View File

@ -1,3 +0,0 @@
export * from './alipay.adapter';
export * from './wechat-pay.adapter';
export * from './stripe.adapter';

View File

@ -1,2 +0,0 @@
export * from './order-postgres.repository';
export * from './payment-postgres.repository';

View File

@ -1,2 +0,0 @@
export * from './order.dto';
export * from './payment.dto';

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,12 +0,0 @@
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;
}

View File

@ -1,2 +0,0 @@
export * from './order.service';
export * from './payment.service';

View File

@ -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 '../../../infrastructure/database/postgres/entities/order.orm'; import { OrderORM } from './entities/order.orm';
@Injectable() @Injectable()
export class OrderPostgresRepository implements IOrderRepository { export class OrderPostgresRepository implements IOrderRepository {

View File

@ -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 '../../../infrastructure/database/postgres/entities/payment.orm'; import { PaymentORM } from './entities/payment.orm';
@Injectable() @Injectable()
export class PaymentPostgresRepository implements IPaymentRepository { export class PaymentPostgresRepository implements IPaymentRepository {

View File

@ -8,8 +8,14 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { OrderService } from '../../application/services/order.service'; import { OrderService } from './order.service';
import { CreateOrderDto } from '../../application/dtos/order.dto'; import { ServiceType } from '../domain/entities/order.entity';
class CreateOrderDto {
serviceType: ServiceType;
serviceCategory?: string;
conversationId?: string;
}
@Controller('orders') @Controller('orders')
export class OrderController { export class OrderController {

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { OrderORM } from '../infrastructure/database/postgres/entities/order.orm'; import { OrderORM } from '../infrastructure/database/postgres/entities/order.orm';
import { OrderPostgresRepository } from '../adapters/outbound/persistence/order-postgres.repository'; import { OrderPostgresRepository } from '../infrastructure/database/postgres/order-postgres.repository';
import { ORDER_REPOSITORY } from '../domain/repositories/order.repository.interface'; import { ORDER_REPOSITORY } from '../domain/repositories/order.repository.interface';
import { OrderService } from '../application/services/order.service'; import { OrderService } from './order.service';
import { OrderController } from '../adapters/inbound/order.controller'; import { OrderController } from './order.controller';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([OrderORM])], imports: [TypeOrmModule.forFeature([OrderORM])],

View File

@ -1,6 +1,6 @@
import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common';
import { OrderEntity, OrderStatus, ServiceType } from '../../domain/entities/order.entity'; import { OrderEntity, OrderStatus, ServiceType } from '../domain/entities/order.entity';
import { IOrderRepository, ORDER_REPOSITORY } from '../../domain/repositories/order.repository.interface'; import { IOrderRepository, ORDER_REPOSITORY } from '../domain/repositories/order.repository.interface';
// Default pricing // Default pricing
const SERVICE_PRICING: Record<string, Record<string, number>> = { const SERVICE_PRICING: Record<string, Record<string, number>> = {
@ -14,7 +14,7 @@ const SERVICE_PRICING: Record<string, Record<string, number>> = {
}, },
}; };
export interface CreateOrderParams { export interface CreateOrderDto {
userId: string; userId: string;
serviceType: ServiceType; serviceType: ServiceType;
serviceCategory?: string; serviceCategory?: string;
@ -28,15 +28,15 @@ export class OrderService {
private readonly orderRepo: IOrderRepository, private readonly orderRepo: IOrderRepository,
) {} ) {}
async createOrder(params: CreateOrderParams): Promise<OrderEntity> { async createOrder(dto: CreateOrderDto): Promise<OrderEntity> {
// Get price based on service type and category // Get price based on service type and category
const price = this.getPrice(params.serviceType, params.serviceCategory); const price = this.getPrice(dto.serviceType, dto.serviceCategory);
const order = OrderEntity.create({ const order = OrderEntity.create({
userId: params.userId, userId: dto.userId,
serviceType: params.serviceType, serviceType: dto.serviceType,
serviceCategory: params.serviceCategory, serviceCategory: dto.serviceCategory,
conversationId: params.conversationId, conversationId: dto.conversationId,
amount: price, amount: price,
currency: 'CNY', currency: 'CNY',
}); });

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More