feat(01): Phase 1 - 基础设施 + 数据库 + 公共模块

- 创建10个微服务目录结构和Maven多模块配置
- 完成15张MySQL 5.7表设计和初始化数据(40意图+8话术分类+管理员)
- 实现common模块:Result、PageResult、异常处理、MyBatisPlus/Redis/RabbitMQ/跨域配置
- 实现BaseEntity、AutoFillHandler、JsonUtils、JsonTypeHandler
- 配置API Gateway路由和全部服务的application.yml
- 添加docker-compose.yml(MySQL+Redis+RabbitMQ)
This commit is contained in:
jiao 2026-05-12 17:55:11 +08:00
commit 31a395be5e
51 changed files with 10128 additions and 0 deletions

37
.planning/PROJECT.md Normal file
View File

@ -0,0 +1,37 @@
# 第九联盟AI坐席辅助系统
## 项目愿景
为第九联盟(9artedu.com)课程顾问/销售(坐席)构建一个基于企业微信会话存档 + LLM 的实时话术推荐系统。当学员通过企微咨询课程时,系统实时分析对话内容,识别学员意图,构建学员画像,并推荐最合适的话术,帮助坐席提高转化率和响应效率。
## 核心业务
- **数据接入**: 企微会话存档拉取(C SDK + JNA)、RSA解密、消息解析
- **AI分析引擎**: 对话上下文理解(最近10轮滑动窗口)、意图识别(40个场景多标签分类)、学员画像构建(5维度标签体系)、话术召回(规则召回+向量召回+热门召回)、话术排序(多因子加权+MMR多样性)、LLM动态话术生成(兜底策略)
- **输出交互**: PC端侧边栏H5(360px)、手机端侧边栏H5(80%高度)、模板卡片消息推送、一键发送
- **管理后台**: 话术库管理(CRUD+分类+导入导出)、数据统计看板、系统配置
## 目标用户
- **主要用户**: 第九联盟课程顾问/销售(坐席)
- **服务对象**: CG美术培训潜在学员(外部联系人)
## 关键指标
- 端到端延迟目标: < 25秒(含15秒企微回调间隔)
- 话术推荐准确率: 基于坐席采纳率和转化率持续优化
- 覆盖40个意图场景、8个销售阶段
## 技术栈
- 后端: Spring Boot 2.7.x + MyBatis-Plus + MySQL 5.7 + Redis 6.x + RabbitMQ 3.x
- 前端: React 18 + TypeScript + Vite + Ant Design / Ant Design Mobile
- LLM: 通义千问API (qwen-turbo/qwen-plus)
- 企微集成: 企业微信JS-SDK 2.0 + JNA调用C SDK
- 部署: Docker + Docker Compose
## 约束条件
- 线上环境为 MySQL 5.7(JSON→TEXT适配,向量存Redis)
- 用户自有服务器部署
- 数据合规要求(使用国产LLM)

63
.planning/REQUIREMENTS.md Normal file
View File

@ -0,0 +1,63 @@
# 需求文档
## v1 需求
### R1. 数据接入层
- **R1-01**: 企微会话存档拉取(C SDK + JNA)
- **R1-02**: RSA解密 + 消息解析
- **R1-03**: 回调接收(msgaudit_notify,15秒间隔)
- **R1-04**: 消息去重与存储
### R2. AI分析引擎
- **R2-01**: 对话上下文管理(最近10轮滑动窗口)
- **R2-02**: 意图识别(40个场景多标签分类)
- **R2-03**: 学员画像构建(5维度标签体系:类型/基础/意向度/关注重点/决策阶段)
- **R2-04**: 话术召回(规则召回+向量召回+热门召回)
- **R2-05**: 话术排序(多因子加权+MMR多样性)
- **R2-06**: LLM动态话术生成(兜底策略)
### R3. 输出交互层
- **R3-01**: PC端侧边栏H5(360px,完整功能)
- **R3-02**: 手机端侧边栏H5(80%高度,精简操作)
- **R3-03**: 模板卡片消息推送(高优先级场景主动推送)
- **R3-04**: 一键发送(sendChatMessage)
- **R3-05**: WebSocket实时推送推荐结果
### R4. 管理后台
- **R4-01**: 话术库管理(CRUD+分类+导入导出)
- **R4-02**: 数据统计看板(使用数据+效果分析)
- **R4-03**: 系统配置(推送设置+免打扰)
- **R4-04**: 管理员登录与权限控制
### R5. 基础设施
- **R5-01**: 企微OAuth2认证 + JWT
- **R5-02**: API网关(路由/限流/鉴权)
- **R5-03**: MySQL 5.7 数据库(15张表)
- **R5-04**: Redis 缓存(会话/向量/Token)
- **R5-05**: RabbitMQ 消息队列(异步处理)
- **R5-06**: Docker Compose 部署
## v2 需求(后续迭代)
- 多企业租户支持
- 更复杂的反馈闭环和模型自优化
- 更丰富的数据分析维度
- 移动端App原生应用
## 数据库表清单
1. archive_messages - 企微会话存档消息
2. conversations - 会话表
3. conversation_turns - 对话轮次表
4. intent_categories - 意图分类表(40个)
5. intent_recognitions - 意图识别记录表
6. customer_profiles - 学员画像表
7. utterances - 话术库表
8. utterance_categories - 话术分类表(8个阶段)
9. utterance_performance - 话术效果表
10. recommendations - 推荐记录表
11. knowledge_documents - 知识库文档表
12. staffs - 员工表
13. customers - 客户表
14. admins - 管理员表
15. operation_logs - 操作日志表

50
.planning/ROADMAP.md Normal file
View File

@ -0,0 +1,50 @@
# 路线图
## Phase 1: 基础设施 + 数据库 + 公共模块
**目标**: 搭建项目骨架,创建所有服务的空壳,完成数据库设计和公共模块
**成功标准**:
- [ ] 10个微服务目录结构完整
- [ ] common模块包含Result、PageResult、异常处理、MyBatis配置、Redis配置、RabbitMQ配置、跨域配置、BaseEntity、AutoFillHandler、JsonUtils、JsonTypeHandler
- [ ] init.sql包含15张表创建语句和初始化数据
- [ ] docker-compose.yml基础服务(MySQL、Redis、RabbitMQ)
- [ ] 网关服务基础路由配置
## Phase 2: 认证服务 + 会话存档服务
**目标**: 实现企微OAuth登录和会话存档拉取
**成功标准**:
- [ ] auth-service: 企微OAuth2登录、JWT签发、JS-SDK签名
- [ ] archive-service: JNA接口、RSA解密、存档拉取、回调接收、消息存储
## Phase 3: 对话服务 + 意图服务
**目标**: 实现对话上下文管理和意图识别
**成功标准**:
- [ ] conversation-service: 会话管理、上下文窗口(Redis List)、轮次解析、消息事件处理
- [ ] intent-service: 40个意图分类、规则匹配+LLM混合识别、学员画像构建、槽位填充
## Phase 4: 推荐服务 + LLM生成服务
**目标**: 实现话术推荐引擎和动态生成
**成功标准**:
- [ ] recommendation-service: 三层召回(规则/向量/热门)、多因子排序、MMR多样性重排、WebSocket推送
- [ ] generation-service: 通义千问API封装、Prompt引擎、Token控制、动态话术生成
## Phase 5: 前端侧边栏H5
**目标**: 实现坐席使用的侧边栏推荐界面
**成功标准**:
- [ ] PC端侧边栏(360px): 学员画像、阶段指示器、话术推荐卡片、一键发送、搜索面板
- [ ] 手机端侧边栏(80%高度): 精简版UI
- [ ] 企微JS-SDK集成、WebSocket实时更新
## Phase 6: 管理后台 + 知识库
**目标**: 实现话术库管理和数据分析
**成功标准**:
- [ ] kb-admin-service: 话术CRUD、分类管理、导入导出、向量生成
- [ ] analytics-service: 数据统计、话术效果分析
- [ ] admin前端: 登录页、话术库管理、数据看板
## Phase 7: 部署配置与集成测试
**目标**: 完成Docker部署和端到端测试
**成功标准**:
- [ ] 所有服务的Dockerfile
- [ ] docker-compose.yml完整编排
- [ ] nginx.conf配置
- [ ] 端到端流程验证(回调→拉取→识别→推荐→推送)

