Compare commits

...

11 Commits

Author SHA1 Message Date
03df696476 fix: 修复 Tomcat 部署错误,将 javax.servlet 依赖更新为 jakarta.servlet 2025-02-23 17:18:31 +08:00
f66549ee61 fix: 移除 work_area 字段,修复用户登录/注册功能 - 1. 移除数据库中 question 表的 work_area 字段 2. 移除 WorkArea 枚举类 3. 修复 UserDao 和 UserServiceImpl 中的返回类型问题 4. 修复前端用户登录/注册功能 5. 修复 van-popup 组件的 ESLint 告警 2025-02-23 17:11:49 +08:00
cebbef1d53 chore: 添加前端 logo 资源文件 2025-02-23 16:40:16 +08:00
81ba7ff43f chore: 移除不需要追踪的构建文件 2025-02-23 16:39:32 +08:00
d7aaaa37e2 chore: 添加 .gitignore 配置文件 2025-02-23 16:38:30 +08:00
083f263d09 feat: 优化问卷完成流程 - 添加完成页面,优化样式和交互,简化后端代码 2025-02-23 16:36:56 +08:00
67a6a13b1d feat: 修改问题跳转逻辑,使用第4题的选择结果来决定跳转,移除后端工作领域判断逻辑 2025-02-23 15:30:26 +08:00
480008777b refactor: 删除工作领域相关代码 2025-02-23 13:48:54 +08:00
208024ed5c refactor: 删除用户注册表单中的工作内容字段 2025-02-23 13:47:49 +08:00
d9c64cb28f feat: 完成后端基础功能开发,包括用户注册、问卷调查等功能 2025-02-23 11:49:13 +08:00
20c57a13d2 feat: 完成后端开发,包括项目规则文档、用户管理、问卷管理、答案管理功能,配置 Docker 环境,修复数据库连接配置 2025-02-20 16:40:13 +08:00
171 changed files with 6280 additions and 3774 deletions

81
.cursorrules Normal file
View File

@ -0,0 +1,81 @@
# 项目规则文档
## 1. 日志查看规则
- 不允许使用 `tail -f` 命令查看日志文件
- 应该使用 `tail` 命令查看日志内容
- 查看日志直接查看项目本身的日志而非tomcat的日志
## 2. 项目结构规则
- 后端项目目录:`backend/`
- 前端项目目录:`frontend/`
- 数据库脚本目录:`database/`
- 文档目录:`doc/`
## 3. 构建和部署规则
- 使用 Maven 构建后端项目:`mvn clean package -DskipTests`
- Tomcat 运行在 Docker 容器中:
- 端口映射18080
- 数据目录:`$DOCKER_DATA_DIR/tomcat`
- 日志目录:`$DOCKER_DATA_DIR/tomcat/logs`
- WAR包目录`$DOCKER_DATA_DIR/tomcat/webapps`
- 应用上下文路径:`/llm-survey-api`
- 复制文件时,使用 `command cp` 命令
- 查看日志始终只查看应用的业务日志
- 部署完成后应该稍等几秒查看下业务日志看看发布有没有错误
## 4. 数据库规则
- 数据库名称:`llm_survey`
- 数据库用户:`dev`
- 数据库地址:`127.0.0.1:3306`
- 使用 `init_database.sh` 脚本初始化数据库
## 5. 代码规范
- Java源代码使用UTF-8编码
- 使用Lombok简化代码
- DAO层继承BaseDao接口
- Service层继承BaseService接口
- 控制器使用RestController注解
## 6. Spring配置规则
- 共享的bean定义放在 `applicationContext.xml`
- MVC相关配置放在 `spring-mvc.xml`
- MyBatis相关配置放在 `spring-mybatis.xml`
- 避免重复的bean定义
## 7. 错误处理规则
- 使用统一的错误处理格式ErrorInfo
- 所有异常由GlobalExceptionHandler处理
- 业务异常使用IllegalArgumentException
## 8. API规范
- RESTful API设计
- 统一的响应格式
- 支持跨域访问
- API文档位于 `doc/api.md`
## 9. 安全规则
- 不在代码中硬编码敏感信息
- 配置信息放在properties文件中
- 使用prepared statement防止SQL注入
## 10. 版本控制
- 使用Git进行版本控制
- 遵循语义化版本规范
- 重要配置文件加入版本控制
## 11. 文件操作
- 当需要复制文件时,使用 `command cp` 命令
- 如果发现目录不存在,首先确认自己当前目录是否正确
- 如果需要创建目录,使用 `command mkdir -p` 命令
- 不要尝试重新安装开发依赖工具比如jdk, nodepython等
## 前端开发
- 前端使用vue3开发
- 代码必须严格遵守 eslint 规则
- 前端项目使用yarn打包用最新的4.x版
- 前端项目根目录下需要有`.yarn.yml`配置文件
- 前端启动开发服务器,需要把切换到前端目录以及启动开发服务器两个命令合并执行
## Vant 框架
- `van-popup`的 `v-model` 需要绑定为`v-model:show`

64
.gitignore vendored Normal file
View File

@ -0,0 +1,64 @@
# IDE - VSCode
.vscode/
*.code-workspace
.history/
# IDE - IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
# Maven
target/
*.war
*.jar
*.ear
.classpath
.project
.settings/
# Node.js
node_modules/
dist/
.cache/
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
.env.*.local
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Build output
backend/target/
frontend/dist/
frontend/dist-ssr/
# Cursor
.cursor/
.cursorrules

View File

@ -14,16 +14,18 @@
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.3.31</spring.version> <spring.version>6.1.4</spring.version>
<mybatis.version>3.5.15</mybatis.version> <mybatis.version>3.5.15</mybatis.version>
<mybatis-spring.version>2.1.2</mybatis-spring.version> <mybatis-spring.version>3.0.3</mybatis-spring.version>
<mysql-connector.version>8.3.0</mysql-connector.version> <mysql-connector.version>8.3.0</mysql-connector.version>
<hikaricp.version>5.1.0</hikaricp.version> <hikaricp.version>5.1.0</hikaricp.version>
<jackson.version>2.16.1</jackson.version> <jackson.version>2.16.1</jackson.version>
<lombok.version>1.18.30</lombok.version> <lombok.version>1.18.30</lombok.version>
<slf4j.version>2.0.11</slf4j.version> <slf4j.version>2.0.11</slf4j.version>
<logback.version>1.4.14</logback.version> <logback.version>1.4.14</logback.version>
<servlet-api.version>4.0.1</servlet-api.version> <servlet-api.version>6.0.0</servlet-api.version>
<pagehelper.version>5.3.3</pagehelper.version>
<aspectj.version>1.9.21</aspectj.version>
</properties> </properties>
<dependencies> <dependencies>
@ -81,6 +83,18 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version> <version>${jackson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- Lombok --> <!-- Lombok -->
<dependency> <dependency>
@ -102,13 +116,20 @@
<version>${logback.version}</version> <version>${logback.version}</version>
</dependency> </dependency>
<!-- Servlet API --> <!-- Jakarta Servlet API -->
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>jakarta.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
<version>${servlet-api.version}</version> <version>${servlet-api.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -122,6 +143,9 @@
<configuration> <configuration>
<release>${java.version}</release> <release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding> <encoding>${project.build.sourceEncoding}</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths> <annotationProcessorPaths>
<path> <path>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@ -0,0 +1,39 @@
package ltd.qubit.survey.common.mybatis;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
* Instant类型处理器
*/
public class InstantTypeHandler extends BaseTypeHandler<Instant> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Instant parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, Timestamp.from(parameter));
}
@Override
public Instant getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnName);
return timestamp != null ? timestamp.toInstant() : null;
}
@Override
public Instant getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnIndex);
return timestamp != null ? timestamp.toInstant() : null;
}
@Override
public Instant getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Timestamp timestamp = cs.getTimestamp(columnIndex);
return timestamp != null ? timestamp.toInstant() : null;
}
}

View File

@ -0,0 +1,52 @@
package ltd.qubit.survey.common.mybatis;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 自定义的 JSON 类型处理器
*/
public class JsonTypeHandler extends BaseTypeHandler<Object> {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
try {
ps.setString(i, MAPPER.writeValueAsString(parameter));
} catch (Exception e) {
throw new SQLException("Error converting JSON to String", e);
}
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parse(rs.getString(columnName));
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parse(rs.getString(columnIndex));
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parse(cs.getString(columnIndex));
}
private Object parse(String json) throws SQLException {
try {
if (json == null || json.isEmpty()) {
return null;
}
return MAPPER.readValue(json, Object.class);
} catch (Exception e) {
throw new SQLException("Error converting String to JSON", e);
}
}
}

View File

@ -8,7 +8,7 @@ import ltd.qubit.survey.service.QuestionService;
import ltd.qubit.survey.service.OptionService; import ltd.qubit.survey.service.OptionService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
@ -52,9 +52,9 @@ public class QuestionController {
*/ */
@GetMapping("/question/next") @GetMapping("/question/next")
public Question getNextQuestion( public Question getNextQuestion(
@PathVariable Long userId, @RequestParam Long userId,
@PathVariable Integer currentQuestionNumber, @RequestParam(defaultValue = "0") Integer currentQuestionNumber,
@PathVariable List<String> selectedOptions) { @RequestParam(required = false) List<String> selectedOptions) {
return questionService.getNextQuestion(userId, currentQuestionNumber, selectedOptions) return questionService.getNextQuestion(userId, currentQuestionNumber, selectedOptions)
.orElseThrow(() -> new IllegalArgumentException("没有更多问题了")); .orElseThrow(() -> new IllegalArgumentException("没有更多问题了"));
} }

View File

@ -6,6 +6,7 @@ import ltd.qubit.survey.service.UserService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -40,6 +41,33 @@ public class UserController {
.orElseThrow(() -> new IllegalArgumentException("用户不存在")); .orElseThrow(() -> new IllegalArgumentException("用户不存在"));
} }
/**
* 根据手机号查询用户
*
* @param phone 手机号
* @return 用户信息
*/
@GetMapping("/user/phone/{phone}")
public User findByPhone(@PathVariable String phone) {
return userService.findByPhone(phone)
.orElse(null);
}
/**
* 更新用户信息
*
* @param id 用户ID
* @param user 用户信息
* @return 更新后的用户信息
*/
@PutMapping("/user/{id}")
public User update(@PathVariable Long id, @RequestBody User user) {
if (!id.equals(user.getId())) {
throw new IllegalArgumentException("用户ID不匹配");
}
return userService.update(user);
}
/** /**
* 检查手机号是否已注册 * 检查手机号是否已注册
* *
@ -48,6 +76,6 @@ public class UserController {
*/ */
@GetMapping("/user/check/{phone}") @GetMapping("/user/check/{phone}")
public boolean checkPhone(@PathVariable String phone) { public boolean checkPhone(@PathVariable String phone) {
return userService.isPhoneRegistered(phone); return userService.findByPhone(phone).isPresent();
} }
} }

View File

@ -20,10 +20,10 @@ public interface OptionDao extends BaseDao<Option, Long> {
* 根据问题ID和选项代码查询 * 根据问题ID和选项代码查询
* *
* @param questionId 问题ID * @param questionId 问题ID
* @param optionCode 选项代码 * @param code 选项代码
* @return 选项对象 * @return 选项对象
*/ */
Optional<Option> findByQuestionIdAndCode(Long questionId, String optionCode); Optional<Option> findByQuestionIdAndCode(Long questionId, String code);
/** /**
* 批量插入选项 * 批量插入选项

View File

@ -3,7 +3,6 @@ package ltd.qubit.survey.dao;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import ltd.qubit.survey.model.Question; import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.WorkArea;
/** /**
* 问题DAO接口 * 问题DAO接口
@ -17,14 +16,6 @@ public interface QuestionDao extends BaseDao<Question, Long> {
*/ */
Optional<Question> findByQuestionNumber(Integer questionNumber); Optional<Question> findByQuestionNumber(Integer questionNumber);
/**
* 根据工作领域查询问题列表
*
* @param workArea 工作领域
* @return 问题列表
*/
List<Question> findByWorkArea(WorkArea workArea);
/** /**
* 查询通用问题列表不针对特定工作领域 * 查询通用问题列表不针对特定工作领域
* *

View File

@ -3,25 +3,19 @@ package ltd.qubit.survey.dao;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import ltd.qubit.survey.model.User; import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/** /**
* 用户DAO接口 * 用户DAO
*/ */
@Mapper
public interface UserDao extends BaseDao<User, Long> { public interface UserDao extends BaseDao<User, Long> {
/** /**
* 根据手机号查询用户 * 根据手机号查询用户
* *
* @param phone 手机号 * @param phone 手机号
* @return 用户对象 * @return 用户信息
*/ */
Optional<User> findByPhone(String phone); Optional<User> findByPhone(@Param("phone") String phone);
/**
* 根据工作领域查询用户列表
*
* @param workArea 工作领域
* @return 用户列表
*/
List<User> findByWorkArea(WorkArea workArea);
} }

View File

@ -1,6 +1,6 @@
package ltd.qubit.survey.model; package ltd.qubit.survey.model;
import java.time.LocalDateTime; import java.time.Instant;
import lombok.Data; import lombok.Data;
/** /**
@ -21,7 +21,7 @@ public class Option {
/** /**
* 选项代码如ABC * 选项代码如ABC
*/ */
private String optionCode; private String code;
/** /**
* 选项内容 * 选项内容
@ -36,5 +36,5 @@ public class Option {
/** /**
* 创建时间 * 创建时间
*/ */
private LocalDateTime createdAt; private Instant createdAt;
} }

View File

@ -1,6 +1,7 @@
package ltd.qubit.survey.model; package ltd.qubit.survey.model;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.Map;
import lombok.Data; import lombok.Data;
/** /**
@ -16,7 +17,7 @@ public class Question {
/** /**
* 问题序号 * 问题序号
*/ */
private Integer questionNumber; private Integer number;
/** /**
* 问题内容 * 问题内容
@ -26,12 +27,7 @@ public class Question {
/** /**
* 问题类型单选多选文本 * 问题类型单选多选文本
*/ */
private QuestionType questionType; private QuestionType type;
/**
* 针对的工作领域为null表示通用问题
*/
private WorkArea workArea;
/** /**
* 是否必答 * 是否必答
@ -41,10 +37,15 @@ public class Question {
/** /**
* 跳转逻辑JSON格式 * 跳转逻辑JSON格式
*/ */
private String nextQuestionLogic; private Map<String, Integer> next;
/**
* 是否是最后一题
*/
private Boolean isLast;
/** /**
* 创建时间 * 创建时间
*/ */
private LocalDateTime createdAt; private Instant createdAt;
} }

View File

@ -1,6 +1,6 @@
package ltd.qubit.survey.model; package ltd.qubit.survey.model;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.List; import java.util.List;
import lombok.Data; import lombok.Data;
@ -30,12 +30,12 @@ public class SurveyResponse {
private List<String> selectedOptions; private List<String> selectedOptions;
/** /**
* 文本答案 * 文本答案用于文本题或需要填写文本的选项
*/ */
private String textAnswer; private String textAnswer;
/** /**
* 创建时间 * 创建时间
*/ */
private LocalDateTime createdAt; private Instant createdAt;
} }

View File

