feat: 完成后端开发,包括项目规则文档、用户管理、问卷管理、答案管理功能,配置 Docker 环境,修复数据库连接配置

This commit is contained in:
胡海星 2025-02-20 16:39:58 +08:00
parent 0852760852
commit 20c57a13d2
127 changed files with 568 additions and 2767 deletions

62
.cursorrules Normal file
View File

@ -0,0 +1,62 @@
# 项目规则文档
## 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进行版本控制
- 遵循语义化版本规范
- 重要配置文件加入版本控制

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# Maven
target/
*.war
*.jar
*.ear
.classpath
.project
.settings/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
dist/
.cache/
# IDE - VSCode
.vscode/
*.code-workspace
.history/
# IDE - IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
# Logs
logs/
*.log
# OS
.DS_Store
Thumbs.db

View File

@ -14,16 +14,18 @@
<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>
<spring.version>6.1.4</spring.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>
<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>
<servlet-api.version>6.0.0</servlet-api.version>
<pagehelper.version>5.3.3</pagehelper.version>
<aspectj.version>1.9.21</aspectj.version>
</properties>
<dependencies>
@ -82,6 +84,13 @@
<version>${jackson.version}</version>
</dependency>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
@ -104,11 +113,18 @@
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
@ -122,6 +138,9 @@
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>

View File