22
.planning/STATE.md Normal file
View File

@ -0,0 +1,22 @@
# 项目状态
## 当前阶段
Phase 1: 基础设施 + 数据库 + 公共模块 (未开始)
## 已完成
- [x] 项目初始化(git repo、.planning目录)
- [x] PROJECT.md 创建
- [x] REQUIREMENTS.md 创建
- [x] ROADMAP.md 创建
- [x] config.json 创建
## 进行中
- [ ] Phase 1 开发
## 待开始
- Phase 2: 认证服务 + 会话存档服务
- Phase 3: 对话服务 + 意图服务
- Phase 4: 推荐服务 + LLM生成服务
- Phase 5: 前端侧边栏H5
- Phase 6: 管理后台 + 知识库
- Phase 7: 部署配置与集成测试

8
.planning/config.json Normal file
View File

@ -0,0 +1,8 @@
{
"mode": "YOLO",
"depth": "Comprehensive",
"execution": "Parallel",
"projectName": "第九联盟AI坐席辅助系统",
"version": "1.0.0",
"createdAt": "2026-05-12"
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"kimi.yoloMode": true
}

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# 九艺AI坐席辅助系统
基于 GSD 框架开发的项目。

View File

@ -0,0 +1,15 @@
package com.artedu.analytics;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 数据分析服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.analytics", "com.artedu.common"})
public class AnalyticsServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AnalyticsServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8088
spring:
application:
name: analytics-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>archive-service</artifactId>
<packaging>jar</packaging>
<name>会话存档服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>net.java.dev.jna</groupId><artifactId>jna</artifactId></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.archive;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 会话存档服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.archive", "com.artedu.common"})
public class ArchiveServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ArchiveServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8082
spring:
application:
name: archive-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>auth-service</artifactId>
<packaging>jar</packaging>
<name>认证授权服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><scope>runtime</scope></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 认证授权服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.auth", "com.artedu.common"})
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}

View File

@ -0,0 +1,39 @@
server:
port: 8081
spring:
application:
name: auth-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
wecom:
corp-id: ${WECOM_CORP_ID:}
agent-id: ${WECOM_AGENT_ID:}
secret: ${WECOM_SECRET:}
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here-at-least-32-characters}
expiration: 86400
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

54
backend/common/pom.xml Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>
<packaging>jar</packaging>
<name>公共模块</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,32 @@
package com.artedu.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 跨域配置
* 允许企微H5侧边栏跨域访问API
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("Authorization");
config.addExposedHeader("X-Request-Id");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,22 @@
package com.artedu.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置
* 启用分页插件(MySQL 5.7方言)
*/
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

View File

@ -0,0 +1,79 @@
package com.artedu.common.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置
* 定义消息队列、交换机和绑定关系
*/
@Configuration
public class RabbitConfig {
public static final String ARCHIVE_EXCHANGE = "archive.exchange";
public static final String CONVERSATION_EXCHANGE = "conversation.exchange";
public static final String QUEUE_NEW_MESSAGE = "archive.new_message.queue";
public static final String QUEUE_INTENT_ANALYZE = "conversation.intent_analyze.queue";
public static final String QUEUE_RECOMMEND = "intent.recommend.queue";
public static final String ROUTING_KEY_NEW_MESSAGE = "archive.message.new";
public static final String ROUTING_KEY_INTENT_ANALYZE = "conversation.intent.analyze";
public static final String ROUTING_KEY_RECOMMEND = "intent.recommend";
@Bean
public DirectExchange archiveExchange() {
return new DirectExchange(ARCHIVE_EXCHANGE, true, false);
}
@Bean
public TopicExchange conversationExchange() {
return new TopicExchange(CONVERSATION_EXCHANGE, true, false);
}
@Bean
public Queue newMessageQueue() {
return QueueBuilder.durable(QUEUE_NEW_MESSAGE)
.withArgument("x-dead-letter-exchange", "")
.withArgument("x-dead-letter-routing-key", QUEUE_NEW_MESSAGE + ".dlq")
.build();
}
@Bean
public Queue intentAnalyzeQueue() {
return QueueBuilder.durable(QUEUE_INTENT_ANALYZE)
.withArgument("x-dead-letter-exchange", "")
.withArgument("x-dead-letter-routing-key", QUEUE_INTENT_ANALYZE + ".dlq")
.build();
}
@Bean
public Queue recommendQueue() {
return QueueBuilder.durable(QUEUE_RECOMMEND)
.withArgument("x-dead-letter-exchange", "")
.withArgument("x-dead-letter-routing-key", QUEUE_RECOMMEND + ".dlq")
.build();
}
@Bean
public Binding newMessageBinding() {
return BindingBuilder.bind(newMessageQueue())
.to(archiveExchange())
.with(ROUTING_KEY_NEW_MESSAGE);
}
@Bean
public Binding intentAnalyzeBinding() {
return BindingBuilder.bind(intentAnalyzeQueue())
.to(conversationExchange())
.with(ROUTING_KEY_INTENT_ANALYZE);
}
@Bean
public Binding recommendBinding() {
return BindingBuilder.bind(recommendQueue())
.to(conversationExchange())
.with(ROUTING_KEY_RECOMMEND);
}
}

View File

@ -0,0 +1,42 @@
package com.artedu.common.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* 配置Key和Value的序列化方式
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(mapper);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@ -0,0 +1,26 @@
package com.artedu.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 通用实体基类
* 所有实体类继承此类,自动填充创建时间和更新时间
*/
@Data
public class BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,30 @@
package com.artedu.common.exception;
import lombok.Getter;
/**
* 业务异常
* 用于抛出可预期的业务逻辑错误
*/
@Getter
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int code;
public BusinessException(String message) {
super(message);
this.code = -1;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = -1;
}
}

View File

@ -0,0 +1,38 @@
package com.artedu.common.exception;
import com.artedu.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
* 统一处理控制器层抛出的异常,转换为标准响应格式
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.findFirst()
.orElse("参数校验失败");
log.warn("参数校验失败: {}", message);
return Result.fail(400, message);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return Result.fail(500, "系统繁忙,请稍后重试");
}
}

View File

