diff --git a/survey-common/src/main/java/com/xiaojusurvey/engine/common/entity/user/Captcha.java b/survey-common/src/main/java/com/xiaojusurvey/engine/common/entity/user/Captcha.java
index 7ac9f1768..270df75a4 100644
--- a/survey-common/src/main/java/com/xiaojusurvey/engine/common/entity/user/Captcha.java
+++ b/survey-common/src/main/java/com/xiaojusurvey/engine/common/entity/user/Captcha.java
@@ -2,8 +2,11 @@
import com.xiaojusurvey.engine.common.entity.BaseEntity;
import lombok.Data;
+import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
+import java.util.Date;
+
/**
* @Author: LYF
* @CreateTime: 2024-06-06
@@ -18,4 +21,8 @@ public class Captcha extends BaseEntity {
*/
private String text;
+ //1 小时过期(3600s)
+ @Indexed(expireAfterSeconds = 3600)
+ private Date expireAt = new Date();
+
}
diff --git a/survey-common/src/main/java/com/xiaojusurvey/engine/common/enums/QuestionTypeEnum.java b/survey-common/src/main/java/com/xiaojusurvey/engine/common/enums/QuestionTypeEnum.java
new file mode 100644
index 000000000..c13e089fa
--- /dev/null
+++ b/survey-common/src/main/java/com/xiaojusurvey/engine/common/enums/QuestionTypeEnum.java
@@ -0,0 +1,80 @@
+package com.xiaojusurvey.engine.common.enums;
+
+import lombok.Getter;
+
+/**
+ * @Author: WYX
+ * @CreateTime: 2025/9/1
+ * @Description:
+ */
+@Getter
+public enum QuestionTypeEnum {
+ // 单行输入框
+ TEXT("text"),
+
+ // 多行输入框
+ TEXTAREA("textarea"),
+
+ // 单项选择
+ RADIO("radio"),
+
+ // 多项选择
+ CHECKBOX("checkbox"),
+
+ // 多项选择
+ BINARY_CHOICE("binary-choice"),
+
+ // 评分
+ RADIO_STAR("radio-star"),
+
+ // nps评分
+ RADIO_NPS("radio-nps"),
+
+ // 投票
+ VOTE("vote"),
+
+ // 多级联动
+ CASCADER("cascader");
+
+ private final String type;
+
+ QuestionTypeEnum(String type) {
+ this.type = type;
+ }
+
+ /**
+ * 获取支持聚合统计的问题类型列表
+ */
+ public static QuestionTypeEnum[] getAggerationSupportTypes() {
+ return new QuestionTypeEnum[]{
+ RADIO, CHECKBOX, BINARY_CHOICE, RADIO_STAR, RADIO_NPS, VOTE, CASCADER
+ };
+ }
+
+ /**
+ * 根据类型字符串获取枚举
+ */
+ public static QuestionTypeEnum fromType(String type) {
+ for (QuestionTypeEnum questionType : values()) {
+ if (questionType.type.equals(type)) {
+ return questionType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 判断是否为评分类题型
+ */
+ public boolean isRatingType() {
+ return this == QuestionTypeEnum.RADIO_NPS || this == QuestionTypeEnum.RADIO_STAR;
+ }
+
+ /**
+ * 判断是否为选项类体现
+ */
+ public boolean isOptionType() {
+ return this == RADIO || this == CHECKBOX || this == VOTE || this == BINARY_CHOICE;
+ }
+
+}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SimpleCaptchaGenerator.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SimpleCaptchaGenerator.java
deleted file mode 100644
index fd5209832..000000000
--- a/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SimpleCaptchaGenerator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.xiaojusurvey.engine.core.auth.captcha;
-
-import com.xiaojusurvey.engine.common.entity.user.Captcha;
-import org.springframework.stereotype.Service;
-
-import java.util.Random;
-
-/**
- * @Author: LYF
- * @CreateTime: 2024-06-06
- * @Description: 简单验证码生成工具类
- */
-@Service("simpleCaptchaGenerator")
-public class SimpleCaptchaGenerator implements CaptchaGenerator {
-
- @Override
- public String generateRandomSvg(String text) {
- // 生成包含验证码的 SVG 数据
- String svgData = "";
-
- return svgData;
- }
-
- @Override
- public Captcha generateRandomText(int length) {
- String chars = "123456789";
- StringBuilder text = new StringBuilder();
- Random random = new Random();
-
- for (int i = 0; i < length; i++) {
- int index = random.nextInt(chars.length());
- text.append(chars.charAt(index));
- }
- Captcha captcha = new Captcha();
- captcha.setText(text.toString());
- return captcha;
- }
-
-
-}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SvgCaptchaGenerator.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SvgCaptchaGenerator.java
new file mode 100644
index 000000000..5631d6245
--- /dev/null
+++ b/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/captcha/SvgCaptchaGenerator.java
@@ -0,0 +1,94 @@
+package com.xiaojusurvey.engine.core.auth.captcha;
+
+import com.xiaojusurvey.engine.common.entity.user.Captcha;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * @Author: WYX
+ * @CreateTime: 2025/8/10
+ * @Description: 彩色字符+噪声svg验证码生成器
+ */
+@Service("svgCaptchaGenerator")
+public class SvgCaptchaGenerator implements CaptchaGenerator {
+ private static final int WIDTH = 150;
+ private static final int HEIGHT = 50;
+ private static final int SIZE = 4; // 对齐 size: 4
+ private static final int NOISE = 3; // 对齐 noise: 3
+ private static final String BG = "#f0f0f0"; // 对齐 background
+ private static final String POOL = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"; // ignore 0o1i
+
+ @Override
+ public Captcha generateRandomText(int length) {
+ ThreadLocalRandom rnd = ThreadLocalRandom.current();
+ String text = IntStream.range(0, length)
+ .mapToObj(i -> String.valueOf(POOL.charAt(rnd.nextInt(POOL.length()))))
+ .collect(Collectors.joining());
+ Captcha c = new Captcha();
+ c.setText(text);
+ return c;
+ }
+
+ @Override
+ public String generateRandomSvg(String text) {
+ ThreadLocalRandom rnd = ThreadLocalRandom.current();
+ StringBuilder svg = new StringBuilder();
+ svg.append("");
+ return svg.toString();
+ }
+
+ private static String randomCubicPath(ThreadLocalRandom rnd) {
+ // 模拟 svg-captcha 的随机贝塞尔干扰线
+ int x1 = -10, y1 = rnd.nextInt(0, HEIGHT);
+ int x2 = rnd.nextInt(0, WIDTH / 2), y2 = rnd.nextInt(0, HEIGHT);
+ int x3 = rnd.nextInt(WIDTH / 2, WIDTH), y3 = rnd.nextInt(0, HEIGHT);
+ int x4 = WIDTH + 10, y4 = rnd.nextInt(0, HEIGHT);
+ return String.format("M %d %d C %d %d, %d %d, %d %d", x1, y1, x2, y2, x3, y3, x4, y4);
+ }
+
+ private static String randomVivid(ThreadLocalRandom rnd) {
+ int h = rnd.nextInt(0, 360);
+ int s = rnd.nextInt(70, 100);
+ int l = rnd.nextInt(40, 65);
+ return String.format("hsl(%d,%d%%,%d%%)", h, s, l);
+ }
+
+}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/impl/AuthServiceImpl.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/impl/AuthServiceImpl.java
index e984d3371..640a3f0e1 100644
--- a/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/impl/AuthServiceImpl.java
+++ b/survey-core/src/main/java/com/xiaojusurvey/engine/core/auth/impl/AuthServiceImpl.java
@@ -32,7 +32,7 @@ public class AuthServiceImpl implements AuthService {
@Resource
private MongoRepository mongoRepository;
- @Resource(name = "simpleCaptchaGenerator")
+ @Resource(name = "svgCaptchaGenerator")
private CaptchaGenerator captchaGenerator;
@Resource
@@ -45,8 +45,8 @@ public class AuthServiceImpl implements AuthService {
@Override
public CaptchaVo captcha() {
Captcha captcha = captchaGenerator.generateRandomText(4);
- mongoRepository.save(captcha);
- return captchaGenerator.generateRandomSvg(captcha);
+ Captcha saved = mongoRepository.save(captcha);
+ return captchaGenerator.generateRandomSvg(saved);
}
@Override
@@ -97,22 +97,20 @@ public UserVo login(UserParam userParam) {
/**
- * 判断验证码是否正确
+ * 判断验证码是否正确(大小写不敏感)
*
* @param captchaId 验证码id
* @param captchaText 需要验证的文本
- * @return
*/
public void checkCaptchaIsCorrect(String captchaId, String captchaText) {
if (ObjectUtils.isEmpty(captchaId) || ObjectUtils.isEmpty(captchaText)) {
throw new ServiceException(RespErrorCode.CAPTCHA_INCORRECT.getMessage(), RespErrorCode.CAPTCHA_INCORRECT.getCode());
}
Captcha captcha = mongoRepository.findById(captchaId, Captcha.class);
- //非空判断
if (ObjectUtils.isEmpty(captcha)) {
throw new ServiceException(RespErrorCode.CAPTCHA_INCORRECT.getMessage(), RespErrorCode.CAPTCHA_INCORRECT.getCode());
}
- if (!captchaText.equals(captcha.getText())) {
+ if (!captchaText.equalsIgnoreCase(captcha.getText())) {
throw new ServiceException(RespErrorCode.CAPTCHA_INCORRECT.getMessage(), RespErrorCode.CAPTCHA_INCORRECT.getCode());
}
}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/DataStatisticService.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/DataStatisticService.java
new file mode 100644
index 000000000..7a33d9a0b
--- /dev/null
+++ b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/DataStatisticService.java
@@ -0,0 +1,19 @@
+package com.xiaojusurvey.engine.core.survey;
+
+import com.xiaojusurvey.engine.core.survey.param.AggregationStatisParam;
+import com.xiaojusurvey.engine.core.survey.param.DataTableParam;
+import com.xiaojusurvey.engine.core.survey.vo.AggregationStatisVO;
+import com.xiaojusurvey.engine.core.survey.vo.DataTableVO;
+
+import java.util.List;
+
+/**
+ * @Author: WYX
+ * @CreateTime: 2025/8/16
+ * @Description: 获取问卷回收数据表格
+ */
+public interface DataStatisticService {
+ DataTableVO getDataTable(DataTableParam param);
+
+ List getAggregationStatis(AggregationStatisParam param);
+}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/dto/SurveyConfCode.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/dto/SurveyConfCode.java
index 93b76e149..1d529f659 100644
--- a/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/dto/SurveyConfCode.java
+++ b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/dto/SurveyConfCode.java
@@ -87,6 +87,8 @@ public static class DataItem {
private Map rangeConfig;
private String starStyle;
private String innerType;
+ // 多级联动配置数据
+ private Map cascaderData;
}
diff --git a/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/impl/DataStatisticServiceImpl.java b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/impl/DataStatisticServiceImpl.java
new file mode 100644
index 000000000..9d54f34f8
--- /dev/null
+++ b/survey-core/src/main/java/com/xiaojusurvey/engine/core/survey/impl/DataStatisticServiceImpl.java
@@ -0,0 +1,787 @@
+package com.xiaojusurvey.engine.core.survey.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.xiaojusurvey.engine.common.entity.survey.SurveyConf;
+import com.xiaojusurvey.engine.common.entity.survey.SurveySubmit;
+import com.xiaojusurvey.engine.common.enums.QuestionTypeEnum;
+import com.xiaojusurvey.engine.core.survey.DataStatisticService;
+import com.xiaojusurvey.engine.core.survey.SurveyConfService;
+import com.xiaojusurvey.engine.core.survey.dto.SurveyConfCode;
+import com.xiaojusurvey.engine.core.survey.param.AggregationStatisParam;
+import com.xiaojusurvey.engine.core.survey.param.DataTableParam;
+import com.xiaojusurvey.engine.core.survey.vo.AggregationStatisVO;
+import com.xiaojusurvey.engine.core.survey.vo.DataTableVO;
+import com.xiaojusurvey.engine.repository.MongoRepository;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.aggregation.*;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @Author: WYX
+ * @CreateTime: 2025/8/16
+ * @Description:
+ */
+@Service
+@Slf4j
+public class DataStatisticServiceImpl implements DataStatisticService {
+ // 特殊单选题型
+ private static final List RADIO_TYPES = Arrays.asList("RADIO_STAR", "RADIO_NPS");
+ private static final String KEY_DATA_CONF = "dataConf";
+ private static final String TYPE_CASCADER = "CASCADER";
+ private static final String SUFFIX_CUSTOM = "_custom";
+ private static final int PHONE_NUMBER_LENGTH = 11;
+ private static final int ID_CARD_LENGTH = 18;
+ private static final int MIN_EMAIL_NAME_VISIBLE_CHARS = 2;
+
+ @Resource
+ private MongoRepository mongoRepository;
+
+ @Resource
+ private MongoTemplate mongoTemplate;
+
+ @Resource
+ private SurveyConfService surveyConfService;
+
+ private static final int MEDIAN_DIVISOR = 2;
+ private static final int DECIMAL_SCALE = 2;
+
+ @Override
+ public DataTableVO getDataTable(DataTableParam param) {
+ // 1. 获取问卷配置
+ SurveyConf surveyConf = surveyConfService.getSurveyConfBySurveyId(param.getSurveyId());
+ if (surveyConf == null || surveyConf.getCode() == null) {
+ return createEmptyResult();
+ }
+
+ // 2. 解析dataList
+ List dataList = extractDataList(surveyConf);
+ List listHead = generateListHead(dataList);
+ Map dataListMap = dataList.stream()
+ .collect(Collectors.toMap(
+ SurveyConfCode.DataItem::getField,
+ item -> item
+ ));
+
+ // 3. 分页查询回收数据
+ Query query = new Query();
+ query.addCriteria(Criteria.where("pageId").is(param.getSurveyId()));
+ query.addCriteria(Criteria.where("isDeleted").ne(true));
+ query.with(Sort.by(Sort.Direction.DESC, "createDate"));
+
+ // 分页处理
+ int skip = (param.getPage() - 1) * param.getPageSize();
+ List surveySubmitList = mongoRepository.page(query, skip / param.getPageSize(),
+ param.getPageSize(), SurveySubmit.class);
+
+ // 总数查询
+ Long total = mongoRepository.count(query, SurveySubmit.class);
+
+ // 4. 数据转换处理
+ List