@ -0,0 +1,51 @@
package com.fasterxml.jackson.databind;
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;
/**
* 自定义的 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

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

@ -0,0 +1 @@
<result property="selectedOptions" column="selected_options" typeHandler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"/>

View File

@ -1,6 +1,6 @@
# 数据库配置
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.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

@ -2,7 +2,7 @@
<!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">
<resultMap id="baseOptionMap" type="ltd.qubit.survey.model.Option">
<id property="id" column="id"/>
<result property="questionId" column="question_id"/>
<result property="optionCode" column="option_code"/>
@ -52,21 +52,21 @@
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="optionMap">
<select id="findById" parameterType="long" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="optionMap">
<select id="findAll" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
ORDER BY question_id, option_code
</select>
<!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="optionMap">
<select id="findByQuestionId" parameterType="long" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}
@ -74,7 +74,7 @@
</select>
<!-- 根据问题ID和选项代码查询 -->
<select id="findByQuestionIdAndCode" resultMap="optionMap">
<select id="findByQuestionIdAndCode" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}

View File

@ -6,7 +6,7 @@
<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="selectedOptions" column="selected_options" typeHandler="com.fasterxml.jackson.databind.JsonTypeHandler"/>
<result property="textAnswer" column="text_answer"/>
<result property="createdAt" column="created_at"/>
</resultMap>
@ -19,7 +19,7 @@
<!-- 插入 -->
<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})
VALUES (#{userId}, #{questionId}, #{selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler}, #{textAnswer})
</insert>
<!-- 批量插入 -->
@ -28,7 +28,7 @@
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.questionId},
#{item.selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
#{item.selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler},
#{item.textAnswer})
</foreach>
</insert>
@ -38,7 +38,7 @@
UPDATE survey_responses
SET user_id = #{userId},
question_id = #{questionId},
selected_options = #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
selected_options = #{selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler},
text_answer = #{textAnswer}
WHERE id = #{id}
</update>

View File

@ -12,6 +12,12 @@
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名配置 -->
<typeAliases>
<package name="ltd.qubit.survey.model"/>
</typeAliases>
<!-- 类型处理器配置 -->
<typeHandlers>
<!-- 枚举类型处理器 -->
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
@ -20,13 +26,13 @@
javaType="ltd.qubit.survey.model.PositionType"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.QuestionType"/>
<!-- JSON类型处理器 -->
<typeHandler handler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"
javaType="java.util.List"/>
</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"/>
<package name="ltd.qubit.survey.dao"/>
</mappers>
</configuration>

View File

@ -3,16 +3,32 @@
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"
xmlns:aop="http://www.springframework.org/schema/aop"
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">
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"/>
<!-- 配置ObjectMapper -->
<bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" primary="true">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<!-- 配置JSON序列化特性 -->
<property name="serializationInclusion">
<value type="com.fasterxml.jackson.annotation.JsonInclude.Include">NON_NULL</value>
</property>
</bean>
<!-- 开启注解扫描排除Controller -->
<context:component-scan base-package="ltd.qubit.survey">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
@ -21,4 +37,21 @@
<!-- 配置事务注解驱动 -->
<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>

View File

@ -20,27 +20,39 @@
<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 name="objectMapper" ref="objectMapper"/>
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</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>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 配置静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 配置跨域支持 -->
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-origins="http://localhost:3000,http://localhost:8080"
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>
<!-- 配置异常处理器 -->
<bean class="ltd.qubit.survey.controller.GlobalExceptionHandler"/>
</beans>

View File

@ -1,8 +1,11 @@
<?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: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/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">
@ -16,13 +19,35 @@
<property name="idleTimeout" value="${jdbc.pool.idleTimeout}"/>
<property name="connectionTimeout" value="${jdbc.pool.connectionTimeout}"/>
<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>
<!-- 配置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"/>
<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>
<!-- 配置Mapper扫描器 -->

View File

@ -1,9 +1,9 @@
<?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"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<display-name>LLM Survey API</display-name>

View File

@ -1,6 +1,6 @@
# 数据库配置
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.password=

View File

@ -2,7 +2,7 @@
<!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">
<resultMap id="baseOptionMap" type="ltd.qubit.survey.model.Option">
<id property="id" column="id"/>
<result property="questionId" column="question_id"/>
<result property="optionCode" column="option_code"/>
@ -52,21 +52,21 @@
</delete>
<!-- 根据ID查询 -->
<select id="findById" parameterType="long" resultMap="optionMap">
<select id="findById" parameterType="long" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE id = #{id}
</select>
<!-- 查询所有 -->
<select id="findAll" resultMap="optionMap">
<select id="findAll" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
ORDER BY question_id, option_code
</select>
<!-- 根据问题ID查询 -->
<select id="findByQuestionId" parameterType="long" resultMap="optionMap">
<select id="findByQuestionId" parameterType="long" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}
@ -74,7 +74,7 @@
</select>
<!-- 根据问题ID和选项代码查询 -->
<select id="findByQuestionIdAndCode" resultMap="optionMap">
<select id="findByQuestionIdAndCode" resultMap="baseOptionMap">
SELECT <include refid="baseColumns"/>
FROM options
WHERE question_id = #{questionId}

View File

@ -6,7 +6,7 @@
<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="selectedOptions" column="selected_options" typeHandler="com.fasterxml.jackson.databind.JsonTypeHandler"/>
<result property="textAnswer" column="text_answer"/>
<result property="createdAt" column="created_at"/>
</resultMap>
@ -19,7 +19,7 @@
<!-- 插入 -->
<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})
VALUES (#{userId}, #{questionId}, #{selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler}, #{textAnswer})
</insert>
<!-- 批量插入 -->
@ -28,7 +28,7 @@
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.questionId},
#{item.selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
#{item.selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler},
#{item.textAnswer})
</foreach>
</insert>
@ -38,7 +38,7 @@
UPDATE survey_responses
SET user_id = #{userId},
question_id = #{questionId},
selected_options = #{selectedOptions,typeHandler=org.apache.ibatis.type.JsonTypeHandler},
selected_options = #{selectedOptions,typeHandler=com.fasterxml.jackson.databind.JsonTypeHandler},
text_answer = #{textAnswer}
WHERE id = #{id}
</update>

View File

@ -12,6 +12,12 @@
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名配置 -->
<typeAliases>
<package name="ltd.qubit.survey.model"/>
</typeAliases>
<!-- 类型处理器配置 -->
<typeHandlers>
<!-- 枚举类型处理器 -->
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
@ -20,13 +26,13 @@
javaType="ltd.qubit.survey.model.PositionType"/>
<typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="ltd.qubit.survey.model.QuestionType"/>
<!-- JSON类型处理器 -->
<typeHandler handler="ltd.qubit.survey.common.mybatis.JsonTypeHandler"
javaType="java.util.List"/>
</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"/>
<package name="ltd.qubit.survey.dao"/>
</mappers>
</configuration>

View File

@ -3,16 +3,32 @@
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"
xmlns:aop="http://www.springframework.org/schema/aop"
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">
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"/>
<!-- 配置ObjectMapper -->
<bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" primary="true">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<!-- 配置JSON序列化特性 -->
<property name="serializationInclusion">
<value type="com.fasterxml.jackson.annotation.JsonInclude.Include">NON_NULL</value>
</property>
</bean>
<!-- 开启注解扫描排除Controller -->
<context:component-scan base-package="ltd.qubit.survey">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
@ -21,4 +37,21 @@
<!-- 配置事务注解驱动 -->
<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>

View File

@ -20,27 +20,39 @@
<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 name="objectMapper" ref="objectMapper"/>
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</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>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 配置静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 配置跨域支持 -->
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-origins="http://localhost:3000,http://localhost:8080"
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>
<!-- 配置异常处理器 -->
<bean class="ltd.qubit.survey.controller.GlobalExceptionHandler"/>
</beans>

View File

@ -1,8 +1,11 @@
<?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: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/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">
@ -16,13 +19,35 @@
<property name="idleTimeout" value="${jdbc.pool.idleTimeout}"/>
<property name="connectionTimeout" value="${jdbc.pool.connectionTimeout}"/>
<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>
<!-- 配置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"/>
<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>
<!-- 配置Mapper扫描器 -->

View File

@ -12,6 +12,7 @@ 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/common/mybatis/JsonTypeHandler.class
ltd/qubit/survey/service/impl/QuestionServiceImpl$1.class
ltd/qubit/survey/service/UserService.class
ltd/qubit/survey/dao/BaseDao.class
@ -25,3 +26,4 @@ ltd/qubit/survey/service/SurveyResponseService.class
ltd/qubit/survey/model/Option.class
ltd/qubit/survey/model/QuestionType.class
ltd/qubit/survey/service/BaseService.class
com/fasterxml/jackson/databind/JsonTypeHandler.class

View File

@ -1,6 +1,7 @@
/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/common/mybatis/JsonTypeHandler.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
@ -8,6 +9,7 @@
/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/com/fasterxml/jackson/databind/JsonTypeHandler.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

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会自动重新部署

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);
}
}

View File

@ -1,55 +0,0 @@
package ltd.qubit.survey.controller;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.model.User;
import ltd.qubit.survey.service.UserService;
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/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 用户注册
*
* @param user 用户信息
* @return 注册成功的用户信息
*/
@PostMapping("/register")
public User register(@RequestBody User user) {
return userService.register(user);
}
/**
* 根据ID查询用户
*
* @param id 用户ID
* @return 用户信息
*/
@GetMapping("/{id}")
public User findById(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在"));
}
/**
* 检查手机号是否已注册
*
* @param phone 手机号
* @return 是否已注册
*/
@GetMapping("/check/{phone}")
public boolean checkPhone(@PathVariable String phone) {
return userService.isPhoneRegistered(phone);
}
}

