feat: 优化问卷完成流程 - 添加完成页面,优化样式和交互,简化后端代码

This commit is contained in:
胡海星 2025-02-23 16:36:56 +08:00
parent 67a6a13b1d
commit 083f263d09
14 changed files with 329 additions and 132 deletions

View File

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

View File

@ -21,7 +21,7 @@ public class Option {
/**
* 选项代码如ABC
*/
private String optionCode;
private String code;
/**
* 选项内容

View File

@ -30,7 +30,7 @@ public class SurveyResponse {
private List<String> selectedOptions;
/**
* 文本答案
* 文本答案用于文本题或需要填写文本的选项
*/
private String textAnswer;

View File

@ -81,19 +81,6 @@ public class SurveyResponseServiceImpl implements SurveyResponseService {
@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.getNumber()));
}
}
}
// 删除用户之前的答案
deleteByUserId(userId);

View File

@ -5,7 +5,7 @@
<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="code" column="code"/>
<result property="content" column="content"/>
<result property="requiresText" column="requires_text"/>
<result property="createdAt" column="created_at"/>
@ -13,21 +13,21 @@
<!-- 基础列 -->
<sql id="baseColumns">
`id`, `question_id`, `option_code`, `content`, `requires_text`, `created_at`
`id`, `question_id`, `code`, `content`, `requires_text`, `created_at`
</sql>
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.Option" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
VALUES (#{questionId}, #{optionCode}, #{content}, #{requiresText})
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`)
VALUES (#{questionId}, #{code}, #{content}, #{requiresText})
</insert>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `option` (`question_id`, `code`, `content`, `requires_text`)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.questionId}, #{item.optionCode}, #{item.content}, #{item.requiresText})
(#{item.questionId}, #{item.code}, #{item.content}, #{item.requiresText})
</foreach>
</insert>
@ -35,7 +35,7 @@
<update id="update" parameterType="ltd.qubit.survey.model.Option">
UPDATE `option`
SET `question_id` = #{questionId},
`option_code` = #{optionCode},
`code` = #{code},
`content` = #{content},
`requires_text` = #{requiresText}
WHERE `id` = #{id}
@ -62,7 +62,7 @@
<select id="findAll" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM `option`
ORDER BY `question_id`, `option_code`
ORDER BY `question_id`, `code`
</select>
<!-- 根据问题ID查询 -->
@ -70,14 +70,14 @@
SELECT <include refid="baseColumns"/>
FROM `option`
WHERE `question_id` = #{questionId}
ORDER BY `option_code`
ORDER BY `code`
</select>
<!-- 根据问题ID和选项代码查询 -->
<select id="findByQuestionIdAndOptionCode" resultMap="optionMap">
<select id="findByQuestionIdAndCode" resultMap="optionMap">
SELECT <include refid="baseColumns"/>
FROM `option`
WHERE `question_id` = #{questionId}
AND `option_code` = #{optionCode}
AND `code` = #{code}
</select>
</mapper>

View File

@ -19,7 +19,9 @@
<!-- 插入 -->
<insert id="insert" parameterType="ltd.qubit.survey.model.SurveyResponse" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `survey_response` (`user_id`, `question_id`, `selected_options`, `text_answer`)
VALUES (#{userId}, #{questionId}, #{selectedOptions,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler}, #{textAnswer})
VALUES (#{userId}, #{questionId},
#{selectedOptions,typeHandler=ltd.qubit.survey.common.mybatis.JsonTypeHandler},
#{textAnswer})
</insert>
<!-- 批量插入 -->

View File

@ -36,14 +36,14 @@ CREATE TABLE IF NOT EXISTS `question` (
-- 创建选项表
CREATE TABLE IF NOT EXISTS `option` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`question_id` BIGINT NOT NULL COMMENT '问题ID',
`option_code` VARCHAR(10) NOT NULL COMMENT '选项代码',
`question_id` BIGINT NOT NULL COMMENT '关联的问题ID',
`code` VARCHAR(10) NOT NULL COMMENT '选项代码',
`content` TEXT NOT NULL COMMENT '选项内容',
`requires_text` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否需要填写文本',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (`question_id`) REFERENCES `question`(`id`),
UNIQUE KEY `uk_question_option` (`question_id`, `option_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选项表';
FOREIGN KEY (`question_id`) REFERENCES `question` (`id`),
UNIQUE KEY `uk_question_code` (`question_id`, `code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题选项表';
-- 创建问卷答案表
CREATE TABLE IF NOT EXISTS `survey_response` (
@ -51,10 +51,10 @@ CREATE TABLE IF NOT EXISTS `survey_response` (
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`question_id` BIGINT NOT NULL COMMENT '问题ID',
`selected_options` JSON DEFAULT NULL COMMENT '选中的选项代码列表',
`text_answer` TEXT DEFAULT NULL COMMENT '文本答案',
`text_answer` TEXT DEFAULT NULL COMMENT '文本答案(用于文本题或需要填写文本的选项)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`question_id`) REFERENCES `question`(`id`)
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
FOREIGN KEY (`question_id`) REFERENCES `question` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷答案表';
-- 清空现有数据
@ -69,7 +69,7 @@ SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (1, '您对大模型如ChatGPT、通义千问、DeepSeek的了解程度', 'SINGLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`) VALUES
INSERT INTO `option` (`question_id`, `code`, `content`) VALUES
(LAST_INSERT_ID(), 'A', '从未接触过'),
(LAST_INSERT_ID(), 'B', '仅在日常简单使用过通用功能(如问答)'),
(LAST_INSERT_ID(), 'C', '在工作中尝试过基础应用'),
@ -78,7 +78,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`) VALUES
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (2, '您觉得大模型可以做到下面哪些事?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -90,7 +90,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (3, '您最关注大模型应用的哪些风险?', 'MULTIPLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -100,7 +100,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `is_required`, `is_last`)
VALUES (4, '您的主要工作内容是:', 'SINGLE_CHOICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`) VALUES
INSERT INTO `option` (`question_id`, `code`, `content`) VALUES
(LAST_INSERT_ID(), 'A', '研发(产品、开发、算法、测试、运维等)'),
(LAST_INSERT_ID(), 'B', '项目管理(项目立项、进度跟踪、风险管理等)'),
(LAST_INSERT_ID(), 'C', '保险(产品、核保、理赔、精算等)'),
@ -116,7 +116,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`) VALUES
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (5, '您在开发过程中最耗时的重复性工作:', 'MULTIPLE_CHOICE', 'RD', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -127,7 +127,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (6, '您希望大模型如何与现有系统集成:', 'MULTIPLE_CHOICE', 'RD', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -141,7 +141,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (7, '项目管理中最常遇到的挑战是:', 'MULTIPLE_CHOICE', 'PROJECT', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -150,7 +150,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (8, '您希望如何利用大模型提升项目管理效率:', 'MULTIPLE_CHOICE', 'PROJECT', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -161,7 +161,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (9, '理赔处理中的主要瓶颈是:', 'MULTIPLE_CHOICE', 'INSURANCE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -171,7 +171,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (10, '大模型可以优化哪些保险工作环节:', 'MULTIPLE_CHOICE', 'INSURANCE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -183,7 +183,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (11, '日常工作中最重复的任务是:', 'MULTIPLE_CHOICE', 'FINANCE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -192,7 +192,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (12, '大模型能如何协助提升财务工作效率:', 'MULTIPLE_CHOICE', 'FINANCE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -203,7 +203,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (13, '客户咨询中最常遇到的重复性问题:', 'MULTIPLE_CHOICE', 'CUSTOMER_SERVICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -212,7 +212,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (14, '您希望大模型如何辅助客服工作:', 'MULTIPLE_CHOICE', 'CUSTOMER_SERVICE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -222,7 +222,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (15, '在运营工作中,最需要自动化支持的任务是:', 'MULTIPLE_CHOICE', 'OPERATION', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -232,7 +232,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (16, '大模型可以如何帮助提升运营效率:', 'MULTIPLE_CHOICE', 'OPERATION', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -243,7 +243,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (17, '在市场拓展和商务沟通中,您最常遇到的挑战是:', 'MULTIPLE_CHOICE', 'MARKET', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -252,7 +252,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (18, '您希望大模型如何帮助提升市场拓展效率:', 'MULTIPLE_CHOICE', 'MARKET', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -262,7 +262,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (19, '人事部门最耗时的日常任务是:', 'MULTIPLE_CHOICE', 'HR', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -271,7 +271,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (20, '您希望大模型如何协助提升人事工作效率:', 'MULTIPLE_CHOICE', 'HR', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -281,7 +281,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (21, '在行政工作中,最耗时的任务是:', 'MULTIPLE_CHOICE', 'ADMIN', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -290,7 +290,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (22, '您希望大模型如何协助提升行政工作效率:', 'MULTIPLE_CHOICE', 'ADMIN', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -300,7 +300,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (23, '您认为大模型在哪些战略层面的决策中可以发挥作用?', 'MULTIPLE_CHOICE', 'EXECUTIVE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),
@ -309,7 +309,7 @@ INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`)
INSERT INTO `question` (`number`, `content`, `type`, `work_area`, `is_required`, `is_last`)
VALUES (24, '在公司管理工作中,您最希望大模型协助哪些任务?', 'MULTIPLE_CHOICE', 'EXECUTIVE', TRUE, FALSE);
INSERT INTO `option` (`question_id`, `option_code`, `content`, `requires_text`) VALUES
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),

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 验证部署结果"