@ -0,0 +1,28 @@
package com.artedu.common.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus字段自动填充处理器
* 自动填充created_at和updated_at字段
*/
@Slf4j
@Component
public class AutoFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@ -0,0 +1,61 @@
package com.artedu.common.mybatis;
import com.artedu.common.util.JsonUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MyBatis JSON类型处理器 - 用于MySQL 5.7 TEXT字段
* 自动将Java对象序列化为JSON字符串存入TEXT字段
* 读取时自动将JSON字符串反序列化为Java对象
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Object.class)
public class JsonTypeHandler extends BaseTypeHandler<Object> {
private final Class<?> type;
public JsonTypeHandler(Class<?> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JsonUtils.toJson(parameter));
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private Object parseJson(String json) {
if (json == null || json.isEmpty()) {
return null;
}
return JsonUtils.fromJson(json, type);
}
}

View File

@ -0,0 +1,32 @@
package com.artedu.common.result;
import lombok.Data;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
/**
* 分页响应结构
*/
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer page;
private Integer size;
private Long total;
private Integer totalPages;
private List<T> list;
public static <T> PageResult<T> of(Integer page, Integer size, Long total, List<T> list) {
PageResult<T> result = new PageResult<>();
result.setPage(page);
result.setSize(size);
result.setTotal(total);
result.setTotalPages((int) Math.ceil((double) total / size));
result.setList(list != null ? list : Collections.emptyList());
return result;
}
}

View File