View File

@ -1,51 +0,0 @@
package ltd.qubit.survey.dao;
import java.util.List;
import java.util.Optional;
/**
* 基础DAO接口定义通用的CRUD操作
*
* @param <T> 实体类型
* @param <K> 主键类型
*/
public interface BaseDao<T, K> {
/**
* 插入一条记录
*
* @param entity 实体对象
* @return 影响的行数
*/
int insert(T entity);
/**
* 根据主键删除记录
*
* @param id 主键
* @return 影响的行数
*/
int deleteById(K id);
/**
* 更新记录
*
* @param entity 实体对象
* @return 影响的行数
*/
int update(T entity);
/**
* 根据主键查询
*
* @param id 主键
* @return 实体对象
*/
Optional<T> findById(K id);
/**
* 查询所有记录
*
* @return 实体对象列表
*/
List<T> findAll();
}

View File

@ -1,43 +0,0 @@
package ltd.qubit.survey.dao;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.Option;
/**
* 选项DAO接口
*/
public interface OptionDao extends BaseDao<Option, Long> {
/**
* 根据问题ID查询选项列表
*
* @param questionId 问题ID
* @return 选项列表
*/
List<Option> findByQuestionId(Long questionId);
/**
* 根据问题ID和选项代码查询
*
* @param questionId 问题ID
* @param optionCode 选项代码
* @return 选项对象
*/
Optional<Option> findByQuestionIdAndCode(Long questionId, String optionCode);
/**
* 批量插入选项
*
* @param options 选项列表
* @return 影响的行数
*/
int batchInsert(List<Option> options);
/**
* 根据问题ID删除所有选项
*
* @param questionId 问题ID
* @return 影响的行数
*/
int deleteByQuestionId(Long questionId);
}

