You've already forked FrameTour-BE
Compare commits
17 Commits
7a35551a7b
...
7ca59a1b0b
Author | SHA1 | Date | |
---|---|---|---|
7ca59a1b0b | |||
f10ede0d2c | |||
9226dfff1d | |||
67f5c274f7 | |||
ff8fe33eb0 | |||
292157885a | |||
ad33b1abef | |||
524627ea73 | |||
1220348bae | |||
e9102e8e58 | |||
e86dc85afe | |||
24f692b69a | |||
b9c65cf030 | |||
58488d2cde | |||
32f7660dc0 | |||
180f89042c | |||
a49450b795 |
@@ -1,6 +1,8 @@
|
|||||||
package com.ycwl.basic.biz;
|
package com.ycwl.basic.biz;
|
||||||
|
|
||||||
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
import com.ycwl.basic.model.mobile.order.IsBuyBatchRespVO;
|
||||||
|
import com.ycwl.basic.model.mobile.order.IsBuyRespVO;
|
||||||
|
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
|
||||||
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
import com.ycwl.basic.model.pc.coupon.entity.CouponEntity;
|
||||||
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
import com.ycwl.basic.model.pc.couponRecord.resp.CouponRecordQueryResp;
|
||||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||||
@@ -15,14 +17,17 @@ import com.ycwl.basic.repository.FaceRepository;
|
|||||||
import com.ycwl.basic.repository.PriceRepository;
|
import com.ycwl.basic.repository.PriceRepository;
|
||||||
import com.ycwl.basic.repository.ScenicRepository;
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
import com.ycwl.basic.repository.TemplateRepository;
|
import com.ycwl.basic.repository.TemplateRepository;
|
||||||
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import org.apache.commons.lang3.Strings;
|
import org.apache.commons.lang3.Strings;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -38,6 +43,9 @@ public class PriceBiz {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FaceRepository faceRepository;
|
private FaceRepository faceRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@Lazy
|
||||||
|
private FaceService faceService;
|
||||||
|
@Autowired
|
||||||
private CouponBiz couponBiz;
|
private CouponBiz couponBiz;
|
||||||
|
|
||||||
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
public List<GoodsListRespVO> listGoodsByScenic(Long scenicId) {
|
||||||
@@ -48,15 +56,16 @@ public class PriceBiz {
|
|||||||
GoodsListRespVO goods = new GoodsListRespVO();
|
GoodsListRespVO goods = new GoodsListRespVO();
|
||||||
goods.setGoodsId(template.getId());
|
goods.setGoodsId(template.getId());
|
||||||
goods.setGoodsName(template.getName());
|
goods.setGoodsName(template.getName());
|
||||||
|
goods.setGoodsType(0);
|
||||||
return goods;
|
return goods;
|
||||||
}).forEach(goodsList::add);
|
}).forEach(goodsList::add);
|
||||||
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
|
||||||
if (scenicConfig != null) {
|
if (scenicConfig != null) {
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
|
||||||
goodsList.add(new GoodsListRespVO(1L, "录像集"));
|
goodsList.add(new GoodsListRespVO(1L, "录像集", 1));
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
|
||||||
goodsList.add(new GoodsListRespVO(2L, "照片集"));
|
goodsList.add(new GoodsListRespVO(2L, "照片集", 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return goodsList;
|
return goodsList;
|
||||||
@@ -140,6 +149,14 @@ public class PriceBiz {
|
|||||||
respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus()));
|
respVO.setBuy(Integer.valueOf(1).equals(orderEntity.getStatus()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (type == -1 && !respVO.isBuy()) {
|
||||||
|
// 查找所有内容是否购买。
|
||||||
|
List<ContentPageVO> list = faceService.faceContentList(faceId);
|
||||||
|
boolean notBuy = list.stream().anyMatch(item -> Integer.valueOf(0).equals(item.getIsBuy()));
|
||||||
|
if (!notBuy) {
|
||||||
|
respVO.setBuy(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
respVO.setShare(false);
|
respVO.setShare(false);
|
||||||
if (face == null || !face.getMemberId().equals(userId)) {
|
if (face == null || !face.getMemberId().equals(userId)) {
|
||||||
respVO.setShare(true);
|
respVO.setShare(true);
|
||||||
|
@@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -134,26 +135,42 @@ public class TemplateBiz {
|
|||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统计每个 placeholder 在模板中出现的次数
|
||||||
|
Map<String, Long> placeholderCounts = templatePlaceholders.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
placeholder -> placeholder,
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
|
||||||
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
|
Map<String, List<SourceEntity>> filteredParams = new HashMap<>();
|
||||||
|
|
||||||
for (String placeholder : templatePlaceholders) {
|
for (Map.Entry<String, Long> entry : placeholderCounts.entrySet()) {
|
||||||
|
String placeholder = entry.getKey();
|
||||||
|
Long requiredCount = entry.getValue();
|
||||||
|
|
||||||
if (placeholder.startsWith("P")) {
|
if (placeholder.startsWith("P")) {
|
||||||
// 图片源:占位符格式为 "P{deviceId}"
|
// 图片源:占位符格式为 "P{deviceId}"
|
||||||
String imageKey = placeholder;
|
String imageKey = placeholder;
|
||||||
if (allTaskParams.containsKey(imageKey)) {
|
if (allTaskParams.containsKey(imageKey)) {
|
||||||
filteredParams.put(imageKey, allTaskParams.get(imageKey));
|
List<SourceEntity> allSources = allTaskParams.get(imageKey);
|
||||||
|
int actualCount = Math.min(requiredCount.intValue(), allSources.size());
|
||||||
|
List<SourceEntity> selectedSources = allSources.subList(0, actualCount);
|
||||||
|
filteredParams.put(imageKey, new ArrayList<>(selectedSources));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 视频源:占位符直接对应设备ID
|
// 视频源:占位符直接对应设备ID
|
||||||
String videoKey = placeholder;
|
String videoKey = placeholder;
|
||||||
if (allTaskParams.containsKey(videoKey)) {
|
if (allTaskParams.containsKey(videoKey)) {
|
||||||
filteredParams.put(videoKey, allTaskParams.get(videoKey));
|
List<SourceEntity> allSources = allTaskParams.get(videoKey);
|
||||||
|
int actualCount = Math.min(requiredCount.intValue(), allSources.size());
|
||||||
|
List<SourceEntity> selectedSources = allSources.subList(0, actualCount);
|
||||||
|
filteredParams.put(videoKey, new ArrayList<>(selectedSources));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}",
|
log.info("filterTaskParams: templateId:{}, original keys:{}, filtered keys:{}, placeholder counts:{}",
|
||||||
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size());
|
templateId, allTaskParams.keySet().size(), filteredParams.keySet().size(), placeholderCounts);
|
||||||
|
|
||||||
return filteredParams;
|
return filteredParams;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile;
|
||||||
|
|
||||||
|
import com.ycwl.basic.annotation.IgnoreToken;
|
||||||
|
import com.ycwl.basic.constant.BaseContextHandler;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端问卷接口控制器
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-09-05
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/questionnaire/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppQuestionnaireController {
|
||||||
|
|
||||||
|
private final QuestionnaireIntegrationService questionnaireIntegrationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷详情
|
||||||
|
* 包含问卷基本信息和所有题目
|
||||||
|
*/
|
||||||
|
@IgnoreToken
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
|
||||||
|
log.info("移动端获取问卷详情, id: {}", id);
|
||||||
|
try {
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||||
|
|
||||||
|
// 检查问卷状态,只有已发布的问卷才能被移动端访问
|
||||||
|
if (questionnaire.getStatus() != 2) {
|
||||||
|
return ApiResponse.fail("问卷未发布或已停止");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.success(questionnaire);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("移动端获取问卷详情失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交问卷答案
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/submit")
|
||||||
|
public ApiResponse<ResponseDetailResponse> submitAnswer(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody SubmitAnswerRequest request) {
|
||||||
|
|
||||||
|
String userId = BaseContextHandler.getUserId();
|
||||||
|
log.info("移动端提交问卷答案, questionnaireId: {}, userId: {}, answers count: {}",
|
||||||
|
id, userId, request.getAnswers() != null ? request.getAnswers().size() : 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置问卷ID和用户ID
|
||||||
|
request.setQuestionnaireId(id);
|
||||||
|
request.setUserId(userId);
|
||||||
|
|
||||||
|
ResponseDetailResponse response = questionnaireIntegrationService.submitAnswer(request);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("移动端提交问卷答案失败, questionnaireId: {}, userId: {}", id, userId, e);
|
||||||
|
return ApiResponse.fail("提交问卷答案失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -91,6 +91,7 @@ public class AppScenicController {
|
|||||||
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable"));
|
resp.setVoucherEnable(scenicConfig.getBoolean("voucher_enable"));
|
||||||
resp.setEnableVoucher(scenicConfig.getBoolean("voucher_enable")); // compactible
|
resp.setEnableVoucher(scenicConfig.getBoolean("voucher_enable")); // compactible
|
||||||
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable"));
|
resp.setGroupingEnable(scenicConfig.getBoolean("grouping_enable"));
|
||||||
|
resp.setShowPhotoWhenWaiting(scenicConfig.getBoolean("show_photo_when_waiting"));
|
||||||
return ApiResponse.success(resp);
|
return ApiResponse.success(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ public class CouponController {
|
|||||||
@GetMapping("/{scenicId}/goodsList")
|
@GetMapping("/{scenicId}/goodsList")
|
||||||
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
public ApiResponse<List<GoodsListRespVO>> scenicGoodsList(@PathVariable Long scenicId) {
|
||||||
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
List<GoodsListRespVO> data = priceBiz.listGoodsByScenic(scenicId);
|
||||||
data.add(new GoodsListRespVO(-1L, "一口价"));
|
data.add(new GoodsListRespVO(-1L, "一口价", -1));
|
||||||
return ApiResponse.success(data);
|
return ApiResponse.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,131 @@
|
|||||||
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||||
|
import com.ycwl.basic.model.pc.printer.req.PrinterPreferredSizeUpdateReq;
|
||||||
|
import com.ycwl.basic.model.pc.printer.req.PrinterStatusUpdateReq;
|
||||||
|
import com.ycwl.basic.model.pc.printer.req.PrinterUsePrinterUpdateReq;
|
||||||
|
import com.ycwl.basic.service.printer.PrinterService;
|
||||||
|
import com.ycwl.basic.utils.ApiConst;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机管理接口
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/pc/printers/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PrinterManageController {
|
||||||
|
|
||||||
|
private final PrinterService printerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机列表查询
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<List<PrinterEntity>> list(@RequestParam(value = "scenicId", required = false) Long scenicId,
|
||||||
|
@RequestParam(value = "status", required = false) Integer status,
|
||||||
|
@RequestParam(value = "name", required = false) String name) {
|
||||||
|
PrinterEntity condition = new PrinterEntity();
|
||||||
|
condition.setScenicId(scenicId);
|
||||||
|
condition.setStatus(status);
|
||||||
|
condition.setName(name);
|
||||||
|
return printerService.list(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<PrinterEntity> detail(@PathVariable("id") Integer id) {
|
||||||
|
ApiResponse<PrinterEntity> response = printerService.get(id);
|
||||||
|
if (response.getData() == null) {
|
||||||
|
return ApiResponse.buildResponse(ApiConst.Code.CODE_NOT_EXIST, "打印机不存在");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增打印机
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<Integer> create(@RequestBody PrinterEntity request) {
|
||||||
|
request.setId(null);
|
||||||
|
return printerService.add(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新打印机信息
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<Integer> update(@PathVariable("id") Integer id, @RequestBody PrinterEntity request) {
|
||||||
|
request.setId(id);
|
||||||
|
return printerService.update(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新打印机状态
|
||||||
|
*/
|
||||||
|
@PatchMapping("/{id}/status")
|
||||||
|
public ApiResponse<Integer> updateStatus(@PathVariable("id") Integer id,
|
||||||
|
@RequestBody PrinterStatusUpdateReq req) {
|
||||||
|
if (req == null || req.getStatus() == null) {
|
||||||
|
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "状态不能为空");
|
||||||
|
}
|
||||||
|
PrinterEntity entity = new PrinterEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setStatus(req.getStatus());
|
||||||
|
return printerService.update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新打印机首选尺寸
|
||||||
|
*/
|
||||||
|
@PatchMapping("/{id}/preferred-size")
|
||||||
|
public ApiResponse<Integer> updatePreferredSize(@PathVariable("id") Integer id,
|
||||||
|
@RequestBody PrinterPreferredSizeUpdateReq req) {
|
||||||
|
if (req == null || (req.getPreferW() == null && req.getPreferH() == null)) {
|
||||||
|
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "首选尺寸不能为空");
|
||||||
|
}
|
||||||
|
PrinterEntity entity = new PrinterEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setPreferW(req.getPreferW());
|
||||||
|
entity.setPreferH(req.getPreferH());
|
||||||
|
return printerService.update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前使用的打印机
|
||||||
|
*/
|
||||||
|
@PatchMapping("/{id}/use-printer")
|
||||||
|
public ApiResponse<Integer> updateUsePrinter(@PathVariable("id") Integer id,
|
||||||
|
@RequestBody PrinterUsePrinterUpdateReq req) {
|
||||||
|
if (req == null) {
|
||||||
|
return ApiResponse.buildResponse(ApiConst.Code.CODE_PARAM_ERROR, "请求参数不能为空");
|
||||||
|
}
|
||||||
|
PrinterEntity entity = new PrinterEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setUsePrinter(req.getUsePrinter());
|
||||||
|
return printerService.update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除打印机
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Integer> delete(@PathVariable("id") Integer id) {
|
||||||
|
return printerService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,313 @@
|
|||||||
|
package com.ycwl.basic.controller.pc;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
|
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.WxMpUtil;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问卷管理 V2 版本控制器 - 基于 zt-questionnaire 集成服务
|
||||||
|
*
|
||||||
|
* @author Claude Code
|
||||||
|
* @date 2025-09-05
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/questionnaire/v2")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionnaireV2Controller {
|
||||||
|
|
||||||
|
private final QuestionnaireIntegrationService questionnaireIntegrationService;
|
||||||
|
private final ScenicRepository scenicRepository;
|
||||||
|
|
||||||
|
// ========== 问卷管理 CRUD 操作 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询问卷列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/")
|
||||||
|
public ApiResponse<PageResponse<QuestionnaireResponse>> listQuestionnaires(
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) Integer status,
|
||||||
|
@RequestParam(required = false) String name) {
|
||||||
|
log.info("分页查询问卷列表, page: {}, pageSize: {}, status: {}, name: {}",
|
||||||
|
page, pageSize, status, name);
|
||||||
|
|
||||||
|
// 参数验证:限制pageSize最大值为100
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageResponse<QuestionnaireResponse> response =
|
||||||
|
questionnaireIntegrationService.getQuestionnaireList(page, pageSize, name, status, null);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("分页查询问卷列表失败", e);
|
||||||
|
return ApiResponse.fail("分页查询问卷列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable Long id) {
|
||||||
|
log.info("获取问卷详情, id: {}", id);
|
||||||
|
try {
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||||
|
return ApiResponse.success(questionnaire);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取问卷详情失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取问卷详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建问卷
|
||||||
|
*/
|
||||||
|
@PostMapping("/")
|
||||||
|
public ApiResponse<QuestionnaireResponse> createQuestionnaire(@Valid @RequestBody CreateQuestionnaireRequest request) {
|
||||||
|
log.info("创建问卷, name: {}, questions count: {}",
|
||||||
|
request.getName(), request.getQuestions() != null ? request.getQuestions().size() : 0);
|
||||||
|
try {
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireIntegrationService.createQuestionnaire(request, "admin");
|
||||||
|
return ApiResponse.success(questionnaire);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建问卷失败", e);
|
||||||
|
return ApiResponse.fail("创建问卷失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新问卷
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<QuestionnaireResponse> updateQuestionnaire(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody CreateQuestionnaireRequest request) {
|
||||||
|
log.info("更新问卷, id: {}", id);
|
||||||
|
try {
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireIntegrationService.updateQuestionnaire(id, request, "admin");
|
||||||
|
return ApiResponse.success(questionnaire);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新问卷失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("更新问卷失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新问卷状态
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
public ApiResponse<String> updateQuestionnaireStatus(@PathVariable Long id, @RequestBody Map<String, Integer> request) {
|
||||||
|
Integer status = request.get("status");
|
||||||
|
log.info("更新问卷状态, id: {}, status: {}", id, status);
|
||||||
|
try {
|
||||||
|
// 根据状态调用不同的方法
|
||||||
|
if (status == 2) {
|
||||||
|
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
|
||||||
|
} else if (status == 3) {
|
||||||
|
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
|
||||||
|
}
|
||||||
|
return ApiResponse.success("问卷状态更新成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新问卷状态失败, id: {}, status: {}", id, status, e);
|
||||||
|
return ApiResponse.fail("更新问卷状态失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布问卷
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}/publish")
|
||||||
|
public ApiResponse<String> publishQuestionnaire(@PathVariable Long id) {
|
||||||
|
log.info("发布问卷, id: {}", id);
|
||||||
|
try {
|
||||||
|
questionnaireIntegrationService.publishQuestionnaire(id, "admin");
|
||||||
|
return ApiResponse.success("问卷发布成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发布问卷失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("发布问卷失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止问卷
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}/stop")
|
||||||
|
public ApiResponse<String> stopQuestionnaire(@PathVariable Long id) {
|
||||||
|
log.info("停止问卷, id: {}", id);
|
||||||
|
try {
|
||||||
|
questionnaireIntegrationService.stopQuestionnaire(id, "admin");
|
||||||
|
return ApiResponse.success("问卷停止成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("停止问卷失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("停止问卷失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除问卷
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<String> deleteQuestionnaire(@PathVariable Long id) {
|
||||||
|
log.info("删除问卷, id: {}", id);
|
||||||
|
try {
|
||||||
|
questionnaireIntegrationService.deleteQuestionnaire(id, "admin");
|
||||||
|
return ApiResponse.success("问卷删除成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除问卷失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("删除问卷失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 问卷答案查看操作 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询问卷答案
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/answers")
|
||||||
|
public ApiResponse<PageResponse<ResponseDetailResponse>> getQuestionnaireAnswers(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) String userId,
|
||||||
|
@RequestParam(required = false) String startTime,
|
||||||
|
@RequestParam(required = false) String endTime) {
|
||||||
|
log.info("分页查询问卷答案, questionnaireId: {}, page: {}, pageSize: {}, userId: {}",
|
||||||
|
id, page, pageSize, userId);
|
||||||
|
|
||||||
|
// 参数验证:限制pageSize最大值为100
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageResponse<ResponseDetailResponse> response =
|
||||||
|
questionnaireIntegrationService.getResponseList(page, pageSize, id, userId, startTime, endTime);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("分页查询问卷答案失败, questionnaireId: {}", id, e);
|
||||||
|
return ApiResponse.fail("分页查询问卷答案失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取特定答案详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/answers/{answerId}")
|
||||||
|
public ApiResponse<ResponseDetailResponse> getQuestionnaireAnswer(@PathVariable Long id, @PathVariable Long answerId) {
|
||||||
|
log.info("获取问卷答案详情, questionnaireId: {}, answerId: {}", id, answerId);
|
||||||
|
try {
|
||||||
|
ResponseDetailResponse answer = questionnaireIntegrationService.getResponseDetail(answerId);
|
||||||
|
return ApiResponse.success(answer);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取问卷答案详情失败, questionnaireId: {}, answerId: {}", id, answerId, e);
|
||||||
|
return ApiResponse.fail("获取问卷答案详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户答题记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/answers/user/{userId}")
|
||||||
|
public ApiResponse<PageResponse<ResponseDetailResponse>> getUserAnswers(
|
||||||
|
@PathVariable String userId,
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) Long questionnaireId) {
|
||||||
|
log.info("查询用户答题记录, userId: {}, page: {}, pageSize: {}, questionnaireId: {}",
|
||||||
|
userId, page, pageSize, questionnaireId);
|
||||||
|
|
||||||
|
// 参数验证:限制pageSize最大值为100
|
||||||
|
if (pageSize > 100) {
|
||||||
|
pageSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageResponse<ResponseDetailResponse> response =
|
||||||
|
questionnaireIntegrationService.getResponseList(page, pageSize, questionnaireId, userId, null, null);
|
||||||
|
return ApiResponse.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询用户答题记录失败, userId: {}", userId, e);
|
||||||
|
return ApiResponse.fail("查询用户答题记录失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 统计功能 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷统计信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/statistics")
|
||||||
|
public ApiResponse<QuestionnaireStatistics> getQuestionnaireStatistics(@PathVariable Long id) {
|
||||||
|
log.info("获取问卷统计信息, id: {}", id);
|
||||||
|
try {
|
||||||
|
QuestionnaireStatistics statistics = questionnaireIntegrationService.getStatistics(id);
|
||||||
|
return ApiResponse.success(statistics);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取问卷统计信息失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("获取问卷统计信息失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载问卷小程序二维码
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/QRCode")
|
||||||
|
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
|
||||||
|
log.info("下载问卷小程序二维码, id: {}", id);
|
||||||
|
try {
|
||||||
|
// 获取问卷详情
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireIntegrationService.getQuestionnaire(id);
|
||||||
|
if (questionnaire == null) {
|
||||||
|
return ApiResponse.fail("问卷不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
MpConfigEntity mpConfig = scenicRepository.getScenicMpConfig(3930324797233434624L);
|
||||||
|
if (mpConfig == null) {
|
||||||
|
return ApiResponse.fail("小程序配置不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String appId = mpConfig.getAppId();
|
||||||
|
String appSecret = mpConfig.getAppSecret();
|
||||||
|
String appState = mpConfig.getState();
|
||||||
|
String path = "pages/questionnaire/index?id=" + id;
|
||||||
|
String filePath = "qr_code_questionnaire_" + id + ".jpg";
|
||||||
|
|
||||||
|
IStorageAdapter adapter = StorageFactory.use();
|
||||||
|
if (adapter.isExists(filePath)) {
|
||||||
|
return ApiResponse.success(adapter.getUrl(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
WxMpUtil.generateWXAQRCode(appId, appSecret, appState, path, filePath);
|
||||||
|
File file = new File(filePath);
|
||||||
|
String s = adapter.uploadFile(null, file, filePath);
|
||||||
|
file.delete();
|
||||||
|
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
|
||||||
|
return ApiResponse.success(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成问卷二维码失败, id: {}", id, e);
|
||||||
|
return ApiResponse.fail("生成二维码失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -24,6 +24,7 @@ Currently implemented:
|
|||||||
- **Scenic Integration** (`com.ycwl.basic.integration.scenic`): ZT-Scenic microservice integration
|
- **Scenic Integration** (`com.ycwl.basic.integration.scenic`): ZT-Scenic microservice integration
|
||||||
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
- **Device Integration** (`com.ycwl.basic.integration.device`): ZT-Device microservice integration
|
||||||
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
- **Render Worker Integration** (`com.ycwl.basic.integration.render`): ZT-Render-Worker microservice integration
|
||||||
|
- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration
|
||||||
|
|
||||||
### Integration Pattern
|
### Integration Pattern
|
||||||
|
|
||||||
@@ -1244,3 +1245,430 @@ logging:
|
|||||||
- **Service-specific management**: Separate cache management per service
|
- **Service-specific management**: Separate cache management per service
|
||||||
- **Debugging support**: Use cache statistics for troubleshooting
|
- **Debugging support**: Use cache statistics for troubleshooting
|
||||||
- **Configuration validation**: Ensure fallback configuration matches service requirements
|
- **Configuration validation**: Ensure fallback configuration matches service requirements
|
||||||
|
|
||||||
|
## Questionnaire Integration (ZT-Questionnaire Microservice)
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
#### Feign Client
|
||||||
|
- **QuestionnaireClient**: Comprehensive questionnaire operations (CRUD, answer submission, statistics)
|
||||||
|
|
||||||
|
#### Service
|
||||||
|
- **QuestionnaireIntegrationService**: High-level questionnaire operations (with automatic fallback for queries)
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
```yaml
|
||||||
|
integration:
|
||||||
|
questionnaire:
|
||||||
|
enabled: true
|
||||||
|
serviceName: zt-questionnaire
|
||||||
|
connectTimeout: 5000
|
||||||
|
readTimeout: 10000
|
||||||
|
retryEnabled: false
|
||||||
|
maxRetries: 3
|
||||||
|
fallback:
|
||||||
|
questionnaire:
|
||||||
|
enabled: true
|
||||||
|
ttlDays: 7
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage Examples
|
||||||
|
|
||||||
|
#### Basic Questionnaire Operations (with Automatic Fallback)
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private QuestionnaireIntegrationService questionnaireService;
|
||||||
|
|
||||||
|
// Get questionnaire details (automatically falls back to cache on failure)
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireService.getQuestionnaire(questionnaireId);
|
||||||
|
|
||||||
|
// Get questionnaire list with filters (automatically falls back to cache on failure)
|
||||||
|
QuestionnaireListResponse list = questionnaireService.getQuestionnaireList(1, 10, "客户调查", 2, null);
|
||||||
|
|
||||||
|
// Get questionnaire statistics (automatically falls back to cache on failure)
|
||||||
|
QuestionnaireStatistics stats = questionnaireService.getStatistics(questionnaireId);
|
||||||
|
|
||||||
|
// Get response records (automatically falls back to cache on failure)
|
||||||
|
ResponseListResponse responses = questionnaireService.getResponseList(1, 10, questionnaireId, null, null, null);
|
||||||
|
|
||||||
|
// Get response details (automatically falls back to cache on failure)
|
||||||
|
ResponseDetailResponse responseDetail = questionnaireService.getResponseDetail(responseId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Questionnaire Management Operations (Direct Operations)
|
||||||
|
```java
|
||||||
|
// Create questionnaire (direct operation, fails immediately on error)
|
||||||
|
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
|
||||||
|
request.setName("客户满意度调查");
|
||||||
|
request.setDescription("收集客户对服务的满意度反馈");
|
||||||
|
request.setIsAnonymous(true);
|
||||||
|
request.setMaxAnswers(1000);
|
||||||
|
|
||||||
|
// Add single-choice question
|
||||||
|
CreateQuestionRequest question1 = new CreateQuestionRequest();
|
||||||
|
question1.setTitle("您对我们的服务满意吗?");
|
||||||
|
question1.setType(1); // 单选题
|
||||||
|
question1.setIsRequired(true);
|
||||||
|
question1.setSort(1);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options1 = new ArrayList<>();
|
||||||
|
options1.add(new CreateQuestionOptionRequest("非常满意", "5", 1));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("满意", "4", 2));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("一般", "3", 3));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("不满意", "2", 4));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("非常不满意", "1", 5));
|
||||||
|
question1.setOptions(options1);
|
||||||
|
|
||||||
|
// Add multiple-choice question
|
||||||
|
CreateQuestionRequest question2 = new CreateQuestionRequest();
|
||||||
|
question2.setTitle("您感兴趣的服务有哪些?");
|
||||||
|
question2.setType(2); // 多选题
|
||||||
|
question2.setIsRequired(false);
|
||||||
|
question2.setSort(2);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options2 = new ArrayList<>();
|
||||||
|
options2.add(new CreateQuestionOptionRequest("技术支持", "tech_support", 1));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("产品培训", "training", 2));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("定制开发", "custom_dev", 3));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("其他", "others", 4));
|
||||||
|
question2.setOptions(options2);
|
||||||
|
|
||||||
|
// Add text area question
|
||||||
|
CreateQuestionRequest question3 = new CreateQuestionRequest();
|
||||||
|
question3.setTitle("您还有什么建议吗?");
|
||||||
|
question3.setType(4); // 文本域题
|
||||||
|
question3.setIsRequired(false);
|
||||||
|
question3.setSort(3);
|
||||||
|
question3.setOptions(null); // 文本域题不需要选项
|
||||||
|
|
||||||
|
request.setQuestions(Arrays.asList(question1, question2, question3));
|
||||||
|
|
||||||
|
QuestionnaireResponse created = questionnaireService.createQuestionnaire(request, "admin");
|
||||||
|
|
||||||
|
// Update questionnaire (direct operation, fails immediately on error)
|
||||||
|
CreateQuestionnaireRequest updateRequest = new CreateQuestionnaireRequest();
|
||||||
|
updateRequest.setName("更新后的客户满意度调查");
|
||||||
|
QuestionnaireResponse updated = questionnaireService.updateQuestionnaire(questionnaireId, updateRequest, "admin");
|
||||||
|
|
||||||
|
// Publish questionnaire (direct operation, fails immediately on error)
|
||||||
|
QuestionnaireResponse published = questionnaireService.publishQuestionnaire(questionnaireId, "admin");
|
||||||
|
|
||||||
|
// Stop questionnaire (direct operation, fails immediately on error)
|
||||||
|
QuestionnaireResponse stopped = questionnaireService.stopQuestionnaire(questionnaireId, "admin");
|
||||||
|
|
||||||
|
// Delete questionnaire (direct operation, fails immediately on error)
|
||||||
|
questionnaireService.deleteQuestionnaire(questionnaireId, "admin");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Answer Submission
|
||||||
|
```java
|
||||||
|
// Submit questionnaire answers (direct operation, no fallback)
|
||||||
|
SubmitAnswerRequest answerRequest = new SubmitAnswerRequest();
|
||||||
|
answerRequest.setQuestionnaireId(questionnaireId);
|
||||||
|
answerRequest.setUserId("user123");
|
||||||
|
|
||||||
|
List<AnswerRequest> answers = new ArrayList<>();
|
||||||
|
// Single-choice answer
|
||||||
|
answers.add(new AnswerRequest(123L, "4")); // 满意
|
||||||
|
// Multiple-choice answer
|
||||||
|
answers.add(new AnswerRequest(124L, "tech_support,training")); // 技术支持和产品培训
|
||||||
|
// Text area answer
|
||||||
|
answers.add(new AnswerRequest(125L, "服务很好,希望能增加更多实用功能"));
|
||||||
|
|
||||||
|
answerRequest.setAnswers(answers);
|
||||||
|
|
||||||
|
ResponseDetailResponse response = questionnaireService.submitAnswer(answerRequest);
|
||||||
|
log.info("答案提交成功,回答ID: {}", response.getId());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Question Types and Answer Formats
|
||||||
|
|
||||||
|
##### 1. Single Choice (Type 1)
|
||||||
|
```java
|
||||||
|
// Creating single-choice question
|
||||||
|
CreateQuestionRequest singleChoice = new CreateQuestionRequest();
|
||||||
|
singleChoice.setTitle("您的性别是?");
|
||||||
|
singleChoice.setType(1);
|
||||||
|
singleChoice.setIsRequired(true);
|
||||||
|
singleChoice.setSort(1);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options = new ArrayList<>();
|
||||||
|
options.add(new CreateQuestionOptionRequest("男", "male", 1));
|
||||||
|
options.add(new CreateQuestionOptionRequest("女", "female", 2));
|
||||||
|
options.add(new CreateQuestionOptionRequest("不愿透露", "prefer_not_to_say", 3));
|
||||||
|
singleChoice.setOptions(options);
|
||||||
|
|
||||||
|
// Submitting single-choice answer
|
||||||
|
AnswerRequest singleChoiceAnswer = new AnswerRequest(123L, "male");
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Multiple Choice (Type 2)
|
||||||
|
```java
|
||||||
|
// Creating multiple-choice question
|
||||||
|
CreateQuestionRequest multipleChoice = new CreateQuestionRequest();
|
||||||
|
multipleChoice.setTitle("您感兴趣的编程语言有哪些?");
|
||||||
|
multipleChoice.setType(2);
|
||||||
|
multipleChoice.setIsRequired(false);
|
||||||
|
multipleChoice.setSort(2);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options = new ArrayList<>();
|
||||||
|
options.add(new CreateQuestionOptionRequest("Java", "java", 1));
|
||||||
|
options.add(new CreateQuestionOptionRequest("Python", "python", 2));
|
||||||
|
options.add(new CreateQuestionOptionRequest("Go", "go", 3));
|
||||||
|
options.add(new CreateQuestionOptionRequest("JavaScript", "javascript", 4));
|
||||||
|
multipleChoice.setOptions(options);
|
||||||
|
|
||||||
|
// Submitting multiple-choice answer (comma-separated values)
|
||||||
|
AnswerRequest multipleChoiceAnswer = new AnswerRequest(124L, "java,python,go");
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 3. Fill in Blank (Type 3)
|
||||||
|
```java
|
||||||
|
// Creating fill-in-blank question
|
||||||
|
CreateQuestionRequest fillInBlank = new CreateQuestionRequest();
|
||||||
|
fillInBlank.setTitle("请输入您的姓名");
|
||||||
|
fillInBlank.setType(3);
|
||||||
|
fillInBlank.setIsRequired(true);
|
||||||
|
fillInBlank.setSort(3);
|
||||||
|
fillInBlank.setOptions(null); // No options needed
|
||||||
|
|
||||||
|
// Submitting fill-in-blank answer
|
||||||
|
AnswerRequest fillAnswer = new AnswerRequest(125L, "张三");
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 4. Text Area (Type 4)
|
||||||
|
```java
|
||||||
|
// Creating text area question
|
||||||
|
CreateQuestionRequest textArea = new CreateQuestionRequest();
|
||||||
|
textArea.setTitle("请详细描述您对我们产品的建议");
|
||||||
|
textArea.setType(4);
|
||||||
|
textArea.setIsRequired(false);
|
||||||
|
textArea.setSort(4);
|
||||||
|
textArea.setOptions(null); // No options needed
|
||||||
|
|
||||||
|
// Submitting text area answer
|
||||||
|
AnswerRequest textAnswer = new AnswerRequest(126L, "建议增加更多功能,提升用户体验...");
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 5. Rating (Type 5)
|
||||||
|
```java
|
||||||
|
// Creating rating question
|
||||||
|
CreateQuestionRequest rating = new CreateQuestionRequest();
|
||||||
|
rating.setTitle("请对我们的服务进行评分(1-10分)");
|
||||||
|
rating.setType(5);
|
||||||
|
rating.setIsRequired(true);
|
||||||
|
rating.setSort(5);
|
||||||
|
rating.setOptions(null); // No options needed, range controlled by frontend
|
||||||
|
|
||||||
|
// Submitting rating answer
|
||||||
|
AnswerRequest ratingAnswer = new AnswerRequest(127L, "8");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Complete Questionnaire Workflow
|
||||||
|
```java
|
||||||
|
// 1. Create questionnaire
|
||||||
|
CreateQuestionnaireRequest createRequest = buildSampleQuestionnaire();
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireService.createQuestionnaire(createRequest, "admin");
|
||||||
|
|
||||||
|
// 2. Publish questionnaire
|
||||||
|
QuestionnaireResponse published = questionnaireService.publishQuestionnaire(questionnaire.getId(), "admin");
|
||||||
|
|
||||||
|
// 3. Users submit answers
|
||||||
|
SubmitAnswerRequest answerRequest = buildSampleAnswers(questionnaire.getId());
|
||||||
|
ResponseDetailResponse answerResponse = questionnaireService.submitAnswer(answerRequest);
|
||||||
|
|
||||||
|
// 4. View statistics
|
||||||
|
QuestionnaireStatistics statistics = questionnaireService.getStatistics(questionnaire.getId());
|
||||||
|
log.info("Statistics - Total responses: {}, Completion rate: {}%",
|
||||||
|
statistics.getTotalResponses(), statistics.getCompletionRate() * 100);
|
||||||
|
|
||||||
|
// 5. Stop questionnaire when done
|
||||||
|
QuestionnaireResponse stopped = questionnaireService.stopQuestionnaire(questionnaire.getId(), "admin");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fallback Cache Management for Questionnaires
|
||||||
|
```java
|
||||||
|
@Autowired
|
||||||
|
private IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
// Check fallback cache status
|
||||||
|
boolean hasQuestionnaireCache = fallbackService.hasFallbackCache("zt-questionnaire", "questionnaire:1001");
|
||||||
|
boolean hasListCache = fallbackService.hasFallbackCache("zt-questionnaire", "questionnaire:list:1:10:null:null:null");
|
||||||
|
boolean hasStatsCache = fallbackService.hasFallbackCache("zt-questionnaire", "questionnaire:statistics:1001");
|
||||||
|
|
||||||
|
// Get cache statistics
|
||||||
|
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats("zt-questionnaire");
|
||||||
|
log.info("Questionnaire fallback cache: {} items, TTL: {} days",
|
||||||
|
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
|
||||||
|
|
||||||
|
// Clear specific cache
|
||||||
|
fallbackService.clearFallbackCache("zt-questionnaire", "questionnaire:1001");
|
||||||
|
|
||||||
|
// Clear all questionnaire caches
|
||||||
|
fallbackService.clearAllFallbackCache("zt-questionnaire");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Question Types and Validation Rules
|
||||||
|
|
||||||
|
| Question Type | Type Value | Description | Options Required | Answer Format |
|
||||||
|
|---------------|------------|-------------|------------------|---------------|
|
||||||
|
| Single Choice | 1 | User can select one answer | Yes (2+ options) | Single option value |
|
||||||
|
| Multiple Choice | 2 | User can select multiple answers | Yes (2+ options) | Comma-separated option values |
|
||||||
|
| Fill in Blank | 3 | User inputs short text | No | Text content (1-200 chars) |
|
||||||
|
| Text Area | 4 | User inputs long text | No | Text content (1-2000 chars) |
|
||||||
|
| Rating | 5 | User provides numerical rating | No | Number as string (e.g., "1", "10") |
|
||||||
|
|
||||||
|
### Answer Validation Rules
|
||||||
|
|
||||||
|
| Question Type | Validation Rules | Example |
|
||||||
|
|---------------|------------------|---------|
|
||||||
|
| Single Choice | Must be existing option value | "male", "female" |
|
||||||
|
| Multiple Choice | Comma-separated existing option values | "java,python", "option1,option2,option3" |
|
||||||
|
| Fill in Blank | Non-empty string, 1-200 characters | "张三", "北京市" |
|
||||||
|
| Text Area | String, 1-2000 characters | "这是一段较长的文本内容..." |
|
||||||
|
| Rating | Numeric string, typically 1-10 range | "1", "5", "10" |
|
||||||
|
|
||||||
|
### Questionnaire Status
|
||||||
|
|
||||||
|
- **1**: Draft - Questionnaire is being edited
|
||||||
|
- **2**: Published - Questionnaire is live and accepting responses
|
||||||
|
- **3**: Stopped - Questionnaire is no longer accepting responses
|
||||||
|
- **4**: Deleted - Questionnaire has been deleted
|
||||||
|
|
||||||
|
### Common Use Cases
|
||||||
|
|
||||||
|
#### Customer Satisfaction Survey
|
||||||
|
```java
|
||||||
|
// Create customer satisfaction questionnaire with rating and feedback
|
||||||
|
CreateQuestionnaireRequest customerSurvey = new CreateQuestionnaireRequest();
|
||||||
|
customerSurvey.setName("客户满意度调查");
|
||||||
|
customerSurvey.setDescription("收集客户对服务的满意度反馈");
|
||||||
|
customerSurvey.setIsAnonymous(true);
|
||||||
|
|
||||||
|
// Add rating question
|
||||||
|
CreateQuestionRequest ratingQ = new CreateQuestionRequest();
|
||||||
|
ratingQ.setTitle("整体满意度评分(1-10分)");
|
||||||
|
ratingQ.setType(5);
|
||||||
|
ratingQ.setIsRequired(true);
|
||||||
|
|
||||||
|
// Add feedback question
|
||||||
|
CreateQuestionRequest feedbackQ = new CreateQuestionRequest();
|
||||||
|
feedbackQ.setTitle("请提供具体的改进建议");
|
||||||
|
feedbackQ.setType(4);
|
||||||
|
feedbackQ.setIsRequired(false);
|
||||||
|
|
||||||
|
customerSurvey.setQuestions(Arrays.asList(ratingQ, feedbackQ));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Product Feature Feedback
|
||||||
|
```java
|
||||||
|
// Create product feature questionnaire with multiple choice and priorities
|
||||||
|
CreateQuestionnaireRequest featureSurvey = new CreateQuestionnaireRequest();
|
||||||
|
featureSurvey.setName("产品功能需求调研");
|
||||||
|
featureSurvey.setIsAnonymous(false);
|
||||||
|
|
||||||
|
// Priority features question
|
||||||
|
CreateQuestionRequest featuresQ = new CreateQuestionRequest();
|
||||||
|
featuresQ.setTitle("您最希望我们优先开发哪些功能?");
|
||||||
|
featuresQ.setType(2); // Multiple choice
|
||||||
|
featuresQ.setIsRequired(true);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> featureOptions = new ArrayList<>();
|
||||||
|
featureOptions.add(new CreateQuestionOptionRequest("移动端适配", "mobile_support", 1));
|
||||||
|
featureOptions.add(new CreateQuestionOptionRequest("数据导出", "data_export", 2));
|
||||||
|
featureOptions.add(new CreateQuestionOptionRequest("API集成", "api_integration", 3));
|
||||||
|
featureOptions.add(new CreateQuestionOptionRequest("高级分析", "advanced_analytics", 4));
|
||||||
|
featuresQ.setOptions(featureOptions);
|
||||||
|
|
||||||
|
featureSurvey.setQuestions(Arrays.asList(featuresQ));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling and HTTP Status Codes
|
||||||
|
|
||||||
|
#### HTTP Status Codes
|
||||||
|
- **200 OK**: Operation successful
|
||||||
|
- **201 Created**: Questionnaire/response created successfully
|
||||||
|
- **400 Bad Request**: Invalid request parameters or validation errors
|
||||||
|
- **404 Not Found**: Questionnaire or response not found
|
||||||
|
- **500 Internal Server Error**: Server error occurred
|
||||||
|
|
||||||
|
#### Common Error Scenarios
|
||||||
|
```java
|
||||||
|
try {
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireService.getQuestionnaire(invalidId);
|
||||||
|
} catch (IntegrationException e) {
|
||||||
|
switch (e.getCode()) {
|
||||||
|
case 404:
|
||||||
|
log.warn("问卷不存在: {}", invalidId);
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
log.warn("请求参数错误: {}", e.getMessage());
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
log.error("服务器内部错误: {}", e.getMessage());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error("未知错误: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Questionnaire Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run questionnaire integration tests
|
||||||
|
mvn test -Dtest=QuestionnaireIntegrationServiceTest
|
||||||
|
|
||||||
|
# Run all integration tests
|
||||||
|
mvn test -Dtest="com.ycwl.basic.integration.*Test"
|
||||||
|
|
||||||
|
# Enable example runner in application-dev.yml
|
||||||
|
integration:
|
||||||
|
questionnaire:
|
||||||
|
example:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Properties
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
integration:
|
||||||
|
questionnaire:
|
||||||
|
enabled: true # Enable questionnaire integration
|
||||||
|
serviceName: zt-questionnaire # Service name for Nacos discovery
|
||||||
|
connectTimeout: 5000 # Connection timeout in ms
|
||||||
|
readTimeout: 10000 # Read timeout in ms
|
||||||
|
retryEnabled: false # Enable retry mechanism
|
||||||
|
maxRetries: 3 # Maximum retry attempts
|
||||||
|
|
||||||
|
fallback:
|
||||||
|
questionnaire:
|
||||||
|
enabled: true # Enable fallback for questionnaire service
|
||||||
|
ttlDays: 7 # Cache TTL in days
|
||||||
|
cachePrefix: "questionnaire:fallback:" # Optional custom prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices for Questionnaire Integration
|
||||||
|
|
||||||
|
#### Query vs Mutation Operations
|
||||||
|
- **Query operations (GET)**: Use fallback - questionnaire details, lists, statistics, responses
|
||||||
|
- **Mutation operations (POST/PUT/DELETE)**: No fallback - create, update, delete, publish, stop, submit
|
||||||
|
|
||||||
|
#### Cache Key Design
|
||||||
|
- `questionnaire:{id}` - Individual questionnaire cache
|
||||||
|
- `questionnaire:list:{page}:{size}:{name}:{status}:{createdBy}` - List cache
|
||||||
|
- `questionnaire:statistics:{id}` - Statistics cache
|
||||||
|
- `response:{id}` - Individual response cache
|
||||||
|
- `responses:list:{page}:{size}:{questionnaireId}:{userId}` - Response list cache
|
||||||
|
|
||||||
|
#### Answer Submission Best Practices
|
||||||
|
- Validate question types before submission
|
||||||
|
- Handle validation errors gracefully
|
||||||
|
- Provide clear error messages for users
|
||||||
|
- Log submission attempts for audit purposes
|
||||||
|
|
||||||
|
#### Performance Considerations
|
||||||
|
- Use appropriate page sizes for questionnaire lists
|
||||||
|
- Cache frequently accessed questionnaires
|
||||||
|
- Monitor response submission patterns
|
||||||
|
- Implement rate limiting for public questionnaires
|
@@ -31,6 +31,11 @@ public class IntegrationProperties {
|
|||||||
*/
|
*/
|
||||||
private RenderWorkerConfig render = new RenderWorkerConfig();
|
private RenderWorkerConfig render = new RenderWorkerConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问卷服务配置
|
||||||
|
*/
|
||||||
|
private QuestionnaireConfig questionnaire = new QuestionnaireConfig();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class ScenicConfig {
|
public static class ScenicConfig {
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +109,7 @@ public class IntegrationProperties {
|
|||||||
private ServiceFallbackConfig scenic = new ServiceFallbackConfig();
|
private ServiceFallbackConfig scenic = new ServiceFallbackConfig();
|
||||||
private ServiceFallbackConfig device = new ServiceFallbackConfig();
|
private ServiceFallbackConfig device = new ServiceFallbackConfig();
|
||||||
private ServiceFallbackConfig render = new ServiceFallbackConfig();
|
private ServiceFallbackConfig render = new ServiceFallbackConfig();
|
||||||
|
private ServiceFallbackConfig questionnaire = new ServiceFallbackConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -131,6 +137,31 @@ public class IntegrationProperties {
|
|||||||
private int maxRetries = 3;
|
private int maxRetries = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class QuestionnaireConfig {
|
||||||
|
/**
|
||||||
|
* 是否启用问卷服务集成
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务名称
|
||||||
|
*/
|
||||||
|
private String serviceName = "zt-questionnaire";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超时配置(毫秒)
|
||||||
|
*/
|
||||||
|
private int connectTimeout = 5000;
|
||||||
|
private int readTimeout = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试配置
|
||||||
|
*/
|
||||||
|
private boolean retryEnabled = false;
|
||||||
|
private int maxRetries = 3;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class ServiceFallbackConfig {
|
public static class ServiceFallbackConfig {
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,118 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.client;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@FeignClient(name = "zt-questionnaire", contextId = "questionnaire", path = "/api")
|
||||||
|
public interface QuestionnaireClient {
|
||||||
|
|
||||||
|
// ==================== 问卷管理接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建问卷
|
||||||
|
*/
|
||||||
|
@PostMapping("/questionnaires")
|
||||||
|
CommonResponse<QuestionnaireResponse> createQuestionnaire(
|
||||||
|
@RequestBody CreateQuestionnaireRequest request,
|
||||||
|
@RequestHeader("X-User-ID") String userId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/questionnaires/{id}")
|
||||||
|
CommonResponse<QuestionnaireResponse> getQuestionnaire(@PathVariable("id") Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/questionnaires")
|
||||||
|
CommonResponse<PageResponse<QuestionnaireResponse>> getQuestionnaireList(
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) Integer status,
|
||||||
|
@RequestParam(required = false) String createdBy
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新问卷
|
||||||
|
*/
|
||||||
|
@PutMapping("/questionnaires/{id}")
|
||||||
|
CommonResponse<QuestionnaireResponse> updateQuestionnaire(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestBody CreateQuestionnaireRequest request,
|
||||||
|
@RequestHeader("X-User-ID") String userId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除问卷
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/questionnaires/{id}")
|
||||||
|
CommonResponse<Void> deleteQuestionnaire(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestHeader("X-User-ID") String userId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布问卷
|
||||||
|
*/
|
||||||
|
@PostMapping("/questionnaires/{id}/publish")
|
||||||
|
CommonResponse<QuestionnaireResponse> publishQuestionnaire(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestHeader("X-User-ID") String userId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止问卷
|
||||||
|
*/
|
||||||
|
@PostMapping("/questionnaires/{id}/stop")
|
||||||
|
CommonResponse<QuestionnaireResponse> stopQuestionnaire(
|
||||||
|
@PathVariable("id") Long id,
|
||||||
|
@RequestHeader("X-User-ID") String userId
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== 答案提交接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交问卷答案
|
||||||
|
*/
|
||||||
|
@PostMapping("/questionnaires/submit")
|
||||||
|
CommonResponse<ResponseDetailResponse> submitAnswer(@RequestBody SubmitAnswerRequest request);
|
||||||
|
|
||||||
|
// ==================== 统计分析接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问卷统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/questionnaires/{id}/statistics")
|
||||||
|
CommonResponse<QuestionnaireStatistics> getStatistics(@PathVariable("id") Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回答记录列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/responses")
|
||||||
|
CommonResponse<PageResponse<ResponseDetailResponse>> getResponseList(
|
||||||
|
@RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize,
|
||||||
|
@RequestParam(required = false) Long questionnaireId,
|
||||||
|
@RequestParam(required = false) String userId,
|
||||||
|
@RequestParam(required = false) String startTime,
|
||||||
|
@RequestParam(required = false) String endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取回答详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/responses/{id}")
|
||||||
|
CommonResponse<ResponseDetailResponse> getResponseDetail(@PathVariable("id") Long id);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.config;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.config.IntegrationProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(prefix = "integration.questionnaire", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class QuestionnaireIntegrationConfig {
|
||||||
|
|
||||||
|
private final IntegrationProperties integrationProperties;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.answer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AnswerDetailResponse {
|
||||||
|
|
||||||
|
@JsonProperty("questionId")
|
||||||
|
private Long questionId;
|
||||||
|
|
||||||
|
@JsonProperty("questionTitle")
|
||||||
|
private String questionTitle;
|
||||||
|
|
||||||
|
@JsonProperty("questionType")
|
||||||
|
private Integer questionType;
|
||||||
|
|
||||||
|
@JsonProperty("answer")
|
||||||
|
private String answer;
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.answer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AnswerRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "问题ID不能为空")
|
||||||
|
@JsonProperty("questionId")
|
||||||
|
private Long questionId;
|
||||||
|
|
||||||
|
@NotBlank(message = "答案内容不能为空")
|
||||||
|
@JsonProperty("answer")
|
||||||
|
private String answer;
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.answer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ResponseDetailResponse {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("questionnaireId")
|
||||||
|
private Long questionnaireId;
|
||||||
|
|
||||||
|
@JsonProperty("userId")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@JsonProperty("ipAddress")
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@JsonProperty("submittedAt")
|
||||||
|
private String submittedAt;
|
||||||
|
|
||||||
|
@JsonProperty("answers")
|
||||||
|
private List<AnswerDetailResponse> answers;
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.answer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SubmitAnswerRequest {
|
||||||
|
|
||||||
|
@JsonProperty("questionnaireId")
|
||||||
|
private Long questionnaireId;
|
||||||
|
|
||||||
|
@JsonProperty("userId")
|
||||||
|
private String userId; // 可选,用于非匿名问卷
|
||||||
|
|
||||||
|
@NotEmpty(message = "答案不能为空")
|
||||||
|
@Valid
|
||||||
|
@JsonProperty("answers")
|
||||||
|
private List<AnswerRequest> answers;
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.question;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CreateQuestionOptionRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "选项文本不能为空")
|
||||||
|
@Size(max = 500, message = "选项文本长度不能超过500字符")
|
||||||
|
@JsonProperty("text")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
@NotBlank(message = "选项值不能为空")
|
||||||
|
@Size(max = 100, message = "选项值长度不能超过100字符")
|
||||||
|
@JsonProperty("value")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@JsonProperty("sort")
|
||||||
|
private Integer sort = 0;
|
||||||
|
}
|
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.question;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateQuestionRequest {
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "问题标题不能为空")
|
||||||
|
@Size(max = 500, message = "问题标题长度不能超过500字符")
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull(message = "问题类型不能为空")
|
||||||
|
@Min(value = 1, message = "问题类型无效")
|
||||||
|
@Max(value = 5, message = "问题类型无效")
|
||||||
|
@JsonProperty("type")
|
||||||
|
private Integer type; // 1:单选 2:多选 3:填空 4:文本域 5:评分
|
||||||
|
|
||||||
|
@JsonProperty("isRequired")
|
||||||
|
private Boolean isRequired = false;
|
||||||
|
|
||||||
|
@JsonProperty("sort")
|
||||||
|
private Integer sort = 0;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty("options")
|
||||||
|
private List<CreateQuestionOptionRequest> options;
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.question;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QuestionOptionResponse {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("text")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
@JsonProperty("value")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@JsonProperty("sort")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@JsonProperty("createdAt")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
@JsonProperty("updatedAt")
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.question;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QuestionResponse {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty("type")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@JsonProperty("typeText")
|
||||||
|
private String typeText;
|
||||||
|
|
||||||
|
@JsonProperty("isRequired")
|
||||||
|
private Boolean isRequired;
|
||||||
|
|
||||||
|
@JsonProperty("sort")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@JsonProperty("createdAt")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
@JsonProperty("updatedAt")
|
||||||
|
private String updatedAt;
|
||||||
|
|
||||||
|
@JsonProperty("options")
|
||||||
|
private List<QuestionOptionResponse> options;
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.questionnaire;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionRequest;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreateQuestionnaireRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "问卷名称不能为空")
|
||||||
|
@Size(max = 255, message = "问卷名称长度不能超过255字符")
|
||||||
|
@JsonProperty("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty("startTime")
|
||||||
|
private String startTime; // 格式: "2024-01-01 00:00:00"
|
||||||
|
|
||||||
|
@JsonProperty("endTime")
|
||||||
|
private String endTime; // 格式: "2024-12-31 23:59:59"
|
||||||
|
|
||||||
|
@JsonProperty("isAnonymous")
|
||||||
|
private Boolean isAnonymous = true;
|
||||||
|
|
||||||
|
@JsonProperty("maxAnswers")
|
||||||
|
private Integer maxAnswers = 0;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty("questions")
|
||||||
|
private List<CreateQuestionRequest> questions;
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.questionnaire;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.question.QuestionResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QuestionnaireResponse {
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonProperty("name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty("status")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@JsonProperty("statusText")
|
||||||
|
private String statusText;
|
||||||
|
|
||||||
|
@JsonProperty("createdBy")
|
||||||
|
private String createdBy;
|
||||||
|
|
||||||
|
@JsonProperty("startTime")
|
||||||
|
private String startTime;
|
||||||
|
|
||||||
|
@JsonProperty("endTime")
|
||||||
|
private String endTime;
|
||||||
|
|
||||||
|
@JsonProperty("isAnonymous")
|
||||||
|
private Boolean isAnonymous;
|
||||||
|
|
||||||
|
@JsonProperty("maxAnswers")
|
||||||
|
private Integer maxAnswers;
|
||||||
|
|
||||||
|
@JsonProperty("createdAt")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
@JsonProperty("updatedAt")
|
||||||
|
private String updatedAt;
|
||||||
|
|
||||||
|
@JsonProperty("questions")
|
||||||
|
private List<QuestionResponse> questions;
|
||||||
|
|
||||||
|
@JsonProperty("statistics")
|
||||||
|
private QuestionnaireStatistics statistics;
|
||||||
|
}
|
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.dto.statistics;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QuestionnaireStatistics {
|
||||||
|
|
||||||
|
@JsonProperty("totalResponses")
|
||||||
|
private Integer totalResponses;
|
||||||
|
|
||||||
|
@JsonProperty("completionRate")
|
||||||
|
private Double completionRate;
|
||||||
|
|
||||||
|
@JsonProperty("averageTime")
|
||||||
|
private Integer averageTime; // 平均答题时间(秒)
|
||||||
|
|
||||||
|
@JsonProperty("questionStats")
|
||||||
|
private List<QuestionStatistics> questionStats;
|
||||||
|
|
||||||
|
@JsonProperty("responsesByDate")
|
||||||
|
private Map<String, Integer> responsesByDate;
|
||||||
|
|
||||||
|
@JsonProperty("createdAt")
|
||||||
|
private String createdAt;
|
||||||
|
|
||||||
|
@JsonProperty("updatedAt")
|
||||||
|
private String updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
class QuestionStatistics {
|
||||||
|
|
||||||
|
@JsonProperty("questionId")
|
||||||
|
private Long questionId;
|
||||||
|
|
||||||
|
@JsonProperty("questionTitle")
|
||||||
|
private String questionTitle;
|
||||||
|
|
||||||
|
@JsonProperty("questionType")
|
||||||
|
private Integer questionType;
|
||||||
|
|
||||||
|
@JsonProperty("totalAnswers")
|
||||||
|
private Integer totalAnswers;
|
||||||
|
|
||||||
|
@JsonProperty("optionStats")
|
||||||
|
private List<OptionStatistics> optionStats;
|
||||||
|
|
||||||
|
@JsonProperty("textAnswers")
|
||||||
|
private List<String> textAnswers; // 用于填空题和文本域题
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
class OptionStatistics {
|
||||||
|
|
||||||
|
@JsonProperty("optionId")
|
||||||
|
private Long optionId;
|
||||||
|
|
||||||
|
@JsonProperty("optionText")
|
||||||
|
private String optionText;
|
||||||
|
|
||||||
|
@JsonProperty("optionValue")
|
||||||
|
private String optionValue;
|
||||||
|
|
||||||
|
@JsonProperty("count")
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@JsonProperty("percentage")
|
||||||
|
private Double percentage;
|
||||||
|
}
|
@@ -0,0 +1,306 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.example;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.AnswerRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionOptionRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.question.CreateQuestionRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.service.QuestionnaireIntegrationService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(prefix = "integration.questionnaire.example", name = "enabled", havingValue = "true")
|
||||||
|
public class QuestionnaireIntegrationExample {
|
||||||
|
|
||||||
|
private final QuestionnaireIntegrationService questionnaireService;
|
||||||
|
private final IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void runExamples() {
|
||||||
|
try {
|
||||||
|
log.info("=== 开始问卷集成服务示例 ===");
|
||||||
|
|
||||||
|
// 示例1:创建问卷
|
||||||
|
createQuestionnaireExample();
|
||||||
|
|
||||||
|
// 示例2:查询问卷
|
||||||
|
queryQuestionnaireExample();
|
||||||
|
|
||||||
|
// 示例3:提交答案
|
||||||
|
submitAnswerExample();
|
||||||
|
|
||||||
|
// 示例4:统计查询
|
||||||
|
statisticsExample();
|
||||||
|
|
||||||
|
// 示例5:Fallback 缓存管理
|
||||||
|
fallbackCacheExample();
|
||||||
|
|
||||||
|
log.info("=== 问卷集成服务示例完成 ===");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("问卷集成服务示例执行失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例1:创建问卷
|
||||||
|
*/
|
||||||
|
private void createQuestionnaireExample() {
|
||||||
|
log.info("--- 示例1:创建客户满意度问卷 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
|
||||||
|
request.setName("客户满意度调查");
|
||||||
|
request.setDescription("用于了解客户对我们服务的满意度");
|
||||||
|
request.setIsAnonymous(true);
|
||||||
|
request.setMaxAnswers(1000);
|
||||||
|
|
||||||
|
// 添加单选题
|
||||||
|
CreateQuestionRequest question1 = new CreateQuestionRequest();
|
||||||
|
question1.setTitle("您对我们的服务满意吗?");
|
||||||
|
question1.setType(1); // 单选题
|
||||||
|
question1.setIsRequired(true);
|
||||||
|
question1.setSort(1);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options1 = new ArrayList<>();
|
||||||
|
options1.add(new CreateQuestionOptionRequest("非常满意", "5", 1));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("满意", "4", 2));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("一般", "3", 3));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("不满意", "2", 4));
|
||||||
|
options1.add(new CreateQuestionOptionRequest("非常不满意", "1", 5));
|
||||||
|
question1.setOptions(options1);
|
||||||
|
|
||||||
|
// 添加多选题
|
||||||
|
CreateQuestionRequest question2 = new CreateQuestionRequest();
|
||||||
|
question2.setTitle("您感兴趣的服务有哪些?");
|
||||||
|
question2.setType(2); // 多选题
|
||||||
|
question2.setIsRequired(false);
|
||||||
|
question2.setSort(2);
|
||||||
|
|
||||||
|
List<CreateQuestionOptionRequest> options2 = new ArrayList<>();
|
||||||
|
options2.add(new CreateQuestionOptionRequest("技术支持", "tech_support", 1));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("产品培训", "training", 2));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("定制开发", "custom_dev", 3));
|
||||||
|
options2.add(new CreateQuestionOptionRequest("其他", "others", 4));
|
||||||
|
question2.setOptions(options2);
|
||||||
|
|
||||||
|
// 添加文本域题
|
||||||
|
CreateQuestionRequest question3 = new CreateQuestionRequest();
|
||||||
|
question3.setTitle("您还有什么建议吗?");
|
||||||
|
question3.setType(4); // 文本域题
|
||||||
|
question3.setIsRequired(false);
|
||||||
|
question3.setSort(3);
|
||||||
|
question3.setOptions(null); // 文本域题不需要选项
|
||||||
|
|
||||||
|
request.setQuestions(Arrays.asList(question1, question2, question3));
|
||||||
|
|
||||||
|
QuestionnaireResponse response = questionnaireService.createQuestionnaire(request, "admin");
|
||||||
|
log.info("✅ 问卷创建成功,ID: {}, 名称: {}", response.getId(), response.getName());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 创建问卷示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例2:查询问卷
|
||||||
|
*/
|
||||||
|
private void queryQuestionnaireExample() {
|
||||||
|
log.info("--- 示例2:查询问卷示例 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取问卷列表(支持 fallback)
|
||||||
|
PageResponse<QuestionnaireResponse> listResponse = questionnaireService.getQuestionnaireList(1, 10, null, null, null);
|
||||||
|
log.info("✅ 问卷列表查询成功,总数: {}, 当前页数据: {}",
|
||||||
|
listResponse.getTotal(), listResponse.getList().size());
|
||||||
|
|
||||||
|
if (listResponse.getList() != null && !listResponse.getList().isEmpty()) {
|
||||||
|
Long questionnaireId = listResponse.getList().get(0).getId();
|
||||||
|
|
||||||
|
// 获取问卷详情(支持 fallback)
|
||||||
|
QuestionnaireResponse detailResponse = questionnaireService.getQuestionnaire(questionnaireId);
|
||||||
|
log.info("✅ 问卷详情查询成功,ID: {}, 名称: {}, 问题数: {}",
|
||||||
|
detailResponse.getId(), detailResponse.getName(),
|
||||||
|
detailResponse.getQuestions() != null ? detailResponse.getQuestions().size() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 查询问卷示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例3:提交答案
|
||||||
|
*/
|
||||||
|
private void submitAnswerExample() {
|
||||||
|
log.info("--- 示例3:提交问卷答案示例 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
SubmitAnswerRequest request = new SubmitAnswerRequest();
|
||||||
|
request.setQuestionnaireId(1001L);
|
||||||
|
request.setUserId("user123");
|
||||||
|
|
||||||
|
List<AnswerRequest> answers = new ArrayList<>();
|
||||||
|
// 单选题答案
|
||||||
|
answers.add(new AnswerRequest(123L, "4")); // 满意
|
||||||
|
// 多选题答案
|
||||||
|
answers.add(new AnswerRequest(124L, "tech_support,training")); // 技术支持和产品培训
|
||||||
|
// 文本域题答案
|
||||||
|
answers.add(new AnswerRequest(125L, "服务很好,希望能增加更多实用功能"));
|
||||||
|
|
||||||
|
request.setAnswers(answers);
|
||||||
|
|
||||||
|
ResponseDetailResponse response = questionnaireService.submitAnswer(request);
|
||||||
|
log.info("✅ 问卷答案提交成功,回答ID: {}, 提交时间: {}",
|
||||||
|
response.getId(), response.getSubmittedAt());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 提交答案示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例4:统计查询
|
||||||
|
*/
|
||||||
|
private void statisticsExample() {
|
||||||
|
log.info("--- 示例4:问卷统计查询示例 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Long questionnaireId = 1001L;
|
||||||
|
|
||||||
|
// 获取问卷统计(支持 fallback)
|
||||||
|
QuestionnaireStatistics stats = questionnaireService.getStatistics(questionnaireId);
|
||||||
|
log.info("✅ 统计查询成功,总回答数: {}, 完成率: {}%, 平均用时: {}秒",
|
||||||
|
stats.getTotalResponses(),
|
||||||
|
stats.getCompletionRate() != null ? stats.getCompletionRate() * 100 : 0,
|
||||||
|
stats.getAverageTime());
|
||||||
|
|
||||||
|
// 获取回答记录列表(支持 fallback)
|
||||||
|
questionnaireService.getResponseList(1, 10, questionnaireId, null, null, null);
|
||||||
|
log.info("✅ 回答记录列表查询成功");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 统计查询示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例5:Fallback 缓存管理
|
||||||
|
*/
|
||||||
|
private void fallbackCacheExample() {
|
||||||
|
log.info("--- 示例5:Fallback 缓存管理示例 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String serviceName = "zt-questionnaire";
|
||||||
|
|
||||||
|
// 检查缓存状态
|
||||||
|
boolean hasQuestionnaireCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:1001");
|
||||||
|
boolean hasListCache = fallbackService.hasFallbackCache(serviceName, "questionnaire:list:1:10:null:null:null");
|
||||||
|
log.info("✅ 缓存状态检查 - 问卷缓存: {}, 列表缓存: {}", hasQuestionnaireCache, hasListCache);
|
||||||
|
|
||||||
|
// 获取缓存统计
|
||||||
|
IntegrationFallbackService.FallbackCacheStats stats = fallbackService.getFallbackCacheStats(serviceName);
|
||||||
|
log.info("✅ 缓存统计 - 缓存项目数: {}, TTL: {} 天",
|
||||||
|
stats.getTotalCacheCount(), stats.getFallbackTtlDays());
|
||||||
|
|
||||||
|
// 清理特定缓存示例(仅演示,实际使用时谨慎操作)
|
||||||
|
// fallbackService.clearFallbackCache(serviceName, "questionnaire:1001");
|
||||||
|
// log.info("✅ 已清理问卷缓存");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ Fallback 缓存管理示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 问卷管理工作流示例
|
||||||
|
*/
|
||||||
|
public void questionnaireWorkflowExample(String userId) {
|
||||||
|
log.info("--- 问卷管理工作流示例 ---");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建问卷
|
||||||
|
CreateQuestionnaireRequest createRequest = createSampleQuestionnaire();
|
||||||
|
QuestionnaireResponse questionnaire = questionnaireService.createQuestionnaire(createRequest, userId);
|
||||||
|
log.info("✅ 步骤1 - 问卷创建成功: {}", questionnaire.getName());
|
||||||
|
|
||||||
|
Long questionnaireId = questionnaire.getId();
|
||||||
|
|
||||||
|
// 2. 发布问卷
|
||||||
|
QuestionnaireResponse published = questionnaireService.publishQuestionnaire(questionnaireId, userId);
|
||||||
|
log.info("✅ 步骤2 - 问卷发布成功,状态: {}", published.getStatusText());
|
||||||
|
|
||||||
|
// 3. 模拟用户提交答案
|
||||||
|
SubmitAnswerRequest answerRequest = createSampleAnswers(questionnaireId);
|
||||||
|
ResponseDetailResponse answerResponse = questionnaireService.submitAnswer(answerRequest);
|
||||||
|
log.info("✅ 步骤3 - 答案提交成功: {}", answerResponse.getId());
|
||||||
|
|
||||||
|
// 4. 查看统计数据
|
||||||
|
QuestionnaireStatistics statistics = questionnaireService.getStatistics(questionnaireId);
|
||||||
|
log.info("✅ 步骤4 - 统计查询成功,回答数: {}", statistics.getTotalResponses());
|
||||||
|
|
||||||
|
// 5. 停止问卷
|
||||||
|
QuestionnaireResponse stopped = questionnaireService.stopQuestionnaire(questionnaireId, userId);
|
||||||
|
log.info("✅ 步骤5 - 问卷停止成功,状态: {}", stopped.getStatusText());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 问卷管理工作流示例失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreateQuestionnaireRequest createSampleQuestionnaire() {
|
||||||
|
CreateQuestionnaireRequest request = new CreateQuestionnaireRequest();
|
||||||
|
request.setName("产品体验调查");
|
||||||
|
request.setDescription("收集用户对产品的使用体验反馈");
|
||||||
|
request.setIsAnonymous(false);
|
||||||
|
request.setMaxAnswers(500);
|
||||||
|
|
||||||
|
// 评分题
|
||||||
|
CreateQuestionRequest ratingQuestion = new CreateQuestionRequest();
|
||||||
|
ratingQuestion.setTitle("请对我们的产品进行评分(1-10分)");
|
||||||
|
ratingQuestion.setType(5); // 评分题
|
||||||
|
ratingQuestion.setIsRequired(true);
|
||||||
|
ratingQuestion.setSort(1);
|
||||||
|
ratingQuestion.setOptions(null); // 评分题不需要选项
|
||||||
|
|
||||||
|
// 填空题
|
||||||
|
CreateQuestionRequest textQuestion = new CreateQuestionRequest();
|
||||||
|
textQuestion.setTitle("请输入您的姓名");
|
||||||
|
textQuestion.setType(3); // 填空题
|
||||||
|
textQuestion.setIsRequired(true);
|
||||||
|
textQuestion.setSort(2);
|
||||||
|
textQuestion.setOptions(null); // 填空题不需要选项
|
||||||
|
|
||||||
|
request.setQuestions(Arrays.asList(ratingQuestion, textQuestion));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubmitAnswerRequest createSampleAnswers(Long questionnaireId) {
|
||||||
|
SubmitAnswerRequest request = new SubmitAnswerRequest();
|
||||||
|
request.setQuestionnaireId(questionnaireId);
|
||||||
|
request.setUserId("test_user");
|
||||||
|
|
||||||
|
List<AnswerRequest> answers = new ArrayList<>();
|
||||||
|
answers.add(new AnswerRequest(1L, "8")); // 评分题答案
|
||||||
|
answers.add(new AnswerRequest(2L, "张三")); // 填空题答案
|
||||||
|
|
||||||
|
request.setAnswers(answers);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,138 @@
|
|||||||
|
package com.ycwl.basic.integration.questionnaire.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.integration.common.exception.IntegrationException;
|
||||||
|
import com.ycwl.basic.integration.common.response.CommonResponse;
|
||||||
|
import com.ycwl.basic.integration.common.response.PageResponse;
|
||||||
|
import com.ycwl.basic.integration.common.service.IntegrationFallbackService;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.client.QuestionnaireClient;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.answer.SubmitAnswerRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.CreateQuestionnaireRequest;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse;
|
||||||
|
import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionnaireIntegrationService {
|
||||||
|
|
||||||
|
private final QuestionnaireClient questionnaireClient;
|
||||||
|
private final IntegrationFallbackService fallbackService;
|
||||||
|
|
||||||
|
private static final String SERVICE_NAME = "zt-questionnaire";
|
||||||
|
|
||||||
|
// ==================== 问卷查询接口(支持 fallback) ====================
|
||||||
|
|
||||||
|
public QuestionnaireResponse getQuestionnaire(Long id) {
|
||||||
|
log.info("获取问卷详情, id: {}", id);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"questionnaire:" + id,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<QuestionnaireResponse> response = questionnaireClient.getQuestionnaire(id);
|
||||||
|
return handleResponse(response, "获取问卷详情失败");
|
||||||
|
},
|
||||||
|
QuestionnaireResponse.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PageResponse<QuestionnaireResponse> getQuestionnaireList(Integer page, Integer pageSize,
|
||||||
|
String name, Integer status, String createdBy) {
|
||||||
|
log.info("获取问卷列表, page: {}, pageSize: {}, name: {}, status: {}, createdBy: {}",
|
||||||
|
page, pageSize, name, status, createdBy);
|
||||||
|
CommonResponse<PageResponse<QuestionnaireResponse>> response =
|
||||||
|
questionnaireClient.getQuestionnaireList(page, pageSize, name, status, createdBy);
|
||||||
|
return handleResponse(response, "获取问卷列表失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestionnaireStatistics getStatistics(Long id) {
|
||||||
|
log.info("获取问卷统计, id: {}", id);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"questionnaire:statistics:" + id,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<QuestionnaireStatistics> response = questionnaireClient.getStatistics(id);
|
||||||
|
return handleResponse(response, "获取问卷统计失败");
|
||||||
|
},
|
||||||
|
QuestionnaireStatistics.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PageResponse<ResponseDetailResponse> getResponseList(Integer page, Integer pageSize, Long questionnaireId,
|
||||||
|
String userId, String startTime, String endTime) {
|
||||||
|
log.info("获取回答记录列表, page: {}, pageSize: {}, questionnaireId: {}, userId: {}",
|
||||||
|
page, pageSize, questionnaireId, userId);
|
||||||
|
CommonResponse<PageResponse<ResponseDetailResponse>> response =
|
||||||
|
questionnaireClient.getResponseList(page, pageSize, questionnaireId, userId, startTime, endTime);
|
||||||
|
return handleResponse(response, "获取回答记录列表失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseDetailResponse getResponseDetail(Long id) {
|
||||||
|
log.info("获取回答详情, id: {}", id);
|
||||||
|
return fallbackService.executeWithFallback(
|
||||||
|
SERVICE_NAME,
|
||||||
|
"response:" + id,
|
||||||
|
() -> {
|
||||||
|
CommonResponse<ResponseDetailResponse> response = questionnaireClient.getResponseDetail(id);
|
||||||
|
return handleResponse(response, "获取回答详情失败");
|
||||||
|
},
|
||||||
|
ResponseDetailResponse.class
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 问卷管理接口(直接执行,不支持 fallback) ====================
|
||||||
|
|
||||||
|
public QuestionnaireResponse createQuestionnaire(CreateQuestionnaireRequest request, String userId) {
|
||||||
|
log.info("创建问卷, name: {}, userId: {}", request.getName(), userId);
|
||||||
|
CommonResponse<QuestionnaireResponse> response = questionnaireClient.createQuestionnaire(request, userId);
|
||||||
|
return handleResponse(response, "创建问卷失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestionnaireResponse updateQuestionnaire(Long id, CreateQuestionnaireRequest request, String userId) {
|
||||||
|
log.info("更新问卷, id: {}, userId: {}", id, userId);
|
||||||
|
CommonResponse<QuestionnaireResponse> response = questionnaireClient.updateQuestionnaire(id, request, userId);
|
||||||
|
return handleResponse(response, "更新问卷失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteQuestionnaire(Long id, String userId) {
|
||||||
|
log.info("删除问卷, id: {}, userId: {}", id, userId);
|
||||||
|
CommonResponse<Void> response = questionnaireClient.deleteQuestionnaire(id, userId);
|
||||||
|
handleResponse(response, "删除问卷失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestionnaireResponse publishQuestionnaire(Long id, String userId) {
|
||||||
|
log.info("发布问卷, id: {}, userId: {}", id, userId);
|
||||||
|
CommonResponse<QuestionnaireResponse> response = questionnaireClient.publishQuestionnaire(id, userId);
|
||||||
|
return handleResponse(response, "发布问卷失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestionnaireResponse stopQuestionnaire(Long id, String userId) {
|
||||||
|
log.info("停止问卷, id: {}, userId: {}", id, userId);
|
||||||
|
CommonResponse<QuestionnaireResponse> response = questionnaireClient.stopQuestionnaire(id, userId);
|
||||||
|
return handleResponse(response, "停止问卷失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseDetailResponse submitAnswer(SubmitAnswerRequest request) {
|
||||||
|
log.info("提交问卷答案, questionnaireId: {}, userId: {}", request.getQuestionnaireId(), request.getUserId());
|
||||||
|
CommonResponse<ResponseDetailResponse> response = questionnaireClient.submitAnswer(request);
|
||||||
|
return handleResponse(response, "提交问卷答案失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
|
private <T> T handleResponse(CommonResponse<T> response, String errorMessage) {
|
||||||
|
if (response == null || !response.isSuccess()) {
|
||||||
|
String msg = response != null && response.getMessage() != null
|
||||||
|
? response.getMessage()
|
||||||
|
: errorMessage;
|
||||||
|
Integer code = response != null ? response.getCode() : 5000;
|
||||||
|
throw new IntegrationException(code, msg, SERVICE_NAME);
|
||||||
|
}
|
||||||
|
return response.getData();
|
||||||
|
}
|
||||||
|
}
|
@@ -10,4 +10,5 @@ import lombok.NoArgsConstructor;
|
|||||||
public class GoodsListRespVO {
|
public class GoodsListRespVO {
|
||||||
private Long goodsId;
|
private Long goodsId;
|
||||||
private String goodsName;
|
private String goodsName;
|
||||||
|
private Integer goodsType;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ycwl.basic.model.pc.printer.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机首选尺寸更新请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PrinterPreferredSizeUpdateReq {
|
||||||
|
/**
|
||||||
|
* 首选宽度
|
||||||
|
*/
|
||||||
|
private Integer preferW;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首选高度
|
||||||
|
*/
|
||||||
|
private Integer preferH;
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.model.pc.printer.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机状态更新请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PrinterStatusUpdateReq {
|
||||||
|
/**
|
||||||
|
* 打印机状态:0-停用,1-启用
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.model.pc.printer.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印机当前使用设备更新请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PrinterUsePrinterUpdateReq {
|
||||||
|
/**
|
||||||
|
* 当前使用打印机标识
|
||||||
|
*/
|
||||||
|
private String usePrinter;
|
||||||
|
}
|
@@ -48,4 +48,5 @@ public class ScenicConfigResp {
|
|||||||
private Boolean voucherEnable;
|
private Boolean voucherEnable;
|
||||||
private Boolean enableVoucher;
|
private Boolean enableVoucher;
|
||||||
private Boolean groupingEnable;
|
private Boolean groupingEnable;
|
||||||
|
private Boolean showPhotoWhenWaiting;
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@ com.ycwl.basic.pricing/
|
|||||||
│ │ DiscountResult.java, DiscountCombinationResult.java
|
│ │ DiscountResult.java, DiscountCombinationResult.java
|
||||||
│ ├── BundleProductItem.java, MobilePriceCalculationRequest.java
|
│ ├── BundleProductItem.java, MobilePriceCalculationRequest.java
|
||||||
│ ├── OnePriceConfigRequest.java, OnePriceInfo.java
|
│ ├── OnePriceConfigRequest.java, OnePriceInfo.java
|
||||||
|
│ ├── BundleDiscountInfo.java # 打包购买优惠信息
|
||||||
│ ├── req/ # 券码管理请求DTO
|
│ ├── req/ # 券码管理请求DTO
|
||||||
│ │ ├── VoucherBatchCreateReq(.java|V2)
|
│ │ ├── VoucherBatchCreateReq(.java|V2)
|
||||||
│ │ ├── VoucherBatchQueryReq.java, VoucherCodeQueryReq.java, VoucherClaimReq.java
|
│ │ ├── VoucherBatchQueryReq.java, VoucherCodeQueryReq.java, VoucherClaimReq.java
|
||||||
@@ -58,6 +59,7 @@ com.ycwl.basic.pricing/
|
|||||||
│ └── PriceOnePriceConfigMapper.java, VoucherPrintRecordMapper.java
|
│ └── PriceOnePriceConfigMapper.java, VoucherPrintRecordMapper.java
|
||||||
└── service/ # 业务层接口与实现
|
└── service/ # 业务层接口与实现
|
||||||
├── IPriceCalculationService.java, IDiscountDetectionService.java, IDiscountProvider.java
|
├── IPriceCalculationService.java, IDiscountDetectionService.java, IDiscountProvider.java
|
||||||
|
├── IBundleDiscountService.java # 打包购买优惠服务接口
|
||||||
├── IProductConfigService.java, IPricingManagementService.java, IPriceBundleService.java
|
├── IProductConfigService.java, IPricingManagementService.java, IPriceBundleService.java
|
||||||
├── ICouponService.java, ICouponManagementService.java
|
├── ICouponService.java, ICouponManagementService.java
|
||||||
├── IOnePricePurchaseService.java, IVoucherService.java, IVoucherUsageService.java
|
├── IOnePricePurchaseService.java, IVoucherService.java, IVoucherUsageService.java
|
||||||
@@ -69,7 +71,8 @@ com.ycwl.basic.pricing/
|
|||||||
├── VoucherServiceImpl.java, VoucherDiscountProvider.java,
|
├── VoucherServiceImpl.java, VoucherDiscountProvider.java,
|
||||||
├── VoucherBatchServiceImpl.java, VoucherCodeServiceImpl.java, VoucherPrintServiceImpl.java,
|
├── VoucherBatchServiceImpl.java, VoucherCodeServiceImpl.java, VoucherPrintServiceImpl.java,
|
||||||
├── VoucherUsageServiceImpl.java,
|
├── VoucherUsageServiceImpl.java,
|
||||||
└── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java
|
├── OnePricePurchaseServiceImpl.java, OnePricePurchaseDiscountProvider.java
|
||||||
|
└── BundleDiscountServiceImpl.java, BundleDiscountProvider.java # 打包购买优惠实现
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心功能
|
## 核心功能
|
||||||
@@ -333,30 +336,37 @@ public interface IDiscountDetectionService {
|
|||||||
|
|
||||||
### 2. 优惠提供者实现(当前实现与优先级)
|
### 2. 优惠提供者实现(当前实现与优先级)
|
||||||
|
|
||||||
#### VoucherDiscountProvider (优先级: 100)
|
#### OnePricePurchaseDiscountProvider (优先级: 120)
|
||||||
|
- 处理一口价优惠逻辑(景区级统一价格)
|
||||||
|
- **最高优先级**,优先于所有其他优惠类型
|
||||||
|
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
|
||||||
|
|
||||||
|
#### BundleDiscountProvider (优先级: 100)
|
||||||
|
- 处理打包购买优惠逻辑(多商品组合优惠)
|
||||||
|
- 支持多种优惠类型:固定减免、百分比折扣、固定价格
|
||||||
|
- 可配置叠加规则(与优惠券、券码、一口价的组合限制)
|
||||||
|
- 自动检测购物车中符合条件的商品组合
|
||||||
|
|
||||||
|
#### VoucherDiscountProvider (优先级: 80)
|
||||||
- 处理券码优惠逻辑
|
- 处理券码优惠逻辑
|
||||||
- 支持用户主动输入券码或自动选择最优券码
|
- 支持用户主动输入券码或自动选择最优券码
|
||||||
- 全场免费券码不可与其他优惠叠加
|
- 全场免费券码不可与其他优惠叠加
|
||||||
|
|
||||||
#### CouponDiscountProvider (优先级: 80)
|
#### CouponDiscountProvider (优先级: 60)
|
||||||
- 处理优惠券优惠逻辑
|
- 处理优惠券优惠逻辑
|
||||||
|
- **最低优先级**,在所有其他优惠之后应用
|
||||||
- 自动选择最优优惠券
|
- 自动选择最优优惠券
|
||||||
- 可与券码叠加使用(除全场免费券码外)
|
|
||||||
|
|
||||||
#### OnePricePurchaseDiscountProvider (优先级: 60)
|
|
||||||
- 处理一口价优惠逻辑(景区级统一价格)
|
|
||||||
- 仅当一口价小于当前金额时产生优惠;是否可与券码/优惠券叠加由配置 `canUseCoupon/canUseVoucher` 决定
|
|
||||||
|
|
||||||
### 3. 优惠应用策略
|
### 3. 优惠应用策略
|
||||||
|
|
||||||
#### 优先级规则
|
#### 优先级规则
|
||||||
```
|
```
|
||||||
券码 (100) → 优惠券 (80) → 一口价 (60)
|
一口价 (120) → 打包购买 (100) → 券码 (80) → 优惠券 (60)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 叠加逻辑
|
#### 叠加逻辑
|
||||||
```java
|
```java
|
||||||
原价 → 券码 → 优惠券 → 一口价 → 最终价格
|
原价 → 一口价 → 打包购买 → 券码 → 优惠券 → 最终价格
|
||||||
|
|
||||||
特殊情况:
|
特殊情况:
|
||||||
- 全场免费券码:直接最终价=0,停止后续优惠
|
- 全场免费券码:直接最终价=0,停止后续优惠
|
||||||
@@ -385,6 +395,88 @@ public class FlashSaleDiscountProvider implements IDiscountProvider {
|
|||||||
// 按优先级排序并注册到 DiscountDetectionService 中
|
// 按优先级排序并注册到 DiscountDetectionService 中
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 打包购买优惠系统 (Bundle Discount System)
|
||||||
|
|
||||||
|
### 1. 核心特性
|
||||||
|
|
||||||
|
打包购买优惠系统是新增的优惠类型,支持多商品组合优惠策略,具有第二高优先级(仅次于一口价)。
|
||||||
|
|
||||||
|
#### 优惠类型支持
|
||||||
|
- **FIXED_DISCOUNT**: 固定减免金额(如满2件减50元)
|
||||||
|
- **PERCENTAGE_DISCOUNT**: 百分比折扣(如多商品组合9折)
|
||||||
|
- **FIXED_PRICE**: 固定套餐价格(如照片+视频套餐199元)
|
||||||
|
|
||||||
|
#### 触发条件
|
||||||
|
- **商品数量要求**: 最低购买数量限制
|
||||||
|
- **商品金额要求**: 最低购买金额限制
|
||||||
|
- **商品类型组合**: 特定商品类型的组合(如照片+视频)
|
||||||
|
|
||||||
|
### 2. 业务规则
|
||||||
|
|
||||||
|
#### 自动检测规则
|
||||||
|
```java
|
||||||
|
// 多商品类型组合优惠
|
||||||
|
- 条件:购买不同类型商品 >= 2种
|
||||||
|
- 优惠:9折优惠
|
||||||
|
- 可叠加:可与优惠券、券码叠加,不可与一口价叠加
|
||||||
|
|
||||||
|
// 大批量购买优惠
|
||||||
|
- 条件:总数量 >= 10件 且 总金额 >= 500元
|
||||||
|
- 优惠:减免50元
|
||||||
|
- 可叠加:可与优惠券、券码叠加,不可与一口价叠加
|
||||||
|
|
||||||
|
// 特定组合套餐
|
||||||
|
- 条件:同时购买照片集和Vlog视频
|
||||||
|
- 优惠:套餐价199元
|
||||||
|
- 可叠加:不可与其他优惠叠加
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 叠加规则配置
|
||||||
|
每个打包优惠规则都可以独立配置与其他优惠的叠加关系:
|
||||||
|
- `canUseWithCoupon`: 是否可与优惠券叠加
|
||||||
|
- `canUseWithVoucher`: 是否可与券码叠加
|
||||||
|
- `canUseWithOnePrice`: 是否可与一口价叠加
|
||||||
|
|
||||||
|
### 3. 核心接口
|
||||||
|
|
||||||
|
#### IBundleDiscountService
|
||||||
|
```java
|
||||||
|
// 检测可用的打包优惠
|
||||||
|
List<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context);
|
||||||
|
|
||||||
|
// 计算打包优惠金额
|
||||||
|
BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products);
|
||||||
|
|
||||||
|
// 获取最优的打包优惠组合
|
||||||
|
BundleDiscountInfo getBestBundleDiscount(List<ProductItem> products, Long scenicId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### BundleDiscountProvider
|
||||||
|
- 实现 `IDiscountProvider` 接口
|
||||||
|
- 优先级:100(第二高,仅次于一口价的120)
|
||||||
|
- 自动集成到统一优惠检测系统
|
||||||
|
|
||||||
|
### 4. 扩展开发
|
||||||
|
|
||||||
|
#### 添加新的打包规则
|
||||||
|
```java
|
||||||
|
// 在 BundleDiscountServiceImpl 中添加新规则
|
||||||
|
private BundleDiscountInfo createNewBundleRule() {
|
||||||
|
BundleDiscountInfo bundle = new BundleDiscountInfo();
|
||||||
|
bundle.setBundleConfigId(4L);
|
||||||
|
bundle.setBundleName("新打包规则");
|
||||||
|
bundle.setDiscountType("PERCENTAGE_DISCOUNT");
|
||||||
|
bundle.setDiscountValue(new BigDecimal("0.85")); // 8.5折
|
||||||
|
bundle.setMinQuantity(5);
|
||||||
|
bundle.setMinAmount(new BigDecimal("300"));
|
||||||
|
// 配置叠加规则...
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据库配置支持
|
||||||
|
后续可以扩展为从数据库加载打包规则配置,替换当前的硬编码规则。
|
||||||
|
|
||||||
## API 接口扩展
|
## API 接口扩展
|
||||||
|
|
||||||
### 1. 价格计算接口扩展
|
### 1. 价格计算接口扩展
|
||||||
|
@@ -0,0 +1,79 @@
|
|||||||
|
package com.ycwl.basic.pricing.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包购买优惠信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BundleDiscountInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包配置ID
|
||||||
|
*/
|
||||||
|
private Long bundleConfigId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包名称
|
||||||
|
*/
|
||||||
|
private String bundleName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包描述
|
||||||
|
*/
|
||||||
|
private String bundleDescription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包优惠类型
|
||||||
|
* FIXED_DISCOUNT: 固定减免金额
|
||||||
|
* PERCENTAGE_DISCOUNT: 百分比折扣
|
||||||
|
* FIXED_PRICE: 固定价格
|
||||||
|
*/
|
||||||
|
private String discountType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优惠值(根据类型不同含义不同)
|
||||||
|
* FIXED_DISCOUNT: 减免金额
|
||||||
|
* PERCENTAGE_DISCOUNT: 折扣百分比(如0.8表示8折)
|
||||||
|
* FIXED_PRICE: 固定价格
|
||||||
|
*/
|
||||||
|
private BigDecimal discountValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 满足条件的商品列表
|
||||||
|
*/
|
||||||
|
private List<ProductItem> eligibleProducts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最低购买数量要求
|
||||||
|
*/
|
||||||
|
private Integer minQuantity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最低购买金额要求
|
||||||
|
*/
|
||||||
|
private BigDecimal minAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际优惠金额
|
||||||
|
*/
|
||||||
|
private BigDecimal actualDiscountAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可与其他优惠叠加
|
||||||
|
*/
|
||||||
|
private Boolean canUseWithCoupon = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可与券码叠加
|
||||||
|
*/
|
||||||
|
private Boolean canUseWithVoucher = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可与一口价叠加
|
||||||
|
*/
|
||||||
|
private Boolean canUseWithOnePrice = true;
|
||||||
|
}
|
@@ -84,4 +84,9 @@ public class DiscountInfo {
|
|||||||
* 一口价信息(如果是一口价优惠)
|
* 一口价信息(如果是一口价优惠)
|
||||||
*/
|
*/
|
||||||
private OnePriceInfo onePriceInfo;
|
private OnePriceInfo onePriceInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包优惠信息(如果是打包优惠)
|
||||||
|
*/
|
||||||
|
private BundleDiscountInfo bundleDiscountInfo;
|
||||||
}
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ycwl.basic.pricing.service;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pricing.dto.BundleDiscountInfo;
|
||||||
|
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||||
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包购买优惠服务接口
|
||||||
|
*/
|
||||||
|
public interface IBundleDiscountService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测可用的打包优惠
|
||||||
|
*
|
||||||
|
* @param context 优惠检测上下文
|
||||||
|
* @return 可用的打包优惠列表
|
||||||
|
*/
|
||||||
|
List<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算打包优惠金额
|
||||||
|
*
|
||||||
|
* @param bundleDiscount 打包优惠信息
|
||||||
|
* @param products 商品列表
|
||||||
|
* @return 优惠金额
|
||||||
|
*/
|
||||||
|
BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否符合打包条件
|
||||||
|
*
|
||||||
|
* @param products 商品列表
|
||||||
|
* @param minQuantity 最少数量要求
|
||||||
|
* @param minAmount 最少金额要求
|
||||||
|
* @return 是否符合条件
|
||||||
|
*/
|
||||||
|
boolean isEligibleForBundle(List<ProductItem> products, Integer minQuantity, BigDecimal minAmount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据商品类型和数量获取打包优惠规则
|
||||||
|
*
|
||||||
|
* @param products 商品列表
|
||||||
|
* @param scenicId 景区ID(可选)
|
||||||
|
* @return 匹配的打包优惠规则
|
||||||
|
*/
|
||||||
|
List<BundleDiscountInfo> getBundleDiscountRules(List<ProductItem> products, Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证打包优惠是否仍然有效
|
||||||
|
*
|
||||||
|
* @param bundleDiscount 打包优惠信息
|
||||||
|
* @param context 优惠检测上下文
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
boolean isBundleDiscountValid(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最优的打包优惠组合
|
||||||
|
*
|
||||||
|
* @param products 商品列表
|
||||||
|
* @param scenicId 景区ID(可选)
|
||||||
|
* @return 最优打包优惠
|
||||||
|
*/
|
||||||
|
BundleDiscountInfo getBestBundleDiscount(List<ProductItem> products, Long scenicId);
|
||||||
|
}
|
@@ -0,0 +1,204 @@
|
|||||||
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pricing.dto.*;
|
||||||
|
import com.ycwl.basic.pricing.service.IBundleDiscountService;
|
||||||
|
import com.ycwl.basic.pricing.service.IDiscountProvider;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包购买优惠提供者
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BundleDiscountProvider implements IDiscountProvider {
|
||||||
|
|
||||||
|
private final IBundleDiscountService bundleDiscountService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderType() {
|
||||||
|
return "BUNDLE_PURCHASE";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 100; // 第二高优先级,仅次于一口价
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DiscountInfo> detectAvailableDiscounts(DiscountDetectionContext context) {
|
||||||
|
List<DiscountInfo> discounts = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (context.getProducts() == null || context.getProducts().isEmpty()) {
|
||||||
|
log.debug("打包优惠检测失败: 商品列表为空");
|
||||||
|
return discounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测所有可用的打包优惠
|
||||||
|
List<BundleDiscountInfo> bundleDiscounts = bundleDiscountService.detectAvailableBundleDiscounts(context);
|
||||||
|
|
||||||
|
for (BundleDiscountInfo bundleDiscount : bundleDiscounts) {
|
||||||
|
if (bundleDiscount.getActualDiscountAmount() != null &&
|
||||||
|
bundleDiscount.getActualDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
|
||||||
|
// 创建优惠信息
|
||||||
|
DiscountInfo discountInfo = new DiscountInfo();
|
||||||
|
discountInfo.setProviderType(getProviderType());
|
||||||
|
discountInfo.setDiscountName(bundleDiscount.getBundleName());
|
||||||
|
discountInfo.setDiscountAmount(bundleDiscount.getActualDiscountAmount());
|
||||||
|
discountInfo.setDiscountDescription(bundleDiscount.getBundleDescription());
|
||||||
|
discountInfo.setBundleDiscountInfo(bundleDiscount);
|
||||||
|
discountInfo.setPriority(getPriority());
|
||||||
|
discountInfo.setStackable(true); // 默认可叠加,具体规则由配置控制
|
||||||
|
|
||||||
|
discounts.add(discountInfo);
|
||||||
|
|
||||||
|
log.info("检测到打包优惠: 名称={}, 优惠金额={}",
|
||||||
|
bundleDiscount.getBundleName(), bundleDiscount.getActualDiscountAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("打包优惠检测失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return discounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DiscountResult applyDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||||
|
DiscountResult result = new DiscountResult();
|
||||||
|
result.setDiscountInfo(discountInfo);
|
||||||
|
result.setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!getProviderType().equals(discountInfo.getProviderType())) {
|
||||||
|
result.setFailureReason("优惠类型不匹配");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
|
||||||
|
if (bundleDiscount == null) {
|
||||||
|
result.setFailureReason("打包优惠信息为空");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查优惠的叠加限制
|
||||||
|
boolean canUseWithOtherDiscounts = checkDiscountCombinationRules(bundleDiscount, context);
|
||||||
|
if (!canUseWithOtherDiscounts) {
|
||||||
|
result.setFailureReason("打包优惠不可与其他优惠叠加使用");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新验证打包优惠有效性
|
||||||
|
if (!bundleDiscountService.isBundleDiscountValid(bundleDiscount, context)) {
|
||||||
|
result.setFailureReason("打包优惠已失效");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际优惠金额
|
||||||
|
BigDecimal actualDiscount = bundleDiscountService.calculateBundleDiscount(bundleDiscount, context.getProducts());
|
||||||
|
if (actualDiscount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
result.setFailureReason("打包优惠金额为零");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用打包优惠
|
||||||
|
BigDecimal finalAmount = context.getCurrentAmount().subtract(actualDiscount);
|
||||||
|
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
finalAmount = BigDecimal.ZERO;
|
||||||
|
actualDiscount = context.getCurrentAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setSuccess(true);
|
||||||
|
result.setActualDiscountAmount(actualDiscount);
|
||||||
|
result.setFinalAmount(finalAmount);
|
||||||
|
result.setFailureReason("打包购买优惠已应用");
|
||||||
|
|
||||||
|
log.info("打包优惠应用成功: 优惠金额={}, 最终金额={}", actualDiscount, finalAmount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("打包优惠应用失败", e);
|
||||||
|
result.setFailureReason("打包优惠应用失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canApply(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||||
|
try {
|
||||||
|
if (!getProviderType().equals(discountInfo.getProviderType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
|
||||||
|
if (bundleDiscount == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查打包优惠是否仍然有效
|
||||||
|
return bundleDiscountService.isBundleDiscountValid(bundleDiscount, context);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查打包优惠可用性失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMaxPossibleDiscount(DiscountInfo discountInfo, DiscountDetectionContext context) {
|
||||||
|
try {
|
||||||
|
BundleDiscountInfo bundleDiscount = discountInfo.getBundleDiscountInfo();
|
||||||
|
if (bundleDiscount != null && bundleDiscount.getActualDiscountAmount() != null) {
|
||||||
|
return bundleDiscount.getActualDiscountAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有预计算的优惠金额,重新计算
|
||||||
|
if (bundleDiscount != null && context.getProducts() != null) {
|
||||||
|
return bundleDiscountService.calculateBundleDiscount(bundleDiscount, context.getProducts());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取打包优惠最大优惠金额失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查优惠叠加规则
|
||||||
|
*/
|
||||||
|
private boolean checkDiscountCombinationRules(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context) {
|
||||||
|
// 检查是否可以与优惠券叠加
|
||||||
|
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithCoupon()) &&
|
||||||
|
Boolean.TRUE.equals(context.getAutoUseCoupon())) {
|
||||||
|
log.debug("打包优惠配置不允许与优惠券叠加使用");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以与券码叠加
|
||||||
|
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithVoucher()) &&
|
||||||
|
(Boolean.TRUE.equals(context.getAutoUseVoucher()) ||
|
||||||
|
context.getVoucherCode() != null)) {
|
||||||
|
log.debug("打包优惠配置不允许与券码叠加使用");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否可以与一口价叠加
|
||||||
|
// 注意:由于一口价优先级更高,这个检查主要用于记录和调试
|
||||||
|
if (Boolean.FALSE.equals(bundleDiscount.getCanUseWithOnePrice())) {
|
||||||
|
log.debug("打包优惠配置不允许与一口价叠加使用");
|
||||||
|
// 这里不返回false,因为一口价会优先应用,打包优惠不会被触发
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,303 @@
|
|||||||
|
package com.ycwl.basic.pricing.service.impl;
|
||||||
|
|
||||||
|
import com.ycwl.basic.pricing.dto.BundleDiscountInfo;
|
||||||
|
import com.ycwl.basic.pricing.dto.DiscountDetectionContext;
|
||||||
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||||
|
import com.ycwl.basic.pricing.enums.ProductType;
|
||||||
|
import com.ycwl.basic.pricing.service.IBundleDiscountService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包购买优惠服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BundleDiscountServiceImpl implements IBundleDiscountService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BundleDiscountInfo> detectAvailableBundleDiscounts(DiscountDetectionContext context) {
|
||||||
|
List<BundleDiscountInfo> bundleDiscounts = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (context.getProducts() == null || context.getProducts().isEmpty()) {
|
||||||
|
log.debug("商品列表为空,无法检测打包优惠");
|
||||||
|
return bundleDiscounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有可能的打包优惠规则
|
||||||
|
List<BundleDiscountInfo> allRules = getBundleDiscountRules(context.getProducts(), context.getScenicId());
|
||||||
|
|
||||||
|
for (BundleDiscountInfo rule : allRules) {
|
||||||
|
if (isBundleDiscountValid(rule, context)) {
|
||||||
|
// 计算实际优惠金额
|
||||||
|
BigDecimal discountAmount = calculateBundleDiscount(rule, context.getProducts());
|
||||||
|
if (discountAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
rule.setActualDiscountAmount(discountAmount);
|
||||||
|
bundleDiscounts.add(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("检测到 {} 个可用的打包优惠", bundleDiscounts.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检测打包优惠失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundleDiscounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal calculateBundleDiscount(BundleDiscountInfo bundleDiscount, List<ProductItem> products) {
|
||||||
|
try {
|
||||||
|
if (bundleDiscount == null || products == null || products.isEmpty()) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算符合条件的商品总金额
|
||||||
|
BigDecimal totalAmount = products.stream()
|
||||||
|
.map(ProductItem::getSubtotal)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
// 计算符合条件的商品总数量
|
||||||
|
int totalQuantity = products.stream()
|
||||||
|
.mapToInt(ProductItem::getQuantity)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// 检查是否满足最低条件
|
||||||
|
if (!isEligibleForBundle(products, bundleDiscount.getMinQuantity(), bundleDiscount.getMinAmount())) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据优惠类型计算折扣
|
||||||
|
return switch (bundleDiscount.getDiscountType()) {
|
||||||
|
case "FIXED_DISCOUNT" -> {
|
||||||
|
// 固定减免金额
|
||||||
|
BigDecimal discount = bundleDiscount.getDiscountValue();
|
||||||
|
yield discount.min(totalAmount); // 优惠不能超过总金额
|
||||||
|
}
|
||||||
|
case "PERCENTAGE_DISCOUNT" -> {
|
||||||
|
// 百分比折扣
|
||||||
|
BigDecimal discountRate = BigDecimal.ONE.subtract(bundleDiscount.getDiscountValue());
|
||||||
|
yield totalAmount.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
case "FIXED_PRICE" -> {
|
||||||
|
// 固定价格
|
||||||
|
BigDecimal fixedPrice = bundleDiscount.getDiscountValue();
|
||||||
|
BigDecimal discount = totalAmount.subtract(fixedPrice);
|
||||||
|
yield discount.max(BigDecimal.ZERO); // 固定价格不能高于原价
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
log.warn("未知的打包优惠类型: {}", bundleDiscount.getDiscountType());
|
||||||
|
yield BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("计算打包优惠金额失败", e);
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEligibleForBundle(List<ProductItem> products, Integer minQuantity, BigDecimal minAmount) {
|
||||||
|
if (products == null || products.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数量要求
|
||||||
|
if (minQuantity != null && minQuantity > 0) {
|
||||||
|
int totalQuantity = products.stream()
|
||||||
|
.mapToInt(ProductItem::getQuantity)
|
||||||
|
.sum();
|
||||||
|
if (totalQuantity < minQuantity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查金额要求
|
||||||
|
if (minAmount != null && minAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal totalAmount = products.stream()
|
||||||
|
.map(ProductItem::getSubtotal)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
if (totalAmount.compareTo(minAmount) < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BundleDiscountInfo> getBundleDiscountRules(List<ProductItem> products, Long scenicId) {
|
||||||
|
List<BundleDiscountInfo> rules = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现获取打包优惠规则的逻辑
|
||||||
|
// 可以从数据库加载配置,或者使用硬编码的规则
|
||||||
|
|
||||||
|
// 示例规则1:多商品打包优惠
|
||||||
|
if (hasMultipleProductTypes(products)) {
|
||||||
|
BundleDiscountInfo multiProductBundle = createMultiProductBundleRule();
|
||||||
|
rules.add(multiProductBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例规则2:大批量优惠
|
||||||
|
if (hasLargeQuantity(products)) {
|
||||||
|
BundleDiscountInfo bulkBundle = createBulkDiscountRule();
|
||||||
|
rules.add(bulkBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例规则3:特定商品组合优惠
|
||||||
|
if (hasSpecificCombination(products)) {
|
||||||
|
BundleDiscountInfo combinationBundle = createCombinationDiscountRule();
|
||||||
|
rules.add(combinationBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("为 {} 个商品获取到 {} 个打包优惠规则", products.size(), rules.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取打包优惠规则失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBundleDiscountValid(BundleDiscountInfo bundleDiscount, DiscountDetectionContext context) {
|
||||||
|
try {
|
||||||
|
if (bundleDiscount == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否满足基本条件
|
||||||
|
if (!isEligibleForBundle(context.getProducts(), bundleDiscount.getMinQuantity(), bundleDiscount.getMinAmount())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以添加更多的验证逻辑,比如时间范围、用户类型等
|
||||||
|
// TODO: 根据实际业务需求实现更多验证逻辑
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("验证打包优惠有效性失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BundleDiscountInfo getBestBundleDiscount(List<ProductItem> products, Long scenicId) {
|
||||||
|
try {
|
||||||
|
List<BundleDiscountInfo> availableRules = getBundleDiscountRules(products, scenicId);
|
||||||
|
|
||||||
|
return availableRules.stream()
|
||||||
|
.filter(rule -> {
|
||||||
|
BigDecimal discount = calculateBundleDiscount(rule, products);
|
||||||
|
rule.setActualDiscountAmount(discount);
|
||||||
|
return discount.compareTo(BigDecimal.ZERO) > 0;
|
||||||
|
})
|
||||||
|
.max(Comparator.comparing(BundleDiscountInfo::getActualDiscountAmount))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取最优打包优惠失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有多种商品类型
|
||||||
|
*/
|
||||||
|
private boolean hasMultipleProductTypes(List<ProductItem> products) {
|
||||||
|
Set<ProductType> productTypes = products.stream()
|
||||||
|
.map(ProductItem::getProductType)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return productTypes.size() >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有大批量商品
|
||||||
|
*/
|
||||||
|
private boolean hasLargeQuantity(List<ProductItem> products) {
|
||||||
|
int totalQuantity = products.stream()
|
||||||
|
.mapToInt(ProductItem::getQuantity)
|
||||||
|
.sum();
|
||||||
|
return totalQuantity >= 10; // 示例:10件以上
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有特定商品组合
|
||||||
|
*/
|
||||||
|
private boolean hasSpecificCombination(List<ProductItem> products) {
|
||||||
|
Set<ProductType> productTypes = products.stream()
|
||||||
|
.map(ProductItem::getProductType)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 示例:照片+视频组合
|
||||||
|
return productTypes.contains(ProductType.PHOTO_SET) &&
|
||||||
|
productTypes.contains(ProductType.VLOG_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建多商品打包规则
|
||||||
|
*/
|
||||||
|
private BundleDiscountInfo createMultiProductBundleRule() {
|
||||||
|
BundleDiscountInfo bundle = new BundleDiscountInfo();
|
||||||
|
bundle.setBundleConfigId(1L);
|
||||||
|
bundle.setBundleName("多商品组合优惠");
|
||||||
|
bundle.setBundleDescription("购买不同类型商品享受组合优惠");
|
||||||
|
bundle.setDiscountType("PERCENTAGE_DISCOUNT");
|
||||||
|
bundle.setDiscountValue(new BigDecimal("0.9")); // 9折
|
||||||
|
bundle.setMinQuantity(2);
|
||||||
|
bundle.setMinAmount(new BigDecimal("100"));
|
||||||
|
bundle.setCanUseWithCoupon(true);
|
||||||
|
bundle.setCanUseWithVoucher(true);
|
||||||
|
bundle.setCanUseWithOnePrice(false); // 不能与一口价叠加
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建大批量优惠规则
|
||||||
|
*/
|
||||||
|
private BundleDiscountInfo createBulkDiscountRule() {
|
||||||
|
BundleDiscountInfo bundle = new BundleDiscountInfo();
|
||||||
|
bundle.setBundleConfigId(2L);
|
||||||
|
bundle.setBundleName("大批量购买优惠");
|
||||||
|
bundle.setBundleDescription("购买数量达到要求享受批量优惠");
|
||||||
|
bundle.setDiscountType("FIXED_DISCOUNT");
|
||||||
|
bundle.setDiscountValue(new BigDecimal("50")); // 减免50元
|
||||||
|
bundle.setMinQuantity(10);
|
||||||
|
bundle.setMinAmount(new BigDecimal("500"));
|
||||||
|
bundle.setCanUseWithCoupon(true);
|
||||||
|
bundle.setCanUseWithVoucher(true);
|
||||||
|
bundle.setCanUseWithOnePrice(false);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建特定组合优惠规则
|
||||||
|
*/
|
||||||
|
private BundleDiscountInfo createCombinationDiscountRule() {
|
||||||
|
BundleDiscountInfo bundle = new BundleDiscountInfo();
|
||||||
|
bundle.setBundleConfigId(3L);
|
||||||
|
bundle.setBundleName("照片+视频套餐");
|
||||||
|
bundle.setBundleDescription("同时购买照片和视频享受套餐优惠");
|
||||||
|
bundle.setDiscountType("FIXED_PRICE");
|
||||||
|
bundle.setDiscountValue(new BigDecimal("199")); // 套餐价199元
|
||||||
|
bundle.setMinQuantity(2);
|
||||||
|
bundle.setMinAmount(new BigDecimal("200"));
|
||||||
|
bundle.setCanUseWithCoupon(false);
|
||||||
|
bundle.setCanUseWithVoucher(false);
|
||||||
|
bundle.setCanUseWithOnePrice(false);
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
}
|
@@ -34,7 +34,7 @@ public class CouponDiscountProvider implements IDiscountProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPriority() {
|
public int getPriority() {
|
||||||
return 80; // 优惠券优先级为80,低于券码的100
|
return 60; // 优惠券优先级最低
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -31,7 +31,7 @@ public class OnePricePurchaseDiscountProvider implements IDiscountProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPriority() {
|
public int getPriority() {
|
||||||
return 60; // 中等优先级,在券码和优惠券之间
|
return 120; // 最高优先级,一口价优先应用
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -36,7 +36,7 @@ public class VoucherDiscountProvider implements IDiscountProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPriority() {
|
public int getPriority() {
|
||||||
return 100; // 券码优先级最高
|
return 80; // 券码优先级第三,仅次于一口价和打包购买
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -14,6 +14,11 @@ import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
|||||||
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderEntity;
|
||||||
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
|
import com.ycwl.basic.model.pc.order.entity.OrderItemEntity;
|
||||||
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
import com.ycwl.basic.model.pc.price.entity.PriceConfigEntity;
|
||||||
|
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
|
||||||
|
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||||
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||||
|
import com.ycwl.basic.pricing.enums.ProductType;
|
||||||
|
import com.ycwl.basic.pricing.service.IPriceCalculationService;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrintTaskEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
import com.ycwl.basic.model.pc.printer.entity.PrinterEntity;
|
||||||
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
import com.ycwl.basic.model.pc.printer.resp.MemberPrintResp;
|
||||||
@@ -52,8 +57,6 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PrinterMapper printerMapper;
|
private PrinterMapper printerMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private PriceRepository priceRepository;
|
|
||||||
@Autowired
|
|
||||||
private SourceMapper sourceMapper;
|
private SourceMapper sourceMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private MemberMapper memberMapper;
|
private MemberMapper memberMapper;
|
||||||
@@ -66,6 +69,8 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
private WxPayService wxPayService;
|
private WxPayService wxPayService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private PrintTaskMapper printTaskMapper;
|
private PrintTaskMapper printTaskMapper;
|
||||||
|
@Autowired
|
||||||
|
private IPriceCalculationService priceCalculationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PrinterResp> listByScenicId(Long scenicId) {
|
public List<PrinterResp> listByScenicId(Long scenicId) {
|
||||||
@@ -205,18 +210,52 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
@Override
|
@Override
|
||||||
public PriceObj queryPrice(Long memberId, Long scenicId) {
|
public PriceObj queryPrice(Long memberId, Long scenicId) {
|
||||||
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
|
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
|
||||||
// 判断几张
|
|
||||||
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, 3, null);
|
// 计算照片总数量
|
||||||
|
long count = userPhotoList.stream()
|
||||||
|
.filter(item -> Objects.nonNull(item.getQuantity()))
|
||||||
|
.mapToInt(MemberPrintResp::getQuantity)
|
||||||
|
.sum();
|
||||||
|
|
||||||
PriceObj obj = new PriceObj();
|
PriceObj obj = new PriceObj();
|
||||||
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
|
if (count == 0) {
|
||||||
obj.setPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
|
// 如果没有照片,返回零价格
|
||||||
obj.setSlashPrice(priceConfig.getSlashPrice().multiply(BigDecimal.valueOf(count)));
|
obj.setPrice(BigDecimal.ZERO);
|
||||||
|
obj.setSlashPrice(BigDecimal.ZERO);
|
||||||
obj.setGoodsType(3);
|
obj.setGoodsType(3);
|
||||||
obj.setFree(false);
|
obj.setFree(false);
|
||||||
obj.setScenicId(scenicId);
|
obj.setScenicId(scenicId);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建价格计算请求
|
||||||
|
PriceCalculationRequest request = new PriceCalculationRequest();
|
||||||
|
request.setUserId(memberId);
|
||||||
|
request.setScenicId(scenicId);
|
||||||
|
|
||||||
|
// 创建照片打印商品项
|
||||||
|
ProductItem photoItem = new ProductItem();
|
||||||
|
photoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||||
|
photoItem.setProductId(scenicId.toString());
|
||||||
|
photoItem.setQuantity(Long.valueOf(count).intValue());
|
||||||
|
photoItem.setPurchaseCount(1);
|
||||||
|
photoItem.setScenicId(scenicId.toString());
|
||||||
|
|
||||||
|
request.setProducts(Collections.singletonList(photoItem));
|
||||||
|
|
||||||
|
// 使用统一价格计算服务
|
||||||
|
PriceCalculationResult result = priceCalculationService.calculatePrice(request);
|
||||||
|
|
||||||
|
// 转换为原有的 PriceObj 格式
|
||||||
|
obj.setPrice(result.getFinalAmount());
|
||||||
|
obj.setSlashPrice(result.getOriginalAmount());
|
||||||
|
obj.setGoodsType(3);
|
||||||
|
obj.setFree(result.getFinalAmount().compareTo(BigDecimal.ZERO) == 0);
|
||||||
|
obj.setScenicId(scenicId);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
|
public boolean addUserPhotoFromSource(Long memberId, Long scenicId, FromSourceReq req) {
|
||||||
req.getIds().forEach(id -> {
|
req.getIds().forEach(id -> {
|
||||||
@@ -250,11 +289,13 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
throw new BaseException("打印机不属于该景区");
|
throw new BaseException("打印机不属于该景区");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PriceConfigEntity priceConfig = priceRepository.getPriceConfigByScenicTypeGoods(scenicId, 3, null);
|
// 验证照片数量
|
||||||
if (priceConfig == null) {
|
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
|
||||||
throw new BaseException("该套餐暂未开放购买");
|
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
|
||||||
|
if (count == 0) {
|
||||||
|
throw new BaseException("没有可打印的照片");
|
||||||
}
|
}
|
||||||
log.info("创建打印订单,价格配置:{}", priceConfig);
|
|
||||||
OrderEntity order = new OrderEntity();
|
OrderEntity order = new OrderEntity();
|
||||||
Long orderId = SnowFlakeUtil.getLongId();
|
Long orderId = SnowFlakeUtil.getLongId();
|
||||||
order.setId(orderId);
|
order.setId(orderId);
|
||||||
@@ -262,9 +303,10 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
MemberRespVO member = memberMapper.getById(memberId);
|
MemberRespVO member = memberMapper.getById(memberId);
|
||||||
order.setOpenId(member.getOpenId());
|
order.setOpenId(member.getOpenId());
|
||||||
order.setScenicId(scenicId);
|
order.setScenicId(scenicId);
|
||||||
order.setType(priceConfig.getType());
|
order.setType(3); // 照片打印类型
|
||||||
batchSetUserPhotoListToPrinter(memberId, scenicId, printerId);
|
batchSetUserPhotoListToPrinter(memberId, scenicId, printerId);
|
||||||
List<MemberPrintResp> userPhotoList = getUserPhotoList(memberId, scenicId);
|
// 重新获取照片列表(包含打印机信息)
|
||||||
|
userPhotoList = getUserPhotoList(memberId, scenicId);
|
||||||
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
|
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
|
||||||
OrderItemEntity orderItem = new OrderItemEntity();
|
OrderItemEntity orderItem = new OrderItemEntity();
|
||||||
orderItem.setOrderId(orderId);
|
orderItem.setOrderId(orderId);
|
||||||
@@ -273,10 +315,28 @@ public class PrinterServiceImpl implements PrinterService {
|
|||||||
orderItem.setGoodsType(3);
|
orderItem.setGoodsType(3);
|
||||||
return orderItem;
|
return orderItem;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
long count = userPhotoList.stream().filter(item -> Objects.nonNull(item.getQuantity())).mapToInt(MemberPrintResp::getQuantity).sum();
|
|
||||||
order.setPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
|
// 使用统一价格计算服务计算最终价格
|
||||||
order.setSlashPrice(priceConfig.getSlashPrice().multiply(BigDecimal.valueOf(count)));
|
|
||||||
order.setPayPrice(priceConfig.getPrice().multiply(BigDecimal.valueOf(count)));
|
PriceCalculationRequest request = new PriceCalculationRequest();
|
||||||
|
request.setUserId(memberId);
|
||||||
|
request.setScenicId(scenicId);
|
||||||
|
|
||||||
|
// 创建照片打印商品项
|
||||||
|
ProductItem photoItem = new ProductItem();
|
||||||
|
photoItem.setProductType(ProductType.PHOTO_PRINT);
|
||||||
|
photoItem.setProductId(scenicId.toString());
|
||||||
|
photoItem.setQuantity(Long.valueOf(count).intValue());
|
||||||
|
photoItem.setPurchaseCount(1);
|
||||||
|
photoItem.setScenicId(scenicId.toString());
|
||||||
|
|
||||||
|
request.setProducts(Collections.singletonList(photoItem));
|
||||||
|
|
||||||
|
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(request);
|
||||||
|
|
||||||
|
order.setPrice(priceResult.getFinalAmount());
|
||||||
|
order.setSlashPrice(priceResult.getOriginalAmount());
|
||||||
|
order.setPayPrice(priceResult.getFinalAmount());
|
||||||
// order.setFaceId();
|
// order.setFaceId();
|
||||||
if (order.getPayPrice().equals(BigDecimal.ZERO)) {
|
if (order.getPayPrice().equals(BigDecimal.ZERO)) {
|
||||||
order.setStatus(OrderStateEnum.PAID.getState());
|
order.setStatus(OrderStateEnum.PAID.getState());
|
||||||
|
@@ -78,6 +78,8 @@
|
|||||||
printers,
|
printers,
|
||||||
use_printer,
|
use_printer,
|
||||||
status,
|
status,
|
||||||
|
prefer_w,
|
||||||
|
prefer_h,
|
||||||
create_time,
|
create_time,
|
||||||
update_time
|
update_time
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@@ -87,6 +89,8 @@
|
|||||||
#{printers},
|
#{printers},
|
||||||
#{usePrinter},
|
#{usePrinter},
|
||||||
#{status},
|
#{status},
|
||||||
|
#{preferW},
|
||||||
|
#{preferH},
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
@@ -116,14 +120,33 @@
|
|||||||
<!-- 更新 -->
|
<!-- 更新 -->
|
||||||
<update id="update">
|
<update id="update">
|
||||||
UPDATE printer
|
UPDATE printer
|
||||||
SET
|
<set>
|
||||||
|
<if test="accessKey != null">
|
||||||
access_key = #{accessKey},
|
access_key = #{accessKey},
|
||||||
|
</if>
|
||||||
|
<if test="name != null">
|
||||||
name = #{name},
|
name = #{name},
|
||||||
|
</if>
|
||||||
|
<if test="scenicId != null">
|
||||||
scenic_id = #{scenicId},
|
scenic_id = #{scenicId},
|
||||||
|
</if>
|
||||||
|
<if test="printers != null">
|
||||||
printers = #{printers},
|
printers = #{printers},
|
||||||
|
</if>
|
||||||
|
<if test="usePrinter != null">
|
||||||
use_printer = #{usePrinter},
|
use_printer = #{usePrinter},
|
||||||
|
</if>
|
||||||
|
<if test="status != null">
|
||||||
status = #{status},
|
status = #{status},
|
||||||
|
</if>
|
||||||
|
<if test="preferW != null">
|
||||||
|
prefer_w = #{preferW},
|
||||||
|
</if>
|
||||||
|
<if test="preferH != null">
|
||||||
|
prefer_h = #{preferH},
|
||||||
|
</if>
|
||||||
update_time = NOW()
|
update_time = NOW()
|
||||||
|
</set>
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
</update>
|
</update>
|
||||||
<update id="updateTaskStatus">
|
<update id="updateTaskStatus">
|
||||||
|
Reference in New Issue
Block a user