View File

@ -19,6 +19,12 @@ const router = createRouter({
component: () => import('@/views/SurveyView.vue'),
meta: { requiresAuth: true },
},
{
path: '/completed',
name: 'completed',
component: () => import('@/views/CompletedView.vue'),
meta: { requiresAuth: true },
},
],
});

View File

@ -44,18 +44,20 @@ export const useSurveyStore = defineStore('survey', () => {
}
// 获取下一个问题
async function fetchNextQuestion(userId, selectedOptions) {
async function fetchNextQuestion(userId, selectedOptions, targetQuestionNumber) {
try {
console.log('获取下一个问题,参数:', { userId, selectedOptions, currentQuestionNumber: currentQuestionNumber.value });
console.log('获取问题,参数:', { userId, selectedOptions, targetQuestionNumber, currentQuestionNumber: currentQuestionNumber.value });
// 如果没有当前问题号,说明是新开始的问卷
const isNewSurvey = !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);
}
const question = await getNextQuestion(
userId,
selectedOptions,
isNewSurvey ? undefined : currentQuestionNumber.value
);
console.log('获取到的问题:', question);
if (question) {
@ -74,7 +76,7 @@ export const useSurveyStore = defineStore('survey', () => {
return question;
} catch (error) {
console.error('获取下一个问题失败:', error);
console.error('获取问题失败:', error);
throw error;
}
}

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

@ -13,7 +13,13 @@
<h1 class="title">欢迎参与 LLM 问卷调查</h1>
<p class="description">
本问卷旨在了解您对大语言模型LLM的使用体验和看法您的反馈对我们非常重要
本问卷旨在了解智慧医疗员工对大语言模型的使用体验和看法您的反馈对我们非常重要
</p>
<p class="description">
此应用使用Cursor生成所有代码包括数据库脚本前后端代码配置文件等均通过提示词指挥AI生成git提交测试代码打包发布等操作也是通过提示词指挥AI完成
</p>
<p class="description">
此项目开发到上线共计耗时1人5小时传统开发方式需要产品前端后端测试4人12天工作量
</p>
<div class="action-buttons">
@ -32,9 +38,9 @@
round
block
type="primary"
@click="router.push('/survey')"
@click="surveyStore.isCompleted ? startNewSurvey() : router.push('/survey')"
>
继续答题
{{ surveyStore.isCompleted ? '重新答题' : '继续答题' }}
</van-button>
<van-button
@ -55,15 +61,23 @@
<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>
@ -92,7 +106,8 @@ function onLogout() {
font-size: 16px;
color: #666;
line-height: 1.5;
margin-bottom: 40px;
margin-bottom: 20px;
text-align: left;
}
.action-buttons {

View File

@ -86,7 +86,6 @@ async function onPhoneBlur() {
const isRegistered = await userStore.checkPhoneNumber(formData.phone);
if (isRegistered) {
showToast('该手机号已注册');
formData.phone = '';
}
} catch (error) {
console.error('检查手机号失败:', error);

View File

@ -7,7 +7,7 @@
@click-left="onBack"
/>
<div v-if="!surveyStore.isCompleted" class="survey-content">
<div class="survey-content">
<div v-if="surveyStore.currentQuestion" class="question-container">
<h2 class="question-title">
{{ surveyStore.currentQuestionNumber }}. {{ surveyStore.currentQuestion.content }}
@ -17,16 +17,25 @@
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.optionCode"
:key="option.code"
clickable
@click="toggleOption(option.optionCode)"
@click="toggleOption(option.code)"
>
<template #title>
<span>{{ option.optionCode }}. {{ option.content }}</span>
<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.optionCode"
:name="option.code"
@click.stop
/>
</template>
@ -37,15 +46,24 @@
<van-cell-group inset>
<van-cell
v-for="option in surveyStore.currentOptions"
:key="option.optionCode"
:key="option.code"
clickable
@click="selectedOption = option.optionCode"
@click="selectedOption = option.code"
>
<template #title>
<span>{{ option.optionCode }}. {{ option.content }}</span>
<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.optionCode" />
<van-radio :name="option.code" />
</template>
</van-cell>
</van-cell-group>
@ -57,7 +75,6 @@
rows="4"
autosize
placeholder="请输入您的答案"
:rules="[{ required: surveyStore.currentQuestion.isRequired, message: '请输入答案' }]"
/>
</div>
<div class="button-container">
@ -74,26 +91,6 @@
<van-empty v-else description="加载中..." />
</div>
<div v-else class="survey-completed">
<van-empty description="感谢您完成问卷调查!">
<template #image>
<van-icon name="success" size="64" color="#07c160" />
</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>
@ -116,21 +113,30 @@ const responses = ref([]);
//
const isValidSelection = computed(() => {
const questionType = surveyStore.currentQuestion?.type;
if (!questionType) return false;
if (questionType === 'MULTIPLE_CHOICE') {
return selectedOptions.value.length > 0;
//
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') {
return selectedOption.value !== '';
//
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() !== '';
return (textAnswer.value?.trim() || '').length > 0;
}
return false;
});
//
function toggleOption(optionCode) {
const index = selectedOptions.value.indexOf(optionCode);
function toggleOption(code) {
const index = selectedOptions.value.indexOf(code);
if (index === -1) {
selectedOptions.value.push(optionCode);
selectedOptions.value.push(code);
} else {
selectedOptions.value.splice(index, 1);
}
@ -144,20 +150,9 @@ function onBack() {
//
async function onNextQuestion() {
const questionType = surveyStore.currentQuestion.type;
if (questionType === 'MULTIPLE_CHOICE' && selectedOptions.value.length === 0) {
showToast('请至少选择一个选项');
return;
} else if (questionType === 'SINGLE_CHOICE' && !selectedOption.value) {
showToast('请选择一个选项');
return;
} else if (questionType === 'TEXT' && !textAnswer.value.trim()) {
showToast('请输入答案');
return;
}
try {
let currentSelection = [];
if (questionType === 'MULTIPLE_CHOICE') {
currentSelection = selectedOptions.value;
} else if (questionType === 'SINGLE_CHOICE') {
@ -165,18 +160,36 @@ async function onNextQuestion() {
}
//
responses.value.push({
const currentResponse = {
questionId: surveyStore.currentQuestion.id,
selectedOptions: currentSelection,
textAnswer: questionType === 'TEXT' ? textAnswer.value.trim() : null
});
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.value = true;
surveyStore.isCompleted = true;
//
localStorage.removeItem('surveyProgress');
//
router.replace('/completed');
return;
} catch (error) {
console.error('提交问卷失败:', error);
@ -205,10 +218,45 @@ async function onNextQuestion() {
//
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('初始化问卷失败,请重试');
@ -289,4 +337,10 @@ onMounted(() => {
margin-top: 24px;
width: 80%;
}
.option-text-input {
margin-top: 8px;
background-color: #f8f8f8;
border-radius: 4px;
}
</style>