View File

@ -1,41 +0,0 @@
package ltd.qubit.survey.dao;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.WorkArea;
/**
* 问题DAO接口
*/
public interface QuestionDao extends BaseDao<Question, Long> {
/**
* 根据问题序号查询
*
* @param questionNumber 问题序号
* @return 问题对象
*/
Optional<Question> findByQuestionNumber(Integer questionNumber);
/**
* 根据工作领域查询问题列表
*
* @param workArea 工作领域
* @return 问题列表
*/
List<Question> findByWorkArea(WorkArea workArea);
/**
* 查询通用问题列表不针对特定工作领域
*
* @return 问题列表
*/
List<Question> findCommonQuestions();
/**
* 获取下一个可用的问题序号
*
* @return 下一个问题序号
*/
Integer getNextQuestionNumber();
}

View File

@ -1,51 +0,0 @@
package ltd.qubit.survey.dao;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.SurveyResponse;
/**
* 问卷答案DAO接口
*/
public interface SurveyResponseDao extends BaseDao<SurveyResponse, Long> {
/**
* 根据用户ID查询答案列表
*
* @param userId 用户ID
* @return 答案列表
*/
List<SurveyResponse> findByUserId(Long userId);
/**
* 根据问题ID查询答案列表
*
* @param questionId 问题ID
* @return 答案列表
*/
List<SurveyResponse> findByQuestionId(Long questionId);
/**
* 根据用户ID和问题ID查询答案
*
* @param userId 用户ID
* @param questionId 问题ID
* @return 答案对象
*/
Optional<SurveyResponse> findByUserIdAndQuestionId(Long userId, Long questionId);
/**
* 批量插入答案
*
* @param responses 答案列表
* @return 影响的行数
*/
int batchInsert(List<SurveyResponse> responses);
/**
* 根据用户ID删除所有答案
*
* @param userId 用户ID
* @return 影响的行数
*/
int deleteByUserId(Long userId);
}

View File

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

View File

@ -1,65 +0,0 @@
package ltd.qubit.survey.model;
import lombok.Data;
/**
* 错误信息
*/
@Data
public class ErrorInfo {
/**
* 错误代码
*/
private String code;
/**
* 错误消息
*/
private String message;
/**
* 错误详情
*/
private String detail;
/**
* 时间戳
*/
private long timestamp;
/**
* 请求路径
*/
private String path;
/**
* 创建错误信息
*
* @param code 错误代码
* @param message 错误消息
* @param detail 错误详情
* @param path 请求路径
* @return 错误信息
*/
public static ErrorInfo of(String code, String message, String detail, String path) {
ErrorInfo error = new ErrorInfo();
error.setCode(code);
error.setMessage(message);
error.setDetail(detail);
error.setPath(path);
error.setTimestamp(System.currentTimeMillis());
return error;
}
/**
* 创建错误信息无详情
*
* @param code 错误代码
* @param message 错误消息
* @param path 请求路径
* @return 错误信息
*/
public static ErrorInfo of(String code, String message, String path) {
return of(code, message, null, path);
}
}

View File

