diff --git a/backend/analytics-service/Dockerfile b/backend/analytics-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/analytics-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/analytics-service/src/main/java/com/artedu/analytics/controller/AnalyticsController.java b/backend/analytics-service/src/main/java/com/artedu/analytics/controller/AnalyticsController.java new file mode 100644 index 0000000..e489f07 --- /dev/null +++ b/backend/analytics-service/src/main/java/com/artedu/analytics/controller/AnalyticsController.java @@ -0,0 +1,46 @@ +package com.artedu.analytics.controller; + +import com.artedu.common.result.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/v1/analytics") +public class AnalyticsController { + + @GetMapping("/dashboard") + public Result> dashboard(@RequestParam String corpId) { + Map result = new HashMap<>(); + result.put("todayConversations", 128); + result.put("todayMessages", 2456); + result.put("activeStaff", 24); + result.put("utteranceAdoptionRate", 0.685); + result.put("conversionRate", 0.123); + result.put("avgResponseTimeMs", 18500); + return Result.success(result); + } + + @GetMapping("/utterance-performance") + public Result>> utterancePerformance( + @RequestParam String corpId, + @RequestParam(defaultValue = "7") Integer days) { + // TODO: 查询真实数据 + return Result.success(List.of()); + } + + @GetMapping("/staff-performance") + public Result>> staffPerformance( + @RequestParam String corpId, + @RequestParam(defaultValue = "7") Integer days) { + // TODO: 查询真实数据 + return Result.success(List.of()); + } +} diff --git a/backend/archive-service/Dockerfile b/backend/archive-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/archive-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/auth-service/Dockerfile b/backend/auth-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/auth-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/conversation-service/Dockerfile b/backend/conversation-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/conversation-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/gateway/Dockerfile b/backend/gateway/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/gateway/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/generation-service/Dockerfile b/backend/generation-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/generation-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/intent-service/Dockerfile b/backend/intent-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/intent-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/kb-admin-service/Dockerfile b/backend/kb-admin-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/kb-admin-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/UtteranceController.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/UtteranceController.java new file mode 100644 index 0000000..bbfe7a5 --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/controller/UtteranceController.java @@ -0,0 +1,70 @@ +package com.artedu.kbadmin.controller; + +import com.artedu.common.result.PageResult; +import com.artedu.common.result.Result; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/v1/admin/utterances") +public class UtteranceController { + + @Autowired + private com.artedu.kbadmin.mapper.UtteranceMapper utteranceMapper; + + @GetMapping + public Result> list( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer size, + @RequestParam(required = false) String status, + @RequestParam(required = false) String keyword) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (status != null && !status.isEmpty()) { + wrapper.eq(com.artedu.kbadmin.entity.Utterance::getStatus, status); + } + if (keyword != null && !keyword.isEmpty()) { + wrapper.like(com.artedu.kbadmin.entity.Utterance::getTitle, keyword) + .or().like(com.artedu.kbadmin.entity.Utterance::getContentText, keyword); + } + wrapper.orderByDesc(com.artedu.kbadmin.entity.Utterance::getUpdatedAt); + + Page result = utteranceMapper.selectPage(new Page<>(page, size), wrapper); + return Result.success(PageResult.of(page, size, result.getTotal(), result.getRecords())); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(utteranceMapper.selectById(id)); + } + + @PostMapping + public Result create(@RequestBody com.artedu.kbadmin.entity.Utterance utterance) { + utterance.setCreatedAt(LocalDateTime.now()); + utterance.setUpdatedAt(LocalDateTime.now()); + utteranceMapper.insert(utterance); + return Result.success("创建成功"); + } + + @PutMapping("/{id}") + public Result update(@PathVariable Long id, @RequestBody com.artedu.kbadmin.entity.Utterance utterance) { + utterance.setId(id); + utterance.setUpdatedAt(LocalDateTime.now()); + utteranceMapper.updateById(utterance); + return Result.success("更新成功"); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + utteranceMapper.deleteById(id); + return Result.success("删除成功"); + } +} diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/Utterance.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/Utterance.java new file mode 100644 index 0000000..be69f15 --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/entity/Utterance.java @@ -0,0 +1,42 @@ +package com.artedu.kbadmin.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("utterances") +public class Utterance { + + @TableId(type = IdType.AUTO) + private Long id; + + private String utteranceId; + private String corpId; + private String title; + private String content; + private String contentText; + private Integer version; + private String status; + private Integer priority; + private String stageTags; + private String intentTags; + private String profileTags; + private String courseTypeTags; + private String emotionTags; + private String topicTags; + private Integer usedCount; + private Integer selectedCount; + private Integer positiveFeedback; + private Integer negativeFeedback; + private Integer conversionCount; + private Double successRate; + private Integer hasVector; + private String createdBy; + private String source; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/UtteranceMapper.java b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/UtteranceMapper.java new file mode 100644 index 0000000..95fe561 --- /dev/null +++ b/backend/kb-admin-service/src/main/java/com/artedu/kbadmin/mapper/UtteranceMapper.java @@ -0,0 +1,9 @@ +package com.artedu.kbadmin.mapper; + +import com.artedu.kbadmin.entity.Utterance; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UtteranceMapper extends BaseMapper { +} diff --git a/backend/recommendation-service/Dockerfile b/backend/recommendation-service/Dockerfile new file mode 100644 index 0000000..858c6fb --- /dev/null +++ b/backend/recommendation-service/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index 6be5cbe..f18b52f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,17 @@ version: '3.8' services: + nginx: + image: nginx:alpine + container_name: ai-assistant-nginx + restart: always + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + depends_on: + - gateway + mysql: image: mysql:5.7 container_name: ai-assistant-mysql @@ -54,6 +65,142 @@ services: timeout: 5s retries: 5 + gateway: + build: ./backend/gateway + container_name: ai-assistant-gateway + restart: always + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - auth-service + - archive-service + - conversation-service + - intent-service + - recommend-service + - generation-service + - kb-admin-service + - analytics-service + + auth-service: + build: ./backend/auth-service + container_name: ai-assistant-auth + restart: always + ports: + - "8081:8081" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + + archive-service: + build: ./backend/archive-service + container_name: ai-assistant-archive + restart: always + ports: + - "8082:8082" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - RABBITMQ_HOST=rabbitmq + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + - rabbitmq + + conversation-service: + build: ./backend/conversation-service + container_name: ai-assistant-conversation + restart: always + ports: + - "8083:8083" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - RABBITMQ_HOST=rabbitmq + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + - rabbitmq + + intent-service: + build: ./backend/intent-service + container_name: ai-assistant-intent + restart: always + ports: + - "8084:8084" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - RABBITMQ_HOST=rabbitmq + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + - rabbitmq + + recommend-service: + build: ./backend/recommendation-service + container_name: ai-assistant-recommend + restart: always + ports: + - "8085:8085" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - RABBITMQ_HOST=rabbitmq + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + - rabbitmq + + generation-service: + build: ./backend/generation-service + container_name: ai-assistant-generation + restart: always + ports: + - "8086:8086" + environment: + - REDIS_HOST=redis + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - redis + + kb-admin-service: + build: ./backend/kb-admin-service + container_name: ai-assistant-kbadmin + restart: always + ports: + - "8087:8087" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + + analytics-service: + build: ./backend/analytics-service + container_name: ai-assistant-analytics + restart: always + ports: + - "8088:8088" + environment: + - MYSQL_HOST=mysql + - REDIS_HOST=redis + - SPRING_PROFILES_ACTIVE=docker + depends_on: + - mysql + - redis + volumes: mysql_data: redis_data: diff --git a/frontend/admin/Dockerfile b/frontend/admin/Dockerfile new file mode 100644 index 0000000..eb95269 --- /dev/null +++ b/frontend/admin/Dockerfile @@ -0,0 +1,11 @@ +FROM node:18-alpine AS builder +WORKDIR /app +COPY package.json . +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html/admin +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/frontend/admin/index.html b/frontend/admin/index.html new file mode 100644 index 0000000..02eeaad --- /dev/null +++ b/frontend/admin/index.html @@ -0,0 +1,12 @@ + + + + + + AI坐席辅助系统 - 管理后台 + + +
+ + + diff --git a/frontend/admin/package.json b/frontend/admin/package.json new file mode 100644 index 0000000..f59b20f --- /dev/null +++ b/frontend/admin/package.json @@ -0,0 +1,26 @@ +{ + "name": "ai-assistant-admin", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "antd": "^5.12.0", + "axios": "^1.6.0", + "@ant-design/charts": "^2.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/frontend/admin/src/App.tsx b/frontend/admin/src/App.tsx new file mode 100644 index 0000000..8a53d06 --- /dev/null +++ b/frontend/admin/src/App.tsx @@ -0,0 +1,22 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import Login from './pages/Login' +import Layout from './components/Layout' +import UtteranceList from './pages/UtteranceList' +import Dashboard from './pages/Dashboard' + +function App() { + return ( + + + } /> + }> + } /> + } /> + } /> + + + + ) +} + +export default App diff --git a/frontend/admin/src/components/Layout.tsx b/frontend/admin/src/components/Layout.tsx new file mode 100644 index 0000000..75d29cd --- /dev/null +++ b/frontend/admin/src/components/Layout.tsx @@ -0,0 +1,39 @@ +import { Layout as AntLayout, Menu, Button } from 'antd' +import { Outlet, useNavigate, useLocation } from 'react-router-dom' +import { DashboardOutlined, CommentOutlined, LogoutOutlined } from '@ant-design/icons' + +const { Sider, Content, Header } = AntLayout + +export default function Layout() { + const navigate = useNavigate() + const location = useLocation() + + const menuItems = [ + { key: '/dashboard', icon: , label: '数据看板' }, + { key: '/utterances', icon: , label: '话术库管理' } + ] + + const handleLogout = () => { + localStorage.removeItem('admin_token') + navigate('/login') + } + + return ( + + +
+ AI坐席助手 +
+ navigate(e.key)} /> + + +
+ +
+ + + +
+ + ) +} diff --git a/frontend/admin/src/main.tsx b/frontend/admin/src/main.tsx new file mode 100644 index 0000000..eaad244 --- /dev/null +++ b/frontend/admin/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/frontend/admin/src/pages/Dashboard.tsx b/frontend/admin/src/pages/Dashboard.tsx new file mode 100644 index 0000000..3c6b1de --- /dev/null +++ b/frontend/admin/src/pages/Dashboard.tsx @@ -0,0 +1,32 @@ +import { Card, Statistic, Row, Col } from 'antd' +import { ArrowUpOutlined, MessageOutlined, UserOutlined, LikeOutlined } from '@ant-design/icons' + +export default function Dashboard() { + return ( +
+

数据看板

+ + + + } /> + + + + + } /> + + + + + } valueStyle={{ color: '#3f8600' }} /> + + + + + } valueStyle={{ color: '#cf1322' }} /> + + + +
+ ) +} diff --git a/frontend/admin/src/pages/Login.tsx b/frontend/admin/src/pages/Login.tsx new file mode 100644 index 0000000..fdb5f5c --- /dev/null +++ b/frontend/admin/src/pages/Login.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react' +import { Form, Input, Button, Card, message } from 'antd' +import { useNavigate } from 'react-router-dom' + +export default function Login() { + const [loading, setLoading] = useState(false) + const navigate = useNavigate() + + const onFinish = async (values: any) => { + setLoading(true) + try { + if (values.username === 'admin' && values.password === 'admin123') { + localStorage.setItem('admin_token', 'mock_token') + message.success('登录成功') + navigate('/dashboard') + } else { + message.error('用户名或密码错误') + } + } finally { + setLoading(false) + } + } + + return ( +
+ +
+ + + + + + + +
+
+
+ ) +} diff --git a/frontend/admin/src/pages/UtteranceList.tsx b/frontend/admin/src/pages/UtteranceList.tsx new file mode 100644 index 0000000..bc013f2 --- /dev/null +++ b/frontend/admin/src/pages/UtteranceList.tsx @@ -0,0 +1,68 @@ +import { useState, useEffect } from 'react' +import { Table, Button, Tag, Space, Modal, Form, Input, Select, message } from 'antd' + +export default function UtteranceList() { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const [modalVisible, setModalVisible] = useState(false) + const [form] = Form.useForm() + + useEffect(() => { + loadData() + }, []) + + const loadData = () => { + setLoading(true) + // TODO: 调用真实API + setData([ + { id: 1, utteranceId: 'UTT-S01-001', title: '首次接待通用开场白', stageTags: '开场白', status: 'ACTIVE', successRate: 0.85 }, + { id: 2, utteranceId: 'UTT-S03-001', title: '推荐3D场景UE地编就业班', stageTags: '课程推荐', status: 'ACTIVE', successRate: 0.72 } + ]) + setLoading(false) + } + + const columns = [ + { title: '话术ID', dataIndex: 'utteranceId' }, + { title: '标题', dataIndex: 'title' }, + { title: '阶段', dataIndex: 'stageTags', render: (t: string) => {t} }, + { title: '状态', dataIndex: 'status', render: (s: string) => {s} }, + { title: '成功率', dataIndex: 'successRate', render: (v: number) => `${(v * 100).toFixed(1)}%` }, + { title: '操作', render: (_: any, record: any) => ( + + + + )} + ] + + return ( +
+
+

话术库管理

+ +
+ + setModalVisible(false)} onOk={() => { message.success('保存成功'); setModalVisible(false) }}> +
+ + + + + + + +