@ -0,0 +1,82 @@
package com.artedu.common.result;
import lombok.Data;
import java.io.Serializable;
/**
* 统一API响应结构
* 所有接口返回此结构,确保前端处理一致
*
* @param <T> 响应数据类型
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态码: 0-成功 其他-失败
*/
private Integer code;
/**
* 提示消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage("success");
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> success(T data, String message) {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> fail(String message) {
Result<T> result = new Result<>();
result.setCode(-1);
result.setMessage(message);
return result;
}
public static <T> Result<T> fail(int code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public boolean isSuccess() {
return this.code != null && this.code == 0;
}
}

View File

@ -0,0 +1,82 @@
package com.artedu.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* JSON工具类 - 用于MySQL 5.7 TEXT字段的序列化/反序列化
* 替代MySQL 8.0的JSON类型
*/
@Slf4j
public class JsonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
public static String toJson(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
log.error("JSON序列化失败: {}", e.getMessage(), e);
return null;
}
}
public static <T> T fromJson(String json, Class<T> clazz) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("JSON反序列化失败: {}", e.getMessage(), e);
return null;
}
}
public static <T> List<T> fromJsonList(String json, Class<T> clazz) {
if (json == null || json.isEmpty()) {
return Collections.emptyList();
}
try {
return OBJECT_MAPPER.readValue(json, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (JsonProcessingException e) {
log.error("JSON反序列化为List失败: {}", e.getMessage(), e);
return Collections.emptyList();
}
}
public static Map<String, Object> fromJsonMap(String json) {
if (json == null || json.isEmpty()) {
return Collections.emptyMap();
}
try {
return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (JsonProcessingException e) {
log.error("JSON反序列化为Map失败: {}", e.getMessage(), e);
return Collections.emptyMap();
}
}
public static String safeJson(Object obj) {
String json = toJson(obj);
return json != null ? json : "{}";
}
public static String safeParse(String json) {
return json != null && !json.isEmpty() ? json : "{}";
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>conversation-service</artifactId>
<packaging>jar</packaging>
<name>对话管理服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.conversation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 对话管理服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.conversation", "com.artedu.common"})
public class ConversationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConversationServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8083
spring:
application:
name: conversation-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

28
backend/gateway/pom.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>gateway</artifactId>
<packaging>jar</packaging>
<name>API网关</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* API网关启动类
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@ -0,0 +1,48 @@
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: auth-service
uri: http://localhost:8081
predicates:
- Path=/api/v1/auth/**
- id: archive-service
uri: http://localhost:8082
predicates:
- Path=/api/v1/archive/**
- id: conversation-service
uri: http://localhost:8083
predicates:
- Path=/api/v1/conversations/**
- id: intent-service
uri: http://localhost:8084
predicates:
- Path=/api/v1/intent/**
- id: recommend-service
uri: http://localhost:8085
predicates:
- Path=/api/v1/recommend/**
- id: generation-service
uri: http://localhost:8086
predicates:
- Path=/api/v1/generation/**
- id: kb-admin-service
uri: http://localhost:8087
predicates:
- Path=/api/v1/admin/**
- id: analytics-service
uri: http://localhost:8088
predicates:
- Path=/api/v1/analytics/**
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>generation-service</artifactId>
<packaging>jar</packaging>
<name>LLM生成服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.generation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* LLM生成服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.generation", "com.artedu.common"})
public class GenerationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GenerationServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8086
spring:
application:
name: generation-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>intent-service</artifactId>
<packaging>jar</packaging>
<name>意图识别服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.intent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 意图识别服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.intent", "com.artedu.common"})
public class IntentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(IntentServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8084
spring:
application:
name: intent-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>kb-admin-service</artifactId>
<packaging>jar</packaging>
<name>话术库管理服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-we

View File

@ -0,0 +1,15 @@
package com.artedu.kbadmin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 话术库管理服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.kbadmin", "com.artedu.common"})
public class KbAdminServiceApplication {
public static void main(String[] args) {
SpringApplication.run(KbAdminServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8087
spring:
application:
name: kb-admin-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

97
backend/pom.xml Normal file
View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>第九联盟AI坐席辅助系统</name>
<description>基于企微会话存档+LLM的实时话术推荐系统</description>
<modules>
<module>common</module>
<module>gateway</module>
<module>auth-service</module>
<module>archive-service</module>
<module>conversation-service</module>
<module>intent-service</module>
<module>recommendation-service</module>
<module>generation-service</module>
<module>kb-admin-service</module>
<module>analytics-service</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<jjwt.version>0.11.5</jjwt.version>
<jna.version>5.13.0</jna.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.artedu</groupId>
<artifactId>ai-assistant-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>recommendation-service</artifactId>
<packaging>jar</packaging>
<name>话术推荐服务</name>
<dependencies>
<dependency><groupId>com.artedu</groupId><artifactId>common</artifactId><version>${project.version}</version></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.artedu.recommend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 话术推荐服务启动类
*/
@SpringBootApplication(scanBasePackages = {"com.artedu.recommend", "com.artedu.common"})
public class RecommendationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(RecommendationServiceApplication.class, args);
}
}

View File

@ -0,0 +1,35 @@
server:
port: 8085
spring:
application:
name: recommendation-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

60
docker-compose.yml Normal file
View File

@ -0,0 +1,60 @@
version: '3.8'
services:
mysql:
image: mysql:5.7
container_name: ai-assistant-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ai_assistant
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:6-alpine
container_name: ai-assistant-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
rabbitmq:
image: rabbitmq:3-management-alpine
container_name: ai-assistant-rabbitmq
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: admin
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysql_data:
redis_data:
rabbitmq_data:

459
init.sql Normal file
View File

@ -0,0 +1,459 @@
-- ============================================================
-- 第九联盟AI坐席辅助系统 - 数据库初始化脚本
-- MySQL 5.7适配版
-- ============================================================
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `ai_assistant` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `ai_assistant`;
-- ============================================================
-- 企微会话存档消息表 (archive_messages)
-- ============================================================
CREATE TABLE IF NOT EXISTS `archive_messages` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`msgid` VARCHAR(255) NOT NULL COMMENT '企微消息唯一ID',
`seq` BIGINT(20) NOT NULL COMMENT '存档序列号,用于增量拉取',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`action` VARCHAR(32) DEFAULT 'send' COMMENT 'send/recall/agree/disagree',
`from_user` VARCHAR(128) NOT NULL COMMENT '发送者userid',
`from_role` VARCHAR(32) NOT NULL COMMENT '发送者角色: INTERNAL-企业内部成员 EXTERNAL-外部联系人 SYSTEM-系统',
`to_user` VARCHAR(128) COMMENT '接收者userid(单聊)',
`tolist` TEXT COMMENT '接收者列表(群聊),JSON数组字符串',
`roomid` VARCHAR(255) COMMENT '群聊ID(群聊时有值)',
`msgtype` VARCHAR(64) NOT NULL COMMENT 'text/image/voice/video/file/link/location/weapp/chatrecord/voip',
`msgtime` BIGINT(20) NOT NULL COMMENT '消息发送时间,UTC毫秒时间戳',
`content` LONGTEXT COMMENT '文本消息内容(长文本使用LONGTEXT)',
`media_data` TEXT COMMENT '媒体消息元数据(sdkfileid/文件大小/时长等),JSON字符串',
`session_id` VARCHAR(128) COMMENT '关联的会话ID(业务生成)',
`decrypt_status` TINYINT DEFAULT 1 COMMENT '解密状态: 1成功 2失败',
`decrypt_error` VARCHAR(512) COMMENT '解密失败原因',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_msgid_corp` (`msgid`, `corp_id`) COMMENT '消息ID+企业ID唯一',
INDEX `idx_seq` (`seq`) COMMENT '序列号索引,用于增量拉取',
INDEX `idx_session_id` (`session_id`) COMMENT '会话ID索引',
INDEX `idx_from_user_msgtime` (`from_user`, `msgtime`) COMMENT '发送者+时间索引',
INDEX `idx_to_user_msgtime` (`to_user`, `msgtime`) COMMENT '接收者+时间索引',
INDEX `idx_roomid_msgtime` (`roomid`, `msgtime`) COMMENT '群聊ID+时间索引',
INDEX `idx_msgtime` (`msgtime`) COMMENT '消息时间索引',
INDEX `idx_corp_msgtype` (`corp_id`, `msgtype`, `msgtime`) COMMENT '企业+消息类型+时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企微会话存档消息表';
-- ============================================================
-- 会话表 (conversations)
-- ============================================================
CREATE TABLE IF NOT EXISTS `conversations` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`session_id` VARCHAR(128) NOT NULL COMMENT '会话唯一ID(业务生成)',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`customer_id` VARCHAR(128) NOT NULL COMMENT '学员(外部联系人)userid',
`customer_name` VARCHAR(64) COMMENT '学员昵称',
`staff_id` VARCHAR(128) NOT NULL COMMENT '坐席(企业成员)userid',
`staff_name` VARCHAR(64) COMMENT '坐席姓名',
`room_id` VARCHAR(128) COMMENT '群聊ID(群聊时有值)',
`status` VARCHAR(32) DEFAULT 'ACTIVE' COMMENT '会话状态: ACTIVE-活跃 IDLE-空闲 CLOSED-关闭 ARCHIVED-归档',
`current_stage` VARCHAR(32) DEFAULT '开场白' COMMENT '当前对话阶段',
`stage_confidence` DECIMAL(4,3) DEFAULT 0.000 COMMENT '阶段判断置信度',
`round_count` INT(11) DEFAULT 0 COMMENT '对话轮次',
`intent_count` INT(11) DEFAULT 0 COMMENT '意图识别次数',
`last_msg_id` VARCHAR(255) COMMENT '最后一条消息ID',
`last_msg_time` TIMESTAMP NULL COMMENT '最后消息时间',
`last_msg_content` VARCHAR(512) COMMENT '最后消息内容摘要',
`start_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '会话开始时间',
`end_time` TIMESTAMP NULL COMMENT '会话结束时间',
`idle_duration` INT(11) DEFAULT 0 COMMENT '空闲时长(分钟)',
`context_summary` TEXT COMMENT '对话摘要(LLM生成)',
`key_events` TEXT COMMENT '关键事件列表,JSON字符串',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_session_id` (`session_id`) COMMENT '会话ID唯一',
INDEX `idx_corp_staff` (`corp_id`, `staff_id`, `status`) COMMENT '企业+坐席+状态',
INDEX `idx_corp_customer` (`corp_id`, `customer_id`, `status`) COMMENT '企业+学员+状态',
INDEX `idx_status_time` (`status`, `last_msg_time`) COMMENT '状态+时间',
INDEX `idx_created_at` (`created_at`) COMMENT '创建时间索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表';
-- ============================================================
-- 对话轮次表 (conversation_turns)
-- ============================================================
CREATE TABLE IF NOT EXISTS `conversation_turns` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`turn_id` VARCHAR(128) NOT NULL COMMENT '轮次ID',
`session_id` VARCHAR(128) NOT NULL COMMENT '会话ID',
`turn_number` INT(11) NOT NULL COMMENT '轮次序号',
`student_msg_id` VARCHAR(255) COMMENT '学员消息ID',
`student_content` TEXT COMMENT '学员消息内容',
`seat_msg_id` VARCHAR(255) COMMENT '坐席回复消息ID',
`seat_content` TEXT COMMENT '坐席回复内容',
`turn_type` VARCHAR(32) DEFAULT 'QUERY' COMMENT '轮次类型: OPEN-开场 QUERY-咨询 OBJECTION-异议 INTEREST-意向 SILENCE-沉默 FOLLOWUP-跟进 CLOSE-收尾',
`primary_intent` VARCHAR(32) COMMENT '主意图编码',
`intent_confidence` DECIMAL(4,3) COMMENT '意图置信度',
`student_emotion` VARCHAR(32) DEFAULT 'NEUTRAL' COMMENT '学员情绪: POSITIVE-积极 NEUTRAL-中性 NEGATIVE-消极 ANXIOUS-焦虑 EXCITED-兴奋',
`key_topics` TEXT COMMENT '涉及主题列表,JSON字符串',
`slot_changes` TEXT COMMENT '槽位变化,JSON字符串',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY `uk_turn_id` (`turn_id`) COMMENT '轮次ID唯一',
INDEX `idx_session_turn` (`session_id`, `turn_number`) COMMENT '会话+轮次号',
INDEX `idx_session_time` (`session_id`, `created_at`) COMMENT '会话+时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话轮次表';
-- ============================================================
-- 意图分类表 (intent_categories)
-- ============================================================
CREATE TABLE IF NOT EXISTS `intent_categories` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`code` VARCHAR(32) NOT NULL COMMENT '意图编码,如INT-COURSE-01',
`name` VARCHAR(64) NOT NULL COMMENT '意图名称',
`domain` VARCHAR(32) NOT NULL COMMENT '所属领域',
`description` VARCHAR(256) COMMENT '意图说明',
`priority` VARCHAR(8) DEFAULT 'P2' COMMENT '优先级: P1-高 P2-中 P3-低',
`trigger_keywords` TEXT COMMENT '触发关键词列表,JSON数组字符串',
`sample_expressions` TEXT COMMENT '典型表达示例,JSON数组字符串',
`status` TINYINT DEFAULT 1 COMMENT '状态: 1启用 0停用',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_code` (`code`) COMMENT '意图编码唯一',
INDEX `idx_domain_priority` (`domain`, `priority`) COMMENT '领域+优先级'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='意图分类表';
-- ============================================================
-- 意图识别记录表 (intent_recognitions)
-- ============================================================
CREATE TABLE IF NOT EXISTS `intent_recognitions` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`session_id` VARCHAR(128) NOT NULL COMMENT '会话ID',
`turn_id` VARCHAR(128) COMMENT '轮次ID',
`msg_id` VARCHAR(255) NOT NULL COMMENT '消息ID',
`customer_id` VARCHAR(128) NOT NULL COMMENT '学员ID',
`staff_id` VARCHAR(128) NOT NULL COMMENT '坐席ID',
`message_content` TEXT COMMENT '消息内容摘要',
`primary_intent_code` VARCHAR(32) NOT NULL COMMENT '主意图编码',
`primary_intent_confidence` DECIMAL(4,3) COMMENT '主意图置信度',
`secondary_intents` TEXT COMMENT '辅意图列表,JSON字符串',
`sentiment` VARCHAR(32) DEFAULT 'NEUTRAL' COMMENT '情感: POSITIVE-积极 NEUTRAL-中性 NEGATIVE-消极',
`sentiment_confidence` DECIMAL(4,3) COMMENT '情感置信度',
`recognition_method` VARCHAR(32) DEFAULT 'HYBRID' COMMENT '识别方法: RULE-规则 LLM-大模型 HYBRID-混合',
`llm_model` VARCHAR(64) COMMENT '使用的LLM模型',
`prompt_tokens` INT(11) COMMENT 'Prompt Token数',
`completion_tokens` INT(11) COMMENT 'Completion Token数',
`elapsed_ms` INT(11) COMMENT '识别耗时(毫秒)',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX `idx_session_msg` (`session_id`, `msg_id`) COMMENT '会话+消息',
INDEX `idx_customer_time` (`customer_id`, `created_at`) COMMENT '学员+时间',
INDEX `idx_intent_code` (`primary_intent_code`, `created_at`) COMMENT '意图编码+时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='意图识别记录表';
-- ============================================================
-- 学员画像表 (customer_profiles)
-- ============================================================
CREATE TABLE IF NOT EXISTS `customer_profiles` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`customer_id` VARCHAR(128) NOT NULL COMMENT '学员userid',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`student_type` VARCHAR(32) COMMENT '学员类型: 在校大学生/转行人员/在职提升/高中毕业生/家长代询',
`student_type_confidence` DECIMAL(4,3) COMMENT '学员类型置信度',
`skill_level` VARCHAR(32) COMMENT '基础水平: 零基础/有美术基础/相关专业/有从业经验',
`skill_level_confidence` DECIMAL(4,3) COMMENT '基础水平置信度',
`intent_level` VARCHAR(16) DEFAULT '低' COMMENT '意向度等级: 高/中/低/无意向',
`intent_score` DECIMAL(6,2) DEFAULT 0.00 COMMENT '意向度分数',
`concern_focus` VARCHAR(32) COMMENT '关注重点: 价格敏感型/就业导向型/师资看重型/时间灵活型/品牌信任型',
`concern_focus_confidence` DECIMAL(4,3) COMMENT '关注重点置信度',
`decision_stage` VARCHAR(32) DEFAULT '信息了解' COMMENT '决策阶段: 信息了解/方案比较/购买决策/报名成交',
`decision_stage_confidence` DECIMAL(4,3) COMMENT '决策阶段置信度',
`interested_courses` TEXT COMMENT '意向课程列表,JSON字符串',
`budget_hint` VARCHAR(64) COMMENT '预算暗示',
`preferred_city` VARCHAR(32) COMMENT '意向城市',
`age` INT(11) COMMENT '年龄',
`education` VARCHAR(32) COMMENT '学历',
`current_occupation` VARCHAR(64) COMMENT '当前职业',
`slot_data` TEXT COMMENT '完整槽位数据,JSON字符串',
`conversation_count` INT(11) DEFAULT 0 COMMENT '对话次数',
`last_conversation_time` TIMESTAMP NULL COMMENT '上次对话时间',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_customer_corp` (`customer_id`, `corp_id`) COMMENT '学员+企业唯一',
INDEX `idx_intent_score` (`intent_score`) COMMENT '意向度分数索引',
INDEX `idx_decision_stage` (`decision_stage`) COMMENT '决策阶段索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员画像表';
-- ============================================================
-- 话术库表 (utterances)
-- ============================================================
CREATE TABLE IF NOT EXISTS `utterances` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`utterance_id` VARCHAR(64) NOT NULL COMMENT '话术唯一ID(业务编码)',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`title` VARCHAR(128) NOT NULL COMMENT '话术标题',
`content` TEXT NOT NULL COMMENT '话术内容(富文本)',
`content_text` TEXT COMMENT '纯文本内容(用于全文检索)',
`version` INT(11) DEFAULT 1 COMMENT '版本号',
`status` VARCHAR(32) DEFAULT 'DRAFT' COMMENT '状态: DRAFT-草稿 ACTIVE-启用 INACTIVE-停用 DEPRECATED-废弃',
`priority` INT(11) DEFAULT 5 COMMENT '优先级(1-10,越小越优先)',
`stage_tags` TEXT COMMENT '适用阶段标签,JSON数组字符串',
`intent_tags` TEXT COMMENT '匹配意图标签,JSON数组字符串',
`profile_tags` TEXT COMMENT '适用画像标签,JSON数组字符串',
`course_type_tags` TEXT COMMENT '适用课程标签,JSON数组字符串',
`emotion_tags` TEXT COMMENT '情感标签,JSON数组字符串',
`topic_tags` TEXT COMMENT '主题标签,JSON数组字符串',
`used_count` INT(11) DEFAULT 0 COMMENT '使用次数',
`selected_count` INT(11) DEFAULT 0 COMMENT '选中次数',
`positive_feedback` INT(11) DEFAULT 0 COMMENT '正面反馈数',
`negative_feedback` INT(11) DEFAULT 0 COMMENT '负面反馈数',
`conversion_count` INT(11) DEFAULT 0 COMMENT '转化次数',
`success_rate` DECIMAL(5,4) DEFAULT 0.0000 COMMENT '成功率(0-1)',
`avg_response_time_ms` INT(11) COMMENT '平均响应时间(毫秒)',
`has_vector` TINYINT DEFAULT 0 COMMENT '是否已生成向量: 0否 1是',
`vector_updated_at` TIMESTAMP NULL COMMENT '向量更新时间',
`trigger_keywords` TEXT COMMENT '触发关键词,JSON字符串',
`variables` TEXT COMMENT '变量定义,JSON字符串',
`created_by` VARCHAR(64) COMMENT '创建人',
`reviewed_by` VARCHAR(64) COMMENT '审核人',
`source` VARCHAR(32) DEFAULT 'MANUAL' COMMENT '来源: MANUAL-人工录入 IMPORTED-导入 LLM_GENERATED-AI生成',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_utterance_id` (`utterance_id`) COMMENT '话术ID唯一',
INDEX `idx_corp_status` (`corp_id`, `status`) COMMENT '企业+状态',
INDEX `idx_success_rate` (`success_rate`) COMMENT '成功率索引',
INDEX `idx_used_count` (`used_count`) COMMENT '使用次数索引',
FULLTEXT KEY `ft_content` (`content_text`) COMMENT '全文检索索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话术库表';
-- ============================================================
-- 话术分类表 (utterance_categories)
-- ============================================================
CREATE TABLE IF NOT EXISTS `utterance_categories` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`code` VARCHAR(32) NOT NULL COMMENT '分类编码',
`name` VARCHAR(64) NOT NULL COMMENT '分类名称',
`parent_code` VARCHAR(32) DEFAULT 'ROOT' COMMENT '父级编码',
`sort_order` INT(11) DEFAULT 0 COMMENT '排序号',
`color` VARCHAR(16) DEFAULT '#1890ff' COMMENT '显示颜色',
`description` VARCHAR(256) COMMENT '分类说明',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY `uk_code` (`code`) COMMENT '分类编码唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话术分类表';
-- ============================================================
-- 话术效果表 (utterance_performance)
-- ============================================================
CREATE TABLE IF NOT EXISTS `utterance_performance` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`utterance_id` VARCHAR(64) NOT NULL COMMENT '话术ID',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`hour_window` VARCHAR(13) NOT NULL COMMENT '时间窗口,格式: 2025-01-15-14',
`show_count` INT(11) DEFAULT 0 COMMENT '展示次数',
`click_count` INT(11) DEFAULT 0 COMMENT '点击次数',
`send_count` INT(11) DEFAULT 0 COMMENT '发送次数',
`positive_count` INT(11) DEFAULT 0 COMMENT '正面反馈数',
`negative_count` INT(11) DEFAULT 0 COMMENT '负面反馈数',
`ignore_count` INT(11) DEFAULT 0 COMMENT '忽略次数',
`conversion_count` INT(11) DEFAULT 0 COMMENT '转化次数',
`ctr` DECIMAL(5,4) COMMENT '点击率',
`conversion_rate` DECIMAL(5,4) COMMENT '转化率',
`score` DECIMAL(6,4) COMMENT '综合评分(EWA算法)',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY `uk_utterance_hour` (`utterance_id`, `hour_window`) COMMENT '话术+时间窗口唯一',
INDEX `idx_corp_hour` (`corp_id`, `hour_window`) COMMENT '企业+时间窗口'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话术效果表';
-- ============================================================
-- 推荐记录表 (recommendations)
-- ============================================================
CREATE TABLE IF NOT EXISTS `recommendations` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`recommendation_id` VARCHAR(128) NOT NULL COMMENT '推荐记录ID',
`session_id` VARCHAR(128) NOT NULL COMMENT '会话ID',
`turn_id` VARCHAR(128) COMMENT '轮次ID',
`customer_id` VARCHAR(128) NOT NULL COMMENT '学员ID',
`staff_id` VARCHAR(128) NOT NULL COMMENT '坐席ID',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`primary_intent` VARCHAR(32) COMMENT '主意图编码',
`secondary_intents` TEXT COMMENT '辅意图列表,JSON字符串',
`student_type` VARCHAR(32) COMMENT '学员类型',
`current_stage` VARCHAR(32) COMMENT '当前阶段',
`student_message` VARCHAR(512) COMMENT '学员消息',
`recommended_count` INT(11) DEFAULT 0 COMMENT '推荐条数',
`recall_sources` TEXT COMMENT '召回来源统计,JSON字符串',
`generation_mode` VARCHAR(32) DEFAULT 'KB_ONLY' COMMENT '生成模式: KB_ONLY-话术库 KB_LLM_MIX-混合 LLM_ONLY-纯AI',
`elapsed_ms` INT(11) COMMENT '推荐耗时(毫秒)',
`staff_action` VARCHAR(32) DEFAULT 'NOT_SEEN' COMMENT '坐席操作: SENT-发送 EDITED-编辑后发送 IGNORED-忽略 NOT_SEEN-未查看',
`feedback` VARCHAR(32) DEFAULT 'NONE' COMMENT '反馈: POSITIVE-正面 NEGATIVE-负面 NONE-无',
`feedback_reason` VARCHAR(128) COMMENT '反馈原因',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`action_time` TIMESTAMP NULL COMMENT '操作时间',
UNIQUE KEY `uk_rec_id` (`recommendation_id`) COMMENT '推荐ID唯一',
INDEX `idx_session` (`session_id`, `created_at`) COMMENT '会话+时间',
INDEX `idx_staff` (`staff_id`, `created_at`) COMMENT '坐席+时间',
INDEX `idx_intent` (`primary_intent`, `created_at`) COMMENT '意图+时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐记录表';
-- ============================================================
-- 知识库文档表 (knowledge_documents)
-- ============================================================
CREATE TABLE IF NOT EXISTS `knowledge_documents` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`doc_id` VARCHAR(64) NOT NULL COMMENT '文档唯一ID',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`title` VARCHAR(256) NOT NULL COMMENT '文档标题',
`content` LONGTEXT COMMENT '文档内容',
`content_text` LONGTEXT COMMENT '纯文本内容(用于全文检索)',
`doc_type` VARCHAR(32) DEFAULT 'KNOWLEDGE' COMMENT '文档类型: KNOWLEDGE-知识 UTTERANCE-话术 FAQ-常见问题',
`category` VARCHAR(64) COMMENT '分类',
`tags` TEXT COMMENT '标签,JSON数组字符串',
`status` VARCHAR(32) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-启用 INACTIVE-停用',
`has_vector` TINYINT DEFAULT 0 COMMENT '是否已生成向量',
`vector_updated_at` TIMESTAMP NULL COMMENT '向量更新时间',
`created_by` VARCHAR(64) COMMENT '创建人',
`updated_by` VARCHAR(64) COMMENT '更新人',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_doc_id` (`doc_id`) COMMENT '文档ID唯一',
INDEX `idx_corp_type` (`corp_id`, `doc_type`, `status`) COMMENT '企业+类型+状态',
FULLTEXT KEY `ft_content` (`content_text`) COMMENT '全文检索'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文档表';
-- ============================================================
-- 员工表 (staffs)
-- ============================================================
CREATE TABLE IF NOT EXISTS `staffs` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`staff_id` VARCHAR(128) NOT NULL COMMENT '企微用户ID',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`name` VARCHAR(64) COMMENT '姓名',
`avatar` VARCHAR(512) COMMENT '头像URL',
`department` VARCHAR(128) COMMENT '部门',
`role` VARCHAR(32) DEFAULT 'ADVISOR' COMMENT '角色: ADMIN-管理员 SUPERVISOR-主管 ADVISOR-顾问',
`status` VARCHAR(32) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-在职 INACTIVE-离职',
`push_settings` TEXT COMMENT '推送设置,JSON字符串',
`last_login_time` TIMESTAMP NULL COMMENT '最后登录时间',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_staff_corp` (`staff_id`, `corp_id`) COMMENT '员工+企业唯一',
INDEX `idx_corp_role` (`corp_id`, `role`) COMMENT '企业+角色'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';
-- ============================================================
-- 客户表 (customers)
-- ============================================================
CREATE TABLE IF NOT EXISTS `customers` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`customer_id` VARCHAR(128) NOT NULL COMMENT '外部联系人userid',
`corp_id` VARCHAR(64) NOT NULL COMMENT '企业ID',
`name` VARCHAR(64) COMMENT '姓名/昵称',
`avatar` VARCHAR(512) COMMENT '头像URL',
`type` VARCHAR(32) DEFAULT 'UNKNOWN' COMMENT '客户类型: UNKNOWN-未知 INDIVIDUAL-个人 PARENT-家长 ENTERPRISE-企业',
`gender` VARCHAR(8) COMMENT '性别: MALE-男 FEMALE-女',
`phone` VARCHAR(32) COMMENT '手机号',
`unionid` VARCHAR(128) COMMENT '微信unionid',
`add_time` TIMESTAMP NULL COMMENT '添加时间',
`status` VARCHAR(32) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-正常 BLOCKED-拉黑 DELETED-删除',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_customer_corp` (`customer_id`, `corp_id`) COMMENT '客户+企业唯一',
INDEX `idx_corp_status` (`corp_id`, `status`) COMMENT '企业+状态'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户表';
-- ============================================================
-- 管理员表 (admins)
-- ============================================================
CREATE TABLE IF NOT EXISTS `admins` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`username` VARCHAR(64) NOT NULL COMMENT '用户名',
`password` VARCHAR(256) NOT NULL COMMENT '密码(BCrypt加密)',
`name` VARCHAR(64) COMMENT '姓名',
`email` VARCHAR(128) COMMENT '邮箱',
`phone` VARCHAR(32) COMMENT '手机号',
`role` VARCHAR(32) DEFAULT 'ADMIN' COMMENT '角色: SUPER_ADMIN-超级管理员 ADMIN-管理员 OPERATOR-运营',
`status` VARCHAR(32) DEFAULT 'ACTIVE' COMMENT '状态: ACTIVE-启用 DISABLED-停用',
`last_login_time` TIMESTAMP NULL COMMENT '最后登录时间',
`last_login_ip` VARCHAR(64) COMMENT '最后登录IP',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY `uk_username` (`username`) COMMENT '用户名唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
-- ============================================================
-- 操作日志表 (operation_logs)
-- ============================================================
CREATE TABLE IF NOT EXISTS `operation_logs` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
`operator_id` VARCHAR(64) NOT NULL COMMENT '操作人ID',
`operator_name` VARCHAR(64) COMMENT '操作人姓名',
`operator_type` VARCHAR(32) DEFAULT 'STAFF' COMMENT '操作人类型: STAFF-员工 ADMIN-管理员',
`module` VARCHAR(64) COMMENT '操作模块',
`action` VARCHAR(64) COMMENT '操作动作',
`description` TEXT COMMENT '操作描述',
`request_params` TEXT COMMENT '请求参数',
`response_result` TEXT COMMENT '响应结果',
`ip` VARCHAR(64) COMMENT '操作IP',
`user_agent` VARCHAR(512) COMMENT 'User-Agent',
`status` TINYINT DEFAULT 1 COMMENT '状态: 1成功 0失败',
`elapsed_ms` INT(11) COMMENT '耗时(毫秒)',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX `idx_operator` (`operator_id`, `created_at`) COMMENT '操作人+时间',
INDEX `idx_module` (`module`, `action`, `created_at`) COMMENT '模块+动作+时间',
INDEX `idx_created_at` (`created_at`) COMMENT '创建时间索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
-- ============================================================
-- 初始化数据
-- ============================================================
-- 40个意图分类
INSERT INTO `intent_categories` (`code`, `name`, `domain`, `description`, `priority`, `trigger_keywords`, `sample_expressions`, `status`) VALUES
('INT-COURSE-01', '课程内容咨询', '课程相关', '询问具体学什么内容', 'P1', '["学什么","课程","内容","教什么","学什么软件"]', '["你们的课程具体学什么内容?","课程都教哪些软件?","能说一下课程体系吗?"]', 1),
('INT-COURSE-02', '课程选择建议', '课程相关', '不知道该选哪个方向', 'P1', '["选哪个","推荐","适合我","怎么选","方向"]', '["我应该学3D建模还是原画?","哪个方向好就业?","您能帮我推荐一下吗?"]', 1),
('INT-COURSE-03', '课程大纲索要', '课程相关', '想看详细课程安排', 'P1', '["大纲","课程表","安排","详细","阶段"]', '["能发一下课程大纲吗?","具体是怎么安排的?","每个阶段学什么?"]', 1),
('INT-COURSE-04', '试听预约咨询', '课程相关', '想试听或体验课程', 'P1', '["试听","体验","免费","公开课","试学"]', '["有免费试听吗?","怎么预约试听课?","可以体验一下吗?"]', 1),
('INT-COURSE-05', '课程更新迭代', '课程相关', '担心课程内容过时', 'P2', '["更新","过时","新版","跟上","行业"]', '["课程内容会更新吗?","跟得上行业吗?","用的是最新技术吗?"]', 1),
('INT-COURSE-06', '线上vs线下选择', '课程相关', '不知道选哪种学习方式', 'P1', '["线上","线下","面授","直播","网课","区别"]', '["线上和线下有什么区别?","线上课程效果好吗?","可以线上学习吗?"]', 1),
('INT-COURSE-07', '课程难度询问', '课程相关', '担心零基础能否学会', 'P1', '["零基础","能学会吗","难吗","没有基础","小白","能不能学会"]', '["零基础能学会吗?","我没有美术基础可以学吗?","课程难度大不大?"]', 1),
('INT-COURSE-08', '学习周期询问', '课程相关', '想知道学多久', 'P2', '["多久","多长时间","几个月","学多久","周期"]', '["课程要学多久?","多长时间能学会?","几个月可以学完?"]', 1),
('INT-PRICE-01', '学费价格询问', '价格相关', '直接问学费多少', 'P1', '["多少钱","学费","价格","费用","收费","怎么收费"]', '["学费多少钱?","课程怎么收费?","一共多少钱?"]', 1),
('INT-PRICE-02', '优惠活动询问', '价格相关', '想知道有没有优惠', 'P1', '["优惠","活动","打折","减免","便宜","特价"]', '["现在有优惠活动吗?","能便宜点吗?","有没有折扣?"]', 1),
('INT-PRICE-03', '分期付款咨询', '价格相关', '关心付款方式', 'P2', '["分期","贷款","月付","首付","月供","花呗"]', '["可以分期付款吗?","有贷款方案吗?","月供多少?"]', 1),
('INT-PRICE-04', '退费政策询问', '价格相关', '关心退费保障', 'P2', '["退费","退款","退钱","学不好退","保障"]', '["学不好可以退费吗?","退费流程是怎样的?","有保障吗?"]', 1),
('INT-PRICE-05', '性价比比较', '价格相关', '和别家比较', 'P2', '["对比","比较","别家","其他机构","性价比","划算"]', '["你们的课程和XX比怎么样?","为什么价格比别人高?","性价比高吗?"]', 1),
('INT-JOB-01', '就业方向咨询', '就业相关', '学完能做什么', 'P1', '["就业","工作","岗位","方向","做什么"]', '["学完能去哪些公司工作?","就业方向有哪些?","能做什么岗位?"]', 1),
('INT-JOB-02', '薪资水平询问', '就业相关', '关心工资待遇', 'P1', '["工资","薪资","月薪","收入","待遇","多少钱一个月"]', '["毕业后工资能拿多少?","行业薪资水平怎么样?","能挣多少?"]', 1),
('INT-JOB-03', '就业率询问', '就业相关', '关心能否找到工作', 'P1', '["就业率","找到工作","保证","推荐","找工作"]', '["就业率高吗?","能保证就业吗?","好找工作吗?"]', 1),
('INT-JOB-04', '就业服务了解', '就业相关', '了解就业推荐服务', 'P2', '["就业服务","推荐","怎么找工作","帮找工作"]', '["你们怎么帮学员找工作?","有就业推荐吗?","就业服务包含什么?"]', 1),
('INT-JOB-05', '合作企业询问', '就业相关', '想知道合作企业', 'P2', '["合作企业","大厂","腾讯","米哈游","网易","公司"]', '["合作企业有哪些?","有米哈游/腾讯这样的公司吗?","就业单位有哪些?"]', 1),
('INT-JOB-06', '作品集指导', '就业相关', '关心作品质量', 'P2', '["作品集","作品","项目","Demo","案例"]', '["学完能做出作品集吗?","作品质量怎么样?","能参与项目吗?"]', 1),
('INT-JOB-07', '实习机会咨询', '就业相关', '想参与真实项目', 'P3', '["实习","真实项目","实训","参与项目","实战"]', '["有实习机会吗?","可以参与真实项目吗?","有企业实训吗?"]', 1),
('INT-TEACH-01', '师资背景询问', '师资相关', '想知道老师是谁', 'P2', '["老师","师资","讲师","谁教的","教学经验"]', '["老师是什么背景?","老师有项目经验吗?","谁来讲课?"]', 1),
('INT-TEACH-02', '教学模式了解', '师资相关', '想知道怎么上课', 'P2', '["怎么上课","教学方式","班型","一对一","多少人"]', '["是怎么上课的?","一个班多少人?","教学模式是怎样的?"]', 1),
('INT-TEACH-03', '课后答疑服务', '师资相关', '关心课后辅导', 'P2', '["答疑","辅导","课后","问问题","指导"]', '["课后有问题可以问老师吗?","答疑及时吗?","有课后辅导吗?"]', 1),
('INT-TEACH-04', '师生比例询问', '师资相关', '关心教学质量', 'P3', '["一个老师","带多少学生","比例","师生"]', '["一个老师带多少学生?","能照顾到每个人吗?","班级多大?"]', 1),
('INT-TEACH-05', '学习效果保障', '师资相关', '担心学不会怎么办', 'P2', '["学不会","怎么办","重修","保障","跟不上"]', '["学不会怎么办?","怎么保证学习效果?","跟不上怎么办?"]', 1),
('INT-BASIC-01', '学历要求咨询', '基础条件', '学历是否够', 'P2', '["学历","要求","高中","大专","本科"]', '["需要什么学历?","高中生可以学吗?","学历要求高吗?"]', 1),
('INT-BASIC-02', '年龄限制询问', '基础条件', '年龄是否合适', 'P2', '["年龄","多大","几岁","限制","30岁"]', '["有没有年龄限制?","30岁了还能学吗?","年龄大能学会吗?"]', 1),
('INT-BASIC-03', '零基础入学', '基础条件', '零基础可行性', 'P1', '["零基础","没有基础","小白","完全不会","从来没"]', '["我没有美术基础可以学吗?","零基础能学会吗?","完全没接触过可以学吗?"]', 1),
('INT-BASIC-04', '证书颁发询问', '基础条件', '学完发什么证书', 'P3', '["证书","毕业证","结业证","证明","认证"]', '["学完发证书吗?","证书有用吗?","有毕业证吗?"]', 1),
('INT-LOC-01', '校区地址询问', '校区相关', '想知道校区在哪', 'P2', '["校区","地址","在哪","位置","交通"]', '["校区在哪里?","附近有地铁吗?","具体地址是什么?"]', 1),
('INT-LOC-02', '校区参观预约', '校区相关', '想去参观', 'P2', '["参观","看看","实地考察","走访","预约"]', '["可以参观校区吗?","怎么预约参观?","能去看看吗?"]', 1),
('INT-LOC-03', '教学设备了解', '校区相关', '关心硬件条件', 'P3', '["设备","电脑","配置","硬件","机房"]', '["需要自备电脑吗?","校区提供设备吗?","电脑配置怎么样?"]', 1),
('INT-LOC-04', '住宿安排咨询', '校区相关', '外地学员住宿', 'P3', '["住宿","宿舍","外地","住哪","租房"]', '["外地学员有住宿吗?","住宿怎么安排?","有宿舍吗?"]', 1),
('INT-QUAL-01', '机构正规性', '机构资质', '是否正规机构', 'P2', '["正规","资质","办学","合法","备案"]', '["你们是正规机构吗?","有办学资质吗?","是国家认可的吗?"]', 1),
('INT-QUAL-02', '口碑评价询问', '机构资质', '想看学员评价', 'P3', '["评价","口碑","反馈","怎么样","好吗"]', '["学员评价怎么样?","有老学员反馈吗?","口碑如何?"]', 1),
('INT-QUAL-03', '媒体报道了解', '机构资质', '想看权威背书', 'P3', '["报道","媒体","电视","新闻","央视"]', '["有上过电视报道吗?","媒体报道过吗?","央视那个是真的吗?"]', 1),
('INT-OTHER-01', '大师课咨询', '其他', '进阶课程', 'P3', '["大师课","研修班","进阶","艺术家","高级"]', '["大师课和普通课什么区别?","研修班怎么报名?","大师课学费多少?"]', 1),
('INT-OTHER-02', '企业培训定制', '其他', 'B端需求', 'P3', '["企业培训","定制","公司","团队","B端"]', '["有企业定制培训吗?","公司团队能报名吗?","企业内训怎么合作?"]', 1),
('INT-OTHER-03', '比赛活动咨询', '其他', 'GGAC等赛事', 'P3', '["比赛","GGAC","大赛","活动","赛事"]', '["GGAC比赛怎么参加?","有比赛推荐吗?","学员能参加大赛吗?"]', 1),
('INT-OTHER-04', '转班转学咨询', '其他', '变更需求', 'P3', '["转班","换方向","转学","调整","换校区"]', '["可以换课程方向吗?","可以换校区吗?","转班怎么办理?"]', 1);
-- 8个话术分类(销售阶段)
INSERT INTO `utterance_categories` (`code`, `name`, `parent_code`, `sort_order`, `color`, `description`) VALUES
('STAGE-01', '开场白', 'ROOT', 1, '#52c41a', '首次接待学员/家长,建立信任,收集基础信息'),
('STAGE-02', '需求探询', 'ROOT', 2, '#1890ff', '了解学员背景和目标,精准画像,挖掘需求'),
('STAGE-03', '课程推荐', 'ROOT', 3, '#fa8c16', '匹配适合的课程方向,精准推荐1-2个方向'),
('STAGE-04', '价值塑造', 'ROOT', 4, '#722ed1', '强调优势和差异化,建立价值认同'),
('STAGE-05', '报价沟通', 'ROOT', 5, '#eb2f96', '价格说明和方案,化解价格顾虑'),
('STAGE-06', '异议处理', 'ROOT', 6, '#f5222d', '应对各类顾虑,消除障碍,建立信心'),
('STAGE-07', '促成报名', 'ROOT', 7, '#faad14', '推动转化成交,促成立即行动'),
('STAGE-08', '跟进维护', 'ROOT', 8, '#13c2c2', '未成交学员跟进,保持联系,等待时机');
-- 系统管理员(密码: admin123)
INSERT INTO `admins` (`username`, `password`, `name`, `email`, `role`, `status`, `created_at`) VALUES
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iOE1Qh6G', '系统管理员', 'admin@9artedu.com', 'SUPER_ADMIN', 'ACTIVE', NOW());

File diff suppressed because it is too large Load Diff