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