From a49450b7958d713562590045c720d4a46d52bc48 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 00:19:19 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(integration):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=97=AE=E5=8D=B7=E6=9C=8D=E5=8A=A1=E9=9B=86=E6=88=90=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增问卷服务配置和客户端接口 - 实现问卷创建、查询、提交答案和统计分析等功能 - 添加问卷集成示例,演示各项功能的使用- 设计并实现问卷服务的 fallback 缓存管理策略 --- .../java/com/ycwl/basic/integration/CLAUDE.md | 430 +++++++++++++++++- .../common/config/IntegrationProperties.java | 31 ++ .../client/QuestionnaireClient.java | 125 +++++ .../QuestionnaireIntegrationConfig.java | 17 + .../dto/answer/AnswerRequest.java | 23 + .../dto/answer/ResponseDetailResponse.java | 44 ++ .../dto/answer/ResponseListResponse.java | 12 + .../dto/answer/SubmitAnswerRequest.java | 25 + .../question/CreateQuestionOptionRequest.java | 34 ++ .../dto/question/CreateQuestionRequest.java | 37 ++ .../dto/question/QuestionOptionResponse.java | 26 ++ .../dto/question/QuestionResponse.java | 37 ++ .../CreateQuestionnaireRequest.java | 38 ++ .../QuestionnaireListResponse.java | 13 + .../questionnaire/QuestionnaireResponse.java | 54 +++ .../statistics/QuestionnaireStatistics.java | 73 +++ .../QuestionnaireIntegrationExample.java | 306 +++++++++++++ .../QuestionnaireIntegrationService.java | 166 +++++++ 18 files changed, 1490 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerRequest.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionOptionResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/CreateQuestionnaireRequest.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireResponse.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/statistics/QuestionnaireStatistics.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java diff --git a/src/main/java/com/ycwl/basic/integration/CLAUDE.md b/src/main/java/com/ycwl/basic/integration/CLAUDE.md index 0047ced1..ef0a123f 100644 --- a/src/main/java/com/ycwl/basic/integration/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/integration/CLAUDE.md @@ -24,6 +24,7 @@ Currently implemented: - **Scenic Integration** (`com.ycwl.basic.integration.scenic`): ZT-Scenic 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 +- **Questionnaire Integration** (`com.ycwl.basic.integration.questionnaire`): ZT-Questionnaire microservice integration ### Integration Pattern @@ -1243,4 +1244,431 @@ logging: - **Proactive cleanup**: Monitor cache size and clean up periodically - **Service-specific management**: Separate cache management per service - **Debugging support**: Use cache statistics for troubleshooting -- **Configuration validation**: Ensure fallback configuration matches service requirements \ No newline at end of file +- **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 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 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 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 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 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 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 \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java b/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java index aa10a5c1..d50510e1 100644 --- a/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java +++ b/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java @@ -31,6 +31,11 @@ public class IntegrationProperties { */ private RenderWorkerConfig render = new RenderWorkerConfig(); + /** + * 问卷服务配置 + */ + private QuestionnaireConfig questionnaire = new QuestionnaireConfig(); + @Data public static class ScenicConfig { /** @@ -104,6 +109,7 @@ public class IntegrationProperties { private ServiceFallbackConfig scenic = new ServiceFallbackConfig(); private ServiceFallbackConfig device = new ServiceFallbackConfig(); private ServiceFallbackConfig render = new ServiceFallbackConfig(); + private ServiceFallbackConfig questionnaire = new ServiceFallbackConfig(); } @Data @@ -131,6 +137,31 @@ public class IntegrationProperties { 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 public static class ServiceFallbackConfig { /** diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java b/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java new file mode 100644 index 00000000..46b0c835 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java @@ -0,0 +1,125 @@ +package com.ycwl.basic.integration.questionnaire.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseDetailResponse; +import com.ycwl.basic.integration.questionnaire.dto.answer.ResponseListResponse; +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.QuestionnaireListResponse; +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 createQuestionnaire( + @RequestBody CreateQuestionnaireRequest request, + @RequestHeader("X-User-ID") String userId + ); + + /** + * 获取问卷详情 + */ + @GetMapping("/questionnaires/{id}") + CommonResponse getQuestionnaire(@PathVariable("id") Long id); + + /** + * 获取问卷列表 + */ + @GetMapping("/questionnaires") + CommonResponse 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 updateQuestionnaire( + @PathVariable("id") Long id, + @RequestBody CreateQuestionnaireRequest request, + @RequestHeader("X-User-ID") String userId + ); + + /** + * 删除问卷 + */ + @DeleteMapping("/questionnaires/{id}") + CommonResponse deleteQuestionnaire( + @PathVariable("id") Long id, + @RequestHeader("X-User-ID") String userId + ); + + /** + * 发布问卷 + */ + @PostMapping("/questionnaires/{id}/publish") + CommonResponse publishQuestionnaire( + @PathVariable("id") Long id, + @RequestHeader("X-User-ID") String userId + ); + + /** + * 停止问卷 + */ + @PostMapping("/questionnaires/{id}/stop") + CommonResponse stopQuestionnaire( + @PathVariable("id") Long id, + @RequestHeader("X-User-ID") String userId + ); + + // ==================== 答案提交接口 ==================== + + /** + * 提交问卷答案 + */ + @PostMapping("/questionnaires/submit") + CommonResponse submitAnswer(@RequestBody SubmitAnswerRequest request); + + // ==================== 统计分析接口 ==================== + + /** + * 获取问卷统计 + */ + @GetMapping("/questionnaires/{id}/statistics") + CommonResponse getStatistics(@PathVariable("id") Long id); + + /** + * 获取回答记录列表 + */ + @GetMapping("/responses") + CommonResponse 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 getResponseDetail(@PathVariable("id") Long id); + + // ==================== 健康检查接口 ==================== + + /** + * 服务健康检查 + */ + @GetMapping("/health") + CommonResponse health(); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java b/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java new file mode 100644 index 00000000..f76ffdad --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java @@ -0,0 +1,17 @@ +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; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@Configuration +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "integration.questionnaire", name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableFeignClients(basePackages = "com.ycwl.basic.integration.questionnaire.client") +public class QuestionnaireIntegrationConfig { + + private final IntegrationProperties integrationProperties; + +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerRequest.java new file mode 100644 index 00000000..cdb98b21 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerRequest.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java new file mode 100644 index 00000000..12f68dce --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java @@ -0,0 +1,44 @@ +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 answers; +} + +@Data +class AnswerDetailResponse { + + @JsonProperty("questionId") + private Long questionId; + + @JsonProperty("questionTitle") + private String questionTitle; + + @JsonProperty("questionType") + private Integer questionType; + + @JsonProperty("answer") + private String answer; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java new file mode 100644 index 00000000..395e7995 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.integration.questionnaire.dto.answer; + +import com.ycwl.basic.integration.common.response.PageResponse; +import lombok.Data; + +@Data +public class ResponseListResponse extends PageResponse { + + public ResponseListResponse() { + super(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java new file mode 100644 index 00000000..ba36c030 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java @@ -0,0 +1,25 @@ +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 { + + @NotNull(message = "问卷ID不能为空") + @JsonProperty("questionnaireId") + private Long questionnaireId; + + @JsonProperty("userId") + private String userId; // 可选,用于非匿名问卷 + + @NotEmpty(message = "答案不能为空") + @Valid + @JsonProperty("answers") + private List answers; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java new file mode 100644 index 00000000..4425cfa3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java @@ -0,0 +1,34 @@ +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; + + public CreateQuestionOptionRequest(String text, String value, Integer sort) { + this.text = text; + this.value = value; + this.sort = sort; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java new file mode 100644 index 00000000..ae72ba8b --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java @@ -0,0 +1,37 @@ +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 { + + @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 options; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionOptionResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionOptionResponse.java new file mode 100644 index 00000000..08ab06b8 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionOptionResponse.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionResponse.java new file mode 100644 index 00000000..6c09052f --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/QuestionResponse.java @@ -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 options; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/CreateQuestionnaireRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/CreateQuestionnaireRequest.java new file mode 100644 index 00000000..a91ef18d --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/CreateQuestionnaireRequest.java @@ -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 questions; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java new file mode 100644 index 00000000..e383583a --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java @@ -0,0 +1,13 @@ +package com.ycwl.basic.integration.questionnaire.dto.questionnaire; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ycwl.basic.integration.common.response.PageResponse; +import lombok.Data; + +@Data +public class QuestionnaireListResponse extends PageResponse { + + public QuestionnaireListResponse() { + super(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireResponse.java new file mode 100644 index 00000000..23913290 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireResponse.java @@ -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 questions; + + @JsonProperty("statistics") + private QuestionnaireStatistics statistics; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/statistics/QuestionnaireStatistics.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/statistics/QuestionnaireStatistics.java new file mode 100644 index 00000000..41475582 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/statistics/QuestionnaireStatistics.java @@ -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 questionStats; + + @JsonProperty("responsesByDate") + private Map 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 optionStats; + + @JsonProperty("textAnswers") + private List 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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java b/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java new file mode 100644 index 00000000..0f1d4d33 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java @@ -0,0 +1,306 @@ +package com.ycwl.basic.integration.questionnaire.example; + +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.QuestionnaireListResponse; +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 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 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) + QuestionnaireListResponse 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 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 answers = new ArrayList<>(); + answers.add(new AnswerRequest(1L, "8")); // 评分题答案 + answers.add(new AnswerRequest(2L, "张三")); // 填空题答案 + + request.setAnswers(answers); + return request; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java b/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java new file mode 100644 index 00000000..a858afe0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java @@ -0,0 +1,166 @@ +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.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.ResponseListResponse; +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.QuestionnaireListResponse; +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 response = questionnaireClient.getQuestionnaire(id); + return handleResponse(response, "获取问卷详情失败"); + }, + QuestionnaireResponse.class + ); + } + + public QuestionnaireListResponse getQuestionnaireList(Integer page, Integer pageSize, + String name, Integer status, String createdBy) { + log.info("获取问卷列表, page: {}, pageSize: {}, name: {}, status: {}, createdBy: {}", + page, pageSize, name, status, createdBy); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "questionnaire:list:" + page + ":" + pageSize + ":" + name + ":" + status + ":" + createdBy, + () -> { + CommonResponse response = + questionnaireClient.getQuestionnaireList(page, pageSize, name, status, createdBy); + return handleResponse(response, "获取问卷列表失败"); + }, + QuestionnaireListResponse.class + ); + } + + public QuestionnaireStatistics getStatistics(Long id) { + log.info("获取问卷统计, id: {}", id); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "questionnaire:statistics:" + id, + () -> { + CommonResponse response = questionnaireClient.getStatistics(id); + return handleResponse(response, "获取问卷统计失败"); + }, + QuestionnaireStatistics.class + ); + } + + public ResponseListResponse getResponseList(Integer page, Integer pageSize, Long questionnaireId, + String userId, String startTime, String endTime) { + log.info("获取回答记录列表, page: {}, pageSize: {}, questionnaireId: {}, userId: {}", + page, pageSize, questionnaireId, userId); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "responses:list:" + page + ":" + pageSize + ":" + questionnaireId + ":" + userId, + () -> { + CommonResponse response = + questionnaireClient.getResponseList(page, pageSize, questionnaireId, userId, startTime, endTime); + return handleResponse(response, "获取回答记录列表失败"); + }, + ResponseListResponse.class + ); + } + + public ResponseDetailResponse getResponseDetail(Long id) { + log.info("获取回答详情, id: {}", id); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "response:" + id, + () -> { + CommonResponse 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 response = questionnaireClient.createQuestionnaire(request, userId); + return handleResponse(response, "创建问卷失败"); + } + + public QuestionnaireResponse updateQuestionnaire(Long id, CreateQuestionnaireRequest request, String userId) { + log.info("更新问卷, id: {}, userId: {}", id, userId); + CommonResponse response = questionnaireClient.updateQuestionnaire(id, request, userId); + return handleResponse(response, "更新问卷失败"); + } + + public void deleteQuestionnaire(Long id, String userId) { + log.info("删除问卷, id: {}, userId: {}", id, userId); + CommonResponse response = questionnaireClient.deleteQuestionnaire(id, userId); + handleResponse(response, "删除问卷失败"); + } + + public QuestionnaireResponse publishQuestionnaire(Long id, String userId) { + log.info("发布问卷, id: {}, userId: {}", id, userId); + CommonResponse response = questionnaireClient.publishQuestionnaire(id, userId); + return handleResponse(response, "发布问卷失败"); + } + + public QuestionnaireResponse stopQuestionnaire(Long id, String userId) { + log.info("停止问卷, id: {}, userId: {}", id, userId); + CommonResponse response = questionnaireClient.stopQuestionnaire(id, userId); + return handleResponse(response, "停止问卷失败"); + } + + public ResponseDetailResponse submitAnswer(SubmitAnswerRequest request) { + log.info("提交问卷答案, questionnaireId: {}, userId: {}", request.getQuestionnaireId(), request.getUserId()); + CommonResponse response = questionnaireClient.submitAnswer(request); + return handleResponse(response, "提交问卷答案失败"); + } + + // ==================== 健康检查接口 ==================== + + public String health() { + log.debug("问卷服务健康检查"); + return fallbackService.executeWithFallback( + SERVICE_NAME, + "health", + () -> { + CommonResponse response = questionnaireClient.health(); + return handleResponse(response, "健康检查失败"); + }, + String.class + ); + } + + // ==================== 工具方法 ==================== + + private T handleResponse(CommonResponse 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(); + } +} \ No newline at end of file From 180f89042cb9adaa65f44413678c2961e181118b Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 01:04:15 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor(questionnaire):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E9=97=AE=E5=8D=B7=E5=88=97=E8=A1=A8=E5=92=8C=E5=9B=9E?= =?UTF-8?q?=E7=AD=94=E8=AE=B0=E5=BD=95=E5=88=97=E8=A1=A8=E7=9A=84=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 QuestionnaireListResponse 和 ResponseListResponse 类移除 - 使用泛型化的 PageResponse 类作为列表响应的基类 - 更新相关接口和方法的返回类型 - 调整示例代码和测试用例 --- .../client/QuestionnaireClient.java | 7 ++-- .../dto/answer/ResponseListResponse.java | 12 ------- .../QuestionnaireListResponse.java | 13 ------- .../QuestionnaireIntegrationExample.java | 4 +-- .../QuestionnaireIntegrationService.java | 35 ++++++------------- 5 files changed, 15 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java delete mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java b/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java index 46b0c835..ff2586f1 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/client/QuestionnaireClient.java @@ -1,11 +1,10 @@ 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.ResponseListResponse; 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.QuestionnaireListResponse; import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse; import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics; import org.springframework.cloud.openfeign.FeignClient; @@ -35,7 +34,7 @@ public interface QuestionnaireClient { * 获取问卷列表 */ @GetMapping("/questionnaires") - CommonResponse getQuestionnaireList( + CommonResponse> getQuestionnaireList( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String name, @@ -100,7 +99,7 @@ public interface QuestionnaireClient { * 获取回答记录列表 */ @GetMapping("/responses") - CommonResponse getResponseList( + CommonResponse> getResponseList( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Long questionnaireId, diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java deleted file mode 100644 index 395e7995..00000000 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseListResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ycwl.basic.integration.questionnaire.dto.answer; - -import com.ycwl.basic.integration.common.response.PageResponse; -import lombok.Data; - -@Data -public class ResponseListResponse extends PageResponse { - - public ResponseListResponse() { - super(); - } -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java deleted file mode 100644 index e383583a..00000000 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/questionnaire/QuestionnaireListResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ycwl.basic.integration.questionnaire.dto.questionnaire; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.ycwl.basic.integration.common.response.PageResponse; -import lombok.Data; - -@Data -public class QuestionnaireListResponse extends PageResponse { - - public QuestionnaireListResponse() { - super(); - } -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java b/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java index 0f1d4d33..69253681 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/example/QuestionnaireIntegrationExample.java @@ -1,5 +1,6 @@ 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; @@ -7,7 +8,6 @@ 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.QuestionnaireListResponse; 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; @@ -126,7 +126,7 @@ public class QuestionnaireIntegrationExample { try { // 获取问卷列表(支持 fallback) - QuestionnaireListResponse listResponse = questionnaireService.getQuestionnaireList(1, 10, null, null, null); + PageResponse listResponse = questionnaireService.getQuestionnaireList(1, 10, null, null, null); log.info("✅ 问卷列表查询成功,总数: {}, 当前页数据: {}", listResponse.getTotal(), listResponse.getList().size()); diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java b/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java index a858afe0..1d2c45d1 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/service/QuestionnaireIntegrationService.java @@ -2,13 +2,12 @@ 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.ResponseListResponse; 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.QuestionnaireListResponse; import com.ycwl.basic.integration.questionnaire.dto.questionnaire.QuestionnaireResponse; import com.ycwl.basic.integration.questionnaire.dto.statistics.QuestionnaireStatistics; import lombok.RequiredArgsConstructor; @@ -40,20 +39,13 @@ public class QuestionnaireIntegrationService { ); } - public QuestionnaireListResponse getQuestionnaireList(Integer page, Integer pageSize, - String name, Integer status, String createdBy) { + public PageResponse getQuestionnaireList(Integer page, Integer pageSize, + String name, Integer status, String createdBy) { log.info("获取问卷列表, page: {}, pageSize: {}, name: {}, status: {}, createdBy: {}", page, pageSize, name, status, createdBy); - return fallbackService.executeWithFallback( - SERVICE_NAME, - "questionnaire:list:" + page + ":" + pageSize + ":" + name + ":" + status + ":" + createdBy, - () -> { - CommonResponse response = - questionnaireClient.getQuestionnaireList(page, pageSize, name, status, createdBy); - return handleResponse(response, "获取问卷列表失败"); - }, - QuestionnaireListResponse.class - ); + CommonResponse> response = + questionnaireClient.getQuestionnaireList(page, pageSize, name, status, createdBy); + return handleResponse(response, "获取问卷列表失败"); } public QuestionnaireStatistics getStatistics(Long id) { @@ -69,20 +61,13 @@ public class QuestionnaireIntegrationService { ); } - public ResponseListResponse getResponseList(Integer page, Integer pageSize, Long questionnaireId, + public PageResponse getResponseList(Integer page, Integer pageSize, Long questionnaireId, String userId, String startTime, String endTime) { log.info("获取回答记录列表, page: {}, pageSize: {}, questionnaireId: {}, userId: {}", page, pageSize, questionnaireId, userId); - return fallbackService.executeWithFallback( - SERVICE_NAME, - "responses:list:" + page + ":" + pageSize + ":" + questionnaireId + ":" + userId, - () -> { - CommonResponse response = - questionnaireClient.getResponseList(page, pageSize, questionnaireId, userId, startTime, endTime); - return handleResponse(response, "获取回答记录列表失败"); - }, - ResponseListResponse.class - ); + CommonResponse> response = + questionnaireClient.getResponseList(page, pageSize, questionnaireId, userId, startTime, endTime); + return handleResponse(response, "获取回答记录列表失败"); } public ResponseDetailResponse getResponseDetail(Long id) { From 32f7660dc04723d2ed9621804240d7ba9bfd21a7 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 01:09:17 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(questionnaire):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=97=AE=E5=8D=B7=E7=AE=A1=E7=90=86=20V2=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了新的 QuestionnaireV2Controller 类,实现了问卷管理的 CRUD操作 - 新增了问卷答案查看和统计功能相关接口 - 重构了 ResponseDetailResponse 类,将 AnswerDetailResponse 类独立出来- 简化了 CreateQuestionOptionRequest 类的结构 --- .../pc/QuestionnaireV2Controller.java | 264 ++++++++++++++++++ .../dto/answer/AnswerDetailResponse.java | 20 ++ .../dto/answer/ResponseDetailResponse.java | 16 -- .../question/CreateQuestionOptionRequest.java | 6 - 4 files changed, 284 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/controller/pc/QuestionnaireV2Controller.java create mode 100644 src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerDetailResponse.java diff --git a/src/main/java/com/ycwl/basic/controller/pc/QuestionnaireV2Controller.java b/src/main/java/com/ycwl/basic/controller/pc/QuestionnaireV2Controller.java new file mode 100644 index 00000000..691dc357 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/QuestionnaireV2Controller.java @@ -0,0 +1,264 @@ +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.utils.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +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; + + // ========== 问卷管理 CRUD 操作 ========== + + /** + * 分页查询问卷列表 + */ + @GetMapping("/") + public ApiResponse> 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 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 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 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 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 updateQuestionnaireStatus(@PathVariable Long id, @RequestBody Map 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 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 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 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> 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 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 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> 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 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 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()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerDetailResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerDetailResponse.java new file mode 100644 index 00000000..5768fea4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/AnswerDetailResponse.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java index 12f68dce..151d5fa8 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/ResponseDetailResponse.java @@ -26,19 +26,3 @@ public class ResponseDetailResponse { @JsonProperty("answers") private List answers; } - -@Data -class AnswerDetailResponse { - - @JsonProperty("questionId") - private Long questionId; - - @JsonProperty("questionTitle") - private String questionTitle; - - @JsonProperty("questionType") - private Integer questionType; - - @JsonProperty("answer") - private String answer; -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java index 4425cfa3..c2db8824 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionOptionRequest.java @@ -25,10 +25,4 @@ public class CreateQuestionOptionRequest { @JsonProperty("sort") private Integer sort = 0; - - public CreateQuestionOptionRequest(String text, String value, Integer sort) { - this.text = text; - this.value = value; - this.sort = sort; - } } \ No newline at end of file From 58488d2cdedc5e1a1be607d827de1f6b0e0de341 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 15:35:42 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor(integration):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=92=8C=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 DefaultConfigIntegrationService 类名以更准确地反映其功能 - 移除不必要的导入和注解 - 统一命名规范 --- .../controller/pc/DefaultConfigController.java | 14 +++++++------- .../example/DefaultConfigIntegrationExample.java | 4 ++-- ... => DeviceDefaultConfigIntegrationService.java} | 3 +-- .../config/QuestionnaireIntegrationConfig.java | 3 --- .../config/RenderWorkerIntegrationConfig.java | 2 -- ... => ScenicDefaultConfigIntegrationService.java} | 2 +- 6 files changed, 11 insertions(+), 17 deletions(-) rename src/main/java/com/ycwl/basic/integration/device/service/{DefaultConfigIntegrationService.java => DeviceDefaultConfigIntegrationService.java} (99%) rename src/main/java/com/ycwl/basic/integration/scenic/service/{DefaultConfigIntegrationService.java => ScenicDefaultConfigIntegrationService.java} (97%) diff --git a/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java b/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java index 8ddfe5cf..f3019931 100644 --- a/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java +++ b/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java @@ -1,7 +1,7 @@ package com.ycwl.basic.controller.pc; import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO; -import com.ycwl.basic.integration.scenic.service.DefaultConfigIntegrationService; +import com.ycwl.basic.integration.scenic.service.ScenicDefaultConfigIntegrationService; import com.ycwl.basic.utils.ApiConst; import com.ycwl.basic.utils.ApiResponse; import lombok.RequiredArgsConstructor; @@ -20,7 +20,7 @@ import java.util.List; @RequiredArgsConstructor public class DefaultConfigController { - private final DefaultConfigIntegrationService defaultConfigIntegrationService; + private final ScenicDefaultConfigIntegrationService scenicDefaultConfigIntegrationService; /** * 获取默认配置列表 @@ -29,7 +29,7 @@ public class DefaultConfigController { public ApiResponse> listDefaultConfigs() { log.info("获取默认配置列表"); try { - List configs = defaultConfigIntegrationService.listDefaultConfigs(); + List configs = scenicDefaultConfigIntegrationService.listDefaultConfigs(); return ApiResponse.success(configs); } catch (Exception e) { log.error("获取默认配置列表失败", e); @@ -44,7 +44,7 @@ public class DefaultConfigController { public ApiResponse getDefaultConfig(@PathVariable String configKey) { log.info("获取默认配置, configKey: {}", configKey); try { - DefaultConfigDTO config = defaultConfigIntegrationService.getDefaultConfig(configKey); + DefaultConfigDTO config = scenicDefaultConfigIntegrationService.getDefaultConfig(configKey); return ApiResponse.success(config); } catch (Exception e) { log.error("获取默认配置失败, configKey: {}", configKey, e); @@ -59,7 +59,7 @@ public class DefaultConfigController { public ApiResponse createDefaultConfig(@RequestBody DefaultConfigDTO request) { log.info("创建默认配置, configKey: {}", request.getConfigKey()); try { - DefaultConfigDTO config = defaultConfigIntegrationService.createDefaultConfig(request); + DefaultConfigDTO config = scenicDefaultConfigIntegrationService.createDefaultConfig(request); return ApiResponse.success(config); } catch (Exception e) { log.error("创建默认配置失败, configKey: {}", request.getConfigKey(), e); @@ -75,7 +75,7 @@ public class DefaultConfigController { @RequestBody DefaultConfigDTO request) { log.info("更新默认配置, configKey: {}", configKey); try { - DefaultConfigDTO config = defaultConfigIntegrationService.updateDefaultConfig(configKey, request); + DefaultConfigDTO config = scenicDefaultConfigIntegrationService.updateDefaultConfig(configKey, request); return ApiResponse.success(config); } catch (Exception e) { log.error("更新默认配置失败, configKey: {}", configKey, e); @@ -90,7 +90,7 @@ public class DefaultConfigController { public ApiResponse deleteDefaultConfig(@PathVariable String configKey) { log.info("删除默认配置, configKey: {}", configKey); try { - defaultConfigIntegrationService.deleteDefaultConfig(configKey); + scenicDefaultConfigIntegrationService.deleteDefaultConfig(configKey); return ApiResponse.buildResponse(ApiConst.Code.CODE_SUCCESS.code(), null, "删除成功"); } catch (Exception e) { log.error("删除默认配置失败, configKey: {}", configKey, e); diff --git a/src/main/java/com/ycwl/basic/integration/device/example/DefaultConfigIntegrationExample.java b/src/main/java/com/ycwl/basic/integration/device/example/DefaultConfigIntegrationExample.java index b17410e2..75fc6109 100644 --- a/src/main/java/com/ycwl/basic/integration/device/example/DefaultConfigIntegrationExample.java +++ b/src/main/java/com/ycwl/basic/integration/device/example/DefaultConfigIntegrationExample.java @@ -2,7 +2,7 @@ package com.ycwl.basic.integration.device.example; import com.ycwl.basic.integration.common.response.PageResponse; import com.ycwl.basic.integration.device.dto.defaults.*; -import com.ycwl.basic.integration.device.service.DefaultConfigIntegrationService; +import com.ycwl.basic.integration.device.service.DeviceDefaultConfigIntegrationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; @@ -23,7 +23,7 @@ import java.util.Map; @ConditionalOnProperty(name = "integration.device.example.default-config.enabled", havingValue = "true") public class DefaultConfigIntegrationExample implements CommandLineRunner { - private final DefaultConfigIntegrationService defaultConfigService; + private final DeviceDefaultConfigIntegrationService defaultConfigService; @Override public void run(String... args) throws Exception { diff --git a/src/main/java/com/ycwl/basic/integration/device/service/DefaultConfigIntegrationService.java b/src/main/java/com/ycwl/basic/integration/device/service/DeviceDefaultConfigIntegrationService.java similarity index 99% rename from src/main/java/com/ycwl/basic/integration/device/service/DefaultConfigIntegrationService.java rename to src/main/java/com/ycwl/basic/integration/device/service/DeviceDefaultConfigIntegrationService.java index dbe702a3..0c496123 100644 --- a/src/main/java/com/ycwl/basic/integration/device/service/DefaultConfigIntegrationService.java +++ b/src/main/java/com/ycwl/basic/integration/device/service/DeviceDefaultConfigIntegrationService.java @@ -11,7 +11,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; -import java.util.HashMap; import java.util.Map; /** @@ -20,7 +19,7 @@ import java.util.Map; @Slf4j @Service @RequiredArgsConstructor -public class DefaultConfigIntegrationService { +public class DeviceDefaultConfigIntegrationService { private final DefaultConfigClient defaultConfigClient; private final IntegrationFallbackService fallbackService; diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java b/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java index f76ffdad..685af806 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/config/QuestionnaireIntegrationConfig.java @@ -4,12 +4,9 @@ import com.ycwl.basic.integration.common.config.IntegrationProperties; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; -import org.springframework.cloud.openfeign.EnableFeignClients; - @Configuration @RequiredArgsConstructor @ConditionalOnProperty(prefix = "integration.questionnaire", name = "enabled", havingValue = "true", matchIfMissing = true) -@EnableFeignClients(basePackages = "com.ycwl.basic.integration.questionnaire.client") public class QuestionnaireIntegrationConfig { private final IntegrationProperties integrationProperties; diff --git a/src/main/java/com/ycwl/basic/integration/render/config/RenderWorkerIntegrationConfig.java b/src/main/java/com/ycwl/basic/integration/render/config/RenderWorkerIntegrationConfig.java index 65e4edce..4666cb60 100644 --- a/src/main/java/com/ycwl/basic/integration/render/config/RenderWorkerIntegrationConfig.java +++ b/src/main/java/com/ycwl/basic/integration/render/config/RenderWorkerIntegrationConfig.java @@ -3,7 +3,6 @@ package com.ycwl.basic.integration.render.config; import com.ycwl.basic.integration.common.config.IntegrationProperties; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; /** @@ -11,7 +10,6 @@ import org.springframework.context.annotation.Configuration; */ @Configuration @ConditionalOnProperty(prefix = "integration.render", name = "enabled", havingValue = "true", matchIfMissing = true) -@EnableFeignClients(basePackages = "com.ycwl.basic.integration.render.client") @RequiredArgsConstructor public class RenderWorkerIntegrationConfig { diff --git a/src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicDefaultConfigIntegrationService.java similarity index 97% rename from src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java rename to src/main/java/com/ycwl/basic/integration/scenic/service/ScenicDefaultConfigIntegrationService.java index 220fab0e..79f2fac1 100644 --- a/src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java +++ b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicDefaultConfigIntegrationService.java @@ -13,7 +13,7 @@ import java.util.List; @Slf4j @Service @RequiredArgsConstructor -public class DefaultConfigIntegrationService { +public class ScenicDefaultConfigIntegrationService { private final DefaultConfigClient defaultConfigClient; From b9c65cf0305b0f7d6c90baf1fde85c5a75f18d8d Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 15:36:37 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(mobile):=20=E6=B7=BB=E5=8A=A0=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E9=97=AE=E5=8D=B7=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppQuestionnaireController 控制器,提供移动端问卷相关接口 - 实现问卷详情获取和问卷答案提交两个主要功能 - 集成 QuestionnaireIntegrationService 服务进行问卷数据处理 - 使用 ApiResponse 统一接口返回格式 - 添加日志记录和异常处理,提高系统稳定性 --- .../mobile/AppQuestionnaireController.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/main/java/com/ycwl/basic/controller/mobile/AppQuestionnaireController.java diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppQuestionnaireController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppQuestionnaireController.java new file mode 100644 index 00000000..e7049371 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppQuestionnaireController.java @@ -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 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 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()); + } + } +} \ No newline at end of file From 24f692b69a7c52f2398bc2b943221100ebf9f396 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 6 Sep 2025 21:46:06 +0800 Subject: [PATCH 6/6] =?UTF-8?q?refactor(questionnaire):=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E9=97=AE=E5=8D=B7=E7=9B=B8=E5=85=B3=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 SubmitAnswerRequest 中的 @NotNull 注解 - 在 CreateQuestionRequest 中添加 id 字段 --- .../questionnaire/dto/answer/SubmitAnswerRequest.java | 1 - .../questionnaire/dto/question/CreateQuestionRequest.java | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java index ba36c030..10f437df 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/answer/SubmitAnswerRequest.java @@ -11,7 +11,6 @@ import java.util.List; @Data public class SubmitAnswerRequest { - @NotNull(message = "问卷ID不能为空") @JsonProperty("questionnaireId") private Long questionnaireId; diff --git a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java index ae72ba8b..2924f937 100644 --- a/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java +++ b/src/main/java/com/ycwl/basic/integration/questionnaire/dto/question/CreateQuestionRequest.java @@ -13,6 +13,8 @@ import java.util.List; @Data public class CreateQuestionRequest { + @JsonProperty("id") + private Long id; @NotBlank(message = "问题标题不能为空") @Size(max = 500, message = "问题标题长度不能超过500字符")