@ -1,40 +0,0 @@
package ltd.qubit.survey.model;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 问题选项实体类
*/
@Data
public class Option {
/**
* 选项ID
*/
private Long id;
/**
* 关联的问题ID
*/
private Long questionId;
/**
* 选项代码如ABC
*/
private String optionCode;
/**
* 选项内容
*/
private String content;
/**
* 是否需要填写文本
*/
private Boolean requiresText;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View File

@ -1,36 +0,0 @@
package ltd.qubit.survey.model;
/**
* 岗位性质枚举
*/
public enum PositionType {
/**
* 管理岗
*/
MANAGEMENT("管理岗"),
/**
* 技术岗
*/
TECHNICAL("技术岗"),
/**
* 业务岗
*/
BUSINESS("业务岗"),
/**
* 职能支持岗
*/
SUPPORT("职能支持岗");
private final String displayName;
PositionType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -1,50 +0,0 @@
package ltd.qubit.survey.model;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 问题实体类
*/
@Data
public class Question {
/**
* 问题ID
*/
private Long id;
/**
* 问题序号
*/
private Integer questionNumber;
/**
* 问题内容
*/
private String content;
/**
* 问题类型单选多选文本
*/
private QuestionType questionType;
/**
* 针对的工作领域为null表示通用问题
*/
private WorkArea workArea;
/**
* 是否必答
*/
private Boolean isRequired;
/**
* 跳转逻辑JSON格式
*/
private String nextQuestionLogic;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View File

@ -1,21 +0,0 @@
package ltd.qubit.survey.model;
/**
* 问题类型枚举
*/
public enum QuestionType {
/**
* 单选题
*/
SINGLE_CHOICE,
/**
* 多选题
*/
MULTIPLE_CHOICE,
/**
* 文本题
*/
TEXT
}

View File

@ -1,41 +0,0 @@
package ltd.qubit.survey.model;
import java.time.LocalDateTime;
import java.util.List;
import lombok.Data;
/**
* 问卷答案实体类
*/
@Data
public class SurveyResponse {
/**
* 答案ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 问题ID
*/
private Long questionId;
/**
* 选中的选项代码列表JSON格式
*/
private List<String> selectedOptions;
/**
* 文本答案
*/
private String textAnswer;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View File

@ -1,40 +0,0 @@
package ltd.qubit.survey.model;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 用户信息实体类
*/
@Data
public class User {
/**
* 用户ID
*/
private Long id;
/**
* 姓名
*/
private String name;
/**
* 手机号码
*/
private String phone;
/**
* 工作领域
*/
private WorkArea workArea;
/**
* 岗位性质
*/
private PositionType positionType;
/**
* 创建时间
*/
private LocalDateTime 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

@ -1,55 +0,0 @@
package ltd.qubit.survey.service;
import java.util.List;
import java.util.Optional;
import org.springframework.transaction.annotation.Transactional;
/**
* 基础Service接口定义通用的CRUD操作
*
* @param <T> 实体类型
* @param <K> 主键类型
*/
@Transactional(readOnly = true)
public interface BaseService<T, K> {
/**
* 新增
*
* @param entity 实体对象
* @return 实体对象
*/
@Transactional
T create(T entity);
/**
* 删除
*
* @param id 主键
*/
@Transactional
void delete(K id);
/**
* 更新
*
* @param entity 实体对象
* @return 实体对象
*/
@Transactional
T update(T entity);
/**
* 根据ID查询
*
* @param id 主键
* @return 实体对象
*/
Optional<T> findById(K id);
/**
* 查询所有
*
* @return 实体对象列表
*/
List<T> findAll();
}

View File

@ -1,42 +0,0 @@
package ltd.qubit.survey.service;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.Option;
/**
* 选项服务接口
*/
public interface OptionService extends BaseService<Option, Long> {
/**
* 根据问题ID查询选项列表
*
* @param questionId 问题ID
* @return 选项列表
*/
List<Option> findByQuestionId(Long questionId);
/**
* 根据问题ID和选项代码查询
*
* @param questionId 问题ID
* @param optionCode 选项代码
* @return 选项对象
*/
Optional<Option> findByQuestionIdAndCode(Long questionId, String optionCode);
/**
* 批量创建选项
*
* @param options 选项列表
* @return 创建成功的选项列表
*/
List<Option> batchCreate(List<Option> options);
/**
* 删除问题的所有选项
*
* @param questionId 问题ID
*/
void deleteByQuestionId(Long questionId);
}

View File

@ -1,54 +0,0 @@
package ltd.qubit.survey.service;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.WorkArea;
import org.springframework.transaction.annotation.Transactional;
/**
* 问题服务接口
*/
@Transactional(readOnly = true)
public interface QuestionService extends BaseService<Question, Long> {
/**
* 根据问题序号查询
*
* @param questionNumber 问题序号
* @return 问题对象
*/
Optional<Question> findByQuestionNumber(Integer questionNumber);
/**
* 根据工作领域查询问题列表
*
* @param workArea 工作领域
* @return 问题列表
*/
List<Question> findByWorkArea(WorkArea workArea);
/**
* 查询通用问题列表不针对特定工作领域
*
* @return 问题列表
*/
List<Question> findCommonQuestions();
/**
* 获取用户的下一个问题
*
* @param userId 用户ID
* @param currentQuestionNumber 当前问题序号
* @param selectedOptions 当前问题的选择项
* @return 下一个问题
*/
Optional<Question> getNextQuestion(Long userId, Integer currentQuestionNumber, List<String> selectedOptions);
/**
* 获取用户的问题列表包括通用问题和针对其工作领域的问题
*
* @param userId 用户ID
* @return 问题列表
*/
List<Question> getUserQuestions(Long userId);
}

View File

@ -1,64 +0,0 @@
package ltd.qubit.survey.service;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.SurveyResponse;
import org.springframework.transaction.annotation.Transactional;
/**
* 问卷答案服务接口
*/
@Transactional(readOnly = true)
public interface SurveyResponseService extends BaseService<SurveyResponse, Long> {
/**
* 根据用户ID查询答案列表
*
* @param userId 用户ID
* @return 答案列表
*/
List<SurveyResponse> findByUserId(Long userId);
/**
* 根据问题ID查询答案列表
*
* @param questionId 问题ID
* @return 答案列表
*/
List<SurveyResponse> findByQuestionId(Long questionId);
/**
* 根据用户ID和问题ID查询答案
*
* @param userId 用户ID
* @param questionId 问题ID
* @return 答案对象
*/
Optional<SurveyResponse> findByUserIdAndQuestionId(Long userId, Long questionId);
/**
* 批量保存答案
*
* @param responses 答案列表
* @return 保存成功的答案列表
*/
@Transactional
List<SurveyResponse> batchSave(List<SurveyResponse> responses);
/**
* 删除用户的所有答案
*
* @param userId 用户ID
*/
@Transactional
void deleteByUserId(Long userId);
/**
* 提交问卷答案
*
* @param userId 用户ID
* @param responses 答案列表
* @return 提交成功的答案列表
*/
@Transactional
List<SurveyResponse> submitSurvey(Long userId, List<SurveyResponse> responses);
}

View File

@ -1,46 +0,0 @@
package ltd.qubit.survey.service;
import java.util.List;
import java.util.Optional;
import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务接口
*/
@Transactional(readOnly = true)
public interface UserService extends BaseService<User, Long> {
/**
* 根据手机号查询用户
*
* @param phone 手机号
* @return 用户对象
*/
Optional<User> findByPhone(String phone);
/**
* 根据工作领域查询用户列表
*
* @param workArea 工作领域
* @return 用户列表
*/
List<User> findByWorkArea(WorkArea workArea);
/**
* 用户注册
*
* @param user 用户信息
* @return 注册成功的用户
*/
@Transactional
User register(User user);
/**
* 检查手机号是否已被注册
*
* @param phone 手机号
* @return 是否已注册
*/
boolean isPhoneRegistered(String phone);
}

View File

@ -1,73 +0,0 @@
package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.OptionDao;
import ltd.qubit.survey.model.Option;
import ltd.qubit.survey.service.OptionService;
import org.springframework.stereotype.Service;
/**
* 选项服务实现类
*/
@Service
@RequiredArgsConstructor
public class OptionServiceImpl implements OptionService {
private final OptionDao optionDao;
@Override
public Option create(Option option) {
option.setCreatedAt(LocalDateTime.now());
optionDao.insert(option);
return option;
}
@Override
public void delete(Long id) {
optionDao.deleteById(id);
}
@Override
public Option update(Option option) {
optionDao.update(option);
return option;
}
@Override
public Optional<Option> findById(Long id) {
return optionDao.findById(id);
}
@Override
public List<Option> findAll() {
return optionDao.findAll();
}
@Override
public List<Option> findByQuestionId(Long questionId) {
return optionDao.findByQuestionId(questionId);
}
@Override
public Optional<Option> findByQuestionIdAndCode(Long questionId, String optionCode) {
return optionDao.findByQuestionIdAndCode(questionId, optionCode);
}
@Override
public List<Option> batchCreate(List<Option> options) {
// 设置创建时间
LocalDateTime now = LocalDateTime.now();
options.forEach(option -> option.setCreatedAt(now));
// 批量插入
optionDao.batchInsert(options);
return options;
}
@Override
public void deleteByQuestionId(Long questionId) {
optionDao.deleteByQuestionId(questionId);
}
}

View File

@ -1,128 +0,0 @@
package ltd.qubit.survey.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.QuestionDao;
import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import ltd.qubit.survey.service.QuestionService;
import ltd.qubit.survey.service.UserService;
import org.springframework.stereotype.Service;
/**
* 问题服务实现类
*/
@Service
@RequiredArgsConstructor
public class QuestionServiceImpl implements QuestionService {
private final QuestionDao questionDao;
private final UserService userService;
private final ObjectMapper objectMapper;
@Override
public Question create(Question question) {
// 如果没有指定问题序号则自动生成
if (question.getQuestionNumber() == null) {
question.setQuestionNumber(questionDao.getNextQuestionNumber());
}
question.setCreatedAt(LocalDateTime.now());
questionDao.insert(question);
return question;
}
@Override
public void delete(Long id) {
questionDao.deleteById(id);
}
@Override
public Question update(Question question) {
questionDao.update(question);
return question;
}
@Override
public Optional<Question> findById(Long id) {
return questionDao.findById(id);
}
@Override
public List<Question> findAll() {
return questionDao.findAll();
}
@Override
public Optional<Question> findByQuestionNumber(Integer questionNumber) {
return questionDao.findByQuestionNumber(questionNumber);
}
@Override
public List<Question> findByWorkArea(WorkArea workArea) {
return questionDao.findByWorkArea(workArea);
}
@Override
public List<Question> findCommonQuestions() {
return questionDao.findCommonQuestions();
}
@Override
public Optional<Question> getNextQuestion(Long userId, Integer currentQuestionNumber, List<String> selectedOptions) {
// 获取当前问题
Optional<Question> currentQuestion = findByQuestionNumber(currentQuestionNumber);
if (currentQuestion.isEmpty()) {
return Optional.empty();
}
// 如果当前问题有跳转逻辑则根据选项判断下一个问题
String nextQuestionLogic = currentQuestion.get().getNextQuestionLogic();
if (nextQuestionLogic != null && !nextQuestionLogic.isEmpty()) {
try {
// 解析跳转逻辑JSON
Map<String, Integer> logic = objectMapper.readValue(nextQuestionLogic,
new TypeReference<Map<String, Integer>>() {});
// 根据选项确定下一个问题序号
for (String option : selectedOptions) {
if (logic.containsKey(option)) {
return findByQuestionNumber(logic.get(option));
}
}
} catch (Exception e) {
// JSON解析错误继续使用默认的下一个问题
}
}
// 如果没有特殊跳转逻辑则返回序号加1的问题
return findByQuestionNumber(currentQuestionNumber + 1);
}
@Override
public List<Question> getUserQuestions(Long userId) {
List<Question> questions = new ArrayList<>();
// 获取用户信息
Optional<User> user = userService.findById(userId);
if (user.isEmpty()) {
return questions;
}
// 添加通用问题
questions.addAll(findCommonQuestions());
// 添加针对用户工作领域的问题
questions.addAll(findByWorkArea(user.get().getWorkArea()));
// 按问题序号排序
questions.sort((q1, q2) -> q1.getQuestionNumber().compareTo(q2.getQuestionNumber()));
return questions;
}
}

View File

@ -1,111 +0,0 @@
package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.SurveyResponseDao;
import ltd.qubit.survey.model.Question;
import ltd.qubit.survey.model.SurveyResponse;
import ltd.qubit.survey.service.QuestionService;
import ltd.qubit.survey.service.SurveyResponseService;
import org.springframework.stereotype.Service;
/**
* 问卷答案服务实现类
*/
@Service
@RequiredArgsConstructor
public class SurveyResponseServiceImpl implements SurveyResponseService {
private final SurveyResponseDao surveyResponseDao;
private final QuestionService questionService;
@Override
public SurveyResponse create(SurveyResponse response) {
response.setCreatedAt(LocalDateTime.now());
surveyResponseDao.insert(response);
return response;
}
@Override
public void delete(Long id) {
surveyResponseDao.deleteById(id);
}
@Override
public SurveyResponse update(SurveyResponse response) {
surveyResponseDao.update(response);
return response;
}
@Override
public Optional<SurveyResponse> findById(Long id) {
return surveyResponseDao.findById(id);
}
@Override
public List<SurveyResponse> findAll() {
return surveyResponseDao.findAll();
}
@Override
public List<SurveyResponse> findByUserId(Long userId) {
return surveyResponseDao.findByUserId(userId);
}
@Override
public List<SurveyResponse> findByQuestionId(Long questionId) {
return surveyResponseDao.findByQuestionId(questionId);
}
@Override
public Optional<SurveyResponse> findByUserIdAndQuestionId(Long userId, Long questionId) {
return surveyResponseDao.findByUserIdAndQuestionId(userId, questionId);
}
@Override
public List<SurveyResponse> batchSave(List<SurveyResponse> responses) {
// 设置创建时间
LocalDateTime now = LocalDateTime.now();
responses.forEach(response -> response.setCreatedAt(now));
// 批量插入
surveyResponseDao.batchInsert(responses);
return responses;
}
@Override
public void deleteByUserId(Long userId) {
surveyResponseDao.deleteByUserId(userId);
}
@Override
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);
// 设置用户ID和创建时间
LocalDateTime now = LocalDateTime.now();
responses.forEach(response -> {
response.setUserId(userId);
response.setCreatedAt(now);
});
// 批量保存答案
surveyResponseDao.batchInsert(responses);
return responses;
}
}

View File

@ -1,72 +0,0 @@
package ltd.qubit.survey.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import ltd.qubit.survey.dao.UserDao;
import ltd.qubit.survey.model.User;
import ltd.qubit.survey.model.WorkArea;
import ltd.qubit.survey.service.UserService;
import org.springframework.stereotype.Service;
/**
* 用户服务实现类
*/
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Override
public User create(User user) {
user.setCreatedAt(LocalDateTime.now());
userDao.insert(user);
return user;
}
@Override
public void delete(Long id) {
userDao.deleteById(id);
}
@Override
public User update(User user) {
userDao.update(user);
return user;
}
@Override
public Optional<User> findById(Long id) {
return userDao.findById(id);
}
@Override
public List<User> findAll() {
return userDao.findAll();
}
@Override
public Optional<User> findByPhone(String phone) {
return userDao.findByPhone(phone);
}
@Override
public List<User> findByWorkArea(WorkArea workArea) {
return userDao.findByWorkArea(workArea);
}
@Override
public User register(User user) {
// 检查手机号是否已注册
if (isPhoneRegistered(user.getPhone())) {
throw new IllegalArgumentException("手机号已被注册");
}
return create(user);
}
@Override
public boolean isPhoneRegistered(String phone) {
return userDao.findByPhone(phone).isPresent();
}
}

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,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>LLM Survey API</display-name>
<!-- Spring配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring/applicationContext.xml
classpath:spring/spring-mybatis.xml
</param-value>
</context-param>
<!-- Spring监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring MVC Servlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 字符编码过滤器 -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

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

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