@ -1,6 +1,6 @@
package ltd.qubit.survey.model; package ltd.qubit.survey.model;
import java.time.LocalDateTime; import java.time.Instant;
import lombok.Data; import lombok.Data;
/** /**
@ -23,11 +23,6 @@ public class User {
*/ */
private String phone; private String phone;
/**
* 工作领域
*/
private WorkArea workArea;
/** /**
* 岗位性质 * 岗位性质
*/ */
@ -36,5 +31,5 @@ public class User {
/** /**
* 创建时间 * 创建时间
*/ */
private LocalDateTime createdAt; private Instant createdAt;
} }

View File

@ -1,51 +0,0 @@
package ltd.qubit.survey.model;
/**
* 工作领域枚举
*/
public enum WorkArea {
/**
* 研发领域
*/
RD("研发"),
/**
* 项目领域
*/
PROJECT("项目"),
/**
* 保险领域
*/
INSURANCE("保险"),
/**
* 财务领域
*/
FINANCE("财务"),
/**
* 运营领域
*/
OPERATION("运营"),
/**
* 客服领域
*/
CUSTOMER_SERVICE("客服"),
/**
* 综合管理领域
*/
ADMIN("综合管理");
private final String displayName;
WorkArea(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -3,7 +3,6 @@ package ltd.qubit.survey.service;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import ltd.qubit.survey.model.Question; import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.WorkArea;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** /**
@ -19,14 +18,6 @@ public interface QuestionService extends BaseService<Question, Long> {
*/ */
Optional<Question> findByQuestionNumber(Integer questionNumber); Optional<Question> findByQuestionNumber(Integer questionNumber);
/**
* 根据工作领域查询问题列表
*
* @param workArea 工作领域
* @return 问题列表
*/
List<Question> findByWorkArea(WorkArea workArea);
/** /**
* 查询通用问题列表不针对特定工作领域 * 查询通用问题列表不针对特定工作领域
* *

View File

@ -3,44 +3,70 @@ package ltd.qubit.survey.service;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import ltd.qubit.survey.model.User; import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 用户服务接口 * 用户服务
*/ */
@Transactional(readOnly = true) public interface UserService {
public interface UserService extends BaseService<User, Long> { /**
* 创建用户
*
* @param user 用户信息
* @return 创建成功的用户信息
*/
User create(User user);
/**
* 删除用户
*
* @param id 用户ID
*/
void delete(Long id);
/**
* 更新用户信息
*
* @param user 用户信息
* @return 更新后的用户信息
*/
User update(User user);
/**
* 根据ID查询用户
*
* @param id 用户ID
* @return 用户信息
*/
Optional<User> findById(Long id);
/**
* 查询所有用户
*
* @return 用户列表
*/
List<User> findAll();
/** /**
* 根据手机号查询用户 * 根据手机号查询用户
* *
* @param phone 手机号 * @param phone 手机号
* @return 用户对象 * @return 用户信息
*/ */
Optional<User> findByPhone(String phone); Optional<User> findByPhone(String phone);
/**
* 根据工作领域查询用户列表
*
* @param workArea 工作领域
* @return 用户列表
*/
List<User> findByWorkArea(WorkArea workArea);
/** /**
* 用户注册 * 用户注册
* *
* @param user 用户信息 * @param user 用户信息
* @return 注册成功的用户 * @return 注册成功的用户信息
*/ */
@Transactional
User register(User user); User register(User user);
/** /**
* 检查手机号是否已被注册 * 用户登录或注册
* *
* @param phone 手机号 * @param user 用户信息
* @return 是否已注册 * @return 登录或注册成功的用户信息
*/ */
boolean isPhoneRegistered(String phone); User loginOrRegister(User user);
} }

View File

@ -1,6 +1,6 @@
package ltd.qubit.survey.service.impl; package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -19,7 +19,7 @@ public class OptionServiceImpl implements OptionService {
@Override @Override
public Option create(Option option) { public Option create(Option option) {
option.setCreatedAt(LocalDateTime.now()); option.setCreatedAt(Instant.now());
optionDao.insert(option); optionDao.insert(option);
return option; return option;
} }
@ -58,7 +58,7 @@ public class OptionServiceImpl implements OptionService {
@Override @Override
public List<Option> batchCreate(List<Option> options) { public List<Option> batchCreate(List<Option> options) {
// 设置创建时间 // 设置创建时间
LocalDateTime now = LocalDateTime.now(); Instant now = Instant.now();
options.forEach(option -> option.setCreatedAt(now)); options.forEach(option -> option.setCreatedAt(now));
// 批量插入 // 批量插入

View File

@ -2,7 +2,7 @@ package ltd.qubit.survey.service.impl;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -11,7 +11,6 @@ import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.QuestionDao; import ltd.qubit.survey.dao.QuestionDao;
import ltd.qubit.survey.model.Question; import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.User; import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import ltd.qubit.survey.service.QuestionService; import ltd.qubit.survey.service.QuestionService;
import ltd.qubit.survey.service.UserService; import ltd.qubit.survey.service.UserService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -29,10 +28,10 @@ public class QuestionServiceImpl implements QuestionService {
@Override @Override
public Question create(Question question) { public Question create(Question question) {
// 如果没有指定问题序号则自动生成 // 如果没有指定问题序号则自动生成
if (question.getQuestionNumber() == null) { if (question.getNumber() == null) {
question.setQuestionNumber(questionDao.getNextQuestionNumber()); question.setNumber(questionDao.getNextQuestionNumber());
} }
question.setCreatedAt(LocalDateTime.now()); question.setCreatedAt(Instant.now());
questionDao.insert(question); questionDao.insert(question);
return question; return question;
} }
@ -63,11 +62,6 @@ public class QuestionServiceImpl implements QuestionService {
return questionDao.findByQuestionNumber(questionNumber); return questionDao.findByQuestionNumber(questionNumber);
} }
@Override
public List<Question> findByWorkArea(WorkArea workArea) {
return questionDao.findByWorkArea(workArea);
}
@Override @Override
public List<Question> findCommonQuestions() { public List<Question> findCommonQuestions() {
return questionDao.findCommonQuestions(); return questionDao.findCommonQuestions();
@ -75,6 +69,11 @@ public class QuestionServiceImpl implements QuestionService {
@Override @Override
public Optional<Question> getNextQuestion(Long userId, Integer currentQuestionNumber, List<String> selectedOptions) { public Optional<Question> getNextQuestion(Long userId, Integer currentQuestionNumber, List<String> selectedOptions) {
// 如果当前问题序号为0返回第一个问题
if (currentQuestionNumber == 0) {
return findByQuestionNumber(1);
}
// 获取当前问题 // 获取当前问题
Optional<Question> currentQuestion = findByQuestionNumber(currentQuestionNumber); Optional<Question> currentQuestion = findByQuestionNumber(currentQuestionNumber);
if (currentQuestion.isEmpty()) { if (currentQuestion.isEmpty()) {
@ -82,21 +81,17 @@ public class QuestionServiceImpl implements QuestionService {
} }
// 如果当前问题有跳转逻辑则根据选项判断下一个问题 // 如果当前问题有跳转逻辑则根据选项判断下一个问题
String nextQuestionLogic = currentQuestion.get().getNextQuestionLogic(); Map<String, Integer> next = currentQuestion.get().getNext();
if (nextQuestionLogic != null && !nextQuestionLogic.isEmpty()) { if (next != null && !next.isEmpty() && selectedOptions != null && !selectedOptions.isEmpty()) {
try {
// 解析跳转逻辑JSON
Map<String, Integer> logic = objectMapper.readValue(nextQuestionLogic,
new TypeReference<Map<String, Integer>>() {});
// 根据选项确定下一个问题序号 // 根据选项确定下一个问题序号
for (String option : selectedOptions) { for (String option : selectedOptions) {
if (logic.containsKey(option)) { if (next.containsKey(option)) {
return findByQuestionNumber(logic.get(option)); return findByQuestionNumber(next.get(option));
} }
} }
} catch (Exception e) { // 如果有通配符跳转规则
// JSON解析错误继续使用默认的下一个问题 if (next.containsKey("*")) {
return findByQuestionNumber(next.get("*"));
} }
} }
@ -115,13 +110,10 @@ public class QuestionServiceImpl implements QuestionService {
} }
// 添加通用问题 // 添加通用问题
questions.addAll(findCommonQuestions()); questions.addAll(findAll());
// 添加针对用户工作领域的问题
questions.addAll(findByWorkArea(user.get().getWorkArea()));
// 按问题序号排序 // 按问题序号排序
questions.sort((q1, q2) -> q1.getQuestionNumber().compareTo(q2.getQuestionNumber())); questions.sort((q1, q2) -> q1.getNumber().compareTo(q2.getNumber()));
return questions; return questions;
} }

View File

@ -1,6 +1,6 @@
package ltd.qubit.survey.service.impl; package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -22,7 +22,7 @@ public class SurveyResponseServiceImpl implements SurveyResponseService {
@Override @Override
public SurveyResponse create(SurveyResponse response) { public SurveyResponse create(SurveyResponse response) {
response.setCreatedAt(LocalDateTime.now()); response.setCreatedAt(Instant.now());
surveyResponseDao.insert(response); surveyResponseDao.insert(response);
return response; return response;
} }
@ -66,7 +66,7 @@ public class SurveyResponseServiceImpl implements SurveyResponseService {
@Override @Override
public List<SurveyResponse> batchSave(List<SurveyResponse> responses) { public List<SurveyResponse> batchSave(List<SurveyResponse> responses) {
// 设置创建时间 // 设置创建时间
LocalDateTime now = LocalDateTime.now(); Instant now = Instant.now();
responses.forEach(response -> response.setCreatedAt(now)); responses.forEach(response -> response.setCreatedAt(now));
// 批量插入 // 批量插入
@ -81,24 +81,11 @@ public class SurveyResponseServiceImpl implements SurveyResponseService {
@Override @Override
public List<SurveyResponse> submitSurvey(Long userId, List<SurveyResponse> responses) { public List<SurveyResponse> submitSurvey(Long userId, List<SurveyResponse> responses) {
// 验证所有必答题是否已回答
List<Question> questions = questionService.getUserQuestions(userId);
for (Question question : questions) {
if (question.getIsRequired()) {
boolean answered = responses.stream()
.anyMatch(response -> response.getQuestionId().equals(question.getId()));
if (!answered) {
throw new IllegalArgumentException(
String.format("问题 %d 为必答题,请填写答案", question.getQuestionNumber()));
}
}
}
// 删除用户之前的答案 // 删除用户之前的答案
deleteByUserId(userId); deleteByUserId(userId);
// 设置用户ID和创建时间 // 设置用户ID和创建时间
LocalDateTime now = LocalDateTime.now(); Instant now = Instant.now();
responses.forEach(response -> { responses.forEach(response -> {
response.setUserId(userId); response.setUserId(userId);
response.setCreatedAt(now); response.setCreatedAt(now);

View File

@ -1,14 +1,14 @@
package ltd.qubit.survey.service.impl; package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.UserDao; import ltd.qubit.survey.dao.UserDao;
import ltd.qubit.survey.model.User; import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import ltd.qubit.survey.service.UserService; import ltd.qubit.survey.service.UserService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 用户服务实现类 * 用户服务实现类
@ -19,19 +19,26 @@ public class UserServiceImpl implements UserService {
private final UserDao userDao; private final UserDao userDao;
@Override @Override
@Transactional
public User create(User user) { public User create(User user) {
user.setCreatedAt(LocalDateTime.now()); user.setCreatedAt(Instant.now());
userDao.insert(user); userDao.insert(user);
return user; return user;
} }
@Override @Override
@Transactional
public void delete(Long id) { public void delete(Long id) {
userDao.deleteById(id); userDao.deleteById(id);
} }
@Override @Override
@Transactional
public User update(User user) { public User update(User user) {
Optional<User> existingUser = findById(user.getId());
if (existingUser.isEmpty()) {
throw new IllegalArgumentException("用户不存在");
}
userDao.update(user); userDao.update(user);
return user; return user;
} }
@ -52,21 +59,25 @@ public class UserServiceImpl implements UserService {
} }
@Override @Override
public List<User> findByWorkArea(WorkArea workArea) { @Transactional
return userDao.findByWorkArea(workArea);
}
@Override
public User register(User user) { public User register(User user) {
// 检查手机号是否已注册 Optional<User> existingUser = findByPhone(user.getPhone());
if (isPhoneRegistered(user.getPhone())) { if (existingUser.isPresent()) {
throw new IllegalArgumentException("手机号已被注册"); throw new IllegalArgumentException("手机号已被注册");
} }
return create(user); return create(user);
} }
@Override @Override
public boolean isPhoneRegistered(String phone) { @Transactional
return userDao.findByPhone(phone).isPresent(); public User loginOrRegister(User user) {
Optional<User> existingUser = findByPhone(user.getPhone());
if (existingUser.isPresent()) {
User updatedUser = existingUser.get();
updatedUser.setName(user.getName());
updatedUser.setPositionType(user.getPositionType());
return update(updatedUser);
}
return register(user);
} }
} }

View File

@ -0,0 +1,29 @@
package ltd.qubit.survey.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.ZoneOffset;
/**
* 自定义的ObjectMapper支持Java 8日期时间类型
*/
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
super();
// 注册Java 8日期时间模块
JavaTimeModule javaTimeModule = new JavaTimeModule();
registerModule(javaTimeModule);
// 配置序列化特性
setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 将日期序列化为时间戳毫秒
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 设置默认时区为UTC
setTimeZone(java.util.TimeZone.getTimeZone(ZoneOffset.UTC));
}
}

View File

@ -1,6 +1,6 @@
# 数据库配置 # 数据库配置
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/llm_survey?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.url=jdbc:mysql://host.docker.internal:3306/llm_survey?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=dev jdbc.username=dev
jdbc.password= jdbc.password=

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义日志文件的存储地址 -->
<property name="LOG_HOME" value="${catalina.base}/logs/llm-survey-api"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件输出的文件名 -->
<fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志文件保留天数 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志,默认的,如果队列的80%已满,则会丢弃TRACE、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度该值会影响性能默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender最多只能添加一个 -->
<appender-ref ref="FILE"/>
</appender>
<!-- MyBatis日志配置 -->
<logger name="org.apache.ibatis" level="DEBUG"/>
<logger name="java.sql" level="DEBUG"/>
<!-- 项目日志配置 -->
<logger name="ltd.qubit.survey" level="DEBUG"/>
<!-- Spring日志配置 -->
<logger name="org.springframework" level="INFO"/>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>

View File

@ -5,7 +5,7 @@
<resultMap id="optionMap" type="ltd.qubit.survey.model.Option"> <resultMap id="optionMap" type="ltd.qubit.survey.model.Option">
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="questionId" column="question_id"/> <result property="questionId" column="question_id"/>
<result property="optionCode" column="option_code"/> <result property="code" column="code"/>
<result property="content" column="content"/> <result property="content" column="content"/>
<result property="requiresText" column="requires_text"/> <result property="requiresText" column="requires_text"/>
<result property="createdAt" column="created_at"/> <result property="createdAt" column="created_at"/>
@ -13,71 +13,71 @@
<!-- 基础列 --> <!-- 基础列 -->
<sql id="baseColumns"> <sql id="baseColumns">
id, question_id, option_code, content, requires_text, created_at `id`, `question_id`, `code`, `content`, `requires_text`, `created_at`
</sql> </sql>
<!-- 插入 --> <!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.Option" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="ltd.qubit.survey.model.Option" useGeneratedKeys="true" keyProperty="id">
INSERT INTO options (question_id, option_code, content, requires_text) INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`)
VALUES (#{questionId}, #{optionCode}, #{content}, #{requiresText}) VALUES (#{questionId}, #{code}, #{content}, #{requiresText})
</insert> </insert>
<!-- 批量插入 --> <!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List"> <insert id="batchInsert" parameterType="java.util.List">
INSERT INTO options (question_id, option_code, content, requires_text) INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`)
VALUES VALUES
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
(#{item.questionId}, #{item.optionCode}, #{item.content}, #{item.requiresText}) (#{item.questionId}, #{item.code}, #{item.content}, #{item.requiresText})
</foreach> </foreach>
</insert> </insert>
<!-- 更新 --> <!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.Option"> <update id="update" parameterType="ltd.qubit.survey.model.Option">
UPDATE options UPDATE `option`
SET question_id = #{questionId}, SET `question_id` = #{questionId},
option_code = #{optionCode}, `code` = #{code},
content = #{content}, `content` = #{content},
requires_text = #{requiresText} `requires_text` = #{requiresText}
WHERE id = #{id} WHERE `id` = #{id}
</update> </update>
<!-- 删除 --> <!-- 删除 -->
<delete id="deleteById" parameterType="long"> <delete id="deleteById" parameterType="long">
DELETE FROM options WHERE id = #{id} DELETE FROM `option` WHERE `id` = #{id}
</delete> </delete>
<!-- 根据问题ID删除 --> <!-- 根据问题ID删除 -->
<delete id="deleteByQuestionId" parameterType="long"> <delete id="deleteByQuestionId" parameterType="long">
DELETE FROM options WHERE question_id = #{questionId} DELETE FROM `option` WHERE `question_id` = #{questionId}
</delete> </delete>
<!-- 根据ID查询 --> <!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="optionMap"> <select id="findById" parameterType="long" resultMap="optionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM options FROM `option`
WHERE id = #{id} WHERE `id` = #{id}
</select> </select>
<!-- 查询所有 --> <!-- 查询所有 -->
<select id="findAll" resultMap="optionMap"> <select id="findAll" resultMap="optionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM options FROM `option`
ORDER BY question_id, option_code ORDER BY `question_id`, `code`
</select> </select>
<!-- 根据问题ID查询 --> <!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="optionMap"> <select id="findByQuestionId" parameterType="long" resultMap="optionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM options FROM `option`
WHERE question_id = #{questionId} WHERE `question_id` = #{questionId}
ORDER BY option_code ORDER BY `code`
</select> </select>
<!-- 根据问题ID和选项代码查询 --> <!-- 根据问题ID和选项代码查询 -->
<select id="findByQuestionIdAndCode" resultMap="optionMap"> <select id="findByQuestionIdAndCode" resultMap="optionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM options FROM `option`
WHERE question_id = #{questionId} WHERE `question_id` = #{questionId}
AND option_code = #{optionCode} AND `code` = #{code}
</select> </select>
</mapper> </mapper>

View File

@ -4,83 +4,76 @@
<!-- 结果映射 --> <!-- 结果映射 -->
<resultMap id="questionMap" type="ltd.qubit.survey.model.Question"> <resultMap id="questionMap" type="ltd.qubit.survey.model.Question">
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="questionNumber" column="question_number"/> <result property="number" column="number"/>
<result property="content" column="content"/> <result property="content" column="content"/>
<result property="questionType" column="question_type"/> <result property="type" column="type"/>
<result property="workArea" column="work_area"/>
<result property="isRequired" column="is_required"/> <result property="isRequired" column="is_required"/>
<result property="nextQuestionLogic" column="next_question_logic"/> <result property="next" column="next" typeHandler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"/>
<result property="isLast" column="is_last"/>
<result property="createdAt" column="created_at"/> <result property="createdAt" column="created_at"/>
</resultMap> </resultMap>
<!-- 基础列 --> <!-- 基础列 -->
<sql id="baseColumns"> <sql id="baseColumns">
id, question_number, content, question_type, work_area, is_required, next_question_logic, created_at `id`, `number`, `content`, `type`, `is_required`, `next`, `is_last`, `created_at`
</sql> </sql>
<!-- 插入 --> <!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.Question" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="ltd.qubit.survey.model.Question" useGeneratedKeys="true" keyProperty="id">
INSERT INTO questions (question_number, content, question_type, work_area, is_required, next_question_logic) INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `next`, `is_last`)
VALUES (#{questionNumber}, #{content}, #{questionType}, #{workArea}, #{isRequired}, #{nextQuestionLogic}) VALUES (#{number}, #{content}, #{type}, #{isRequired},
#{next,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
#{isLast})
</insert> </insert>
<!-- 更新 --> <!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.Question"> <update id="update" parameterType="ltd.qubit.survey.model.Question">
UPDATE questions UPDATE `question`
SET question_number = #{questionNumber}, SET `number` = #{number},
content = #{content}, `content` = #{content},
question_type = #{questionType}, `type` = #{type},
work_area = #{workArea}, `is_required` = #{isRequired},
is_required = #{isRequired}, `next` = #{next,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
next_question_logic = #{nextQuestionLogic} `is_last` = #{isLast}
WHERE id = #{id} WHERE `id` = #{id}
</update> </update>
<!-- 删除 --> <!-- 删除 -->
<delete id="deleteById" parameterType="long"> <delete id="deleteById" parameterType="long">
DELETE FROM questions WHERE id = #{id} DELETE FROM `question` WHERE `id` = #{id}
</delete> </delete>
<!-- 根据ID查询 --> <!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="questionMap"> <select id="findById" parameterType="long" resultMap="questionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM questions FROM `question`
WHERE id = #{id} WHERE `id` = #{id}
</select> </select>
<!-- 查询所有 --> <!-- 查询所有 -->
<select id="findAll" resultMap="questionMap"> <select id="findAll" resultMap="questionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM questions FROM `question`
ORDER BY question_number ORDER BY `number`
</select> </select>
<!-- 根据问题序号查询 --> <!-- 根据问题序号查询 -->
<select id="findByQuestionNumber" parameterType="int" resultMap="questionMap"> <select id="findByQuestionNumber" parameterType="int" resultMap="questionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM questions FROM `question`
WHERE question_number = #{questionNumber} WHERE `number` = #{number}
</select>
<!-- 根据工作领域查询 -->
<select id="findByWorkArea" parameterType="string" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
WHERE work_area = #{workArea}
ORDER BY question_number
</select> </select>
<!-- 查询通用问题 --> <!-- 查询通用问题 -->
<select id="findCommonQuestions" resultMap="questionMap"> <select id="findCommonQuestions" resultMap="questionMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM questions FROM `question`
WHERE work_area IS NULL ORDER BY `number`
ORDER BY question_number
</select> </select>
<!-- 获取下一个问题序号 --> <!-- 获取下一个问题序号 -->
<select id="getNextQuestionNumber" resultType="int"> <select id="getNextQuestionNumber" resultType="int">
SELECT COALESCE(MAX(question_number) + 1, 1) SELECT COALESCE(MAX(`number`) + 1, 1)
FROM questions FROM `question`
</select> </select>
</mapper> </mapper>

View File

@ -6,88 +6,90 @@
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="userId" column="user_id"/> <result property="userId" column="user_id"/>
<result property="questionId" column="question_id"/> <result property="questionId" column="question_id"/>
<result property="selectedOptions" column="selected_options" typeHandler="org.apache.ibatis.type.JsonTypeHandler"/> <result property="selectedOptions" column="selected_options" typeHandler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"/>
<result property="textAnswer" column="text_answer"/> <result property="textAnswer" column="text_answer"/>
<result property="createdAt" column="created_at"/> <result property="createdAt" column="created_at"/>
</resultMap> </resultMap>
<!-- 基础列 --> <!-- 基础列 -->
<sql id="baseColumns"> <sql id="baseColumns">
id, user_id, question_id, selected_options, text_answer, created_at `id`, `user_id`, `question_id`, `selected_options`, `text_answer`, `created_at`
</sql> </sql>
<!-- 插入 --> <!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.SurveyResponse" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="ltd.qubit.survey.model.SurveyResponse" useGeneratedKeys="true" keyProperty="id">
INSERT INTO survey_responses (user_id, question_id, selected_options, text_answer) INSERT INTO `survey_response` (`user_id`, `question_id`, `selected_options`, `text_answer`)
VALUES (#{userId}, #{questionId}, #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler}, #{textAnswer}) VALUES (#{userId}, #{questionId},
#{selectedOptions,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
#{textAnswer})
</insert> </insert>
<!-- 批量插入 --> <!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List"> <insert id="batchInsert" parameterType="java.util.List">
INSERT INTO survey_responses (user_id, question_id, selected_options, text_answer) INSERT INTO `survey_response` (`user_id`, `question_id`, `selected_options`, `text_answer`)
VALUES VALUES
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.questionId}, (#{item.userId}, #{item.questionId},
#{item.selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler}, #{item.selectedOptions,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
#{item.textAnswer}) #{item.textAnswer})
</foreach> </foreach>
</insert> </insert>
<!-- 更新 --> <!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.SurveyResponse"> <update id="update" parameterType="ltd.qubit.survey.model.SurveyResponse">
UPDATE survey_responses UPDATE `survey_response`
SET user_id = #{userId}, SET `user_id` = #{userId},
question_id = #{questionId}, `question_id` = #{questionId},
selected_options = #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler}, `selected_options` = #{selectedOptions,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
text_answer = #{textAnswer} `text_answer` = #{textAnswer}
WHERE id = #{id} WHERE `id` = #{id}
</update> </update>
<!-- 删除 --> <!-- 删除 -->
<delete id="deleteById" parameterType="long"> <delete id="deleteById" parameterType="long">
DELETE FROM survey_responses WHERE id = #{id} DELETE FROM `survey_response` WHERE `id` = #{id}
</delete> </delete>
<!-- 根据用户ID删除 --> <!-- 根据用户ID删除 -->
<delete id="deleteByUserId" parameterType="long"> <delete id="deleteByUserId" parameterType="long">
DELETE FROM survey_responses WHERE user_id = #{userId} DELETE FROM `survey_response` WHERE `user_id` = #{userId}
</delete> </delete>
<!-- 根据ID查询 --> <!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="responseMap"> <select id="findById" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM survey_responses FROM `survey_response`
WHERE id = #{id} WHERE `id` = #{id}
</select> </select>
<!-- 查询所有 --> <!-- 查询所有 -->
<select id="findAll" resultMap="responseMap"> <select id="findAll" resultMap="responseMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM survey_responses FROM `survey_response`
ORDER BY user_id, question_id ORDER BY `user_id`, `question_id`
</select> </select>
<!-- 根据用户ID查询 --> <!-- 根据用户ID查询 -->
<select id="findByUserId" parameterType="long" resultMap="responseMap"> <select id="findByUserId" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM survey_responses FROM `survey_response`
WHERE user_id = #{userId} WHERE `user_id` = #{userId}
ORDER BY question_id ORDER BY `question_id`
</select> </select>
<!-- 根据问题ID查询 --> <!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="responseMap"> <select id="findByQuestionId" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM survey_responses FROM `survey_response`
WHERE question_id = #{questionId} WHERE `question_id` = #{questionId}
ORDER BY user_id ORDER BY `user_id`
</select> </select>
<!-- 根据用户ID和问题ID查询 --> <!-- 根据用户ID和问题ID查询 -->
<select id="findByUserIdAndQuestionId" resultMap="responseMap"> <select id="findByUserIdAndQuestionId" resultMap="responseMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM survey_responses FROM `survey_response`
WHERE user_id = #{userId} WHERE `user_id` = #{userId}
AND question_id = #{questionId} AND `question_id` = #{questionId}
</select> </select>
</mapper> </mapper>

View File

@ -4,65 +4,54 @@
<!-- 结果映射 --> <!-- 结果映射 -->
<resultMap id="userMap" type="ltd.qubit.survey.model.User"> <resultMap id="userMap" type="ltd.qubit.survey.model.User">
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"/> <result property="phone" column="phone"/>
<result property="workArea" column="work_area"/> <result property="name" column="name"/>
<result property="positionType" column="position_type"/> <result property="positionType" column="position_type"/>
<result property="createdAt" column="created_at"/> <result property="createdAt" column="created_at"/>
</resultMap> </resultMap>
<!-- 基础列 --> <!-- 基础列 -->
<sql id="baseColumns"> <sql id="columns">
id, name, phone, work_area, position_type, created_at id, phone, name, position_type, created_at
</sql> </sql>
<!-- 插入 --> <!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.User" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" parameterType="ltd.qubit.survey.model.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (name, phone, work_area, position_type) INSERT INTO user (phone, name, position_type, created_at)
VALUES (#{name}, #{phone}, #{workArea}, #{positionType}) VALUES (#{phone}, #{name}, #{positionType}, NOW())
</insert> </insert>
<!-- 更新 --> <!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.User"> <update id="update" parameterType="ltd.qubit.survey.model.User">
UPDATE users UPDATE user
SET name = #{name}, SET name = #{name},
phone = #{phone},
work_area = #{workArea},
position_type = #{positionType} position_type = #{positionType}
WHERE id = #{id} WHERE id = #{id}
</update> </update>
<!-- 删除 --> <!-- 删除 -->
<delete id="deleteById" parameterType="long"> <delete id="deleteById" parameterType="long">
DELETE FROM users WHERE id = #{id} DELETE FROM user WHERE id = #{id}
</delete> </delete>
<!-- 根据ID查询 --> <!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="userMap"> <select id="findById" parameterType="long" resultMap="userMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="columns"/>
FROM users FROM user
WHERE id = #{id} WHERE id = #{id}
</select> </select>
<!-- 查询所有 --> <!-- 查询所有 -->
<select id="findAll" resultMap="userMap"> <select id="findAll" resultMap="userMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="columns"/>
FROM users FROM user
ORDER BY id ORDER BY id
</select> </select>
<!-- 根据手机号查询 --> <!-- 根据手机号查询 -->
<select id="findByPhone" parameterType="string" resultMap="userMap"> <select id="findByPhone" parameterType="string" resultMap="userMap">
SELECT <include refid="baseColumns"/> SELECT <include refid="columns"/>
FROM users FROM user
WHERE phone = #{phone} WHERE phone = #{phone}
</select> </select>
<!-- 根据工作领域查询 -->
<select id="findByWorkArea" parameterType="string" resultMap="userMap">
SELECT <include refid="baseColumns"/>
FROM users
WHERE work_area = #{workArea}
ORDER BY id
</select>
</mapper> </mapper>

View File

@ -12,21 +12,28 @@
<setting name="logImpl" value="SLF4J"/> <setting name="logImpl" value="SLF4J"/>
</settings> </settings>
<!-- 类型别名配置 -->
<typeAliases>
<package name="ltd.qubit.survey.model"/>
</typeAliases>
<!-- 类型处理器配置 -->
<typeHandlers> <typeHandlers>
<!-- 枚举类型处理器 --> <!-- 枚举类型处理器 -->
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.WorkArea"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler" <typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.PositionType"/> javaType="ltd.qubit.survey.model.PositionType"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler" <typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.QuestionType"/> javaType="ltd.qubit.survey.model.QuestionType"/>
<!-- JSON类型处理器 -->
<typeHandler handler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"
javaType="java.util.List"/>
<!-- Instant类型处理器 -->
<typeHandler handler="ltd.qubit.survey.common.mybatis.InstantTypeHandler"
javaType="java.time.Instant"/>
</typeHandlers> </typeHandlers>
<!-- 映射器配置 -->
<mappers> <mappers>
<!-- 映射文件 --> <package name="ltd.qubit.survey.dao"/>
<mapper resource="mybatis/mapper/UserMapper.xml"/>
<mapper resource="mybatis/mapper/QuestionMapper.xml"/>
<mapper resource="mybatis/mapper/OptionMapper.xml"/>
<mapper resource="mybatis/mapper/SurveyResponseMapper.xml"/>
</mappers> </mappers>
</configuration> </configuration>

View File

@ -3,16 +3,22 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"> http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 加载属性文件 --> <!-- 加载属性文件 -->
<context:property-placeholder location="classpath:application.properties"/> <context:property-placeholder location="classpath:application.properties"/>
<!-- 配置自定义ObjectMapper -->
<bean id="objectMapper" class="ltd.qubit.survey.utils.CustomObjectMapper" primary="true"/>
<!-- 开启注解扫描排除Controller --> <!-- 开启注解扫描排除Controller -->
<context:component-scan base-package="ltd.qubit.survey"> <context:component-scan base-package="ltd.qubit.survey">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
@ -21,4 +27,21 @@
<!-- 配置事务注解驱动 --> <!-- 配置事务注解驱动 -->
<tx:annotation-driven/> <tx:annotation-driven/>
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="list*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务切面 -->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* ltd.qubit.survey.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans> </beans>

View File

@ -20,27 +20,39 @@
<mvc:message-converters> <mvc:message-converters>
<!-- 配置JSON转换器 --> <!-- 配置JSON转换器 -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper"> <property name="objectMapper" ref="objectMapper"/>
<bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="supportedMediaTypes">
<property name="dateFormat"> <list>
<bean class="java.text.SimpleDateFormat"> <value>application/json;charset=UTF-8</value>
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </list>
</bean>
</property> </property>
</bean> </bean>
<!-- 配置字符串转换器 -->
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property> </property>
</bean> </bean>
</mvc:message-converters> </mvc:message-converters>
</mvc:annotation-driven> </mvc:annotation-driven>
<!-- 配置静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 配置跨域支持 --> <!-- 配置跨域支持 -->
<mvc:cors> <mvc:cors>
<mvc:mapping path="/**" <mvc:mapping path="/**"
allowed-origins="*" allowed-origins="http://localhost:3000,http://localhost:8080"
allowed-methods="GET,POST,PUT,DELETE,OPTIONS" allowed-methods="GET,POST,PUT,DELETE,OPTIONS"
allowed-headers="Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers" allowed-headers="Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers"
allow-credentials="true" allow-credentials="true"
max-age="3600"/> max-age="3600"/>
</mvc:cors> </mvc:cors>
<!-- 配置异常处理器 -->
<bean class="ltd.qubit.survey.controller.GlobalExceptionHandler"/>
</beans> </beans>

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"> http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 配置数据源 --> <!-- 配置数据源 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
@ -16,13 +19,35 @@
<property name="idleTimeout" value="${jdbc.pool.idleTimeout}"/> <property name="idleTimeout" value="${jdbc.pool.idleTimeout}"/>
<property name="connectionTimeout" value="${jdbc.pool.connectionTimeout}"/> <property name="connectionTimeout" value="${jdbc.pool.connectionTimeout}"/>
<property name="connectionTestQuery" value="${jdbc.pool.connectionTestQuery}"/> <property name="connectionTestQuery" value="${jdbc.pool.connectionTestQuery}"/>
<!-- 其他配置 -->
<property name="autoCommit" value="true"/>
<property name="poolName" value="HikariPool-Survey"/>
<property name="maxLifetime" value="1800000"/>
<property name="validationTimeout" value="5000"/>
<property name="leakDetectionThreshold" value="60000"/>
</bean> </bean>
<!-- 配置SqlSessionFactory --> <!-- 配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/> <property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> <property name="mapperLocations" value="classpath*:mybatis/mapper/*.xml"/>
<!-- 配置插件 -->
<property name="plugins">
<array>
<!-- 分页插件 -->
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
helperDialect=mysql
reasonable=true
supportMethodsArguments=true
params=count=countSql
</value>
</property>
</bean>
</array>
</property>
</bean> </bean>
<!-- 配置Mapper扫描器 --> <!-- 配置Mapper扫描器 -->

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="4.0"> version="6.0">
<display-name>LLM Survey API</display-name> <display-name>LLM Survey API</display-name>

View File

@ -1,18 +0,0 @@
# 数据库配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/llm_survey?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=dev
jdbc.password=
# 连接池配置
jdbc.pool.minimumIdle=5
jdbc.pool.maximumPoolSize=20
jdbc.pool.idleTimeout=300000
jdbc.pool.connectionTimeout=20000
jdbc.pool.connectionTestQuery=SELECT 1
# 日志配置
logging.level.root=INFO
logging.level.ltd.qubit.survey=DEBUG
logging.level.org.springframework=INFO
logging.level.org.mybatis=DEBUG

View File

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ltd.qubit.survey.dao.OptionDao">
<!-- 结果映射 -->
<resultMap id="optionMap" type="ltd.qubit.survey.model.Option">
<id property="id" column="id"/>
<result property="questionId" column="question_id"/>
<result property="optionCode" column="option_code"/>
<result property="content" column="content"/>
<result property="requiresText" column="requires_text"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!-- 基础列 -->
<sql id="baseColumns">
id, question_id, option_code, content, requires_text, created_at
</sql>
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.Option" useGeneratedKeys="true" keyProperty="id">
INSERT INTO options (question_id, option_code, content, requires_text)
VALUES (#{questionId}, #{optionCode}, #{content}, #{requiresText})
</insert>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO options (question_id, option_code, content, requires_text)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.questionId}, #{item.optionCode}, #{item.content}, #{item.requiresText})
</foreach>
</insert>
<!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.Option">
UPDATE options
SET question_id = #{questionId},
option_code = #{optionCode},
content = #{content},
requires_text = #{requiresText}
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById" parameterType="long">
DELETE FROM options WHERE id = #{id}
</delete>
<!-- 根据问题ID删除 -->
<delete id="deleteByQuestionId" parameterType="long">
DELETE FROM options WHERE question_id = #{questionId}
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM options
ORDER BY question_id, option_code
</select>
<!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}
ORDER BY option_code
</select>
<!-- 根据问题ID和选项代码查询 -->
<select id="findByQuestionIdAndCode" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}
AND option_code = #{optionCode}
</select>
</mapper>

View File

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ltd.qubit.survey.dao.QuestionDao">
<!-- 结果映射 -->
<resultMap id="questionMap" type="ltd.qubit.survey.model.Question">
<id property="id" column="id"/>
<result property="questionNumber" column="question_number"/>
<result property="content" column="content"/>
<result property="questionType" column="question_type"/>
<result property="workArea" column="work_area"/>
<result property="isRequired" column="is_required"/>
<result property="nextQuestionLogic" column="next_question_logic"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!-- 基础列 -->
<sql id="baseColumns">
id, question_number, content, question_type, work_area, is_required, next_question_logic, created_at
</sql>
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.Question" useGeneratedKeys="true" keyProperty="id">
INSERT INTO questions (question_number, content, question_type, work_area, is_required, next_question_logic)
VALUES (#{questionNumber}, #{content}, #{questionType}, #{workArea}, #{isRequired}, #{nextQuestionLogic})
</insert>
<!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.Question">
UPDATE questions
SET question_number = #{questionNumber},
content = #{content},
question_type = #{questionType},
work_area = #{workArea},
is_required = #{isRequired},
next_question_logic = #{nextQuestionLogic}
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById" parameterType="long">
DELETE FROM questions WHERE id = #{id}
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
ORDER BY question_number
</select>
<!-- 根据问题序号查询 -->
<select id="findByQuestionNumber" parameterType="int" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
WHERE question_number = #{questionNumber}
</select>
<!-- 根据工作领域查询 -->
<select id="findByWorkArea" parameterType="string" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
WHERE work_area = #{workArea}
ORDER BY question_number
</select>
<!-- 查询通用问题 -->
<select id="findCommonQuestions" resultMap="questionMap">
SELECT <include refid="baseColumns"/>
FROM questions
WHERE work_area IS NULL
ORDER BY question_number
</select>
<!-- 获取下一个问题序号 -->
<select id="getNextQuestionNumber" resultType="int">
SELECT COALESCE(MAX(question_number) + 1, 1)
FROM questions
</select>
</mapper>

View File

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ltd.qubit.survey.dao.SurveyResponseDao">
<!-- 结果映射 -->
<resultMap id="responseMap" type="ltd.qubit.survey.model.SurveyResponse">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="questionId" column="question_id"/>
<result property="selectedOptions" column="selected_options" typeHandler="org.apache.ibatis.type.JsonTypeHandler"/>
<result property="textAnswer" column="text_answer"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!-- 基础列 -->
<sql id="baseColumns">
id, user_id, question_id, selected_options, text_answer, created_at
</sql>
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.SurveyResponse" useGeneratedKeys="true" keyProperty="id">
INSERT INTO survey_responses (user_id, question_id, selected_options, text_answer)
VALUES (#{userId}, #{questionId}, #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler}, #{textAnswer})
</insert>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO survey_responses (user_id, question_id, selected_options, text_answer)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.questionId},
#{item.selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
#{item.textAnswer})
</foreach>
</insert>
<!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.SurveyResponse">
UPDATE survey_responses
SET user_id = #{userId},
question_id = #{questionId},
selected_options = #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
text_answer = #{textAnswer}
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById" parameterType="long">
DELETE FROM survey_responses WHERE id = #{id}
</delete>
<!-- 根据用户ID删除 -->
<delete id="deleteByUserId" parameterType="long">
DELETE FROM survey_responses WHERE user_id = #{userId}
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/>
FROM survey_responses
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="responseMap">
SELECT <include refid="baseColumns"/>
FROM survey_responses
ORDER BY user_id, question_id
</select>
<!-- 根据用户ID查询 -->
<select id="findByUserId" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/>
FROM survey_responses
WHERE user_id = #{userId}
ORDER BY question_id
</select>
<!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="responseMap">
SELECT <include refid="baseColumns"/>
FROM survey_responses
WHERE question_id = #{questionId}
ORDER BY user_id
</select>
<!-- 根据用户ID和问题ID查询 -->
<select id="findByUserIdAndQuestionId" resultMap="responseMap">
SELECT <include refid="baseColumns"/>
FROM survey_responses
WHERE user_id = #{userId}
AND question_id = #{questionId}
</select>
</mapper>

View File

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ltd.qubit.survey.dao.UserDao">
<!-- 结果映射 -->
<resultMap id="userMap" type="ltd.qubit.survey.model.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"/>
<result property="workArea" column="work_area"/>
<result property="positionType" column="position_type"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<!-- 基础列 -->
<sql id="baseColumns">
id, name, phone, work_area, position_type, created_at
</sql>
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (name, phone, work_area, position_type)
VALUES (#{name}, #{phone}, #{workArea}, #{positionType})
</insert>
<!-- 更新 -->
<update id="update" parameterType="ltd.qubit.survey.model.User">
UPDATE users
SET name = #{name},
phone = #{phone},
work_area = #{workArea},
position_type = #{positionType}
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById" parameterType="long">
DELETE FROM users WHERE id = #{id}
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="userMap">
SELECT <include refid="baseColumns"/>
FROM users
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="userMap">
SELECT <include refid="baseColumns"/>
FROM users
ORDER BY id
</select>
<!-- 根据手机号查询 -->
<select id="findByPhone" parameterType="string" resultMap="userMap">
SELECT <include refid="baseColumns"/>
FROM users
WHERE phone = #{phone}
</select>
<!-- 根据工作领域查询 -->
<select id="findByWorkArea" parameterType="string" resultMap="userMap">
SELECT <include refid="baseColumns"/>
FROM users
WHERE work_area = #{workArea}
ORDER BY id
</select>
</mapper>

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置日志实现 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<typeHandlers>
<!-- 枚举类型处理器 -->
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.WorkArea"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.PositionType"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.QuestionType"/>
</typeHandlers>
<mappers>
<!-- 映射文件 -->
<mapper resource="mybatis/mapper/UserMapper.xml"/>
<mapper resource="mybatis/mapper/QuestionMapper.xml"/>
<mapper resource="mybatis/mapper/OptionMapper.xml"/>
<mapper resource="mybatis/mapper/SurveyResponseMapper.xml"/>
</mappers>
</configuration>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 加载属性文件 -->
<context:property-placeholder location="classpath:application.properties"/>
<!-- 开启注解扫描排除Controller -->
<context:component-scan base-package="ltd.qubit.survey">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 配置事务注解驱动 -->
<tx:annotation-driven/>
</beans>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 开启Controller注解扫描 -->
<context:component-scan base-package="ltd.qubit.survey.controller" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 开启SpringMVC注解驱动 -->
<mvc:annotation-driven>
<mvc:message-converters>
<!-- 配置JSON转换器 -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 配置跨域支持 -->
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-methods="GET,POST,PUT,DELETE,OPTIONS"
allowed-headers="Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers"
allow-credentials="true"
max-age="3600"/>
</mvc:cors>
</beans>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置数据源 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 连接池配置 -->
<property name="minimumIdle" value="${jdbc.pool.minimumIdle}"/>
<property name="maximumPoolSize" value="${jdbc.pool.maximumPoolSize}"/>
<property name="idleTimeout" value="${jdbc.pool.idleTimeout}"/>
<property name="connectionTimeout" value="${jdbc.pool.connectionTimeout}"/>
<property name="connectionTestQuery" value="${jdbc.pool.connectionTestQuery}"/>
</bean>
<!-- 配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>
</bean>
<!-- 配置Mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="ltd.qubit.survey.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>

View File

@ -1,27 +0,0 @@
ltd/qubit/survey/controller/GlobalExceptionHandler.class
ltd/qubit/survey/dao/QuestionDao.class
ltd/qubit/survey/model/PositionType.class
ltd/qubit/survey/service/OptionService.class
ltd/qubit/survey/controller/UserController.class
ltd/qubit/survey/service/impl/SurveyResponseServiceImpl.class
ltd/qubit/survey/dao/SurveyResponseDao.class
ltd/qubit/survey/dao/OptionDao.class
ltd/qubit/survey/controller/QuestionController.class
ltd/qubit/survey/service/impl/UserServiceImpl.class
ltd/qubit/survey/service/impl/QuestionServiceImpl.class
ltd/qubit/survey/model/SurveyResponse.class
ltd/qubit/survey/model/User.class
ltd/qubit/survey/service/QuestionService.class
ltd/qubit/survey/service/impl/QuestionServiceImpl$1.class
ltd/qubit/survey/service/UserService.class
ltd/qubit/survey/dao/BaseDao.class
ltd/qubit/survey/service/impl/OptionServiceImpl.class
ltd/qubit/survey/dao/UserDao.class
ltd/qubit/survey/model/ErrorInfo.class
ltd/qubit/survey/controller/SurveyController.class
ltd/qubit/survey/model/Question.class
ltd/qubit/survey/model/WorkArea.class
ltd/qubit/survey/service/SurveyResponseService.class
ltd/qubit/survey/model/Option.class
ltd/qubit/survey/model/QuestionType.class
ltd/qubit/survey/service/BaseService.class

View File

@ -1,26 +0,0 @@
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/dao/QuestionDao.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/UserService.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/OptionService.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/Question.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/QuestionService.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/BaseService.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/User.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/impl/OptionServiceImpl.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/impl/QuestionServiceImpl.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/SurveyResponseService.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/Option.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/controller/GlobalExceptionHandler.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/QuestionType.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/dao/UserDao.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/SurveyResponse.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/impl/SurveyResponseServiceImpl.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/ErrorInfo.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/controller/UserController.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/service/impl/UserServiceImpl.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/dao/SurveyResponseDao.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/controller/QuestionController.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/WorkArea.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/model/PositionType.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/dao/BaseDao.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/controller/SurveyController.java
/Volumes/working/qubit/project/llm-survey/backend/src/main/java/ltd/qubit/survey/dao/OptionDao.java

View File

@ -1,73 +1,375 @@
-- 创建数据库 -- 创建数据库
CREATE DATABASE IF NOT EXISTS llm_survey DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE DATABASE IF NOT EXISTS `llm_survey` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE llm_survey; USE `llm_survey`;
-- 删除旧表(如果存在)
DROP TABLE IF EXISTS `survey_response`;
DROP TABLE IF EXISTS `option`;
DROP TABLE IF EXISTS `question`;
DROP TABLE IF EXISTS `user`;
-- 创建用户表 -- 创建用户表
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS `user` (
id BIGINT PRIMARY KEY AUTO_INCREMENT, `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '姓名', `name` VARCHAR(50) NOT NULL COMMENT '姓名',
phone VARCHAR(20) NOT NULL COMMENT '手机号码', `phone` VARCHAR(20) NOT NULL COMMENT '手机号',
work_area VARCHAR(20) NOT NULL COMMENT '工作领域', `position_type` VARCHAR(20) NOT NULL COMMENT '岗位性质',
position_type VARCHAR(20) NOT NULL COMMENT '岗位性质', `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_phone` (`phone`)
UNIQUE KEY uk_phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
-- 创建问题表 -- 创建问题表
CREATE TABLE IF NOT EXISTS questions ( CREATE TABLE IF NOT EXISTS `question` (
id BIGINT PRIMARY KEY AUTO_INCREMENT, `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
question_number INT NOT NULL COMMENT '问题序号', `number` INT NOT NULL COMMENT '问题序号',
content TEXT NOT NULL COMMENT '问题内容', `content` TEXT NOT NULL COMMENT '问题内容',
question_type VARCHAR(20) NOT NULL COMMENT '问题类型', `type` VARCHAR(20) NOT NULL COMMENT '问题类型',
work_area VARCHAR(20) COMMENT '针对的工作领域NULL表示通用问题', `is_required` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否必答',
is_required BOOLEAN DEFAULT TRUE COMMENT '是否必答', `next` JSON DEFAULT NULL COMMENT '跳转逻辑',
next_question_logic JSON COMMENT '跳转逻辑', `is_last` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否是最后一题',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_question_number (question_number) UNIQUE KEY `uk_number` (`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题表';
-- 创建选项表 -- 创建选项表
CREATE TABLE IF NOT EXISTS options ( CREATE TABLE IF NOT EXISTS `option` (
id BIGINT PRIMARY KEY AUTO_INCREMENT, `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
question_id BIGINT NOT NULL COMMENT '关联的问题ID', `question_id` BIGINT NOT NULL COMMENT '关联的问题ID',
option_code VARCHAR(10) NOT NULL COMMENT '选项代码如A、B、C', `code` VARCHAR(10) NOT NULL COMMENT '选项代码',
content TEXT NOT NULL COMMENT '选项内容', `content` TEXT NOT NULL COMMENT '选项内容',
requires_text BOOLEAN DEFAULT FALSE COMMENT '是否需要填写文本', `requires_text` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否需要填写文本',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (question_id) REFERENCES questions(id), FOREIGN KEY (`question_id`) REFERENCES `question` (`id`),
UNIQUE KEY uk_question_option (question_id, option_code) UNIQUE KEY `uk_question_code` (`question_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题选项表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题选项表';
-- 创建答案表 -- 创建问卷答案表
CREATE TABLE IF NOT EXISTS survey_responses ( CREATE TABLE IF NOT EXISTS `survey_response` (
id BIGINT PRIMARY KEY AUTO_INCREMENT, `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID', `user_id` BIGINT NOT NULL COMMENT '用户ID',
question_id BIGINT NOT NULL COMMENT '问题ID', `question_id` BIGINT NOT NULL COMMENT '问题ID',
selected_options JSON COMMENT '选中的选项代码列表', `selected_options` JSON DEFAULT NULL COMMENT '选中的选项代码列表',
text_answer TEXT COMMENT '文本答案', `text_answer` TEXT DEFAULT NULL COMMENT '文本答案(用于文本题或需要填写文本的选项)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
FOREIGN KEY (question_id) REFERENCES questions(id) FOREIGN KEY (`question_id`) REFERENCES `question` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷答案表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷答案表';
-- 插入基础问题数据 -- 清空现有数据
INSERT INTO questions (question_number, content, question_type, is_required) VALUES SET FOREIGN_KEY_CHECKS = 0;
(1, '您的工作领域', 'SINGLE_CHOICE', TRUE), TRUNCATE TABLE `survey_response`;
(2, '岗位性质', 'SINGLE_CHOICE', TRUE); TRUNCATE TABLE `option`;
TRUNCATE TABLE `question`;
TRUNCATE TABLE `user`;
SET FOREIGN_KEY_CHECKS = 1;
-- 插入基础选项数据 -- 插入通用认知问题
INSERT INTO options (question_id, option_code, content) VALUES INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
(1, 'A', '研发'), VALUES (1, '您对大模型如ChatGPT、通义千问、DeepSeek的了解程度', 'SINGLE_CHOICE', TRUE, FALSE);
(1, 'B', '项目'),
(1, 'C', '保险'),
(1, 'D', '财务'),
(1, 'E', '运营'),
(1, 'F', '客服'),
(1, 'G', '综合管理');
INSERT INTO options (question_id, option_code, content) VALUES INSERT INTO `option` (`question_id`, `code`, `content`) VALUES
(2, 'A', '管理岗'), (LAST_INSERT_ID(), 'A', '从未接触过'),
(2, 'B', '技术岗'), (LAST_INSERT_ID(), 'B', '仅在日常简单使用过通用功能(如问答)'),
(2, 'C', '业务岗'), (LAST_INSERT_ID(), 'C', '在工作中尝试过基础应用'),
(2, 'D', '职能支持岗'); (LAST_INSERT_ID(), 'D', '深度研究过技术原理');
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (2, '您觉得大模型可以做到下面哪些事?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '精准知识问答', FALSE),
(LAST_INSERT_ID(), 'B', '文档撰写/报告生成/代码编写/图片视频生成等', FALSE),
(LAST_INSERT_ID(), 'C', '数据清洗与分析', FALSE),
(LAST_INSERT_ID(), 'D', '客户沟通与服务', FALSE),
(LAST_INSERT_ID(), 'E', '风险识别与预警', FALSE),
(LAST_INSERT_ID(), 'F', '流程自动化', FALSE),
(LAST_INSERT_ID(), 'G', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (3, '您最关注大模型应用的哪些风险?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '数据隐私泄露', FALSE),
(LAST_INSERT_ID(), 'B', '生成内容不准确', FALSE),
(LAST_INSERT_ID(), 'C', '合规审查风险', FALSE),
(LAST_INSERT_ID(), 'D', '技术使用门槛高', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (4, '您的主要工作内容是:', 'SINGLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`) VALUES
(LAST_INSERT_ID(), 'A', '研发(产品、开发、算法、测试、运维等)'),
(LAST_INSERT_ID(), 'B', '项目管理(项目立项、进度跟踪、风险管理等)'),
(LAST_INSERT_ID(), 'C', '保险(产品、核保、理赔、精算等)'),
(LAST_INSERT_ID(), 'D', '财务(会计、税务、审计等)'),
(LAST_INSERT_ID(), 'E', '客服(咨询、投诉、回访等)'),
(LAST_INSERT_ID(), 'F', '运营(新媒体运营、广告宣传、活动策划、数据分析等)'),
(LAST_INSERT_ID(), 'G', '市场拓展(渠道拓展、商务沟通、产品推广等)'),
(LAST_INSERT_ID(), 'H', '人力资源(招聘、培训、绩效、薪酬等)'),
(LAST_INSERT_ID(), 'I', '综合事务(行政、法务等)'),
(LAST_INSERT_ID(), 'J', '公司高管(战略规划、组织架构、制度建设等)');
-- 研发部门专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (5, '您在开发过程中最耗时的重复性工作:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '文档编写(如需求文档、技术文档等)', FALSE),
(LAST_INSERT_ID(), 'B', '产品原型、界面设计(如使用图片生成模型自动生成等)', FALSE),
(LAST_INSERT_ID(), 'C', '代码编写', FALSE),
(LAST_INSERT_ID(), 'D', '调试与测试', FALSE),
(LAST_INSERT_ID(), 'E', '系统监控与维护', FALSE),
(LAST_INSERT_ID(), 'F', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (6, '您希望大模型如何与现有系统集成:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '回答技术问题', FALSE),
(LAST_INSERT_ID(), 'B', '自动生成代码片段(如 Github Copilot等', FALSE),
(LAST_INSERT_ID(), 'C', '智能测试用例生成', FALSE),
(LAST_INSERT_ID(), 'D', '生成需求文档、技术文档', FALSE),
(LAST_INSERT_ID(), 'E', '完整项目生成如Cursor等', FALSE),
(LAST_INSERT_ID(), 'F', '代码重构与优化', FALSE),
(LAST_INSERT_ID(), 'G', '辅助设计算法如DeepSeek等', FALSE),
(LAST_INSERT_ID(), 'H', '其他', TRUE);
-- 项目管理专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (7, '项目管理中最常遇到的挑战是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '项目进度跟踪与更新', FALSE),
(LAST_INSERT_ID(), 'B', '风险评估与管控', FALSE),
(LAST_INSERT_ID(), 'C', '项目报告生成', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (8, '您希望如何利用大模型提升项目管理效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动生成立项报告、进度报告、总结报告等', FALSE),
(LAST_INSERT_ID(), 'B', '风险预测与预警', FALSE),
(LAST_INSERT_ID(), 'C', '项目资料自动化整理', FALSE),
(LAST_INSERT_ID(), 'D', '知识库管理', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
-- 保险专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (9, '理赔处理中的主要瓶颈是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '理赔文档处理', FALSE),
(LAST_INSERT_ID(), 'B', '医疗票据审核与核对', FALSE),
(LAST_INSERT_ID(), 'C', '客户资料信息录入与处理', FALSE),
(LAST_INSERT_ID(), 'D', '理赔规则理解与应用', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (10, '大模型可以优化哪些保险工作环节:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '新员工入职培训', FALSE),
(LAST_INSERT_ID(), 'B', '保险产品设计的优化', FALSE),
(LAST_INSERT_ID(), 'C', '自动生成理赔报告与告知书', FALSE),
(LAST_INSERT_ID(), 'D', '自动化资料审核如OCR识别票据数据、自动识别既往症等', FALSE),
(LAST_INSERT_ID(), 'E', '异常案件智能预警', FALSE),
(LAST_INSERT_ID(), 'F', '其他', TRUE);
-- 财务专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (11, '日常工作中最重复的任务是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '财务数据整理与报表生成', FALSE),
(LAST_INSERT_ID(), 'B', '发票和报销单审核', FALSE),
(LAST_INSERT_ID(), 'C', '财务审计与合规检查', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (12, '大模型能如何协助提升财务工作效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '各种报表格式的自动转换', FALSE),
(LAST_INSERT_ID(), 'B', '自动生成财务报表与分析摘要', FALSE),
(LAST_INSERT_ID(), 'C', '自动化审计和合规检查', FALSE),
(LAST_INSERT_ID(), 'D', '财务数据智能分析与预测', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
-- 客服专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (13, '客户咨询中最常遇到的重复性问题:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '参保资格咨询', FALSE),
(LAST_INSERT_ID(), 'B', '理赔进度查询', FALSE),
(LAST_INSERT_ID(), 'C', '材料补交通知', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (14, '您希望大模型如何辅助客服工作:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动生成客户回复模板', FALSE),
(LAST_INSERT_ID(), 'B', '客户咨询自动分类与转接', FALSE),
(LAST_INSERT_ID(), 'C', '智能分析客户情绪与需求', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
-- 运营专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (15, '在运营工作中,最需要自动化支持的任务是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '热点讯息的获取和跟踪', FALSE),
(LAST_INSERT_ID(), 'B', '数据分析与报告生成', FALSE),
(LAST_INSERT_ID(), 'C', '社交媒体内容创作', FALSE),
(LAST_INSERT_ID(), 'D', '活动效果评估与预测', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (16, '大模型可以如何帮助提升运营效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动抓取热点讯息', FALSE),
(LAST_INSERT_ID(), 'B', '自动生成社交媒体内容', FALSE),
(LAST_INSERT_ID(), 'C', '用户评论分析与舆情监测', FALSE),
(LAST_INSERT_ID(), 'D', '活动数据自动分析与报告生成', FALSE),
(LAST_INSERT_ID(), 'E', '其他', TRUE);
-- 市场拓展专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (17, '在市场拓展和商务沟通中,您最常遇到的挑战是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '市场分析和竞争对手跟踪', FALSE),
(LAST_INSERT_ID(), 'B', '渠道拓展计划的自动化和优化', FALSE),
(LAST_INSERT_ID(), 'C', '商务沟通中的信息处理与反馈跟踪', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (18, '您希望大模型如何帮助提升市场拓展效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动生成市场分析报告与趋势预测', FALSE),
(LAST_INSERT_ID(), 'B', '根据目标客户数据生成个性化营销策略', FALSE),
(LAST_INSERT_ID(), 'C', '自动化生成商务沟通邮件和提案文档', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
-- 人力资源专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (19, '人事部门最耗时的日常任务是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '招聘简历筛选与面试安排', FALSE),
(LAST_INSERT_ID(), 'B', '员工培训与学习进度管理', FALSE),
(LAST_INSERT_ID(), 'C', '绩效评估与报告生成', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (20, '您希望大模型如何协助提升人事工作效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动筛选招聘简历并推荐候选人', FALSE),
(LAST_INSERT_ID(), 'B', '自动化培训内容推送与学习路径规划', FALSE),
(LAST_INSERT_ID(), 'C', '绩效评估与员工反馈的自动化分析', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
-- 综合管理专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (21, '在行政工作中,最耗时的任务是:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '合同审查与管理', FALSE),
(LAST_INSERT_ID(), 'B', '会议纪要整理', FALSE),
(LAST_INSERT_ID(), 'C', '文档管理与更新', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (22, '您希望大模型如何协助提升行政工作效率:', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '自动生成合同和协议模板', FALSE),
(LAST_INSERT_ID(), 'B', '自动化会议纪要整理与分发', FALSE),
(LAST_INSERT_ID(), 'C', '自动化文档归档与管理', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
-- 公司高管专属问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (23, '您认为大模型在哪些战略层面的决策中可以发挥作用?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '市场趋势预测与分析', FALSE),
(LAST_INSERT_ID(), 'B', '组织结构优化与调整', FALSE),
(LAST_INSERT_ID(), 'C', '业务流程优化与重组', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (24, '在公司管理工作中,您最希望大模型协助哪些任务?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`) VALUES
(LAST_INSERT_ID(), 'A', '数据分析与报告自动生成', FALSE),
(LAST_INSERT_ID(), 'B', '战略规划与方案优化', FALSE),
(LAST_INSERT_ID(), 'C', '业务协同与跨部门信息流通', FALSE),
(LAST_INSERT_ID(), 'D', '其他', TRUE);
-- 开放建议问题
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (25, '您对大模型培训的具体期待:', 'TEXT', FALSE, FALSE);
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (26, '您认为公司引入AI需提前防范的风险', 'TEXT', FALSE, TRUE);
-- 设置问题跳转逻辑
UPDATE `question`
SET `next` = JSON_OBJECT(
'A', 5, -- 研发部门跳转到问题5
'B', 7, -- 项目管理跳转到问题7
'C', 9, -- 保险部门跳转到问题9
'D', 11, -- 财务部门跳转到问题11
'E', 13, -- 客服部门跳转到问题13
'F', 15, -- 运营部门跳转到问题15
'G', 17, -- 市场拓展跳转到问题17
'H', 19, -- 人力资源跳转到问题19
'I', 21, -- 综合管理跳转到问题21
'J', 23 -- 公司高管跳转到问题23
)
WHERE `number` = 4;
-- 设置每个领域最后一题的跳转逻辑
-- 研发部门
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 6;
-- 项目管理
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 8;
-- 保险部门
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 10;
-- 财务部门
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 12;
-- 客服部门
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 14;
-- 运营部门
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 16;
-- 市场拓展
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 18;
-- 人力资源
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 20;
-- 综合管理
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 22;
-- 公司高管
UPDATE `question` SET `next` = JSON_OBJECT('*', 25) WHERE `number` = 24;
-- 第一个开放性问题跳转到第二个
UPDATE `question` SET `next` = JSON_OBJECT('*', 26) WHERE `number` = 25;
-- 设置最后一题标记
UPDATE `question` SET `is_last` = TRUE WHERE `number` = 26;

44
deploy-local.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
# 获取脚本所在目录的绝对路径
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 设置Docker数据目录
DOCKER_DATA_DIR="/Volumes/working/docker"
TOMCAT_WEBAPPS_DIR="$DOCKER_DATA_DIR/tomcat/webapps"
TOMCAT_LOGS_DIR="$DOCKER_DATA_DIR/tomcat/logs/llm-survey-api"
echo "=== 开始部署 ==="
# 1. 进入后端项目目录
cd "$SCRIPT_DIR/backend" || exit 1
echo "✓ 已切换到后端项目目录"
# 2. 清理并打包项目
echo "正在打包后端项目..."
mvn clean package -DskipTests
if [ $? -ne 0 ]; then
echo "✗ Maven打包失败"
exit 1
fi
echo "✓ Maven打包成功"
# 3. 复制WAR包到Tomcat的webapps目录
echo "正在复制WAR包到Tomcat..."
cp target/llm-survey-api.war "$TOMCAT_WEBAPPS_DIR/"
if [ $? -ne 0 ]; then
echo "✗ WAR包复制失败"
exit 1
fi
echo "✓ WAR包复制成功"
# 4. 等待部署完成
echo "等待应用部署完成..."
sleep 5
# 5. 检查部署日志
echo "检查业务日志..."
tail "$TOMCAT_LOGS_DIR/app.log"
echo "=== 部署完成 ==="
echo "请访问 http://localhost:18080/llm-survey-api 验证部署结果"

74
doc/deployment.md Normal file
View File

@ -0,0 +1,74 @@
# 部署文档
## 1. 开发环境配置
### 1.1 环境变量
- `DOCKER_DATA_DIR`: Docker数据目录默认为 `/Volumes/working/docker`
### 1.2 开发环境组件
- Tomcat: 运行在Docker容器中
- 数据目录: `$DOCKER_DATA_DIR/tomcat`
- 日志目录: `$DOCKER_DATA_DIR/tomcat/logs`
- 应用目录: `$DOCKER_DATA_DIR/tomcat/webapps`
## 2. 项目构建
### 2.1 编译打包
```bash
# 进入后端项目目录
cd backend
# 清理并打包项目(跳过测试)
mvn clean package -DskipTests
# 打包结果
# - WAR包位置backend/target/llm-survey-api.war
```
### 2.2 数据库初始化
```bash
# 进入数据库脚本目录
cd database
# 添加执行权限
chmod +x init_database.sh
# 执行初始化脚本
./init_database.sh
# 初始化内容
# - 创建数据库llm_survey
# - 创建表users, questions, options, survey_responses
# - 插入基础数据:工作领域和岗位性质相关的问题和选项
```
## 3. 开发环境部署
### 3.1 部署WAR包
```bash
# 复制WAR包到Tomcat的webapps目录
cp backend/target/llm-survey-api.war $DOCKER_DATA_DIR/tomcat/webapps/
# 部署后的访问地址
# - 上下文路径:/llm-survey-api
# - API基础路径/llm-survey-api/api
```
### 3.2 查看部署结果
```bash
# 查看Tomcat日志
tail -f $DOCKER_DATA_DIR/tomcat/logs/catalina.out
# 检查应用是否成功部署
ls -l $DOCKER_DATA_DIR/tomcat/webapps/llm-survey-api/
```
### 3.3 验证部署
- 访问测试接口:`http://localhost:8080/llm-survey-api/user/check/13800000000`
- 预期返回:`false`(表示手机号未注册)
## 4. 注意事项
1. 确保MySQL服务已启动且能够通过localhost:3306访问
2. 确保Tomcat容器已启动且8080端口可访问
3. 部署前确保数据库已正确初始化
4. 如需重新部署可直接覆盖webapps目录下的WAR包Tomcat会自动重新部署

View File

@ -2,155 +2,213 @@
--- ---
## 第一部分:基本信息(必填) ## 第一部分:通用认知调研(必答)
1. 您所属部门:
A. 研发部
B. 项目部
C. 保险部
D. 财务部
E. 客服部
F. 运营部
G. 综合管理部
2. 岗位性质:
A. 管理岗
B. 技术岗
C. 业务岗
D. 职能支持岗
---
## 第二部分:通用认知调研(必答)
1. 您对大模型如ChatGPT、通义千问、DeepSeek的了解程度 1. 您对大模型如ChatGPT、通义千问、DeepSeek的了解程度
A. 从未接触过 A. 从未接触过
B. 仅简单使用过通用功能(如问答) B. 仅在日常简单使用过通用功能(如问答)
C. 在工作中尝试过基础应用 C. 在工作中尝试过基础应用
D. 深度研究过技术原理 D. 深度研究过技术原理
2. 您认为以下哪些业务场景最需要效率提升?(多选) 2. 您觉得大模型可以做到下面哪些事?(多选)
A. 文档撰写/报告生成 A. 精准知识问答
B. 数据清洗与分析 B. 文档撰写/报告生成/代码编写/图片视频生成等
C. 客户沟通与服务 C. 数据清洗与分析
D. 风险识别与预警 D. 客户沟通与服务
E. 流程自动化 E. 风险识别与预警
F. 其他____________ F. 流程自动化
G. 如有其他答案请详细说明____________
3. 您最关注大模型应用的哪些风险?(多选) 3. 您最关注大模型应用的哪些风险?(多选)
A. 数据隐私泄露 A. 数据隐私泄露
B. 生成内容不准确 B. 生成内容不准确
C. 合规审查风险 C. 合规审查风险
D. 技术使用门槛高 D. 技术使用门槛高
E. 其他____________ (需填写内容) E. 如有其他答案请详细说明____________ (需填写内容)
4. 您的主要工作内容是:
A. 研发(产品、开发、算法、测试、运维等)
B. 项目管理(项目立项、进度跟踪、风险管理等)
C. 保险(产品、核保、理赔、精算等)
D. 财务(会计、税务、审计等)
E. 客服(咨询、投诉、回访等)
F. 运营(新媒体运营、广告宣传、活动策划、数据分析等)
G. 市场拓展(渠道拓展、商务沟通、产品推广等)
H. 人力资源(招聘、培训、绩效、薪酬等)
I. 综合事务(行政、法务等)
J. 公司高管(战略规划、组织架构、制度建设等)
--- ---
## 第三部分:部门专属问题 ## 第二部分:领域专属问题
**(系统将根据第一部分选择的部门自动跳转)** **(系统将根据第4题选择的工作领域自动跳转**
### 研发部 ### 研发
4. 您在开发过程中最耗时的重复性工作: 5. 您在开发过程中最耗时的重复性工作:(多选)
A. 保险条款文档编写 A. 文档编写(如需求文档、技术文档等)
B. 医疗知识图谱维护 B. 产品原型、界面设计(如使用图片生成模型自动生成等)
C. API接口调试 C. 代码编写
D. 其他____________ D. 调试与测试
E. 系统监控与维护
F. 如有其他答案请详细说明____________
5. 您希望大模型如何与现有系统集成: 6. 您希望大模型如何与现有系统集成:(多选)
A. 自动生成代码片段如DeepSeek-Coder A. 回答技术问题
B. 智能测试用例生成(如通义千问测试场景模拟) B. 自动生成代码片段(如 Github Copilot等
C. 需求文档结构化如ChatGPT生成PRD框架 C. 智能测试用例生成
D. 其他____________ D. 生成需求文档、技术文档
E. 完整项目生成如Cursor等
F. 代码重构与优化
G. 辅助设计算法如DeepSeek等
H. 如有其他答案请详细说明____________
### 项目部 ### 项目管理
6. 惠民保产品设计中最需要数据支持的环节:
A. 参保人群画像分析
B. 竞品方案快速解析(如通义千问政策解读)
C. 定价模型优化
D. 其他____________
7. 您希望如何用大模型提升方案输出效率: 7. 项目管理中最常遇到的挑战是:
A. 自动生成PPT框架ChatGPT生成大纲 A. 项目进度跟踪与更新
B. 从政策文件中提取关键条款DeepSeek语义解析 B. 风险评估与管控
C. 风险测算报告自动化 C. 项目报告生成
D. 其他____________ D. 如有其他答案,请详细说明____________
### 保险部 8. 您希望如何利用大模型提升项目管理效率:
8. 理赔处理中的主要效率瓶颈: A. 自动生成立项报告、进度报告、总结报告等
A. 材料完整性核验 B. 风险预测与预警
B. 医疗票据信息提取如OCR+通义千问核对) C. 项目资料自动化整理
C. 案件风险分级 D. 知识库管理
D. 其他____________ E. 如有其他答案请详细说明____________
9. 您认为大模型可优化的理赔环节: ### 保险
A. 自动生成理赔告知书ChatGPT模板生成
B. 异常案件预警提示DeepSeek数据分析
C. 保险规则智能问答
D. 其他____________
### 财务部 9.理赔处理中的主要瓶颈是:
10. 日常工作中重复性最高的任务: A. 理赔文档处理
A. 发票信息录入核对 B. 医疗票据审核与核对
B. 报销单据合规审查 C. 客户资料信息录入与处理
C. 财务报表数据汇总 D. 理赔规则理解与应用
D. 其他____________ E. 如有其他答案请详细说明____________
11. 您希望大模型协助完成的财务工作: 10. 大模型可以优化哪些保险工作环节:
A. 自动提取票据关键字段通义千问OCR识别 A. 新员工入职培训
B. 生成财务分析摘要ChatGPT文本总结 B. 保险产品设计的优化
C. 异常收支模式检测DeepSeek风险预测 B. 自动生成理赔报告与告知书
D. 其他____________ C. 自动化资料审核如OCR识别票据数据、自动识别既往症等
D. 异常案件智能预警
E. 如有其他答案请详细说明____________
### 客服部 ### 财务
12. 客户咨询中最常遇到的重复性问题:
A. 理赔进度查询 11. 日常工作中最重复的任务是:
B. 参保资格咨询 A. 财务数据整理与报表生成
B. 发票和报销单审核
C. 财务审计与合规检查
D. 如有其他答案请详细说明____________
12.大模型能如何协助提升财务工作效率:
A. 各种报表格式的自动转换
B. 自动生成财务报表与分析摘要
C. 自动化审计和合规检查
D. 财务数据智能分析与预测
E. 如有其他答案请详细说明____________
### 客服
13. 客户咨询中最常遇到的重复性问题:
A. 参保资格咨询
B. 理赔进度查询
C. 材料补交通知 C. 材料补交通知
D. 其他____________ D. 如有其他答案,请详细说明____________
13. 您希望大模型如何辅助客服工作: 14. 您希望大模型如何辅助客服工作:
A. 自动生成个性化回复话术ChatGPT对话生成 A. 自动生成客户回复模板
B. 客户情绪实时识别(通义千问情感分析) B. 客户咨询自动分类与转接
C. 咨询问题自动分类派单DeepSeek意图分类 C. 智能分析客户情绪与需求
D. 其他____________ D. 如有其他答案,请详细说明____________
### 运营部 ### 运营
14. 运营数据分析中的主要痛点:
A. 多平台数据整合
B. 参保率预测模型优化DeepSeek时序预测
C. 宣传文案创意生成(通义千问文案生成)
D. 其他____________
15. 您希望大模型赋能的运营场景: 15. 在运营工作中,最需要自动化支持的任务是:
A. 自动生成社交媒体图文ChatGPT+通义万相) A. 热点讯息的获取和跟踪
B. 用户评论情感分析DeepSeek语义分析 B. 数据分析与报告生成
C. 活动效果模拟预测 C. 社交媒体内容创作
D. 其他____________ D. 活动效果评估与预测
D. 如有其他答案请详细说明____________
### 综合管理部 16.大模型可以如何帮助提升运营效率:
16. 日常工作中最耗时的行政事务: A. 自动抓取热点讯息
A. 合同条款审查 B. 自动生成社交媒体内容
C. 用户评论分析与舆情监测
D. 活动数据自动分析与报告生成
E. 如有其他答案请详细说明____________
### 市场拓展
17. 在市场拓展和商务沟通中,您最常遇到的挑战是:
A. 市场分析和竞争对手跟踪
B. 渠道拓展计划的自动化和优化
C. 商务沟通中的信息处理与反馈跟踪
D. 如有其他答案请详细说明____________
18.您希望大模型如何帮助提升市场拓展效率:
A. 自动生成市场分析报告与趋势预测
B. 根据目标客户数据生成个性化营销策略
C. 自动化生成商务沟通邮件和提案文档
D. 如有其他答案请详细说明____________
### 人力资源
19. 人事部门最耗时的日常任务是:
A. 招聘简历筛选与面试安排
B. 员工培训与学习进度管理
C. 绩效评估与报告生成
D. 如有其他答案请详细说明____________
20. 您希望大模型如何协助提升人事工作效率:
A. 自动筛选招聘简历并推荐候选人
B. 自动化培训内容推送与学习路径规划
C. 绩效评估与员工反馈的自动化分析
D. 如有其他答案请详细说明____________
### 综合管理
21. 在行政工作中,最耗时的任务是:
A. 合同审查与管理
B. 会议纪要整理 B. 会议纪要整理
C. 制度文档更新 C. 文档管理与更新
D. 其他____________ D. 如有其他答案,请详细说明____________
17. 您希望大模型协助完成的行政工作: 22. 您希望大模型如何协助提升行政工作效率:
A. 自动生成招投标文件模板ChatGPT框架生成 A. 自动生成合同和协议模板
B. 合同风险点智能排查(通义千问法律审查) B. 自动化会议纪要整理与分发
C. 流程说明书自动更新DeepSeek版本迭代 C. 自动化文档归档与管理
D. 其他____________ D. 如有其他答案请详细说明____________
### 公司高管
23. 您认为大模型在哪些战略层面的决策中可以发挥作用?
A. 市场趋势预测与分析
B. 组织结构优化与调整
C. 业务流程优化与重组
D. 如有其他答案请详细说明____________
24. 在公司管理工作中,您最希望大模型协助哪些任务?
A. 数据分析与报告自动生成
B. 战略规划与方案优化
C. 业务协同与跨部门信息流通
D. 如有其他答案请详细说明____________
--- ---
## 第四部分:开放建议(选填) ## 第四部分:开放建议(选填)
18. 您对大模型培训的具体期待: 25. 您对大模型培训的具体期待:
____________________________ ____________________________
19. 您认为公司引入AI需提前防范的风险 26. 您认为公司引入AI需提前防范的风险
____________________________ ____________________________
--- ---
### 问卷说明 ### 问卷说明
20. **逻辑跳转**系统将根据所选部门显示专属问题总题量约10-12题。
21. **工具示例** - **逻辑跳转**系统将根据第1题的答案显示专属问题总题量约10-12题。
- **工具示例**
- 通用大模型ChatGPTOpenAI、通义千问阿里云、DeepSeek深度求索 - 通用大模型ChatGPTOpenAI、通义千问阿里云、DeepSeek深度求索
22. **提交方式**匿名填写预计耗时5-8分钟。 - **提交方式**匿名填写预计耗时5-8分钟。

14
frontend/.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
/* eslint-env node */
module.exports = {
root: true,
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier'],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'vue/multi-word-component-names': 'off',
'vue/no-v-model-argument': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
};

7
frontend/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100,
"trailingComma": "es5"
}

Binary file not shown.

2
frontend/.yarnrc.yml Normal file
View File

@ -0,0 +1,2 @@
nodeLinker: node-modules
enableGlobalCache: true

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
<title>LLM 问卷调查</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

38
frontend/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "llm-survey",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.6.7",
"pinia": "^2.1.7",
"vant": "^4.8.3",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0",
"esbuild": "^0.25.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.21.1",
"less": "^4.2.0",
"postcss-px-to-viewport": "^1.1.1",
"prettier": "^3.2.5",
"rollup": "^4.34.8",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.12"
},
"packageManager": "yarn@4.6.0"
}

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

27
frontend/src/App.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</template>
<script setup>
//
</script>
<style>
/* 全局样式 */
html,
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100%;
}
#app {
height: 100%;
}
</style>

View File

@ -0,0 +1,56 @@
import request from '@/utils/request';
// 获取用户的问题列表
export function getUserQuestions(userId) {
return request({
url: `/question/user/${userId}`,
method: 'get',
});
}
// 获取问题的选项列表
export function getQuestionOptions(questionId) {
return request({
url: `/question/${questionId}/option`,
method: 'get',
});
}
// 获取下一个问题
export function getNextQuestion(userId, selectedOptions, currentQuestionNumber) {
console.log('API调用参数:', { userId, selectedOptions, currentQuestionNumber });
const params = {
userId
};
// 只有在不是第一次请求时才添加这些参数
if (selectedOptions) {
params.selectedOptions = selectedOptions.join(',');
}
if (currentQuestionNumber !== undefined) {
params.currentQuestionNumber = currentQuestionNumber;
}
return request({
url: '/question/next',
method: 'get',
params
});
}
// 提交问卷答案
export function submitSurvey(userId, responses) {
return request({
url: `/survey/submit/${userId}`,
method: 'post',
data: responses,
});
}
// 获取用户的问卷答案
export function getUserResponses(userId) {
return request({
url: `/survey/user/${userId}`,
method: 'get',
});
}

35
frontend/src/api/user.js Normal file
View File

@ -0,0 +1,35 @@
import request from '@/utils/request';
// 用户注册
export function register(data) {
return request({
url: '/user/register',
method: 'post',
data,
});
}
// 更新用户信息
export function updateUser(data) {
return request({
url: `/user/${data.id}`,
method: 'put',
data,
});
}
// 根据手机号获取用户信息
export function getUserInfoByPhone(phone) {
return request({
url: `/user/phone/${phone}`,
method: 'get',
});
}
// 根据ID获取用户信息
export function getUserInfo(id) {
return request({
url: `/user/${id}`,
method: 'get',
});
}

12
frontend/src/main.js Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import 'vant/lib/index.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');

View File

@ -0,0 +1,49 @@
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/register',
name: 'register',
component: () => import('@/views/RegisterView.vue'),
},
{
path: '/survey',
name: 'survey',
component: () => import('@/views/SurveyView.vue'),
meta: { requiresAuth: true },
},
{
path: '/completed',
name: 'completed',
component: () => import('@/views/CompletedView.vue'),
meta: { requiresAuth: true },
},
],
});
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userId = localStorage.getItem('userId');
// 如果是从注册页面来的,允许直接进入需要认证的页面
if (from.name === 'register' && to.meta.requiresAuth) {
next();
return;
}
// 其他情况下检查认证
if (to.meta.requiresAuth && !userId) {
next({ name: 'register' });
} else {
next();
}
});
export default router;

View File

@ -0,0 +1,135 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import {
getUserQuestions,
getQuestionOptions,
getNextQuestion,
submitSurvey,
getUserResponses
} from '@/api/survey';
import { showToast } from 'vant';
export const useSurveyStore = defineStore('survey', () => {
const currentQuestion = ref(null);
const questionOptions = ref([]);
const userResponses = ref([]);
const currentQuestionNumber = ref(null);
const currentOptions = ref([]);
const isCompleted = ref(false);
// 获取用户的问题列表
async function fetchUserQuestions(userId) {
try {
const response = await getUserQuestions(userId);
return response;
} catch (error) {
showToast('获取问题列表失败:' + error.message);
throw error;
}
}
// 获取问题的选项列表
async function fetchQuestionOptions(questionId) {
try {
console.log('获取问题选项问题ID:', questionId);
const options = await getQuestionOptions(questionId);
console.log('获取到的选项:', options);
currentOptions.value = options;
return options;
} catch (error) {
console.error('获取问题选项失败:', error);
showToast('获取问题选项失败:' + error.message);
throw error;
}
}
// 获取下一个问题
async function fetchNextQuestion(userId, selectedOptions, targetQuestionNumber) {
try {
console.log('获取问题,参数:', { userId, selectedOptions, targetQuestionNumber, currentQuestionNumber: currentQuestionNumber.value });
let question;
if (targetQuestionNumber) {
// 如果指定了目标问题号,直接获取该问题
const questions = await getUserQuestions(userId);
question = questions.find(q => q.number === targetQuestionNumber);
} else {
// 否则获取下一题
question = await getNextQuestion(userId, selectedOptions, currentQuestionNumber.value);
}
console.log('获取到的问题:', question);
if (question) {
currentQuestion.value = question;
currentQuestionNumber.value = question.number;
// 获取问题的选项
console.log('开始获取问题选项');
const options = await getQuestionOptions(question.id);
console.log('获取到的选项:', options);
currentOptions.value = options;
} else {
console.log('没有更多问题了');
isCompleted.value = true;
}
return question;
} catch (error) {
console.error('获取问题失败:', error);
throw error;
}
}
// 提交问卷答案
async function submitSurveyResponses(userId, responses) {
try {
const response = await submitSurvey(userId, responses);
userResponses.value = responses;
return response;
} catch (error) {
showToast('提交问卷失败:' + error.message);
throw error;
}
}
// 获取用户的问卷答案
async function fetchUserResponses(userId) {
try {
const response = await getUserResponses(userId);
userResponses.value = response;
return response;
} catch (error) {
showToast('获取问卷答案失败:' + error.message);
throw error;
}
}
// 重置问卷状态
function resetSurvey() {
currentQuestion.value = null;
questionOptions.value = [];
userResponses.value = [];
currentQuestionNumber.value = null;
currentOptions.value = [];
isCompleted.value = false;
// 清除本地存储中的问卷相关数据
localStorage.removeItem('surveyProgress');
localStorage.removeItem('surveyResponses');
}
return {
currentQuestion,
questionOptions,
userResponses,
currentQuestionNumber,
currentOptions,
isCompleted,
fetchUserQuestions,
fetchQuestionOptions,
fetchNextQuestion,
submitSurveyResponses,
fetchUserResponses,
resetSurvey,
};
});

109
frontend/src/stores/user.js Normal file
View File

@ -0,0 +1,109 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { register, getUserInfo, getUserInfoByPhone, updateUser } from '@/api/user';
import { showToast } from 'vant';
import { useSurveyStore } from './survey';
export const useUserStore = defineStore('user', () => {
const userId = ref(localStorage.getItem('userId') || '');
const userInfo = ref(null);
// 用户登录/注册
async function loginOrRegister(data) {
try {
console.log('发送登录/注册请求,数据:', data);
const response = await getUserInfoByPhone(data.phone);
let userData;
if (response) {
// 用户存在,更新信息
console.log('用户已存在,更新信息');
userData = await updateUser({
...response,
name: data.name,
positionType: data.positionType
});
} else {
// 用户不存在,注册新用户
console.log('用户不存在,创建新用户');
userData = await register(data);
}
console.log('用户数据:', userData);
if (!userData || !userData.id) {
throw new Error('登录失败:无效的用户数据');
}
// 保存用户信息
userId.value = String(userData.id);
userInfo.value = userData;
localStorage.setItem('userId', userId.value);
console.log('用户信息已保存userId:', userId.value);
return userData;
} catch (error) {
console.error('登录/注册请求失败:', error);
if (error.response) {
console.error('错误响应:', error.response);
}
const message = error.response?.data?.message || error.message || '登录失败,请稍后重试';
showToast(message);
throw error;
}
}
// 获取用户信息
async function fetchUserInfo(id) {
try {
console.log('获取用户信息:', id);
const response = await getUserInfo(id);
console.log('获取用户信息响应:', response);
const userData = response.data || response;
userInfo.value = userData;
return userData;
} catch (error) {
console.error('获取用户信息失败:', error);
if (error.response) {
console.error('错误响应:', error.response);
}
const message = error.response?.data?.message || error.message || '获取用户信息失败,请稍后重试';
showToast(message);
throw error;
}
}
// 根据手机号获取用户信息
async function getUserInfoByPhoneNumber(phone) {
try {
console.log('根据手机号获取用户信息:', phone);
const response = await getUserInfoByPhone(phone);
console.log('获取用户信息响应:', response);
return response;
} catch (error) {
console.error('获取用户信息失败:', error);
if (error.response) {
console.error('错误响应:', error.response);
}
throw error;
}
}
// 退出登录
function logout() {
const surveyStore = useSurveyStore();
userId.value = null;
userInfo.value = null;
localStorage.removeItem('userId');
surveyStore.resetSurvey();
}
return {
userId,
userInfo,
loginOrRegister,
fetchUserInfo,
getUserInfoByPhone: getUserInfoByPhoneNumber,
logout,
};
});

View File

@ -0,0 +1,57 @@
import axios from 'axios';
import { showToast } from 'vant';
const request = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
console.log('发送请求:', {
url: config.url,
method: config.method,
data: config.data,
headers: config.headers
});
return config;
},
(error) => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
console.log('收到响应:', {
status: response.status,
statusText: response.statusText,
data: response.data,
headers: response.headers
});
return response.data;
},
(error) => {
console.error('响应错误:', {
message: error.message,
config: error.config,
response: error.response ? {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
headers: error.response.headers
} : null
});
const message = error.response?.data?.message || '请求失败,请稍后重试';
showToast(message);
return Promise.reject(error);
}
);
export default request;

View File

@ -0,0 +1,88 @@
<template>
<div class="completed-view">
<van-nav-bar title="问卷完成" />
<div class="content">
<van-empty description="感谢您完成问卷调查!">
<template #image>
<van-icon name="success" size="64" color="#ffffff" />
</template>
<div class="completion-message">
<p>您的反馈对我们非常重要</p>
<p>我们将根据调查结果持续改进和优化</p>
</div>
</van-empty>
<van-button
round
type="primary"
class="home-button"
@click="router.replace('/')"
>
返回首页
</van-button>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
</script>
<style scoped>
.completed-view {
min-height: 100vh;
background-color: #f7f8fa;
}
.content {
padding: 40px 20px;
text-align: center;
}
.completion-message {
margin: 24px 0;
color: #666;
line-height: 1.8;
}
.completion-message p:first-child {
font-size: 20px;
font-weight: bold;
color: #323233;
margin-bottom: 12px;
}
.completion-message p {
margin: 8px 0;
}
.home-button {
margin-top: 32px;
width: 80%;
}
/* 自定义图标样式 */
:deep(.van-empty__image) {
width: 120px !important;
height: 120px !important;
margin-bottom: 24px;
}
:deep(.van-icon) {
background-color: #07c160;
border-radius: 50%;
padding: 16px;
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3);
}
:deep(.van-empty__description) {
color: #323233;
font-size: 22px;
font-weight: bold;
margin-bottom: 8px;
white-space: nowrap;
padding: 0 20px;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="home-view">
<van-nav-bar title="LLM 问卷调查" />
<div class="content">
<van-image
class="logo"
width="200"
height="200"
src="/logo.png"
fit="contain"
/>
<h1 class="title">欢迎参与 LLM 问卷调查</h1>
<p class="description">
本问卷旨在了解智慧医疗员工对大语言模型的使用体验和看法您的反馈对我们非常重要
</p>
<p class="description">
此应用使用Cursor生成所有代码包括数据库脚本前后端代码配置文件等均通过提示词指挥AI生成git提交测试代码打包发布等操作也是通过提示词指挥AI完成
</p>
<p class="description">
此项目开发到上线共计耗时1人5小时传统开发方式需要产品前端后端测试4人12天工作量
</p>
<div class="action-buttons">
<van-button
v-if="!userStore.userId"
round
block
type="primary"
@click="router.push('/register')"
>
开始参与
</van-button>
<template v-else>
<van-button
round
block
type="primary"
@click="surveyStore.isCompleted ? startNewSurvey() : router.push('/survey')"
>
{{ surveyStore.isCompleted ? '重新答题' : '继续答题' }}
</van-button>
<van-button
round
block
type="default"
@click="onLogout"
class="logout-btn"
>
退出登录
</van-button>
</template>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { useSurveyStore } from '@/stores/survey';
import { showToast } from 'vant';
const router = useRouter();
const userStore = useUserStore();
const surveyStore = useSurveyStore();
function onLogout() {
userStore.logout();
showToast('已退出登录');
}
//
function startNewSurvey() {
surveyStore.resetSurvey();
router.push('/survey');
}
</script>
<style scoped>
.home-view {
min-height: 100vh;
background-color: #f7f8fa;
}
.content {
padding: 40px 20px;
text-align: center;
}
.logo {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #323233;
margin-bottom: 16px;
}
.description {
font-size: 16px;
color: #666;
line-height: 1.5;
margin-bottom: 20px;
text-align: left;
}
.action-buttons {
max-width: 300px;
margin: 0 auto;
}
.logout-btn {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<div class="register-view">
<van-nav-bar title="用户登录" />
<van-form @submit="onSubmit" class="register-form">
<van-cell-group inset>
<van-field
v-model="formData.phone"
name="phone"
label="手机号"
placeholder="请输入手机号"
:rules="[
{ required: true, message: '请填写手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
]"
@blur="onPhoneBlur"
/>
<van-field
v-model="formData.name"
name="name"
label="姓名"
placeholder="请输入姓名"
:rules="[{ required: true, message: '请填写姓名' }]"
/>
<van-field
v-model="displayPositionType"
name="positionType"
label="岗位性质"
readonly
clickable
placeholder="请选择岗位性质"
:rules="[{ required: true, message: '请选择岗位性质' }]"
@click="showPositionTypePicker = true"
/>
<!-- eslint-disable-next-line vue/no-v-model-argument -->
<van-popup v-model:show="showPositionTypePicker" position="bottom">
<van-picker
:columns="positionTypeOptions"
@confirm="onPositionTypeConfirm"
@cancel="showPositionTypePicker = false"
show-toolbar
title="选择岗位性质"
/>
</van-popup>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit">
登录
</van-button>
<p class="tip-text">首次登录的用户将自动完成注册</p>
</div>
</van-form>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { showToast, showNotify } from 'vant';
const router = useRouter();
const userStore = useUserStore();
const formData = reactive({
phone: '',
name: '',
positionType: '',
});
const showPositionTypePicker = ref(false);
const displayPositionType = ref('');
const positionTypeOptions = [
{ text: '管理岗', value: 'MANAGEMENT' },
{ text: '技术岗', value: 'TECHNICAL' },
{ text: '业务岗', value: 'BUSINESS' },
{ text: '职能支持岗', value: 'SUPPORT' }
];
//
async function onPhoneBlur() {
if (formData.phone && /^1[3-9]\d{9}$/.test(formData.phone)) {
try {
const userInfo = await userStore.getUserInfoByPhone(formData.phone);
if (userInfo) {
//
formData.name = userInfo.name;
formData.positionType = userInfo.positionType;
displayPositionType.value = positionTypeOptions.find(
opt => opt.value === userInfo.positionType
)?.text || '';
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
}
//
function onPositionTypeConfirm({ selectedOptions }) {
formData.positionType = selectedOptions[0].value;
displayPositionType.value = selectedOptions[0].text;
showPositionTypePicker.value = false;
}
//
async function onSubmit() {
try {
console.log('开始注册,表单数据:', formData);
const response = await userStore.loginOrRegister(formData);
console.log('注册成功,响应数据:', response);
// 使 showNotify
showNotify({
type: 'success',
message: '注册成功',
duration: 2000,
position: 'top'
});
//
setTimeout(() => {
console.log('准备跳转到问卷页面');
router.push({
name: 'survey',
replace: true
}).catch(err => {
console.error('路由跳转失败:', err);
showToast('页面跳转失败,请刷新重试');
});
}, 500);
} catch (error) {
console.error('注册失败:', error);
showToast(error.message || '注册失败,请重试');
}
}
</script>
<style scoped>
.register-view {
min-height: 100vh;
background-color: #f7f8fa;
}
.register-form {
padding: 20px;
}
.submit-btn {
margin: 16px;
}
.tip-text {
font-size: 14px;
color: #999;
text-align: center;
margin-top: 12px;
}
</style>

View File

@ -0,0 +1,346 @@
<template>
<div class="survey-view">
<van-nav-bar
title="问卷调查"
left-text="返回"
left-arrow
@click-left="onBack"
/>
<div class="survey-content">
<div v-if="surveyStore.currentQuestion" class="question-container">
<h2 class="question-title">
{{ surveyStore.currentQuestionNumber }}. {{ surveyStore.currentQuestion.content }}
<span v-if="surveyStore.currentQuestion.type === 'MULTIPLE_CHOICE'" class="question-type">(多选)</span>
</h2>
<van-checkbox-group v-if="surveyStore.currentQuestion.type === 'MULTIPLE_CHOICE'" v-model="selectedOptions" class="options-container">
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.code"
clickable
@click="toggleOption(option.code)"
>
<template #title>
<span>{{ option.code }}. {{ option.content }}</span>
<template v-if="option.requiresText && selectedOptions.includes(option.code)">
<van-field
v-model="textAnswer"
type="text"
placeholder="请输入具体内容"
class="option-text-input"
@click.stop
/>
</template>
</template>
<template #right-icon>
<van-checkbox
:name="option.code"
@click.stop
/>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-radio-group v-else-if="surveyStore.currentQuestion.type === 'SINGLE_CHOICE'" v-model="selectedOption" class="options-container">
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.code"
clickable
@click="selectedOption = option.code"
>
<template #title>
<span>{{ option.code }}. {{ option.content }}</span>
<template v-if="option.requiresText && selectedOption === option.code">
<van-field
v-model="textAnswer"
type="text"
placeholder="请输入具体内容"
class="option-text-input"
@click.stop
/>
</template>
</template>
<template #right-icon>
<van-radio :name="option.code" />
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
<div v-else-if="surveyStore.currentQuestion.type === 'TEXT'" class="text-input-container">
<van-field
v-model="textAnswer"
type="textarea"
rows="4"
autosize
placeholder="请输入您的答案"
/>
</div>
<div class="button-container">
<van-button
type="primary"
block
:disabled="!isValidSelection"
@click="onNextQuestion"
>
{{ surveyStore.currentQuestion?.isLast ? '完成' : '下一题' }}
</van-button>
</div>
</div>
<van-empty v-else description="加载中..." />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { useSurveyStore } from '@/stores/survey';
import { showToast } from 'vant';
const router = useRouter();
const userStore = useUserStore();
const surveyStore = useSurveyStore();
const selectedOption = ref('');
const selectedOptions = ref([]);
const textAnswer = ref('');
const responses = ref([]);
//
const isValidSelection = computed(() => {
const questionType = surveyStore.currentQuestion?.type;
if (!questionType) return false;
if (questionType === 'MULTIPLE_CHOICE') {
//
const needsText = selectedOptions.value.some(code => {
const option = surveyStore.currentOptions.find(opt => opt.code === code);
return option?.requiresText;
});
return selectedOptions.value.length > 0 && (!needsText || (textAnswer.value?.trim() || '').length > 0);
} else if (questionType === 'SINGLE_CHOICE') {
//
const selectedOpt = surveyStore.currentOptions.find(opt => opt.code === selectedOption.value);
return selectedOption.value && (!selectedOpt?.requiresText || (textAnswer.value?.trim() || '').length > 0);
} else if (questionType === 'TEXT') {
return (textAnswer.value?.trim() || '').length > 0;
}
return false;
});
//
function toggleOption(code) {
const index = selectedOptions.value.indexOf(code);
if (index === -1) {
selectedOptions.value.push(code);
} else {
selectedOptions.value.splice(index, 1);
}
}
//
function onBack() {
router.back();
}
//
async function onNextQuestion() {
const questionType = surveyStore.currentQuestion.type;
try {
let currentSelection = [];
if (questionType === 'MULTIPLE_CHOICE') {
currentSelection = selectedOptions.value;
} else if (questionType === 'SINGLE_CHOICE') {
currentSelection = [selectedOption.value];
}
//
const currentResponse = {
questionId: surveyStore.currentQuestion.id,
selectedOptions: currentSelection,
textAnswer: textAnswer.value?.trim() || null
};
//
const existingIndex = responses.value.findIndex(r => r.questionId === currentResponse.questionId);
if (existingIndex !== -1) {
responses.value[existingIndex] = currentResponse;
} else {
responses.value.push(currentResponse);
}
//
localStorage.setItem('surveyProgress', JSON.stringify({
currentQuestionNumber: surveyStore.currentQuestionNumber,
responses: responses.value
}));
//
if (surveyStore.currentQuestion.isLast) {
try {
await surveyStore.submitSurveyResponses(userStore.userId, responses.value);
showToast('问卷提交成功');
surveyStore.isCompleted = true;
//
localStorage.removeItem('surveyProgress');
//
router.replace('/completed');
return;
} catch (error) {
console.error('提交问卷失败:', error);
showToast(error.response?.data?.message || '提交失败,请重试');
return;
}
}
//
try {
await surveyStore.fetchNextQuestion(userStore.userId, currentSelection);
//
selectedOption.value = '';
selectedOptions.value = [];
textAnswer.value = '';
} catch (error) {
console.error('获取下一题失败:', error);
showToast(error.response?.data?.message || '获取下一题失败,请重试');
}
} catch (error) {
console.error('问卷处理失败:', error);
showToast(error.response?.data?.message || '操作失败,请重试');
}
}
//
async function initSurvey() {
try {
//
const savedProgress = localStorage.getItem('surveyProgress');
if (savedProgress) {
const progress = JSON.parse(savedProgress);
responses.value = progress.responses || [];
//
const lastResponse = responses.value[responses.value.length - 1];
if (lastResponse) {
if (lastResponse.selectedOptions.length === 1) {
selectedOption.value = lastResponse.selectedOptions[0];
} else {
selectedOptions.value = lastResponse.selectedOptions;
}
textAnswer.value = lastResponse.textAnswer || '';
}
//
const nextQuestionNumber = progress.currentQuestionNumber + 1;
await surveyStore.fetchNextQuestion(
userStore.userId,
[],
nextQuestionNumber
);
//
if (!surveyStore.currentQuestion) {
await surveyStore.fetchNextQuestion(
userStore.userId,
[],
progress.currentQuestionNumber
);
}
} else {
surveyStore.resetSurvey(); //
responses.value = [];
//
await surveyStore.fetchNextQuestion(userStore.userId);
}
} catch (error) {
console.error('初始化问卷失败:', error);
showToast('初始化问卷失败,请重试');
}
}
//
onMounted(() => {
if (!userStore.userId) {
router.replace('/register');
return;
}
initSurvey();
});
</script>
<style scoped>
.survey-view {
min-height: 100vh;
background-color: #f7f8fa;
}
.survey-content {
padding: 20px;
}
.question-container {
margin-bottom: 20px;
}
.question-title {
font-size: 18px;
font-weight: bold;
line-height: 1.5;
margin-bottom: 16px;
padding: 0 16px;
}
.options-container {
margin-top: 20px;
}
.button-container {
margin-top: 24px;
padding: 0 16px;
}
.survey-completed {
padding: 40px 20px;
text-align: center;
}
.restart-button {
margin-top: 20px;
width: 80%;
}
.question-type {
font-size: 14px;
color: #666;
margin-left: 8px;
}
.text-input-container {
padding: 16px;
background-color: #fff;
border-radius: 8px;
margin: 0 16px;
}
.completion-message {
margin: 16px 0;
color: #666;
line-height: 1.6;
}
.home-button {
margin-top: 24px;
width: 80%;
}
.option-text-input {
margin-top: 8px;
background-color: #f8f8f8;
border-radius: 4px;
}
</style>

48
frontend/vite.config.js Normal file
View File

@ -0,0 +1,48 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
import { fileURLToPath, URL } from 'node:url';
import postcssPxToViewport from 'postcss-px-to-viewport';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// 自动导入 Vant 组件
Components({
resolvers: [VantResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
css: {
postcss: {
plugins: [
// 移动端适配
postcssPxToViewport({
viewportWidth: 375, // 设计稿宽度
viewportUnit: 'vw',
fontViewportUnit: 'vw',
selectorBlackList: ['.ignore-'], // 不转换的类名
minPixelValue: 1, // 最小转换数值
mediaQuery: false, // 是否转换媒体查询中的像素值
}),
],
},
},
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:18080/llm-survey-api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});

3595
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

161
pom.xml
View File

@ -1,161 +0,0 @@
<?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>ltd.qubit</groupId>
<artifactId>llm-survey-api</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.3.31</spring.version>
<mybatis.version>3.5.15</mybatis.version>
<mybatis-spring.version>2.1.2</mybatis-spring.version>
<mysql-connector.version>8.3.0</mysql-connector.version>
<hikaricp.version>5.1.0</hikaricp.version>
<jackson.version>2.16.1</jackson.version>
<lombok.version>1.18.30</lombok.version>
<slf4j.version>2.0.11</slf4j.version>
<logback.version>1.4.14</logback.version>
<servlet-api.version>4.0.1</servlet-api.version>
</properties>
<dependencies>
<!-- Spring Framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>llm-survey-api</finalName>
<plugins>
<!-- 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- WAR打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
<!-- 资源文件配置 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

View File

@ -1,78 +0,0 @@
package ltd.qubit.survey.controller;
import lombok.extern.slf4j.Slf4j;
import ltd.qubit.survey.model.ErrorInfo;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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(BindException.class)
public ResponseEntity<ErrorInfo> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.reduce((a, b) -> a + "; " + b)
.orElse("参数错误");
ErrorInfo error = ErrorInfo.of(
"INVALID_ARGUMENT",
"参数校验失败",
message,
e.getObjectName());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
/**
* 处理业务异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorInfo> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("业务异常", e);
ErrorInfo error = ErrorInfo.of(
"BAD_REQUEST",
e.getMessage(),
e.getClass().getName());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
/**
* 处理数据访问异常
*/
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorInfo> handleDataAccessException(DataAccessException e) {
log.error("数据访问异常", e);
ErrorInfo error = ErrorInfo.of(
"DATABASE_ERROR",
"数据库访问错误",
e.getMessage(),
e.getClass().getName());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 处理其他未知异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorInfo> handleUnknownException(Exception e) {
log.error("系统异常", e);
ErrorInfo error = ErrorInfo.of(
"SYSTEM_ERROR",
"系统错误",
e.getMessage(),
e.getClass().getName());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

View File

@ -1,62 +0,0 @@
package ltd.qubit.survey.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.Option;
import ltd.qubit.survey.service.QuestionService;
import ltd.qubit.survey.service.OptionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 问题控制器
*/
@RestController
@RequestMapping("/api/questions")
@RequiredArgsConstructor
public class QuestionController {
private final QuestionService questionService;
private final OptionService optionService;
/**
* 获取用户的问题列表
*
* @param userId 用户ID
* @return 问题列表
*/
@GetMapping("/user/{userId}")
public List<Question> getUserQuestions(@PathVariable Long userId) {
return questionService.getUserQuestions(userId);
}
/**
* 获取问题的选项列表
*
* @param questionId 问题ID
* @return 选项列表
*/
@GetMapping("/{questionId}/options")
public List<Option> getQuestionOptions(@PathVariable Long questionId) {
return optionService.findByQuestionId(questionId);
}
/**
* 获取下一个问题
*
* @param userId 用户ID
* @param currentQuestionNumber 当前问题序号
* @param selectedOptions 选中的选项列表
* @return 下一个问题
*/
@GetMapping("/next")
public Question getNextQuestion(
@PathVariable Long userId,
@PathVariable Integer currentQuestionNumber,
@PathVariable List<String> selectedOptions) {
return questionService.getNextQuestion(userId, currentQuestionNumber, selectedOptions)
.orElseThrow(() -> new IllegalArgumentException("没有更多问题了"));
}
}

View File

@ -1,58 +0,0 @@
package ltd.qubit.survey.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.model.SurveyResponse;
import ltd.qubit.survey.service.SurveyResponseService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 问卷控制器
*/
@RestController
@RequestMapping("/api/survey")
@RequiredArgsConstructor
public class SurveyController {
private final SurveyResponseService surveyResponseService;
/**
* 提交问卷答案
*
* @param userId 用户ID
* @param responses 答案列表
* @return 提交成功的答案列表
*/
@PostMapping("/submit/{userId}")
public List<SurveyResponse> submitSurvey(
@PathVariable Long userId,
@RequestBody List<SurveyResponse> responses) {
return surveyResponseService.submitSurvey(userId, responses);
}
/**
* 获取用户的问卷答案
*
* @param userId 用户ID
* @return 答案列表
*/
@GetMapping("/user/{userId}")
public List<SurveyResponse> getUserResponses(@PathVariable Long userId) {
return surveyResponseService.findByUserId(userId);
}
/**
* 获取问题的所有答案
*
* @param questionId 问题ID
* @return 答案列表
*/
@GetMapping("/question/{questionId}")
public List<SurveyResponse> getQuestionResponses(@PathVariable Long questionId) {
return surveyResponseService.findByQuestionId(questionId);
}
}

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