Przeglądaj źródła

志愿,测评接口移植

mingfu 1 miesiąc temu
rodzic
commit
465972db4f
43 zmienionych plików z 4598 dodań i 23 usunięć
  1. 8 0
      ie-admin/pom.xml
  2. 85 0
      ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontHollandController.java
  3. 106 0
      ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontMbtiController.java
  4. 71 0
      ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontMentalHealthController.java
  5. 294 0
      ie-admin/src/main/java/com/ruoyi/web/controller/ie/VoluntaryController.java
  6. 43 0
      ie-admin/src/main/java/com/ruoyi/web/domain/CategoryDto.java
  7. 19 0
      ie-admin/src/main/java/com/ruoyi/web/domain/CommDto.java
  8. 12 0
      ie-admin/src/main/java/com/ruoyi/web/domain/Constant.java
  9. 29 0
      ie-admin/src/main/java/com/ruoyi/web/domain/DynamicTable.java
  10. 70 0
      ie-admin/src/main/java/com/ruoyi/web/domain/PaperDto.java
  11. 91 0
      ie-admin/src/main/java/com/ruoyi/web/domain/QuestionDto.java
  12. 120 0
      ie-admin/src/main/java/com/ruoyi/web/domain/TestMarjorDto.java
  13. 523 0
      ie-admin/src/main/java/com/ruoyi/web/domain/VoluntaryDto.java
  14. 133 0
      ie-admin/src/main/java/com/ruoyi/web/domain/ZytbDto.java
  15. 93 0
      ie-admin/src/main/java/com/ruoyi/web/service/CacheService.java
  16. 163 0
      ie-admin/src/main/java/com/ruoyi/web/service/EnrollRateCalculator.java
  17. 110 0
      ie-admin/src/main/java/com/ruoyi/web/service/ExamUtilService.java
  18. 1053 0
      ie-admin/src/main/java/com/ruoyi/web/service/SyTestMajorService.java
  19. 1172 0
      ie-admin/src/main/java/com/ruoyi/web/service/VoluntaryService.java
  20. 95 0
      ie-admin/src/main/java/com/ruoyi/web/util/IosVerifyUtil.java
  21. 110 0
      ie-admin/src/main/java/com/ruoyi/web/util/MentalHealthExporter.java
  22. 164 0
      ie-admin/src/main/java/com/ruoyi/web/util/VolunteerExporter.java
  23. 5 0
      ie-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  24. 1 1
      ie-system/src/main/java/com/ruoyi/sy/domain/SyMajorCareerProspects.java
  25. 1 3
      ie-system/src/main/java/com/ruoyi/sy/domain/SyMajorOverview.java
  26. 1 1
      ie-system/src/main/java/com/ruoyi/sy/domain/SyVocationalPostDetail.java
  27. 2 0
      ie-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java
  28. 5 0
      ie-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java
  29. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/criteria/MarjorTdxCriteria.java
  30. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/criteria/UniversityTdxCriteria.java
  31. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/domain/BBusiWishUniversities.java
  32. 3 3
      ie-system/src/main/java/com/ruoyi/syzy/domain/BBusiWishUniversitiesSubject.java
  33. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/mapper/BCustomerMarjorsMapper.java
  34. 0 1
      ie-system/src/main/java/com/ruoyi/syzy/mapper/BCustomerUniversitiesMapper.java
  35. 0 1
      ie-system/src/main/java/com/ruoyi/syzy/service/IBBusiWishUniversitiesService.java
  36. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/service/IBCustomerMarjorsService.java
  37. 0 1
      ie-system/src/main/java/com/ruoyi/syzy/service/IBCustomerUniversitiesService.java
  38. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/service/impl/BBusiHollandRecordsServiceImpl.java
  39. 0 2
      ie-system/src/main/java/com/ruoyi/syzy/service/impl/BBusiWishUniversitySubmitMarjorsServiceImpl.java
  40. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/service/impl/BCustomerMarjorsServiceImpl.java
  41. 1 1
      ie-system/src/main/java/com/ruoyi/syzy/service/impl/BCustomerUniversitiesServiceImpl.java
  42. 2 2
      ie-system/src/main/java/com/ruoyi/syzy/utils/BatchUtil.java
  43. 6 0
      pom.xml

+ 8 - 0
ie-admin/pom.xml

@@ -17,6 +17,10 @@
 
     <dependencies>
 
+        <dependency>
+            <groupId>org.jxls</groupId>
+            <artifactId>jxls-poi</artifactId>
+        </dependency>
         <!-- spring-boot-devtools -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -60,6 +64,10 @@
             <groupId>com.ie</groupId>
             <artifactId>ie-generator</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 85 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontHollandController.java

@@ -0,0 +1,85 @@
+package com.ruoyi.web.controller.ie;
+
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.syzy.domain.BBusiHollandQuestions;
+import com.ruoyi.syzy.domain.BBusiHollandRecords;
+import com.ruoyi.syzy.domain.BBusiHollandSteps;
+import com.ruoyi.syzy.service.IBBusiHollandQuestionsService;
+import com.ruoyi.syzy.service.IBBusiHollandRecordsService;
+import com.ruoyi.syzy.service.IBBusiHollandStepsService;
+import com.ruoyi.web.service.CommService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@Api(tags = "生涯测评-职业兴趣测评(HOLLAND)")
+@RequestMapping("front/syzy/holland")
+public class FrontHollandController extends BaseController {
+
+    @Autowired
+    private IBBusiHollandStepsService stepsService;
+    @Autowired
+    private IBBusiHollandQuestionsService questionsService;
+    @Autowired
+    private IBBusiHollandRecordsService recordsService;
+
+    @Autowired
+    private CommService commService;
+    @GetMapping("steps")
+    @ApiOperation("01 测试步骤")
+    public AjaxResult steps() {
+        BBusiHollandSteps exam = new BBusiHollandSteps();
+        exam.setStatus(1);
+        List<BBusiHollandSteps> data = stepsService.selectBBusiHollandStepsList(exam);
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("questions")
+    @ApiOperation("02 步骤题目")
+    public TableDataInfo question(@ApiParam("stepId") @RequestParam Long stepId) {
+        commService.requireVip();
+
+        List<BBusiHollandQuestions> arr = questionsService.selectQuestionsByStepId(stepId);
+        for (int i = 0; i < arr.size(); i++) {
+            arr.get(i).setPercent((i + 1) / arr.size() * 100);
+        }
+        return getDataTable(arr);
+    }
+
+    @PostMapping("save")
+    @ApiOperation("03 保存测试")
+    public AjaxResult save(@RequestBody BBusiHollandRecords data) {
+        long startTime = System.currentTimeMillis();
+        data.setCustomerCode(SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        recordsService.insertBBusiHollandRecords(data);
+        logger.debug("used times:{}",System.currentTimeMillis()-startTime);
+        return AjaxResult.success("生成评测报告成功!", data.getCode());
+    }
+
+    @GetMapping("record")
+    @ApiOperation("04 测评记录")
+    public TableDataInfo record(@ApiParam(value = "页数", example = "1") @RequestParam Integer pageNum,
+        @ApiParam(value = "页大小", example = "15") @RequestParam Integer pageSize) {
+        startPage();
+        BBusiHollandRecords exam = new BBusiHollandRecords();
+        exam.setCustomerCode(SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        exam.setStatus(1);
+        List<BBusiHollandRecords> arr = recordsService.selectBBusiHollandRecordsList(exam);
+        return getDataTable(arr);
+    }
+
+    @GetMapping("record/detail")
+    @ApiOperation("05 测评详情")
+    public AjaxResult record(@ApiParam("测评批次") @RequestParam String code) {
+        return AjaxResult.success(recordsService.info(code));
+    }
+
+}

+ 106 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontMbtiController.java

@@ -0,0 +1,106 @@
+package com.ruoyi.web.controller.ie;
+
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.syzy.domain.BBusiMbtiMajor;
+import com.ruoyi.syzy.domain.BBusiMbtiQuestions;
+import com.ruoyi.syzy.domain.BBusiMbtiRecords;
+import com.ruoyi.syzy.domain.BBusiMbtiSteps;
+import com.ruoyi.syzy.service.IBBusiMbtiQuestionsService;
+import com.ruoyi.syzy.service.IBBusiMbtiRecordsService;
+import com.ruoyi.syzy.service.IBBusiMbtiStepsService;
+import com.ruoyi.util.PageUtil;
+import com.ruoyi.web.service.CommService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@Api(tags = "生涯测试-职业性格测评(MBTI)")
+@RequestMapping("front/syzy/mbti")
+public class FrontMbtiController extends BaseController {
+
+    @Autowired
+    private IBBusiMbtiStepsService stepsService;
+    @Autowired
+    private IBBusiMbtiQuestionsService questionsService;
+    @Autowired
+    private IBBusiMbtiRecordsService recordsService;
+
+    @Autowired
+    private CommService commService;
+    @GetMapping("steps")
+    @ApiOperation("01 测试步骤")
+    public AjaxResult steps() {
+        BBusiMbtiSteps exam = new BBusiMbtiSteps();
+        exam.setStatus(1);
+        List<BBusiMbtiSteps> data = stepsService.selectBBusiMbtiStepsList(exam);
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("questions")
+    @ApiOperation("02 步骤题目")
+    public TableDataInfo question(@ApiParam("stepId") @RequestParam Long stepId) {
+        commService.requireVip();
+
+        BBusiMbtiQuestions exam = new BBusiMbtiQuestions();
+        exam.setStepId(stepId);
+        exam.setStatus(1);
+        List<BBusiMbtiQuestions> arr = questionsService.selectBBusiMbtiQuestionsList(exam);
+        for (int i = 0; i < arr.size(); i++) {
+            arr.get(i).setPercent((i + 1) / arr.size() * 100);
+        }
+        return getDataTable(arr);
+    }
+
+    @PostMapping("save")
+    @ApiOperation("03 保存测试")
+    public AjaxResult save(@RequestBody BBusiMbtiRecords data) {
+        long startTime = System.currentTimeMillis();
+        data.setCustomerCode(SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        recordsService.insertBBusiMbtiRecords(data);
+        logger.debug("used times:{}", System.currentTimeMillis() - startTime);
+        return AjaxResult.success("生成评测报告成功!", data.getCode());
+    }
+
+    @GetMapping("record")
+    @ApiOperation("04 测评记录")
+    public TableDataInfo record(@ApiParam(value = "页数", example = "1") @RequestParam Integer pageNum,
+        @ApiParam(value = "页大小", example = "15") @RequestParam Integer pageSize) {
+        startPage();
+        BBusiMbtiRecords exam = new BBusiMbtiRecords();
+        exam.setCustomerCode(SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        exam.setStatus(1);
+        List<BBusiMbtiRecords> arr = recordsService.selectBBusiMbtiRecordsList(exam);
+        return getDataTable(arr);
+    }
+
+    @GetMapping("record/detail")
+    @ApiOperation("05 测评详情")
+    public AjaxResult record(@ApiParam("测评批次") @RequestParam String code) {
+        return AjaxResult.success(recordsService.info(code));
+    }
+
+    @GetMapping("record/major")
+    @ApiOperation("06 测评推荐专业")
+    public TableDataInfo major(@ApiParam("测评批次") @RequestParam String code,
+                               @ApiParam(value = "页数", example = "1") @RequestParam Integer pageNum,
+                               @ApiParam(value = "页大小", example = "5") @RequestParam Integer pageSize) {
+        List<BBusiMbtiMajor> majorList = recordsService.major(code);
+        return getDataTable(PageUtil.buildListForPage(majorList, pageNum, pageSize));
+    }
+
+    @GetMapping("majors")
+    @ApiOperation("06 兴趣码专业")
+    public AjaxResult majors(@ApiParam("兴趣码") @RequestParam String code) {
+        List<BBusiMbtiMajor> majorList = recordsService.majors(code);
+        return AjaxResult.success(majorList);
+    }
+
+}

+ 71 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/ie/FrontMentalHealthController.java

@@ -0,0 +1,71 @@
+package com.ruoyi.web.controller.ie;
+
+import com.google.common.collect.Lists;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.sy.domain.SyTestExaminee;
+import com.ruoyi.util.PageUtil;
+import com.ruoyi.web.domain.TestMarjorDto;
+import com.ruoyi.web.service.SyTestMajorService;
+import com.ruoyi.web.util.MentalHealthExporter;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Api(tags = "05 健康测试")
+@RestController
+@RequestMapping("front/mentalHealth/")
+public class FrontMentalHealthController extends BaseController {
+    private final SyTestMajorService syTestMajorService;
+
+    public FrontMentalHealthController(SyTestMajorService syTestMajorService) {
+        this.syTestMajorService = syTestMajorService;
+    }
+
+    @GetMapping("list")
+    @ApiOperation("01 Get MentalHealth form history list")
+    public TableDataInfo getList(@ApiParam(value = "页数", example = "1") @RequestParam Integer pageNum,
+                              @ApiParam(value = "页大小", example = "15") @RequestParam Integer pageSize) {
+        startPage();
+        List<SyTestExaminee> examineeList = syTestMajorService.getTestListWithStatus(4, new Integer[]{6, 7});
+        List<TestMarjorDto.MentalHealth> mentalHealthList = Lists.newArrayList();
+        for(SyTestExaminee e: examineeList) {
+            mentalHealthList.add(new TestMarjorDto.MentalHealth(e.getExamineeId(), e.getBeginTime()));
+        }
+        return getDataTable(PageUtil.getDtoListWithPageable(examineeList, mentalHealthList));
+    }
+
+    @GetMapping("report")
+    @ApiOperation("02 Returns result")
+    public AjaxResult getReport(@RequestParam Long examineeId) {
+        TestMarjorDto.MentalHealthResult result = new TestMarjorDto.MentalHealthResult();
+        SyTestExaminee examinee = syTestMajorService.getTestExaminee(4, examineeId);
+        if(null != examinee) {
+            result.setEndTime(examinee.getEndTime());
+            for (TestMarjorDto.TestMatchStat stat : syTestMajorService.toTestMatchStats(examinee)) {
+                result.setValue(stat.getCode(), stat.getCount());
+            }
+        }
+        return AjaxResult.success("查询成功!", result);
+    }
+
+    @GetMapping("/export")
+    @ApiOperation("03 export result")
+    public void export(@RequestParam Long examineeId, HttpServletResponse response)
+    {
+        SyTestExaminee examinee = syTestMajorService.getTestExaminee(4, examineeId);
+        List<TestMarjorDto.TestMatchStat> examineeStatList = syTestMajorService.toTestMatchStats(examinee);
+        Map<String, Integer> categoryScoreMap = examineeStatList.stream().collect(Collectors.toMap(TestMarjorDto.TestMatchStat::getCode, TestMarjorDto.TestMatchStat::getCount));
+        MentalHealthExporter.export(response, examineeId, categoryScoreMap);
+    }
+}

+ 294 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/ie/VoluntaryController.java

@@ -0,0 +1,294 @@
+package com.ruoyi.web.controller.ie;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.google.common.collect.Lists;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.NumberUtils;
+import com.ruoyi.common.utils.PageUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.ie.service.IAWishRecordService;
+import com.ruoyi.syzy.service.IBBusiWishLocationSubmitsService;
+import com.ruoyi.web.domain.Constant;
+import com.ruoyi.web.domain.VoluntaryDto;
+import com.ruoyi.web.service.CommService;
+import com.ruoyi.web.service.VoluntaryService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RestController
+@Api(tags = "单招志愿")
+public class VoluntaryController extends BaseController {
+    private final VoluntaryService voluntaryService;
+    private final IAWishRecordService aWishRecordService;
+    private final IBBusiWishLocationSubmitsService busiWishLocationSubmitsService;
+    private final CommService commService;
+
+    public VoluntaryController(VoluntaryService voluntaryService, IAWishRecordService aWishRecordService, IBBusiWishLocationSubmitsService busiWishLocationSubmitsService, CommService commService) {
+        this.voluntaryService = voluntaryService;
+        this.aWishRecordService = aWishRecordService;
+        this.busiWishLocationSubmitsService = busiWishLocationSubmitsService;
+        this.commService = commService;
+    }
+
+    @PostMapping("getAIRenderRules")
+    @ApiOperation("查询规则")
+    public R<List<VoluntaryDto.AIRenderRule>> getAIRenderRules(@RequestBody VoluntaryDto.AIRenderRequest req) {
+        return voluntaryService.getAIRenderRules(req);
+    }
+
+    @PostMapping("postSingleResultSample")
+    @ApiOperation("单院校专业结果")
+    public R<VoluntaryDto.SingleResponse> postSingleResultSample(@RequestBody VoluntaryDto.SingleRequest req) {
+        return R.ok();
+    }
+
+    @PostMapping("postCalculateResult")
+    @ApiOperation("技能分计算")
+    public R<VoluntaryDto.SingleResponse> postCalculateResult(@RequestBody String data) {
+        commService.requireVip();
+
+        VoluntaryDto.SingleRequest req = new VoluntaryDto.SingleRequest();
+        JSONObject root = JSONObject.parseObject(data);
+        Map<String, String> form = req.getForm();
+        for (String field : root.keySet()) {
+            form.put(field, root.getString(field));
+        }
+        req.setUniversityCode(form.remove("universityCode"));
+        req.setMajorCode(form.remove("majorCode"));
+        req.setMajorEnrollCode(form.remove("majorEnrollCode"));
+        return voluntaryService.postSingleResult(req, true);
+    }
+
+    @PostMapping("postSingleResult")
+    @ApiOperation("单院校专业结果")
+    public R<VoluntaryDto.SingleResponse> postSingleResult(@RequestBody String data) {
+        commService.requireVip();
+
+        VoluntaryDto.SingleRequest req = new VoluntaryDto.SingleRequest();
+        JSONObject root = JSONObject.parseObject(data);
+        Map<String, String> form = req.getForm();
+        for (String field : root.keySet()) {
+            form.put(field, root.getString(field));
+        }
+        req.setUniversityCode(form.remove("universityCode"));
+        req.setMajorCode(form.remove("majorCode"));
+        req.setMajorEnrollCode(form.remove("majorEnrollCode"));
+        return voluntaryService.postSingleResult(req, false);
+    }
+
+    @PostMapping("postMultipleResultSample")
+    @ApiOperation("多院校专业结果")
+    public R<List<VoluntaryDto.SingleResponse>> postMultipleResultSample(@RequestBody VoluntaryDto.MultipleRequest req) {
+        return R.ok();
+    }
+
+    @PostMapping("postMultipleResult")
+    @ApiOperation("多院校专业结果")
+    public R<List<VoluntaryDto.SingleResponse>> postMultipleResult(@RequestBody String data) {
+        commService.requireVip();
+
+        VoluntaryDto.MultipleRequest req = new VoluntaryDto.MultipleRequest();
+        JSONObject root = JSONObject.parseObject(data);
+        req.setMajorCategory(root.getString("majorCategory"));
+        req.setUniversities(getCollegeMajor(root));
+        root.remove("majorCategory");
+        root.remove("universities");
+        return voluntaryService.postMultipleResult(req, false);
+    }
+
+    private List<VoluntaryDto.CollegeMajorDto> getCollegeMajor(JSONObject root) {
+        List<VoluntaryDto.CollegeMajorDto> universities = Lists.newArrayList();
+        if (root.containsKey("universities")) {
+            root.getJSONArray("universities").forEach(l -> {
+                JSONObject lo = (JSONObject) l;
+                VoluntaryDto.CollegeMajorDto cm = new VoluntaryDto.CollegeMajorDto();
+                cm.setCode(lo.getString("code"));
+                List<String> marjorCodeList = Lists.newArrayList();
+                if(lo.containsKey("majorCodes")) {
+                    lo.getJSONArray("majorCodes").forEach(lm -> {
+                        marjorCodeList.add((String) lm);
+                    });
+                }
+                cm.setMajorCodes(marjorCodeList);
+                Map<String, String> form = cm.getForm();
+                for (String field : lo.keySet()) {
+                    form.put(field, lo.getString(field));
+                }
+                universities.add(cm);
+            });
+        }
+        return universities;
+    }
+
+    @PostMapping("postAIResultSample")
+    @ApiOperation("AI多院校专业结果")
+    public TableDataInfo postAIResultSample(@RequestBody VoluntaryDto.AIRequest req) {
+        return new TableDataInfo(Lists.newArrayList(), 0);
+    }
+
+    @PostMapping("postAIResult")
+    @ApiOperation("AI多院校专业结果")
+    public TableDataInfo postAIResult(@RequestBody String data) {
+        commService.requireVip();
+
+        VoluntaryDto.AIRequest req = new VoluntaryDto.AIRequest();
+        JSONObject root = JSONObject.parseObject(data);
+        req.setPageNum(root.getInteger("pageNum"));
+        req.setPageSize(root.getInteger("pageSize"));
+        req.setMajorCategory(root.getString("majorCategory"));
+        req.setUniversities(getCollegeMajor(root));
+        root.remove("pageNum");
+        root.remove("pageSize");
+        root.remove("majorCategory");
+        root.remove("universities");
+
+        VoluntaryDto.AIRequestFilter filter = req.getFilter();
+        if (root.containsKey("filter")) {
+            String tempValue;
+            JSONObject filterObj = root.getJSONObject("filter");
+            if (StringUtils.isNotBlank(tempValue = filterObj.getString("enumPickType"))) {
+                try {
+                    filter.setEnumPickType(VoluntaryDto.EnumPickType.valueOf(tempValue));
+                } catch (IllegalArgumentException e) {
+
+                }
+            }
+            if (StringUtils.isNotBlank(tempValue = filterObj.getString("enumPickEmpty"))) {
+                try {
+                    filter.setEnumPickEmpty(NumberUtils.isNumeric(tempValue) ? VoluntaryDto.EnumPickEmpty.values()[Integer.parseInt(tempValue)] : VoluntaryDto.EnumPickEmpty.valueOf(tempValue));
+                } catch (IllegalArgumentException e) {
+
+                }
+            }
+            if (filterObj.containsKey("hasClearing")) {
+                filter.setHasClearing(filterObj.getBoolean("hasClearing")); // true: 仅看补录 false:不看补录 null:不限。默认null
+            }
+            if (filterObj.containsKey("majorTypes")) {
+                filter.setMajorTypes(filterObj.getJSONArray("majorTypes").stream().map(t -> (String) t).collect(Collectors.toList()));
+            }
+            if (filterObj.containsKey("keyword")) {
+                filter.setKeyword(filterObj.getString("keyword"));
+            }
+            if (filterObj.containsKey("level")) {
+                filter.setLevel(filterObj.getJSONArray("level").stream().map(t -> (String) t).collect(Collectors.toList()));
+            }
+            if (filterObj.containsKey("type")) {
+                filter.setType(filterObj.getJSONArray("type").stream().map(t -> (String) t).collect(Collectors.toList()));
+            }
+            if (filterObj.containsKey("natureTypeCN")) {
+                filter.setNatureTypeCN(filterObj.getJSONArray("natureTypeCN").stream().map(t -> (String) t).collect(Collectors.toList()));
+            }
+            if (filterObj.containsKey("location")) {
+                filter.setLocation(filterObj.getJSONArray("location").stream().map(t -> (String) t).collect(Collectors.toList()));
+            }
+            root.remove("filter");
+        }
+        Map<String, String> form = req.getForm();
+        for (String field : root.keySet()) {
+            form.put(field, root.getString(field));
+        }
+        return voluntaryService.postAIResult(req);
+    }
+
+    // 填报
+    @GetMapping("getVoluntaryConfig")
+    @ApiOperation("取填报配置信息")
+    public R<VoluntaryDto.VoluntaryConfig> getVoluntaryConfig() { // 如果有填报配置相关,放在这里
+        return voluntaryService.getVoluntaryConfig();
+    }
+
+    @GetMapping("getVoluntaryList")
+    @ApiOperation("查询志愿列表")
+    public TableDataInfo getVoluntaryList(@ApiParam @RequestParam(required = false) VoluntaryDto.EnumVoluntaryType type,
+                                          @ApiParam @RequestParam Integer pageNum, @ApiParam @RequestParam Integer pageSize) { // 我的志愿表 // 后台填充快照缺省
+        PageUtils.startPage(pageNum, pageSize);
+        List<JSONObject> list = voluntaryService.getVoluntaryList(type);
+        return getDataTable(list);
+    }
+
+    @PostMapping("submitVoluntary")
+    @ApiOperation("填报志愿")
+    public R<Long> submitVoluntary(@RequestBody String body) { // 填报 // 前端+后台按需要剔除一些不需快照的信息(目前主要是院校信息)
+        JSONObject model = JSONObject.parseObject(body);
+        return voluntaryService.submitVoluntary(model);
+    }
+
+    @GetMapping("getVoluntary")
+    @ApiOperation("查询志愿详情")
+    public R<JSONObject> getVoluntary(@ApiParam @RequestParam Long id) { // 志愿表详情 // 后台填充快照缺省
+        return voluntaryService.getVoluntary(id);
+    }
+
+    @Log(title = "删除志愿记录", businessType = BusinessType.DELETE)
+    @DeleteMapping("removeVoluntary/{id}")
+    public AjaxResult remove(@PathVariable Long id)
+    {
+        Map cond = new HashMap();
+        cond.put("userId", SecurityUtils.getLoginUser().getUserId());
+        cond.put("id", id);
+        return toAjax(aWishRecordService.deleteAWishRecordByMap(cond));
+    }
+
+    @GetMapping("getExamTypesNoToken")
+    @ApiOperation("查询考生类型")
+    public R<List<String>> getExamTypesNoToken(@RequestParam(required = false) String provinceName) {
+        return getExamTypes(provinceName, null);
+    }
+
+    @GetMapping("getExamTypes")
+    @ApiOperation("查询考生类型")
+    public R<List<String>> getExamTypes(@RequestParam(required = false) String provinceName) {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        return getExamTypes(StringUtils.isNotBlank(provinceName) ? provinceName : sysUser.getLocation(), sysUser);
+    }
+
+    private R<List<String>> getExamTypes(String provinceName, SysUser sysUser) {
+        List<String> examTypes;
+        if(Constant.PROVINCE_HUNAN.equals(provinceName)) {
+            examTypes = Lists.newArrayList(Constant.EXAM_TYPE_PG, Constant.EXAM_TYPE_ZZ);
+        } else if(Constant.PROVINCE_HENAN.equals(provinceName)) {
+            examTypes = Lists.newArrayList(Constant.EXAM_TYPE_ZG);
+        } else {
+            examTypes = Lists.newArrayList(Constant.EXAM_TYPE_PG, Constant.EXAM_TYPE_ZZ, Constant.EXAM_TYPE_ZG);
+        }
+        return R.ok(examTypes); // TODO MF
+    }
+
+    @GetMapping("getExamMajorsNoToken")
+    @ApiOperation("查询考生专业类")
+    public R<List<Map>> getExamMajorsNoToken(String provinceName, String examType) {
+        return getExamMajors(provinceName, examType, null); // TODO MF
+    }
+
+    @GetMapping("getExamMajors")
+    @ApiOperation("查询考生专业类")
+    public R<List<Map>> getExamMajors(String provinceName, String examType) {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser(); // TODO MF
+        return getExamMajors(StringUtils.isNotBlank(provinceName) ? provinceName : sysUser.getLocation(), StringUtils.isNotBlank(examType) ? examType : sysUser.getExamType().title(), sysUser);
+    }
+
+    private R<List<Map>> getExamMajors(String provinceName, String examType, SysUser sysUser) {
+        List<Map> examMajors;
+        examMajors = busiWishLocationSubmitsService.examMajors(provinceName).stream().map(t -> {
+            Map d = new HashMap();
+            d.put("code", t.getCourse());
+            d.put("name", t.getCourseName());
+            return d;
+        }).collect(Collectors.toList());
+        return R.ok(examMajors); // TODO MF
+    }
+}

+ 43 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/CategoryDto.java

@@ -0,0 +1,43 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.collect.Lists;
+import com.ruoyi.common.utils.StringUtils;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class CategoryDto {
+    @Data
+    @ApiModel("用户选择专业类")
+    public static class UserCategory {
+        @ApiModelProperty("专业代码")
+        String majorCategoryCode;
+        @ApiModelProperty("测试id")
+        Long examineeId;
+        @ApiModelProperty("1价值2职业3知识")
+        Integer testType;
+
+        @JsonIgnore
+        public List<String> getCodes() {
+            return StringUtils.isNotBlank(majorCategoryCode) ? Arrays.asList(majorCategoryCode.split(",")) : Lists.newArrayList();
+        }
+    }
+
+    @Data
+    @ApiModel("用户选择专业")
+    public static class UserMajor {
+        @ApiModelProperty("专业代码")
+        String majorCode;
+        @ApiModelProperty("1价值2职业3知识")
+        Integer testType;
+
+        @JsonIgnore
+        public List<String> getCodes() {
+            return StringUtils.isNotBlank(majorCode) ? Arrays.asList(majorCode.split(",")) : Lists.newArrayList();
+        }
+    }
+}

+ 19 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/CommDto.java

@@ -0,0 +1,19 @@
+package com.ruoyi.web.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+public class CommDto {
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class AreaSchoolNodeDto {
+        String label;
+        Long value;
+        Integer type;
+        List<AreaSchoolNodeDto> children;
+    }
+}

+ 12 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/Constant.java

@@ -0,0 +1,12 @@
+package com.ruoyi.web.domain;
+
+public class Constant {
+    public static String EXAM_TYPE_ZG = "职高对口升学";
+    public static String EXAM_TYPE_PG = "单招(应届普高)";
+    public static String EXAM_TYPE_ZZ = "单招(中职)";
+
+    public static String PROVINCE_HENAN = "河南";
+    public static String PROVINCE_HUNAN = "湖南";
+
+    public static String CFG_MAJOR_SCORES = "exam.major.scores";
+}

+ 29 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/DynamicTable.java

@@ -0,0 +1,29 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.google.common.collect.Maps;
+import lombok.Data;
+import org.apache.commons.compress.utils.Lists;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DynamicTable<T, V> {
+    String name;
+    List<String> columns;
+    List<T> rows = Lists.newArrayList();
+    Map<String, V> options;
+
+    public void appendRow(T row) {
+        rows.add(row);
+    }
+
+    public V addOption(String name, V value) {
+        if (null == options) {
+            options = Maps.newHashMap();
+        }
+        return options.put(name, value);
+    }
+}

+ 70 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/PaperDto.java

@@ -0,0 +1,70 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.mxjb.domain.MxjbContants;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.apache.commons.compress.utils.Lists;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@ApiModel("考卷")
+public class PaperDto {
+    @ApiModelProperty("测评id")
+    Long evaluationId;
+    @ApiModelProperty("考生类型 1是测评evaluation(default),2是竞赛competitor 3 ai 4 test 6 ie")
+    Integer examineeType;
+    @ApiModelProperty("考生答题卡")
+    Long examineeId;
+    @ApiModelProperty("测评名称")
+    String name;
+    @ApiModelProperty("考试开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    Date beginTime;
+    @ApiModelProperty("考试结束时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    Date endTime;
+    @Deprecated
+    @ApiModelProperty("考试时间 弃用")
+    Long minutes;
+    @ApiModelProperty("考试时间 单位秒")
+    Long remaining;
+    @ApiModelProperty("试卷类型 standard(AI已有) custom(AI新建)")
+    String paperType;
+    @Excel(name = "试卷id")
+    private Long paperId;
+    @ApiModelProperty("阅卷类型 1 自阅卷 2 老师阅卷")
+    private String scoringType;
+    @ApiModelProperty("是否公布")
+    private Boolean publish;
+    @ApiModelProperty("考场模式 90, 120, xxx")
+    private Long mode;
+    @ApiModelProperty("答卷状态 examinee_status")
+    Long state;
+    @ApiModelProperty("测评 evaluation_status")
+    Long evaluationState;
+    @ApiModelProperty("答卷状态名称")
+    String stateStr;
+    @ApiModelProperty("是否可答题")
+    Boolean allowAnswer;
+    @ApiModelProperty("是否可阅卷")
+    Boolean allowScore;
+    List<QuestionDto> questions = Lists.newArrayList();
+    @ApiModelProperty("试卷统计")
+    Map<String, DynamicTable> questionStat;
+
+    public void calculateAllow() {
+        allowAnswer = false;
+        allowScore = false;
+        if (MxjbContants.ExamineeStatusExamine.equals(state)) {
+            allowAnswer = true;
+        } else if (MxjbContants.ExamineeStatusReview.equals(state)) {
+            allowScore = true;
+        }
+    }
+}

+ 91 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/QuestionDto.java

@@ -0,0 +1,91 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.ruoyi.mxjb.domain.MxjbContants;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@ApiModel("考题")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class QuestionDto implements Serializable {
+    @ApiModelProperty("考生类型 1是测评evaluation(default),2是竞赛competitor ")
+    Integer examineeType;
+    @ApiModelProperty("考生id")
+    Long examineeId;
+    @ApiModelProperty("考生号")
+    String customerCode;
+    @ApiModelProperty("姓名")
+    String customerName;
+    @ApiModelProperty("班名")
+    String className;
+    @ApiModelProperty("考题id")
+    Long questionId;
+    @ApiModelProperty("考题序号")
+    Long seq;
+    @ApiModelProperty("学科Id")
+    Long subjectId;
+    @ApiModelProperty("知识点Id")
+    Long knowledgeId;
+    @ApiModelProperty("知识点")
+    String knowledgeName;
+    @ApiModelProperty("考题类型 question_type")
+    String typeId;
+    @ApiModelProperty("考题类型名称")
+    String type;
+    @ApiModelProperty("考题题干")
+    String title;
+    @ApiModelProperty("考题来源")
+    String source;
+
+    @ApiModelProperty("考生答案")
+    String answer;
+    @ApiModelProperty("考生附件")
+    List<String> attachments;
+    private Long duration;
+    @ApiModelProperty("试题解析")
+    String parse;
+    @ApiModelProperty("答题正确状态")
+    Long state;
+    @ApiModelProperty("得分级别")
+    String scoreLevel = "E";
+    @ApiModelProperty("总分")
+    Long scoreTotal;
+    @ApiModelProperty("得分")
+    Long score;
+    @ApiModelProperty("得分率")
+    Double scoreRate;
+    @ApiModelProperty("试题视频")
+    String videoUrl;
+    @ApiModelProperty("视频类型")
+    Integer aliIdType = MxjbContants.Ali_Id_question;
+
+
+    @ApiModelProperty("错误个数")
+    Long wrongCount;
+    @ApiModelProperty("备注")
+    String comment;
+    List<String> options;
+    List<String> answers;
+    List<QuestionDto> subQuestions;
+    @ApiModelProperty("收藏")
+    boolean collect;
+    @ApiModelProperty("当前题")
+    boolean current;
+    @ApiModelProperty("测评时间")
+    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
+    Date time;
+    @ApiModelProperty("测试时间")
+    @JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
+    Date createTime;
+
+    public String getAnswer() {
+        return (null == answer || "null".equals(answer)) ? "" : answer;
+    }
+}

+ 120 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/TestMarjorDto.java

@@ -0,0 +1,120 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.sy.domain.SyMajorCategory;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class TestMarjorDto<T> {
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class CategoryNode {
+        String code;
+        String name;
+        List<CategoryNode> nodes;
+    }
+
+    @Data
+    public static class TestStats {
+        private String matchCode;
+        private boolean noMatch;
+        private List<TestMatchStat> matchStats;
+        private Map<String, Map<String, Integer>> catKeyValueMap;
+
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TestMatchStat {
+        String code;
+        Integer count;
+        boolean matched;
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TestResultDto<T> {
+        Long examineeId;
+        List<SyMajorCategory> selectedList;
+        List<T> datas;
+        String matchedCode;
+        /** 测试完成 **/
+        Boolean testCompleted;
+        /** 选译完成 **/
+        Boolean selectCompleted;
+        /** 无可匹配专业 **/
+        Boolean noMatch;
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MentalHealth {
+        Long examineeId;
+        @JsonFormat(pattern = "yyyy-MM-dd")
+        Date endTime;
+    }
+
+    @Data
+    public static class MentalHealthResult {
+        // 测评时间
+        @JsonFormat(pattern = "yyyy-MM-dd")
+        Date endTime;
+        // 学习焦虑
+        Integer study;
+        // 对人焦虑
+        Integer social;
+        // 孤独倾向
+        Integer alone;
+        // 自责倾向
+        Integer blamed;
+        // 过敏倾向
+        Integer sensitive;
+        // 身体症状
+        Integer body;
+        // 恐怖倾向
+        Integer fear;
+        // 冲动倾向
+        Integer implust;
+
+        public void setValue(String name, Integer value) {
+            switch(name) {
+                case "学习焦虑":
+                    study = value;
+                    break;
+                case "对人焦虑":
+                    social = value;
+                    break;
+                case "孤独倾向":
+                    alone = value;
+                    break;
+                case "自责倾向":
+                    blamed = value;
+                    break;
+                case "过敏倾向":
+                    sensitive = value;
+                    break;
+                case "身体症状":
+                    body = value;
+                    break;
+                case "恐怖倾向":
+                    fear = value;
+                    break;
+                case "冲动倾向":
+                    implust = value;
+                    break;
+                case "效度量表":
+                    break;
+            }
+        }
+    }
+}

+ 523 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/VoluntaryDto.java

@@ -0,0 +1,523 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.syzy.domain.BBusiWishUniversities;
+import com.ruoyi.web.service.EnrollRateCalculator;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.apache.commons.compress.utils.Lists;
+import org.apache.commons.lang3.tuple.MutablePair;
+import org.apache.commons.lang3.tuple.Triple;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class VoluntaryDto {
+    // dynamic render rules
+    // TODO: 部分规则不需要向前端返回:比如性别与考生类别,此类信息直接在当前用户中获取
+    // 只需要向前端返回需要收集信息的部分
+    @Data
+    @ApiModel
+    public static class AIRenderRule {
+        // 类别;与AIResponse中的结论类型相对应;前会根据此类型分组展示表单,收集信息
+        EnumRuleCategory enumRuleCategory;
+        // 字段名,必填。前段会将收集的值以此名称返回
+        // 请保持名称按一定的规则定义,如分数类型的由score开头等
+        // 这样可以和原来定义的SingleRequest/MultipleRequest/AIRequest等吻合
+        String fieldName;
+        // 前端输入类型,见类型注释
+        // 如果是分制,以filedName+`Total`返回分制选择结果
+        EnumInputType enumInputType;
+        // 校验规则
+        // TODO: 先暂定前端自行生成,如果不行再考虑后台返回。
+        // TODO: 前端会将录取规则设置为必填,其它规则设置为选填。如分制类输入,前端会将选中分制作为max校验。
+        Boolean required;
+        Integer min;
+        Integer max;
+        String regex;
+        // 词典选项类选项 // 词典类选项的优先级最高
+        String dictOptions;
+        // 非词典选项 // 分制类规则,将多分制在此options中返回
+        String[] options;
+        // 对于多选时增加一个互斥选项
+        String mutexOption;
+        // 前端渲染字段
+        String label; // 输入标题,必须填充
+        String description; // 结尾描述文案,可选填充
+        String placeholder; // 占位提示文案 // 选填,缺省前端会自行补充,后台返回的优先级高
+        String tips; // 输入框下的特殊说明文案,可选填充
+        // 特殊配置
+        String defaultValue; // 缺省/初始化时的默认值
+        Boolean dotDisable; // 数值输入时,是否带小数点,默认带
+        String keyboardMode; // 当enumInputType==number时响应3种模式:number card car,默认number
+    }
+
+    @Data
+    @ApiModel
+    public static class AIRenderRequest {
+        @ApiModelProperty("0 default; 1 AI。AI时职业技能输入得分率 2. 技能分计算")
+        int renderType;
+        @ApiModelProperty("一级专业大类")
+        String majorCategory;
+        @ApiModelProperty("二级专业细分集合")
+        String[] majorTypes;
+        @ApiModelProperty("三级专业集合")
+        String[] majorCodes;
+        @ApiModelProperty("专业招生代码")
+        String[] majorEnrollCodes;
+        @ApiModelProperty("学校")
+        String universityCode;
+        @ApiModelProperty("当前已经填写的表单信息")
+        Map<String, String> form;
+    }
+
+    // 公共
+    @Data
+    @ApiModel
+    public static class VoluntaryModel<I, O> {
+        @ApiModelProperty("voluntaryType")
+        EnumVoluntaryType voluntaryType;
+        @ApiModelProperty("id")
+        Long id;
+        @ApiModelProperty("年份")
+        Integer year;
+        @ApiModelProperty("名称")
+        String name;
+        @ApiModelProperty("批次名")
+        String batchName;
+        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+        Date createTime;
+        @ApiModelProperty("条件")
+        I request; // 当前的填写信息,AIRequest.filter可以不保存后台自行决定
+        @ApiModelProperty("用户")
+        User userSnapshot; // 用户相关快照信息,因为有的计算条件是从用户信息上取的(这个后台自己存,前端不传)
+        // 考虑传输节流,AIResponse.university保留需要快照的属性
+        // 如:code,enrollCode // TODO 按需罗列(最好是前端只传university.code,后端来决定快照哪些信息)
+        // 删除AIResponse.majorDetails.recommends // 志愿表中不需要它
+        // 前端主动请求此model时,后台自动填充缺省信息
+        @ApiModelProperty("详情")
+        List<O> details; // 填报的院校专业
+    }
+    @Data
+    public static class User {
+        String name;
+        String sex;
+        String examType;
+        String provinceName;
+    }
+    @Data
+    @ApiModel
+    public static class VoluntaryConfig {
+        // 目前只有填报数量的配置,院校2个,专业4个
+        @ApiModelProperty
+        int collegeLimit = 2;
+        @ApiModelProperty
+        int majorLimit = 4;
+    }
+
+    // Request
+    @Data
+    @ApiModel
+    public static class SingleRequest {
+        @ApiModelProperty("院校代码")
+        String universityCode;
+        @ApiModelProperty("专业代码")
+        String majorCode;
+        @ApiModelProperty("专业招生代码")
+        String majorEnrollCode;
+        // 因为单选学校,所以不需要显式提供scoreSkillLimit
+
+        @ApiModelProperty
+        Map<String, String> form = Maps.newHashMap();
+    }
+    @Data
+    @ApiModel
+    public static class MultipleRequest {
+        // TODO: 院校查询也需要支持此参数,来查询包含此专业大类的院校
+        // TODO: 先暂定限制大类,可能需要限制专业细分2级分类
+        @ApiModelProperty
+        String majorCategory; // 因为多选时想尽量让规则重合,所以限制专业大类
+        @ApiModelProperty
+        List<CollegeMajorDto> universities; // 多个学校,先定2个,会受分数类型限制
+    }
+    @Data
+    @ApiModel
+    public static class AIRequest extends MultipleRequest {
+        @ApiModelProperty
+        Integer pageNum;
+        @ApiModelProperty
+        Integer pageSize;
+
+        // 推荐列表筛选条件 // 筛选条件因为可以不保存,所以单独提取出来
+        @ApiModelProperty
+        AIRequestFilter filter = new AIRequestFilter();
+        // TODO: 会尽快统计出来一些需要额外补充的信息
+        @ApiModelProperty
+        Map<String, String> form = Maps.newHashMap();
+    }
+    @Data
+    @ApiModel
+    public static class CollegeMajorDto {
+        @ApiModelProperty
+        String code; // university code
+        @ApiModelProperty
+        List<String> majorCodes; // major codes in university
+        @ApiModelProperty
+        List<String> majorEnrollCodes; // major enroll codes in university
+        @ApiModelProperty
+        Map<String, String> form = Maps.newHashMap();
+    }
+    @Data
+    @ApiModel
+    public static class AIRequestFilter {
+        @ApiModelProperty
+        List<String> majorTypes; // majorCode 二级分类
+        @ApiModelProperty
+        EnumPickType enumPickType; // 默认All
+        @ApiModelProperty
+        EnumPickEmpty enumPickEmpty; // 默认null, EnumPickType与EnumPickEmpty会互斥,一个传了另一个就会不传
+        // TODO: 后台确认一下,如果A学校有的10个专业,有5个不满足条件,有3个有概率,2个没有概率。
+        //       则展示时冲稳保展示A学校+3个有概率专业;无概率查询显示A学校+2个无概率专业
+        @ApiModelProperty
+        Boolean hasClearing; // true: 仅看补录 false:不看补录 null:不限。默认null
+        // 院校通用筛选
+        @ApiModelProperty
+        String keyword; // 院校名称
+        @ApiModelProperty
+        List<String> level; // 办学层次
+        @ApiModelProperty
+        List<String> type; // 院校类型
+        @ApiModelProperty
+        List<String> natureTypeCN; // 办学类型
+        @ApiModelProperty
+        List<String> location; // 院校省份
+    }
+    public enum EnumPickType {
+        /** 有概率类型 **/
+        // TODO    A 根据近3年有的录取分取平均值得到1个投档分
+        //         B 胡总给出一组浮动范围 +N-M分 属于有机会录取的,概率值可以先线性分布,也可以配比例尺
+        //         C B的配置需要做一套全局的,特定的学校可能会独立指定,因为头部学校和差学校的浮动范围肯定是不一样的
+        All(""), // 全部
+        Danger("冲"), // 冲
+        Normal("稳"), // 稳
+        Safety("保"); //
+
+        String label;
+        EnumPickType(String label) {
+            this.label = label;
+        }
+
+        public String label() {
+            return label;
+        }
+    }
+    public enum EnumPickEmpty {
+        /** 无概率类型 **/
+        // 无概率:录取规则通过,特殊要求+专业要求不通过
+        EnrollPass,
+        // 新增/政策变动:无法计算录取规则
+        // 如:没有往年录取数据
+        New,
+    }
+
+    // Response
+    @Data
+    @ApiModel
+    public static class SingleResponse {
+        @ApiModelProperty
+        String universityCode;
+        @ApiModelProperty
+        String majorCode;
+        @ApiModelProperty
+        String majorEnrollCode; // 专业招生代码
+        @ApiModelProperty
+        String majorGroup; // 专业组
+        @ApiModelProperty
+        String majorName; // 专业
+        @ApiModelProperty
+        String majorDirection;
+        @ApiModelProperty
+        EnumPickType enumPickType;
+        @ApiModelProperty
+        Integer enrollRate; // 100进制,按原优志愿,为null表示无概率
+        @ApiModelProperty
+        String enrollRateText; // 概率描述,按原优志愿
+        @ApiModelProperty
+        String tips; // 其它说明
+        @ApiModelProperty
+        List<MajorEnrollHistory> histories; // 历年数据
+        @ApiModelProperty
+        List<MajorClearingHistory> clearings; // 补录数据
+        @ApiModelProperty
+        List<MajorEnrollRule> rules; // 所有结论,前端可根据enumRuleType展示
+        List<MajorEnrollRule> improves; // 各分项提升结论
+        @JsonIgnore
+        Double score;
+        @JsonIgnore
+        EnumPickEmpty enumPickEmpty;
+    }
+
+    // paging response
+    @Data
+    @ApiModel
+    public static class AIResponse {
+        @ApiModelProperty
+        Integer enrollRate;
+        @ApiModelProperty
+        String enrollRateText;
+        @ApiModelProperty
+        EnumPickType enumPickType;
+        @ApiModelProperty
+        EnumPickEmpty enumPickEmpty;
+        @ApiModelProperty
+        String enrollCode; // 专业代码 TODO: 具体实现的时候看放在这里合不合适
+        @ApiModelProperty
+        BBusiWishUniversities university; // university base info.
+        @ApiModelProperty
+        List<SingleResponse> majorDetails; // major enroll rate details
+    }
+    @Data
+    @ApiModel
+    public static class MajorEnrollHistory {
+        @ApiModelProperty
+        Integer year; // 年份
+        @ApiModelProperty
+        String score; // Important: 返回差分 = myScore - historyScore; 返回'+N,-N'。以后可能返回真实分数
+        @ApiModelProperty
+        Integer plan; // 计划人数
+        @ApiModelProperty
+        Integer enroll; // 录取人数
+    }
+
+    @Data
+    @ApiModel
+    public static class MajorClearingHistory {
+        @ApiModelProperty
+        Integer year; // 年份
+        @ApiModelProperty
+        String score; // 同上,也暂时返回分差,返回'+N,-N'。
+        @ApiModelProperty
+        Integer realNum; // 补录人数
+    }
+
+    @Data
+    @ApiModel
+    public static class MajorEnrollRule {
+        @ApiModelProperty
+        EnumRuleCategory category; // 规则类别
+        @ApiModelProperty
+        EnumRuleType type; // 规则类型
+        @ApiModelProperty
+        String improveType; // 分类提升类型,随导入文件生成
+        @ApiModelProperty
+        String content; // 规则描述性文字
+        @ApiModelProperty
+        String description; // 有的规则会有额外的说明内容
+        @ApiModelProperty
+        Integer enrollRate; // 如果type=ScoreTotal,填充此字段
+        @ApiModelProperty
+        String enrollRateText; // 如果type=ScoreTotal,填充此字段
+        @ApiModelProperty
+        EnumPickType enumPickType;
+        @ApiModelProperty
+        Integer year;
+        @ApiModelProperty
+        String value; // 用户填写的值
+        @ApiModelProperty
+        Boolean valid; // true: 通过规则;false: 未通过规则; null: 未填写相关信息
+        @ApiModelProperty
+        String missingValue; // 差的数值: history.score-user.score。比如valid=false, 表示距离通过差的分值。
+        @ApiModelProperty
+        String failedMessage; // valid=false 或者 valid=null 时的文案说明
+        @JsonIgnore
+        EnrollRateCalculator.RateLevel rl;
+    }
+
+    public enum EnumInputType {
+        Text, // 普通文本,一般情况下输入均使用此类。数值也可以使用此类型,结合校验规则使用
+        Score, // AI的特殊输入类型,带分制的分数 // 此类输入之后可能涉及输入条件动态变化,NOTE:后面再考虑这种情况。
+        Number, // 数值,会限制键盘。一般分数类、或者数值类的使用此类型。
+        Radio, // radio单选。如果固定2个,用radio,否则用picker。一般超过3个会折行,影响美观
+        Picker, // picker单选,一般使用Picker做单选。
+        Eyesight,
+        Checkbox // 多选
+        // TODO:其它待发现
+    }
+
+    public enum EnumVoluntaryType {
+        AI,
+        Multiple
+    }
+    public enum EnumRuleCategory {
+        None,
+        Enroll, // 录取规则
+        Special // 特殊要求
+    }
+    public enum EnumRuleType {
+        None,
+        // 1 总分,必须定义,有特殊展示
+        // 2 学考与文化分,一般有特殊展示
+        // 3 除学考与文化成绩外,录取规则涉及的分数,会有展示信息
+        // 除以上3点外,均属于附加要求
+        ScoreTotal, // 总分
+        ScoreUnion, // 学考分,一般针对普高,为固定分,也有部分院校要求普高生也进行校考
+        ScoreBase, // 文化分,校考,一般针对非普高
+        ScoreSkill, // 技能分
+        ScoreSingle, // 单科分
+        Special, // 特殊要求, 包含判断的规则项
+        Readonly, // 只读项不参与计算,仅在详情页展示
+        other // 其它
+    }
+
+    public static class FormulaScoreStat {
+        Set<String> validTotalSet = Sets.newHashSet("ScoreBase","ScoreSingle","ScoreSkill");
+        Map<String, List<Triple<Double, Double, Double>>> groupItemListMap = Maps.newHashMap(); // 提分项目的 项目原始分 OriScore, 项目总分 Total,项目得分率 Rate
+        Map<String, MutablePair<Double, Double>> groupTypeStatMap = Maps.newHashMap(); // 提分类型的 得分 Score,满分合计 Total
+        Double groupAllTotal = 0.0; // 提分项满分合计
+
+        Map<String, MutablePair<Double, Double>> typeKeyScoreMap = Maps.newHashMap(); // 类型项目的 得分 Score 及 原始分 OriScore
+        Map<String, MutablePair<Double, Double>> typeScoreMap = Maps.newHashMap(); // 类型的 得分 Score 及 原始分 Total
+        MutablePair<Double, Double> typeTotal = new MutablePair<>(0.0, 0.0); // 综合得分 及 原始分
+        Integer typeAllTotal = 0; // 类型满分合计
+
+        public boolean isValid() {
+            return (typeScoreMap.containsKey("ScoreSingle") || typeScoreMap.containsKey("ScoreBase")) && typeScoreMap.containsKey("ScoreSkill");
+        }
+
+        public boolean isScoreValid() {
+            return (typeScoreMap.containsKey("ScoreSingle") || typeScoreMap.containsKey("ScoreBase"));
+        }
+
+        public Integer getAllTotal() {
+            return typeAllTotal;
+        }
+        /**
+         * 按分类项统计
+         * @param itemType ScoreSingle/ScoreBase, ScoreSkill
+         * @param itemName Single对应科目
+         * @param oriScore
+         * @param rate
+         * @return
+         */
+        public Double addType(String itemType, String itemName, Double oriScore, Double rate) {
+            Double score = oriScore * rate;
+            if (StringUtils.isBlank(itemType)) {
+                return score;
+            }
+            merge(typeKeyScoreMap, itemType + "_" + itemName, score, oriScore); // 记录明细
+            merge(typeScoreMap, itemType, score, oriScore); // 按类汇总
+            if (validTotalSet.contains(itemType)) {
+                typeTotal.setLeft(typeTotal.getLeft() + score); // 所有汇总
+                typeTotal.setRight(typeTotal.getRight() + oriScore);
+            }
+            return score;
+        }
+
+        public void addTypeTotal(Integer total) {
+            typeAllTotal += total;
+        }
+
+        public Double getTypeValue(String valueType, String itemName, Boolean isOri) {
+            if ("StatSingle".equals(valueType)) { // 明细分
+                String dataKey = "ScoreSingle_" + itemName;
+                MutablePair<Double, Double> p = typeKeyScoreMap.get(dataKey);
+                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
+            } else if ("StatSkill".equals(valueType)) { // 明细分
+                String dataKey = "ScoreSkill_" + itemName;
+                MutablePair<Double, Double> p = typeKeyScoreMap.get(dataKey);
+                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
+            } else if ("StatCategoryScore".equals(valueType)) { // 分类分
+                if(StringUtils.isBlank(itemName)) {
+                    return isOri ? typeTotal.getRight() : typeTotal.getLeft();
+                }
+                MutablePair<Double, Double> p = typeScoreMap.get(itemName);
+                if(null == p) {
+                    p = typeScoreMap.get("ScoreBase".equals(itemName) ? "ScoreSingle" : "ScoreBase");
+                }
+                return null != p ? (isOri ? p.getRight() : p.getLeft()) : null;
+            } else if ("StatRateScore".equals(valueType)) { // 综合分
+                return isOri ? typeTotal.getRight() : typeTotal.getLeft();
+            }
+            return null;
+        }
+
+        /**
+         * 按提分项统计  学考,综合测试
+         * @param itemGroup
+         * @param oriScore
+         * @param total
+         * @param rate
+         * @return
+         */
+        public Double addGroup(String itemGroup, Double oriScore, Double total, Double rate) {
+            Double rateScore = oriScore * rate;
+            if (StringUtils.isBlank(itemGroup)) {
+                return rateScore;
+            }
+            List<Triple<Double, Double, Double>> itemList = groupItemListMap.get(itemGroup);
+            if (null == itemList) {
+                itemList = Lists.newArrayList();
+                groupItemListMap.put(itemGroup, itemList);
+            }
+            itemList.add(Triple.of(oriScore, total, rate));
+            Double rateTotal = total * rate;
+            merge(groupTypeStatMap, itemGroup, rateScore, rateTotal);
+            groupAllTotal += rateTotal;
+            return rateScore;
+        }
+
+        public List<MajorEnrollRule> getImproveScore(Double diffValue) {
+            List<MajorEnrollRule> improvesList = Lists.newArrayList();
+            if (groupTypeStatMap.containsKey("学考")) {
+                MajorEnrollRule base = new MajorEnrollRule();
+                base.setCategory(EnumRuleCategory.Enroll);
+                base.setType(EnumRuleType.ScoreBase);
+                base.setImproveType("学考");
+                base.setMissingValue("0");
+                improvesList.add(base);
+            }
+            for (String itemGroup : groupTypeStatMap.keySet()) {
+                if ("学考".equals(itemGroup)) {
+                    continue;
+                }
+                MajorEnrollRule enrollRule = new MajorEnrollRule();
+                enrollRule.setCategory(EnumRuleCategory.Enroll);
+                enrollRule.setType("技能测试".equals(itemGroup) ? EnumRuleType.ScoreSkill : EnumRuleType.other);
+                enrollRule.setImproveType(itemGroup);
+                MutablePair<Double, Double> scorePair = groupTypeStatMap.get(itemGroup);
+                Double missRateScore = diffValue * scorePair.getRight() / groupAllTotal;
+                enrollRule.setMissingValue(String.valueOf(Math.round(getOriScore(groupItemListMap.get(itemGroup), missRateScore)))); // 差分 * 本类组总分/所有组总分
+                improvesList.add(enrollRule);
+            }
+            return improvesList;
+        }
+
+        // 原始分, 总分, 得分率
+        private Double getOriScore(List<Triple<Double, Double, Double>> itemList, Double missRateScore) {
+            Double statTotal = itemList.stream().mapToDouble(t -> t.getMiddle()).sum();
+            Double oriTotal = 0.0;
+            for (Triple<Double, Double, Double> t : itemList) {
+                oriTotal += Math.min(t.getMiddle() - t.getLeft(), missRateScore * t.getMiddle() / statTotal / t.getRight());
+            }
+            return oriTotal;
+        }
+        private void merge(Map<String, MutablePair<Double, Double>> pairMap, String key, Double value, Double oriValue) {
+            MutablePair<Double, Double> exist = pairMap.get(key);
+            if (null == exist) {
+                exist = new MutablePair<>(value, oriValue);
+                pairMap.put(key, exist);
+            } else {
+                exist.setLeft(exist.getLeft() + value);
+                exist.setRight(exist.getRight() + oriValue);
+            }
+        }
+
+    }
+}

+ 133 - 0
ie-admin/src/main/java/com/ruoyi/web/domain/ZytbDto.java

@@ -0,0 +1,133 @@
+package com.ruoyi.web.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.syzy.domain.BBusiWishUniversities;
+import com.ruoyi.syzy.domain.BBusiWishUniversitySubmitMarjors;
+import com.ruoyi.syzy.domain.BBusiWishUniversitySubmitRecruitPlan;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class ZytbDto {
+    //二、专业详情
+    @Data
+    public static class ZytbVoluntaryMarjorCond {
+        //2.1入参:collegeCode 招生专业组编码,year年份
+        private String universityId;
+        @JsonProperty("jCode")
+        private String jCode;
+        private String collegeCode;
+        private Integer year;
+        private String mode;
+        private String batchName;
+        private Integer score;
+        private Integer batchMinScore;
+        private Long wishResId; // 指定志愿表Id
+    }
+
+    @Data
+    public static class ZytbVoluntaryMarjorDetail {
+        //2.2返参
+        //(1)majorName,marjorDirection,marjorBelongs,planCount,xuefei,xuezhi,
+        private Long id;
+        private String marjorId;
+        private String marjorBelongs;
+        private String marjorName;
+        private String marjorDirection;
+        private Integer planCount;
+        private String xuefei;
+        private String xuezhi;
+        //(2)该专业历史数据
+        @ApiModelProperty("推荐方式:0-冲刺,1-稳妥,2-保守")
+        private Integer pickType;
+        private Integer enrollOver = 0; // 1(上溢出0%) 0 -1(下溢出100%)
+        private Integer enrollRatio;
+        private String enrollRatioText;
+
+        private String professionType;
+        private List<String> typeNames;
+        private String level;
+        private Boolean enrollFluctuate;
+
+        //numReal录取人数,lineDiff线差,score录取最低分,seat录取最低位次
+        // 预留字段:enrollRatio,enrollRatioText
+        private BBusiWishUniversitySubmitMarjors[] histories = new BBusiWishUniversitySubmitMarjors[4];
+        private BBusiWishUniversitySubmitMarjors history;
+    }
+
+    @Data
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public static class ZytbVolunteerRes {
+        //一、列表
+        //1.1入参:(1 2 3)均为非必填
+        // (1)分数Integer score,组合String mode(如物理,化学,生物),mode1物理/历史或理科/文科,batchName批次(本科或专科),batch批次(1,2,3,4分别对应本科专科)
+        // (2)学校基础参数:String managerType, type, level,features,natureTypeCN,universityName(院校名称)
+        // (3)超稳保:Integer pickType (根据这个字段来计算学生使用哪个位次/分数来查找submit_major中的专业线)
+        // (4)专业名称:List<String> majors,专业的中文名称,对应submit_major表中的marjorName及marjorDirection
+
+        //1.2返参
+        @ApiModelProperty("推荐方式:0-冲刺,1-稳妥,2-保守")
+        private Integer pickType;
+        @ApiModelProperty("专项说明")
+        private String specialProject;
+        @ApiModelProperty("录取概率溢出(预留字段)")
+        private Integer enrollOver = 0; // 1(上溢出0%) 0 -1(下溢出100%)
+        @ApiModelProperty("录取概率(预留字段)")
+        private String enrollRatio;
+        @ApiModelProperty("录取概率说明(预留字段)")
+        //风险极大,风险中等,风险极小
+        private String enrollRatioText;
+        //院校基础信息
+        //location,cityName, managerType, type, level,features,natureTypeCN,ranking,
+        private Long universityId;
+        @JsonProperty("jCode")
+        private String jCode;
+        private BBusiWishUniversities university;
+        //招生计划(submit_recruit_plan表最新一年) :
+        // year年份,planCount计划数,course选科科目;
+        // collegeCode 院校招生代码
+        // majorCount 该专业组(collegeCode)下包含的专业数量
+        private BBusiWishUniversitySubmitRecruitPlan recruitPlan;
+
+        //历年数据(取submit_marjor表最近三年的专业组):  numReal录取人数,lineDiff线差,score录取最低分,seat录取最低位次
+        private BBusiWishUniversitySubmitMarjors[] histories = new  BBusiWishUniversitySubmitMarjors[4];
+        private BBusiWishUniversitySubmitMarjors history;
+
+        private List<ZytbVoluntaryMarjorDetail> marjorList;
+    }
+//
+//    @Data
+//    public static class ZytbCollegeRes {
+//        Long universityId;
+//        String name;
+//        String collegeCode;
+//        String universityCode;
+//        List<ZytbSubmitMajorRes> submitMajors;
+//    }
+//
+//    @Data
+//    public static class ZytbSubmitMajorRes {
+//        Long submitMarjorId;
+//        String marjorBelongs;
+//        String marjorName;
+//        String marjorDirection;
+//    }
+
+    public static void main(String[] args) {
+        String aa= "化学,生物";
+        String[] bb = StringUtils.split(aa,",");
+        System.out.println(bb);
+        Collections.reverse(Arrays.asList(bb));
+        String cc0= StringUtils.join(bb,",");
+        System.out.println(cc0);
+        System.out.println(cc0.equalsIgnoreCase("生物,化学"));
+        String cc = Arrays.toString(bb);
+        System.out.println(cc);
+    }
+}

+ 93 - 0
ie-admin/src/main/java/com/ruoyi/web/service/CacheService.java

@@ -0,0 +1,93 @@
+package com.ruoyi.web.service;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mxjb.domain.MxjbContants;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysDictDataService;
+import com.ruoyi.system.service.ISysUserService;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class CacheService {
+    private final ISysConfigService sysConfigService;
+    private final ISysDictDataService sysDictDataService;
+    private final ISysUserService sysUserService;
+
+    private final String zyLocationsKey = "zy.locations";
+    private final String chooseQuestionTypesKey = "question.choose.types";
+    private final String multiQuestionTypesKey = "question.multi.types";
+    public static final Set<String> MultiQuestionTypeSet = Sets.newHashSet("多选题", "不定项选择题");
+    public static final Set<String> ChooseQuestionTypeSet = Sets.newHashSet("单选题", "选择题", "单项选择", "判断题","单项选择题");
+    public static final Set<String> ChooseItemQuestionTypeSet = Sets.newHashSet("单选题", "选择题", "单项选择", "单项选择题");
+    public static final Set<String> ChooseRightQuestionTypeSet = Sets.newHashSet("判断题");
+    public static Map<String, String> quesitonTypeMap;
+
+    public CacheService(ISysConfigService sysConfigService, ISysDictDataService sysDictDataService, ISysUserService sysUserService) {
+        this.sysConfigService = sysConfigService;
+        this.sysDictDataService = sysDictDataService;
+        this.sysUserService = sysUserService;
+
+        quesitonTypeMap = Maps.newHashMap();
+        quesitonTypeMap.put("复选总分", MxjbContants.QuestionTypeMultiTotal); // 4
+        quesitonTypeMap.put("复选评价", MxjbContants.QuestionTypeMultiScore); // 5
+        quesitonTypeMap.put("单选计数", MxjbContants.QuestionTypeChooseCount); // 6
+        quesitonTypeMap.put("单选分值", MxjbContants.QuestionTypeChooseScore); // 7
+    }
+
+    public String getQuestionType(String qtype) {
+        String type = quesitonTypeMap.get(qtype);
+        if (null != type) {
+            return type;
+        }
+        if (selectChooseQuestionTypes().contains(qtype)) {
+            return MxjbContants.QuestionTypeChoose;
+        }
+        return selectMultiQuestionTypes().contains(qtype) ? MxjbContants.QuestionTypeMultiChoose : MxjbContants.QuestionTypeSubjective;
+    }
+    public Set<String > selectMultiQuestionTypes () {
+        String multiTypes = sysConfigService.selectConfigByKey(multiQuestionTypesKey);
+        if(StringUtils.isNotBlank(multiTypes)) {
+            return Sets.newHashSet(StringUtils.split(multiTypes, ","));
+        }
+        return MultiQuestionTypeSet;
+    }
+
+    public Set<String > selectChooseQuestionTypes () {
+        String chooseTypes = sysConfigService.selectConfigByKey(chooseQuestionTypesKey);
+        if(StringUtils.isNotBlank(chooseTypes)) {
+            return Sets.newHashSet(StringUtils.split(chooseTypes, ","));
+        }
+        return ChooseQuestionTypeSet;
+    }
+
+    public Set<String > getZyLocations() {
+        String locations = sysConfigService.selectConfigByKey(zyLocationsKey);
+        if(StringUtils.isNotBlank(locations)) {
+            return Sets.newHashSet(StringUtils.split(locations, ","));
+        }
+        return Sets.newHashSet();
+    }
+
+    public Map<String,String> selectDictDataMapByType(String dictType){
+        List<SysDictData> dictDatas = sysDictDataService.selectDictDataByType(dictType);
+        Map<String, String> dictMap = new HashMap<>();
+        if(org.apache.commons.collections.CollectionUtils.isNotEmpty(dictDatas)) {
+            for(SysDictData data : dictDatas) {
+                dictMap.put(data.getDictValue(), data.getDictLabel());
+            }
+        }
+        return dictMap;
+    }
+
+    public String getValue(String value, String defValue) {
+        return StringUtils.isNotBlank(value) ? value : defValue;
+    }
+}

+ 163 - 0
ie-admin/src/main/java/com/ruoyi/web/service/EnrollRateCalculator.java

@@ -0,0 +1,163 @@
+package com.ruoyi.web.service;
+
+import com.google.common.collect.Maps;
+import com.ruoyi.web.domain.VoluntaryDto;
+import lombok.Data;
+import org.apache.commons.compress.utils.Lists;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  冲、稳、保概率判定
+ *  冲:[0%, 70%], 稳:(70%, 85%), 保:[85%, 99%]
+ */
+@Service
+public class EnrollRateCalculator {
+    private Map<Long, List<RateLevel>> rateRangesMap = Maps.newHashMap();
+    private Integer enrollLineRate, enrollRateStep;
+
+    public EnrollRateCalculator() {
+        clean();
+    }
+
+    public void clean() {
+        rateRangesMap.clear();
+        enrollLineRate = 65;
+        enrollRateStep = 5;
+    }
+
+    public RateLevel calSchoolEnrollRate(Integer scoreTotal, Double enrollMinScore, Double score) {
+        if (null == enrollMinScore || null == score) {
+            return null;
+        }
+        Long maxScore = Math.round(enrollMinScore * 1.1);
+        Long minScore = Math.round(enrollMinScore * 0.9);
+        if (score >= maxScore) {
+            return new RateLevel(98, VoluntaryDto.EnumPickType.Safety);
+        } else if (score <= minScore) {
+            return new RateLevel(2, VoluntaryDto.EnumPickType.Danger);
+        }
+        Double userRate = 2 + (Math.round(score) - minScore) * 96.0 / (maxScore - minScore);
+        if (userRate >= 80) {
+            return new RateLevel(userRate.intValue(), VoluntaryDto.EnumPickType.Safety);
+        } else if (userRate >= 60) {
+            return new RateLevel(userRate.intValue(), VoluntaryDto.EnumPickType.Normal);
+        } else if (userRate < 40) {
+            return new RateLevel(userRate.intValue(), VoluntaryDto.EnumPickType.Danger);
+        }
+        return new RateLevel(userRate.intValue(), VoluntaryDto.EnumPickType.Danger);
+    }
+
+    /**
+     * 查找合适的层次
+     * @param scoreTotal
+     * @param enrollLine
+     * @param score
+     * @return
+     */
+    public RateLevel satisfy(Integer scoreTotal, Double enrollLine, Double score) {
+        List<RateLevel> rateLevels = init(scoreTotal, enrollLine);
+        int intScore = (int) Math.round(score);
+        for (int i = 0; i < rateLevels.size(); i++) {
+            RateLevel l = rateLevels.get(i);
+            if ((intScore > l.min || intScore == l.min && l.incMin) && (intScore < l.max || intScore == l.max && l.incMax)) {
+                return l;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 初始化缓存
+     * @param scoreTotal
+     * @param fEnrollLine
+     * @return
+     */
+    public List<RateLevel> init(Integer scoreTotal, Double fEnrollLine) {
+        int enrollLine = (int) Math.round(fEnrollLine);
+        Long key = scoreTotal * 1000L + enrollLine;
+        List<RateLevel> rateLevels = rateRangesMap.get(key);
+        if (null == rateLevels) {
+            rateLevels = Lists.newArrayList();
+            rateRangesMap.put(key, rateLevels);
+
+            // 65%以上的计算
+            Integer floatMax = (scoreTotal - enrollLine) / 2; // 向上取分数线与总分间的一半
+            Integer aboveSteps = (100 - enrollLineRate + enrollRateStep - 1) / enrollRateStep;
+            Integer aboveSize = floatMax / aboveSteps;
+            rateLevels.add(new RateLevel(enrollLineRate, enrollLine, enrollLine, true, true)); // 半开半闭区间
+            for (int step = 0; step < aboveSteps; step++) {
+                Integer min = enrollLine + step * aboveSize;
+                Integer max = min + aboveSize;
+                Integer rate = enrollLineRate + (step + 1) * enrollRateStep;
+                if(rate >= 99) {
+                    rateLevels.add(new RateLevel(99, min, 99999, false, false)); // 上限99%不封顶
+                    break;
+                } else {
+                    rateLevels.add(new RateLevel(rate, min, max, false, true)); // 半开半闭区间
+                }
+            }
+            // 65%以下的计算 66
+            Integer belowSteps = (enrollLineRate + enrollRateStep - 1) / enrollRateStep;
+            Integer belowSize = enrollLine / belowSteps;
+            RateLevel range;
+            for (int step = 0; step < belowSteps; step++) {
+                Integer max = enrollLine - step * belowSize;
+                Integer rate = enrollLineRate - (step + 1) * enrollRateStep;
+                if(rate <= 0) {
+                    rateLevels.add(new RateLevel(0, 0, max, true, false));
+                    break;
+                } else {
+                    rateLevels.add(new RateLevel(rate, max - belowSize, max, true, false));
+                }
+            }
+        }
+        return rateLevels;
+    }
+
+    @Data
+    public static class RateLevel {
+        int rate;
+        int min;
+        int max;
+        boolean incMin;
+        boolean incMax;
+        VoluntaryDto.EnumPickType type; // 冲/稳/宝
+        String typeLabel; //
+
+        public RateLevel(int rate, Integer min, Integer max, boolean incMin, boolean incMax) {
+            this.rate = rate;
+            this.min = min;
+            this.max = max;
+            this.incMin = incMin;
+            this.incMax = incMax;
+            // [0%, 70%], 稳:(70%, 85%), 保:[85%, 99%]
+            if (rate <= 70) {
+                type = VoluntaryDto.EnumPickType.Danger;
+                typeLabel = "风险极高";
+            } else if (rate < 85) {
+                type = VoluntaryDto.EnumPickType.Normal;
+                typeLabel = "风险中等";
+            } else {
+                type = VoluntaryDto.EnumPickType.Safety;
+                typeLabel = "风险较低";
+            }
+        }
+
+        public RateLevel(int rate, VoluntaryDto.EnumPickType type) {
+            this.rate = rate;
+            this.type = type;
+            if(type.equals(VoluntaryDto.EnumPickType.Danger)) {
+                typeLabel = "风险极高";
+            } else if(type.equals(VoluntaryDto.EnumPickType.Normal)) {
+                typeLabel = "风险中等";
+            } else if(type.equals(VoluntaryDto.EnumPickType.Safety)) {
+                typeLabel = "风险较低";
+            } else {
+                typeLabel = "";
+            }
+        }
+    }
+}

+ 110 - 0
ie-admin/src/main/java/com/ruoyi/web/service/ExamUtilService.java

@@ -0,0 +1,110 @@
+package com.ruoyi.web.service;
+
+import com.google.common.collect.Lists;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mxjb.domain.MxjbContants;
+import com.ruoyi.sy.domain.SyTestAnswers;
+import com.ruoyi.sy.domain.SyTestQuestion;
+import com.ruoyi.web.domain.PaperDto;
+import com.ruoyi.web.domain.QuestionDto;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ExamUtilService {
+    private final RedisCache redisCache;
+    private final CacheService cacheService;
+
+    public ExamUtilService(RedisCache redisCache, CacheService cacheService) {
+        this.redisCache = redisCache;
+        this.cacheService = cacheService;
+    }
+
+    public String getExamineeAnswersKey(String type, Long examineeId) {
+        return type + "EA::" + examineeId;
+    }
+
+    public void updateTempAnswerAndSave(PaperDto paperDto, Long examineeId, String examineeType) {
+        String type = MxjbContants.getExamineeTypeKey(examineeType);
+        String key = getExamineeAnswersKey(type, examineeId);
+        Map<String, QuestionDto> questionDtoMap = redisCache.getCacheMap(key);
+        if (MapUtils.isNotEmpty(questionDtoMap)) {
+            Long lastQuestionId = questionDtoMap.get("0").getQuestionId();
+            paperDto.getQuestions().forEach(t -> {
+                QuestionDto dto = questionDtoMap.get(t.getQuestionId().toString());
+                if (null != dto) {
+                    t.setAnswer(dto.getAnswer());
+                    t.setAttachments(dto.getAttachments());
+                    t.setDuration(dto.getDuration());
+                    t.setCurrent(t.getQuestionId().equals(lastQuestionId));
+                }
+            });
+        } else if (CollectionUtils.isNotEmpty(paperDto.getQuestions())) {
+            paperDto.getQuestions().get(0).setCurrent(true);
+        }
+    }
+
+    public QuestionDto toQuestionDto(SyTestQuestion t, boolean fillParseAndAnswer, Map<Long, SyTestAnswers> questionAnswersMap) {
+        QuestionDto dto = new QuestionDto();
+        dto.setQuestionId(t.getQuestionId());
+        dto.setSeq(t.getSeq());
+        dto.setTypeId(cacheService.getQuestionType(t.getQtpye()));
+        dto.setType(t.getQtpye());
+        dto.setTitle(t.getTitle());
+        dto.setScoreTotal(t.getScore());
+        dto.setOptions(getOptions(t.getOptionA(), t.getOptionB(), t.getOptionC(), t.getOptionD(), t.getOptionE()));
+        if (fillParseAndAnswer) {
+            dto.setParse(t.getParse());
+        }
+        if (null != questionAnswersMap) {
+            SyTestAnswers answers = questionAnswersMap.get(t.getQuestionId());
+            if (null != answers) {
+                dto.setDuration(answers.getDuration());
+                setAnswerAttachemnts(dto, answers.getAnswer());
+            } else {
+                dto.setAnswer(StringUtils.EMPTY);
+                dto.setAttachments(new ArrayList<>());
+            }
+        } else {
+            dto.setAnswer(StringUtils.EMPTY);
+            dto.setAttachments(new ArrayList<>());
+        }
+
+        dto.setSource(t.getQuesitonCateogry());
+        return dto;
+    }
+
+    public List<String> getOptions(String... options) {
+        List<String> optionList = Lists.newArrayList();
+        for (String option : options) {
+            if (StringUtils.isNotBlank(option)) {
+                optionList.add(option);
+            }
+        }
+        return optionList;
+    }
+
+    public void setAnswerAttachemnts(QuestionDto dto, String answer) {
+        if (StringUtils.isNotBlank(answer)) {
+            String[] answerArray = StringUtils.splitByWholeSeparatorPreserveAllTokens(answer, "$@");
+            if (answerArray.length > 0) {
+                dto.setAnswer(answerArray[0]);
+                if (answerArray.length > 1) {
+                    List<String> attacheList = Lists.newArrayList();
+                    for (int i = 1; i < answerArray.length; i++) {
+                        if (StringUtils.isNotBlank(answerArray[i]) && !"null".equals(answerArray[i])) {
+                            attacheList.add(answerArray[i]);
+                        }
+                    }
+                    dto.setAttachments(attacheList);
+                }
+            }
+        }
+    }
+}

+ 1053 - 0
ie-admin/src/main/java/com/ruoyi/web/service/SyTestMajorService.java

@@ -0,0 +1,1053 @@
+package com.ruoyi.web.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.mxjb.domain.MxjbContants;
+import com.ruoyi.sy.domain.*;
+import com.ruoyi.sy.mapper.*;
+import com.ruoyi.web.domain.CategoryDto;
+import com.ruoyi.web.domain.PaperDto;
+import com.ruoyi.web.domain.QuestionDto;
+import com.ruoyi.web.domain.TestMarjorDto;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Service
+public class SyTestMajorService {
+    private final SyKnowledgeInterstMajorMapper syKnowledgeInterstMajorMapper;
+    private final SyKnowledgeInterstTypeMapper syKnowledgeInterstTypeMapper;
+    private final SyMajorCategoryMapper syMajorCategoryMapper;
+    private final SyMajorMajorMapper syMajorMajorMapper;
+    private final SyMajorSubjectMapper syMajorSubjectMapper;
+    private final SyOccupationInterstMajorMapper syOccupationInterstMajorMapper;
+    private final SyOccupationInterstTypeMapper syOccupationInterstTypeMapper;
+    private final SyPhilosophyViewMajorMapper syPhilosophyViewMajorMapper;
+    private final SyPhilosophyViewTypeMapper syPhilosophyViewTypeMapper;
+    private final SyTestAnswersMapper syTestAnswersMapper;
+    private final SyTestExamineeMapper syTestExamineeMapper;
+    private final SyTestPaperMapper syTestPaperMapper;
+    private final SyTestQuestionMapper syTestQuestionMapper;
+    private final SyTestSelectCategoryMapper syTestSelectCategoryMapper;
+    private final SyTestSelectMajorMapper syTestSelectMajorMapper;
+    private final ExamUtilService examUtilService;
+    private final RedisCache redisCache;
+    private Set<String> marjorCodeSet;
+
+    private ObjectMapper om = new ObjectMapper();
+    // key1 category, key2 value(RIA...)
+    // key1 category, key2 value(喜欢, 擅长)
+    private TypeReference<Map<String, Map<String, Integer>>> keyCatValueTypeReference = new TypeReference<Map<String, Map<String, Integer>>>() {};
+    private TypeReference<Map<String, Integer>> keyValueTypeReference = new TypeReference<Map<String, Integer>>() {};
+
+    public SyTestMajorService(SyKnowledgeInterstMajorMapper syKnowledgeInterstMajorMapper, SyKnowledgeInterstTypeMapper syKnowledgeInterstTypeMapper, SyMajorCategoryMapper syMajorCategoryMapper,
+                              SyMajorMajorMapper syMajorMajorMapper, SyMajorSubjectMapper syMajorSubjectMapper, SyOccupationInterstMajorMapper syOccupationInterstMajorMapper,
+                              SyOccupationInterstTypeMapper syOccupationInterstTypeMapper, SyPhilosophyViewMajorMapper syPhilosophyViewMajorMapper, SyPhilosophyViewTypeMapper syPhilosophyViewTypeMapper,
+                              SyTestAnswersMapper syTestAnswersMapper, SyTestExamineeMapper syTestExamineeMapper, SyTestPaperMapper syTestPaperMapper, SyTestQuestionMapper syTestQuestionMapper, SyTestSelectCategoryMapper syTestSelectCategoryMapper,
+                              SyTestSelectMajorMapper syTestSelectMajorMapper, ExamUtilService examUtilService, RedisCache redisCache) {
+        this.syKnowledgeInterstMajorMapper = syKnowledgeInterstMajorMapper;
+        this.syKnowledgeInterstTypeMapper = syKnowledgeInterstTypeMapper;
+        this.syMajorCategoryMapper = syMajorCategoryMapper;
+        this.syMajorMajorMapper = syMajorMajorMapper;
+        this.syMajorSubjectMapper = syMajorSubjectMapper;
+        this.syOccupationInterstMajorMapper = syOccupationInterstMajorMapper;
+        this.syOccupationInterstTypeMapper = syOccupationInterstTypeMapper;
+        this.syPhilosophyViewMajorMapper = syPhilosophyViewMajorMapper;
+        this.syPhilosophyViewTypeMapper = syPhilosophyViewTypeMapper;
+        this.syTestAnswersMapper = syTestAnswersMapper;
+        this.syTestExamineeMapper = syTestExamineeMapper;
+        this.syTestPaperMapper = syTestPaperMapper;
+        this.syTestQuestionMapper = syTestQuestionMapper;
+        this.syTestSelectCategoryMapper = syTestSelectCategoryMapper;
+        this.syTestSelectMajorMapper = syTestSelectMajorMapper;
+        this.examUtilService = examUtilService;
+        this.redisCache = redisCache;
+    }
+
+    public Set<String> loadMarjorCodes() {
+        if(null == marjorCodeSet) {
+            marjorCodeSet = syMajorMajorMapper.selectSyMajorMajorList(new SyMajorMajor()).stream().map( t -> t.getMajorCode()).collect(Collectors.toSet());
+        }
+        return marjorCodeSet;
+    }
+    public List<SyTestExaminee> getTestListWithStatus(Integer testType, Integer[] states) {
+        Map cond = Maps.newHashMap();
+        cond.put("paperId", testType.longValue());
+        cond.put("customerCode", SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        cond.put("states", states);
+        return syTestExamineeMapper.selectSyTestExamineeLatest(cond);
+    }
+
+    public List<SyTestExaminee> getTestList(Integer testType) {
+        SyTestExaminee cond = new SyTestExaminee();
+        cond.setPaperId(testType.longValue());
+        cond.setCustomerCode(SecurityUtils.getLoginUser().getUser().getUserId().toString());
+        return syTestExamineeMapper.selectSyTestExamineeList(cond);
+    }
+
+    public SyTestExaminee getTestExaminee(Integer testType, Long examineeId) {
+        SyTestExaminee examinee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+        return examinee;
+    }
+
+    public List<TestMarjorDto.TestMatchStat> toTestMatchStats(SyTestExaminee examinee) {
+        if (StringUtils.isNotBlank(examinee.getStats())) {
+            TestMarjorDto.TestStats testStat = null;
+            try {
+                testStat = om.readValue(examinee.getStats(), TestMarjorDto.TestStats.class);
+                return testStat.getMatchStats();
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+            }
+        }
+        return Lists.newArrayList();
+    }
+
+    public AjaxResult saveUserSelectedCategory(CategoryDto.UserCategory params) {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        SyTestExaminee examinee = null != params.getExamineeId() ? syTestExamineeMapper.selectSyTestExamineeById(params.getExamineeId()) : getLatestExaminee(params.getTestType(), sysUser.getCode());
+
+        SyTestSelectCategory categoryCond = new SyTestSelectCategory();
+        categoryCond.setExamineeId(examinee.getExamineeId());
+        List<SyTestSelectCategory> categoryList = syTestSelectCategoryMapper.selectSyTestSelectCategoryList(categoryCond);
+        Map<String, SyTestSelectCategory> oldCategoryMap = categoryList.stream().collect(Collectors.toMap(SyTestSelectCategory::getMajorCategoryCode, Function.identity()));
+
+        SyTestSelectCategory mxjbCategoryCond = new SyTestSelectCategory();
+        for(String code : Sets.newHashSet(params.getCodes())) {
+            SyTestSelectCategory oldCategory = oldCategoryMap.remove(code);
+            if (null != oldCategory) {
+                mxjbCategoryCond.setId(oldCategory.getId());
+                mxjbCategoryCond.setState(1L);
+                syTestSelectCategoryMapper.updateSyTestSelectCategory(mxjbCategoryCond);
+            } else {
+                mxjbCategoryCond.setId(null);
+                mxjbCategoryCond.setState(1L);
+                mxjbCategoryCond.setExamineeId(examinee.getExamineeId());
+                mxjbCategoryCond.setCustomerCode(examinee.getCustomerCode());
+                mxjbCategoryCond.setMajorCategoryCode(code);
+                syTestSelectCategoryMapper.insertSyTestSelectCategory(mxjbCategoryCond);
+            }
+        }
+        if (oldCategoryMap.size() > 0) {
+            List<Long> oldIds = oldCategoryMap.values().stream().map(SyTestSelectCategory::getId).collect(Collectors.toList());
+            syTestSelectCategoryMapper.deleteSyTestSelectCategoryByIds(oldIds.toArray(new Long[oldIds.size()]));
+        }
+        return AjaxResult.success();
+    }
+
+    public AjaxResult getUserSelectedMajor() {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        Map categoryCond = Maps.newHashMap();
+        categoryCond.put("customerCode", sysUser.getCode());
+        List<SyMajorCategory> categoryList = syMajorCategoryMapper.selectUserSyMajorMajor(categoryCond);
+        return AjaxResult.success(categoryList);
+    }
+
+
+    public AjaxResult saveUserSelectedMajor(CategoryDto.UserMajor param) {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        SyTestSelectMajor categoryCond = new SyTestSelectMajor();
+        categoryCond.setCustomerCode(sysUser.getCode());
+        Map<String, SyTestSelectMajor> oldCategoryMap = Maps.newHashMap();
+        Set<String> existCodeSet = Sets.newHashSet();
+        List<Long> deleteIdList = Lists.newArrayList();
+        for(SyTestSelectMajor major : syTestSelectMajorMapper.selectSyTestSelectMajorList(categoryCond)) {
+            if (existCodeSet.add(major.getMajorCode())) {
+                oldCategoryMap.put(major.getMajorCode(), major);
+            } else {
+                deleteIdList.add(major.getId());
+            }
+        }
+        if (deleteIdList.size() > 0) {
+            syTestSelectMajorMapper.deleteSyTestSelectMajorByIds(deleteIdList.toArray(new Long[deleteIdList.size()]));
+        }
+        SyTestSelectMajor mxjbCategoryCond = new SyTestSelectMajor();
+        for(String code : Sets.newHashSet(param.getCodes())) {
+            SyTestSelectMajor oldCategory = oldCategoryMap.remove(code);
+            if (null != oldCategory) {
+                mxjbCategoryCond.setId(oldCategory.getId());
+                mxjbCategoryCond.setState(1L);
+                syTestSelectMajorMapper.updateSyTestSelectMajor(mxjbCategoryCond);
+            } else {
+                mxjbCategoryCond.setId(null);
+                mxjbCategoryCond.setState(1L);
+                mxjbCategoryCond.setCustomerCode(sysUser.getCode());
+                mxjbCategoryCond.setUserId(sysUser.getUserId());
+                mxjbCategoryCond.setMajorCode(code);
+                syTestSelectMajorMapper.insertSyTestSelectMajor(mxjbCategoryCond);
+            }
+        }
+        if (oldCategoryMap.size() > 0) {
+            List<Long> oldIds = oldCategoryMap.values().stream().map(SyTestSelectMajor::getId).collect(Collectors.toList());
+            syTestSelectMajorMapper.deleteSyTestSelectMajorByIds(oldIds.toArray(new Long[oldIds.size()]));
+        }
+        return AjaxResult.success();
+    }
+
+    public AjaxResult loadAllMajorSubjectTree() {
+        Map cond = new HashMap();
+        List<SyMajorCategory> majorCategoryList = syMajorCategoryMapper.selectAllMajorList(cond);;
+        return AjaxResult.success(buildTree(majorCategoryList));
+    }
+
+    public AjaxResult loadSelectMajorSubjectTree() {
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        SyTestExaminee examineeCond = new SyTestExaminee();
+        examineeCond.setCustomerCode(sysUser.getCode());
+        examineeCond.setState(MxjbContants.ExamineeStatusPublish);
+        List<SyTestExaminee> existList = syTestExamineeMapper.selectSyTestExamineeList(examineeCond);
+        List<Long> examineeIds = existList.stream().map(t -> t.getExamineeId()).collect(Collectors.toList());
+
+        if (CollectionUtils.isEmpty(examineeIds)) {
+            return AjaxResult.success(examineeIds);
+        }
+
+        Map cond = new HashMap();
+        cond.put("examineeIds", examineeIds);
+        List<SyMajorCategory> majorCategoryList = syMajorCategoryMapper.selectSelectedMajorList(cond);;
+        return AjaxResult.success(buildTree(majorCategoryList));
+    }
+
+    private List<TestMarjorDto.CategoryNode> buildTree(List<SyMajorCategory> majorCategoryList) {
+        List<TestMarjorDto.CategoryNode> categoryNodeList = Lists.newArrayList();
+        if (CollectionUtils.isEmpty(majorCategoryList)) {
+            return categoryNodeList;
+        }
+        Map<String, TestMarjorDto.CategoryNode> categoryNodeMap = Maps.newHashMap();
+        for(SyMajorCategory mc : majorCategoryList) {
+            TestMarjorDto.CategoryNode n = categoryNodeMap.get(mc.getMajorSubjectCode());
+            if (null == n) {
+                n = new TestMarjorDto.CategoryNode(mc.getMajorSubjectCode(), mc.getMajorSubjectName(), Lists.newArrayList());
+                categoryNodeMap.put(mc.getMajorSubjectCode(), n);
+                categoryNodeList.add(n);
+            }
+            String snKey = mc.getMajorSubjectCode() + "_" + mc.getMajorCategoryCode();
+            TestMarjorDto.CategoryNode sn = categoryNodeMap.get(snKey);
+            if (null == sn) {
+                sn = new TestMarjorDto.CategoryNode(mc.getMajorCategoryCode(), mc.getMajorCategoryName(), Lists.newArrayList());
+                categoryNodeMap.put(snKey, sn);
+                n.getNodes().add(sn);
+            }
+            TestMarjorDto.CategoryNode ssn = new TestMarjorDto.CategoryNode(mc.getMajorCode(), mc.getMajorName(), null);
+            sn.getNodes().add(ssn);
+        }
+        return categoryNodeList.size() != 1 ? categoryNodeList : categoryNodeList.get(0).getNodes();
+    }
+
+    private List<SyMajorCategory> findMatchCategoryList(Integer testType, String matchCode) {
+        if (StringUtils.isBlank(matchCode)) {
+            return Lists.newArrayList();
+        }
+        Map cond = new HashMap();
+        List<SyMajorCategory> majorCategoryList = null;
+        switch (testType) {
+            case 1:
+                cond.put("matchCodes", StringUtils.split(matchCode, ","));
+                majorCategoryList = syMajorCategoryMapper.selectSyMajorCategoryListByView(cond);
+                break;
+            case 2:
+                cond.put("matchCodes", Lists.newArrayList(matchCode));
+                majorCategoryList = syMajorCategoryMapper.selectSyMajorCategoryListByOccupation(cond);
+                break;
+            case 3:
+                cond.put("matchCodes", StringUtils.split(matchCode, ","));
+                majorCategoryList = syMajorCategoryMapper.selectSyMajorCategoryListByKnowledge(cond);
+                break;
+            default:
+                majorCategoryList = Lists.newArrayList();
+        }
+        return majorCategoryList;
+    }
+    public AjaxResult loadMatchCategoryTree(Integer testType, String matchCode, Integer level) {
+        List<SyMajorCategory> majorCategoryList = findMatchCategoryList(testType, matchCode);
+        Map<String, TestMarjorDto.CategoryNode> categoryNodeMap = Maps.newHashMap();
+        List<TestMarjorDto.CategoryNode> categoryNodeList = Lists.newArrayList();
+        for(SyMajorCategory mc : majorCategoryList) {
+            TestMarjorDto.CategoryNode n = categoryNodeMap.get(mc.getMajorSubjectCode());
+            if (null == n) {
+                n = new TestMarjorDto.CategoryNode(mc.getMajorSubjectCode(), mc.getMajorSubjectName(), Lists.newArrayList());
+                categoryNodeMap.put(mc.getMajorSubjectCode(), n);
+                categoryNodeList.add(n);
+            }
+            TestMarjorDto.CategoryNode sn = new TestMarjorDto.CategoryNode(mc.getMajorCategoryCode(), mc.getMajorCategoryName(), null);
+            n.getNodes().add(sn);
+        }
+        return AjaxResult.success(categoryNodeList);
+    }
+
+
+    public AjaxResult getAllTestMajorResult() throws JsonProcessingException {
+        Map allResult = new HashMap();
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        Map categoryCond = Maps.newHashMap();
+        categoryCond.put("customerCode", sysUser.getCode());
+        List<SyMajorCategory> categoryList = syMajorCategoryMapper.selectUserSyMajorMajor(categoryCond);
+        allResult.put("selectedList", categoryList);
+        allResult.put("philosophy", getTestResult(getLatestExaminee(1, sysUser.getCode())));
+        allResult.put("occupation", getTestResult(getLatestExaminee(2, sysUser.getCode())));
+        allResult.put("knowledge", getTestResult(getLatestExaminee(3, sysUser.getCode())));
+        return AjaxResult.success(allResult);
+    }
+
+    public AjaxResult loadTestMajorResult(Integer testType, String customerCode) throws JsonProcessingException {
+        SyTestExaminee examinee = getLatestExaminee(testType, customerCode);
+        return loadTestMajorResult(examinee);
+    }
+    public AjaxResult loadTestMajorResult(Long examineeId) throws JsonProcessingException {
+        SyTestExaminee examinee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+        return loadTestMajorResult(examinee);
+    }
+    public AjaxResult loadTestMajorResult(SyTestExaminee examinee) throws JsonProcessingException {
+        TestMarjorDto.TestResultDto dto;
+        return null == (dto = getTestResult(examinee)) ? AjaxResult.success() : AjaxResult.success(dto);
+    }
+
+    private TestMarjorDto.TestResultDto getTestResult(SyTestExaminee examinee) throws JsonProcessingException {
+        if (null == examinee) {
+            return null;
+        }
+        boolean testCompleted = examinee.getState() >= MxjbContants.ExamineeStatusPublish;
+
+        SyTestPaper paper = syTestPaperMapper.selectSyTestPaperById(examinee.getPaperId());
+        TestMarjorDto.TestStats testStat = null;
+        Map<String, TestMarjorDto.TestMatchStat> categoryCountMap = Maps.newHashMap();
+        Map<String, Map<String, Integer>> catKeyValueMap = null;
+        String matchCode = null;
+        boolean noMatch = false;
+        if (StringUtils.isNotBlank(examinee.getStats())) {
+            testStat = om.readValue(examinee.getStats(), TestMarjorDto.TestStats.class);
+            if (CollectionUtils.isNotEmpty(testStat.getMatchStats())) {
+                for (TestMarjorDto.TestMatchStat ts : testStat.getMatchStats()) {
+                    categoryCountMap.put(ts.getCode(), ts);
+                }
+            }
+            catKeyValueMap = testStat.getCatKeyValueMap();
+            matchCode = testStat.getMatchCode();
+            noMatch = testStat.isNoMatch();
+        }
+        boolean selectCompleted = noMatch || isSelectCompleted(examinee.getExamineeId());
+
+        Map categoryCond = Maps.newHashMap();
+        categoryCond.put("examineeId", examinee.getExamineeId());
+        List<SyMajorCategory> selectedList = syMajorCategoryMapper.selectUserSyMajorCategory(categoryCond);
+
+        switch (paper.getCategory()) {
+            case 1:
+                Map<String, SyPhilosophyViewType> viewTypeMap = Maps.newHashMap();
+                List<SyPhilosophyViewType> viewTypeList = syPhilosophyViewTypeMapper.selectSyPhilosophyViewTypeList(new SyPhilosophyViewType());
+
+                for (SyPhilosophyViewType type : viewTypeList) {
+                    type.setViewMajorList(Lists.newArrayList());
+                    viewTypeMap.put(type.getCode(), type);
+                    TestMarjorDto.TestMatchStat ts = categoryCountMap.get(type.getCode());
+                    if (null != ts) {
+                        Integer total = ts.getCount();
+                        type.setTotal(null != total ? total : 0);
+                        type.setMatched(ts.isMatched());
+                    } else {
+                        type.setTotal(0);
+                        type.setMatched(false);
+                    }
+                    if (null != catKeyValueMap) {
+                        type.setStats(catKeyValueMap.get(type.getCode()));
+                    }
+                }
+                for (SyPhilosophyViewMajor viewMajor : syPhilosophyViewMajorMapper.selectSyPhilosophyViewMajorList(new SyPhilosophyViewMajor())) {
+                    viewTypeMap.get(viewMajor.getMatchCode()).getViewMajorList().add(viewMajor);
+                }
+                Collections.sort(viewTypeList, new Comparator<SyPhilosophyViewType>() {
+                    @Override
+                    public int compare(SyPhilosophyViewType o1, SyPhilosophyViewType o2) {
+                        int iRet = -o1.getTotal().compareTo(o2.getTotal());
+                        if (0 == iRet) {
+                            iRet = o1.getCode().compareTo(o2.getCode());
+                        }
+                        return iRet;
+                    }
+                });
+                return new TestMarjorDto.TestResultDto(examinee.getExamineeId(), selectedList, viewTypeList, matchCode, testCompleted, selectCompleted, noMatch);
+            case 2:
+                List<SyOccupationInterstType> occupationTypeList = syOccupationInterstTypeMapper.selectSyOccupationInterstTypeList(new SyOccupationInterstType());
+                for (SyOccupationInterstType type : occupationTypeList) {
+                    TestMarjorDto.TestMatchStat ts = categoryCountMap.get(type.getCode());
+                    if (null != ts) {
+                        Integer total = ts.getCount();
+                        type.setTotal(null != total ? total : 0);
+                        type.setMatched(ts.isMatched());
+                    } else {
+                        type.setTotal(0);
+                        type.setMatched(false);
+                    }
+                    if (null != catKeyValueMap) {
+                        type.setStats(catKeyValueMap.get(type.getCode()));
+                    }
+                }
+                Collections.sort(occupationTypeList, new Comparator<SyOccupationInterstType>() {
+                    @Override
+                    public int compare(SyOccupationInterstType o1, SyOccupationInterstType o2) {
+                        int iRet = -o1.getTotal().compareTo(o2.getTotal());
+                        if (0 == iRet) {
+                            iRet = o1.getCode().compareTo(o2.getCode());
+                        }
+                        return iRet;
+                    }
+                });
+                return new TestMarjorDto.TestResultDto(examinee.getExamineeId(), selectedList, occupationTypeList, matchCode, testCompleted, selectCompleted, noMatch);
+            case 3:
+                List<SyKnowledgeInterstType> knowledgeTypeList = syKnowledgeInterstTypeMapper.selectSyKnowledgeInterstTypeList(new SyKnowledgeInterstType());
+                for (SyKnowledgeInterstType type : knowledgeTypeList) {
+                    TestMarjorDto.TestMatchStat ts = categoryCountMap.get(type.getId().toString());
+                    if (null != ts) {
+                        Integer total = ts.getCount();
+                        type.setTotal(null != total ? total : 0);
+                        type.setMatched(ts.isMatched());
+                    } else {
+                        type.setTotal(0);
+                        type.setMatched(false);
+                    }
+                }
+                Collections.sort(knowledgeTypeList, new Comparator<SyKnowledgeInterstType>() {
+                    @Override
+                    public int compare(SyKnowledgeInterstType o1, SyKnowledgeInterstType o2) {
+                        int iRet = -o1.getTotal().compareTo(o2.getTotal());
+                        if (0 == iRet) {
+                            iRet = o1.getCode().compareTo(o2.getCode());
+                        }
+                        return iRet;
+                    }
+                });
+                return new TestMarjorDto.TestResultDto(examinee.getExamineeId(), selectedList, knowledgeTypeList, matchCode, testCompleted, selectCompleted, noMatch);
+        }
+        return null;
+    }
+
+    private TestMarjorDto.TestStats getStats(Integer testType, Long paperId, List<QuestionDto> questionlist) {
+        SyTestQuestion cond = new SyTestQuestion();
+        cond.setPaperId(paperId);
+        Map<Long, SyTestQuestion> stdQuestionMap = syTestQuestionMapper.selectSyTestQuestionList(cond).stream().collect(Collectors.toMap(SyTestQuestion::getQuestionId, Function.identity()));
+
+        TestMarjorDto.TestStats testStats = new TestMarjorDto.TestStats();
+        // key1=category, key2=value, value=score
+        Map<String, Map<String, Integer>> catKeyValueMap = null;
+        List<TestMarjorDto.TestMatchStat> matchStats = null;
+        String matchedCode = null;
+        boolean noMatch = false;
+        switch (testType) {
+            case 1:
+                // k1=value(A/B/D/F), k2=questionCategory, value=按answer求和, 复选总分(限制总分不重复, 3), 复选评价(限制总分不重复, 6)
+                catKeyValueMap = Maps.newHashMap();
+                matchStats = Lists.newArrayList();
+                for (QuestionDto ques : questionlist) {
+                    String normalAnswer = MxjbContants.getNormalAnswer(ques.getAnswer(), "");
+                    if (StringUtils.isBlank(normalAnswer)) {
+                        continue;
+                    }
+                    SyTestQuestion stdQuestion = stdQuestionMap.get(ques.getQuestionId());
+                    if(StringUtils.isNotBlank(normalAnswer)) {
+                        String[] answerScores = normalAnswer.split(",");
+                        for (int i = 0; i < answerScores.length; i++) {
+                            Integer cnt = Integer.parseInt(answerScores[i]);
+                            String newValue = getAnswerValue(stdQuestion, i);
+                            if (StringUtils.isNotBlank(newValue)) {
+                                Map<String, Integer> valueMap = catKeyValueMap.get(newValue);
+                                if (null == valueMap) {
+                                    valueMap = Maps.newHashMap();
+                                    catKeyValueMap.put(newValue, valueMap);
+                                }
+                                Integer oldCount = valueMap.get(stdQuestion.getQuesitonCateogry());
+                                valueMap.put(stdQuestion.getQuesitonCateogry(), null != oldCount ? oldCount + cnt : cnt);
+                            }
+                        }
+                    }
+                }
+                for (String category : catKeyValueMap.keySet()) {
+                    TestMarjorDto.TestMatchStat testMatchStat = new TestMarjorDto.TestMatchStat();
+                    testMatchStat.setCode(category);
+                    Integer sum = 0;
+                    for (Integer v : catKeyValueMap.get(category).values()) {
+                        sum += v;
+                    }
+                    testMatchStat.setCount(sum);
+                    testMatchStat.setMatched(false);
+                    matchStats.add(testMatchStat);
+                }
+                Collections.sort(matchStats, new Comparator<TestMarjorDto.TestMatchStat>() {
+                    @Override
+                    public int compare(TestMarjorDto.TestMatchStat o1, TestMarjorDto.TestMatchStat o2) {
+                        return -o1.getCount().compareTo(o2.getCount());
+                    }
+                });
+                matchedCode = getMatchedCodeByTop(matchStats, 3);
+                break;
+            case 2:
+                // k1=questionCategory, k2=value(Like/Familiar), value=对应个数
+                catKeyValueMap = Maps.newHashMap();
+                matchStats = Lists.newArrayList();
+                String newValue;
+                for (QuestionDto ques : questionlist) {
+                    SyTestQuestion stdQuestion = stdQuestionMap.get(ques.getQuestionId());
+                    String normalAnswer = MxjbContants.getNormalAnswer(ques.getAnswer(), "");
+                    if (StringUtils.isNotBlank(normalAnswer) && StringUtils.isNotBlank((newValue = getAnswerValue(stdQuestion, normalAnswer.charAt(0) - 'A')))) {;
+                        Map<String, Integer> valueMap = catKeyValueMap.get(stdQuestion.getQuesitonCateogry());
+                        if (null == valueMap) {
+                            valueMap = Maps.newHashMap();
+                            catKeyValueMap.put(stdQuestion.getQuesitonCateogry(), valueMap);
+                        }
+                        String valueName = "Like".equals(newValue) ? "喜欢" : "擅长"; // 最多7个
+                        Integer oldCount = valueMap.get(valueName);
+                        valueMap.put(valueName, null != oldCount ? Math.min(oldCount + 1, 7) : 1);
+                    }
+                }
+                for (String category : catKeyValueMap.keySet()) {
+                    TestMarjorDto.TestMatchStat testMatchStat = new TestMarjorDto.TestMatchStat();
+                    testMatchStat.setCode(category);
+                    Integer sum = 0;
+                    for (Integer v : catKeyValueMap.get(category).values()) {
+                        sum += v;
+                    }
+                    testMatchStat.setCount(sum);
+                    testMatchStat.setMatched(false);
+                    matchStats.add(testMatchStat);
+                }
+                Collections.sort(matchStats, new Comparator<TestMarjorDto.TestMatchStat>() {
+                    @Override
+                    public int compare(TestMarjorDto.TestMatchStat o1, TestMarjorDto.TestMatchStat o2) {
+                        return -o1.getCount().compareTo(o2.getCount());
+                    }
+                });
+                List<String> matchCodeList = Lists.newArrayList();
+                noMatch = getMatchedCode(matchStats, matchCodeList);
+                matchedCode = StringUtils.join(matchCodeList, ",");
+                break;
+            case 3:
+                // id 对应 questionCategory, value 对应分值
+                Map<String, Integer> keyValueMap = Maps.newHashMap();
+                for (QuestionDto ques : questionlist) {
+                    SyTestQuestion stdQuestion = stdQuestionMap.get(ques.getQuestionId());
+                    Integer oldValue = keyValueMap.get(stdQuestion.getQuesitonCateogry());
+                    String normalAnswer = MxjbContants.getNormalAnswer(ques.getAnswer(), "");
+                    if (StringUtils.isNotBlank(normalAnswer)) {
+                        keyValueMap.put(stdQuestion.getQuesitonCateogry(), Integer.parseInt(getAnswerValue(stdQuestion, normalAnswer.charAt(0) - 'A')) + (null != oldValue ? oldValue : 0));
+                    }
+                }
+                matchStats = Lists.newArrayList();
+                for (String category : keyValueMap.keySet()) {
+                    TestMarjorDto.TestMatchStat testMatchStat = new TestMarjorDto.TestMatchStat();
+                    testMatchStat.setCode(category);
+                    testMatchStat.setCount(keyValueMap.get(category));
+                    testMatchStat.setMatched(false);
+                    matchStats.add(testMatchStat);
+                }
+                Collections.sort(matchStats, new Comparator<TestMarjorDto.TestMatchStat>() {
+                    @Override
+                    public int compare(TestMarjorDto.TestMatchStat o1, TestMarjorDto.TestMatchStat o2) {
+                        return -o1.getCount().compareTo(o2.getCount());
+                    }
+                });
+                matchedCode = getMatchedCodeByTop(matchStats, 3);
+                break;
+        }
+        testStats.setCatKeyValueMap(catKeyValueMap);
+        testStats.setMatchStats(matchStats);
+        testStats.setMatchCode(matchedCode);
+        testStats.setNoMatch(StringUtils.isNotBlank(matchedCode) && (noMatch || CollectionUtils.isEmpty(findMatchCategoryList(testType, matchedCode))));
+        return testStats;
+    }
+
+    /**
+     * 根据用户选择查询对应的值
+     * @param stdQuestion
+     * @param optionIdx
+     * @return
+     */
+    private String getAnswerValue(SyTestQuestion stdQuestion, int optionIdx) {
+        switch(optionIdx) {
+            case 0:
+                return stdQuestion.getValueA();
+            case 1:
+                return stdQuestion.getValueB();
+            case 2:
+                return stdQuestion.getValueC();
+            case 3:
+                return stdQuestion.getValueD();
+            case 4:
+                return stdQuestion.getValueE();
+        }
+        return "";
+    }
+
+    public List<SyTestQuestion> getPaperQuestions(Long paperId) {
+        SyTestQuestion cond = new SyTestQuestion();
+        cond.setPaperId(paperId);
+        List<SyTestQuestion> questionsList = syTestQuestionMapper.selectSyTestQuestionList(cond);
+        return questionsList;
+    }
+
+    public AjaxResult saveTestPaper(PaperDto paperDto) {
+        Long examineeId = paperDto.getExamineeId();
+        SyTestExaminee examinee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+        if (examinee.getState() >= MxjbContants.ExamineeStatusReview) {
+            return AjaxResult.error(examineeId + " 已经在批阅的不可保存答案");
+        }
+        String key = examUtilService.getExamineeAnswersKey(MxjbContants.ExamineeTypeTestKey, paperDto.getExamineeId());
+        if (CollectionUtils.isEmpty(paperDto.getQuestions())) {
+            Map<String, QuestionDto> questionDtoMap = redisCache.getCacheMap(key);
+            questionDtoMap.remove("0");
+            paperDto.setQuestions(Lists.newArrayList(questionDtoMap.values()));
+        }
+
+        if (CollectionUtils.isEmpty(paperDto.getQuestions()) || getPaperQuestions(examinee.getPaperId()).size() != paperDto.getQuestions().size()) {
+            return AjaxResult.error(examineeId + "未答完题时不能保存");
+        }
+        SyTestPaper paper = syTestPaperMapper.selectSyTestPaperById(examinee.getPaperId());
+        updateTestAnswers(paperDto.getQuestions(), paperDto.getExamineeId());
+        SyTestExaminee examineeCond = new SyTestExaminee();
+        examineeCond.setExamineeId(paperDto.getExamineeId());
+        examineeCond.setState(MxjbContants.ExamineeStatusPublish);
+        examineeCond.setEndTime(new Date());
+
+        TestMarjorDto.TestStats testStats = examinee.getPaperId().equals(4L) ? getMentalHealth(paperDto.getQuestions()) : getStats(paper.getCategory(), paper.getPaperId(), paperDto.getQuestions());
+        try {
+            examineeCond.setStats(om.writeValueAsString(testStats));
+        } catch (JsonProcessingException e) {
+            return AjaxResult.error(examineeId + " 已经在批阅的不可保存答案");
+        }
+        syTestExamineeMapper.updateSyTestExaminee(examineeCond);
+        redisCache.deleteObject(key);
+        return AjaxResult.success("成功");
+    }
+
+    public AjaxResult updateExamineeStats(Long examineeId) {
+        SyTestExaminee examinee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+        SyTestPaper paper = syTestPaperMapper.selectSyTestPaperById(examinee.getPaperId());
+
+        SyTestAnswers answerCond = new SyTestAnswers();
+        answerCond.setExamineeId(examineeId);
+
+        List<QuestionDto> questionlist = Lists.newArrayList();
+        List<SyTestAnswers> answersList = syTestAnswersMapper.selectSyTestAnswersList(answerCond);
+        for(SyTestAnswers a : answersList) {
+            QuestionDto dto = new QuestionDto();
+            dto.setQuestionId(a.getQuestionId());
+            dto.setAnswer(a.getAnswer());
+            questionlist.add(dto);
+        }
+
+        SyTestExaminee examineeCond = new SyTestExaminee();
+        examineeCond.setExamineeId(examinee.getExamineeId());
+        TestMarjorDto.TestStats testStats = examinee.getPaperId().equals(4L) ? getMentalHealth(questionlist) : getStats(paper.getCategory(), paper.getPaperId(), questionlist);
+        try {
+            examineeCond.setStats(om.writeValueAsString(testStats));
+        } catch (JsonProcessingException e) {
+            return AjaxResult.error(examineeId + " 统计序列化异常");
+        }
+        syTestExamineeMapper.updateSyTestExaminee(examineeCond);
+        return AjaxResult.success();
+    }
+
+    private TestMarjorDto.TestStats getMentalHealth(List<QuestionDto> questionlist) {
+        SyTestQuestion cond = new SyTestQuestion();
+        cond.setPaperId(4L);
+        Map<Long, SyTestQuestion> stdQuestionMap = Maps.newHashMap();
+        Map<String, Integer> categoryValueMap = Maps.newLinkedHashMap();
+        for (SyTestQuestion q : syTestQuestionMapper.selectSyTestQuestionList(cond)) {
+            stdQuestionMap.put(q.getQuestionId(), q);
+            categoryValueMap.putIfAbsent(q.getQuesitonCateogry(), 0);
+        }
+        for (QuestionDto ques : questionlist) {
+            String normalAnswer = MxjbContants.getNormalAnswer(ques.getAnswer(), "");
+            if (StringUtils.isBlank(normalAnswer)) {
+                continue;
+            }
+            SyTestQuestion stdQuestion = stdQuestionMap.get(ques.getQuestionId());
+            if(StringUtils.isNotBlank(normalAnswer)) {
+                String value = normalAnswer.equals("A") ? stdQuestion.getValueA() : stdQuestion.getValueB();
+                Integer v = NumberUtils.toInt(value, 0);
+                String c = stdQuestion.getQuesitonCateogry();
+                Integer ov = categoryValueMap.get(c);
+                categoryValueMap.put(c, null != ov ? ov + v : v);
+            }
+        }
+        TestMarjorDto.TestStats testStats = new TestMarjorDto.TestStats();
+        List<TestMarjorDto.TestMatchStat> matchStats = Lists.newArrayList();
+        for (String name : categoryValueMap.keySet()) {
+            matchStats.add(new TestMarjorDto.TestMatchStat(name, categoryValueMap.get(name), false));
+        }
+        testStats.setMatchStats(matchStats);
+        return testStats;
+    }
+
+    private String getMatchedCodeByTop(List<TestMarjorDto.TestMatchStat> matchStats, Integer top) {
+        if (CollectionUtils.isEmpty(matchStats)) {
+            return "";
+        }
+        List<String> matchCodeList = Lists.newArrayList();
+        for (TestMarjorDto.TestMatchStat ts : matchStats) {
+            matchCodeList.add(ts.getCode());
+            ts.setMatched(true);
+            if (matchCodeList.size() == top) {
+                break;
+            }
+        }
+        return StringUtils.join(matchCodeList, ",");
+    }
+
+    private boolean getMatchedCode(List<TestMarjorDto.TestMatchStat> matchStats, List<String> codeList) {
+        if (CollectionUtils.isEmpty(matchStats)) {
+            return true;
+        }
+        Set<String> validCodeSet = syOccupationInterstMajorMapper.selectSyOccupationInterstMajorList(new SyOccupationInterstMajor()).stream().map(t -> t.getMatchCode()).collect(Collectors.toSet());
+        TestMarjorDto.TestMatchStat ts1 = matchStats.get(0);
+        TestMarjorDto.TestMatchStat ts2 = null ;
+        String code;
+        if (matchStats.size() == 1 || (ts1.getCount() - (ts2 = matchStats.get(1)).getCount()) > 4) { // 只有一个或可忽略第二个时
+            ts1.setMatched(true);
+            code = ts1.getCode() + "**";
+        } else {
+            TestMarjorDto.TestMatchStat ts3 = null ;
+            String code2;
+            if (matchStats.size() == 2 || (ts2.getCount() - (ts3 = matchStats.get(2)).getCount()) > 4) { // 只有两个或忽略第3个时
+                ts1.setMatched(true);
+                ts2.setMatched(true);
+                code = ts1.getCode() + ts2.getCode() + "*";
+                if (!validCodeSet.contains(code)) {
+                    if (ts1.getCount() == ts2.getCount() && validCodeSet.contains((code2 = ts2.getCode() + ts1.getCode() + "*"))) {
+                        matchStats.add(0, matchStats.remove(1)); // 交换 12位置
+                        code = code2;
+                    } else if (validCodeSet.contains((code2 = ts1.getCode() + "**"))) { // 降级
+                        code = code2;
+                    } else if (validCodeSet.contains((code2 = ts2.getCode() + "**"))) {
+                        matchStats.add(0, matchStats.remove(1));
+                        code = code2;
+                    }
+                }
+            } else {
+                code = ts1.getCode() + ts2.getCode() + ts3.getCode();
+                List<TestMarjorDto.TestMatchStat> needMatchStats = Lists.newArrayList();
+                List<TestMarjorDto.TestMatchStat> remainMatchStats;
+                if (ts2.getCount() > ts3.getCount()) { // ts2 固定
+                    remainMatchStats = splitStats(matchStats, 2, ts3.getCount(), needMatchStats);
+                    if (null != (code2 = matchStatsTry(validCodeSet, matchStats, needMatchStats))) {
+                        code = code2;
+                    } else {
+                        matchStats.addAll(needMatchStats);
+                        if (validCodeSet.contains((code2 = ts1.getCode() + ts2.getCode() + "*"))) {
+                            code = code2;
+                        } else if (validCodeSet.contains((code2 = ts1.getCode() + "**"))) {
+                            code = code2;
+                        }
+                    }
+                    matchStats.addAll(remainMatchStats);
+                } else { // ts2 == ts3
+                    remainMatchStats = splitStats(matchStats, 1, ts2.getCount(), needMatchStats);
+                    if (ts1.getCount() == ts2.getCount()) { // 3 of needMatchStats
+                        needMatchStats.add(0, matchStats.remove(0));
+                    }  // t1 + 2 of needMatchStats
+                    if (null != (code2 = matchStatsTry(validCodeSet, matchStats, needMatchStats))) {
+                        code = code2;
+                    }  else {
+                        if (matchStats.size() == 0) {
+                            code = needMatchStats.get(0).getCode() + "**";
+                        } else {
+                            code = matchStats.get(0).getCode() + "**";
+                        }
+                        matchStats.addAll(needMatchStats);
+                    }
+                    matchStats.addAll(remainMatchStats);
+                }
+            }
+        }
+        codeList.add(code);
+        return !validCodeSet.contains(code);
+    }
+
+    private String matchStatsTry(Set<String> validCodeSet, List<TestMarjorDto.TestMatchStat> matchStats, List<TestMarjorDto.TestMatchStat> needMatchStats) {
+        String code = null;
+        if (matchStats.size() == 0) { // 任意组合3个
+            List<List<TestMarjorDto.TestMatchStat>> permutaionList = Lists.newArrayList();
+            buildPermutation(needMatchStats.toArray(new TestMarjorDto.TestMatchStat[needMatchStats.size()]), 3, Lists.newArrayList(), Sets.newHashSet(), permutaionList);
+            for (List<TestMarjorDto.TestMatchStat> tsList : permutaionList) {
+                TestMarjorDto.TestMatchStat ts1 = tsList.get(0);
+                TestMarjorDto.TestMatchStat ts2 = tsList.get(1);
+                TestMarjorDto.TestMatchStat ts3 = tsList.get(2);
+                if (validCodeSet.contains((code = ts1.getCode() + ts2.getCode() + ts3.getCode()))) {
+                    matchStats.addAll(tsList);
+                    return combineMatch(code, ts1, ts2, ts3);
+                }
+            }
+        } else if(matchStats.size() == 1) { // 任意组合2个
+            TestMarjorDto.TestMatchStat ts1 = matchStats.get(0);
+            List<List<TestMarjorDto.TestMatchStat>> permutaionList = Lists.newArrayList();
+            buildPermutation(needMatchStats.toArray(new TestMarjorDto.TestMatchStat[needMatchStats.size()]), 2, Lists.newArrayList(), Sets.newHashSet(), permutaionList);
+            for (List<TestMarjorDto.TestMatchStat> matchStatList : permutaionList) {
+                TestMarjorDto.TestMatchStat ts2 = matchStatList.get(0);
+                TestMarjorDto.TestMatchStat ts3 = matchStatList.get(1);
+                if (validCodeSet.contains((code = ts1.getCode() + ts2.getCode() + ts3.getCode()))) {
+                    matchStats.addAll(matchStatList);
+                    return combineMatch(code, ts1, ts2, ts3);
+                }
+            }
+        } else { // 前两个可能交换
+            TestMarjorDto.TestMatchStat ts1 = matchStats.get(0);
+            TestMarjorDto.TestMatchStat ts2 = matchStats.get(1);
+            for (TestMarjorDto.TestMatchStat ts3 : needMatchStats) {
+                if (validCodeSet.contains((code = ts1.getCode() + ts2.getCode() + ts3.getCode()))) {
+                    matchStats.add(ts3);
+                    return combineMatch(code, ts1, ts2, ts3);
+                } else if (ts1.getCount() == ts2.getCount()) {
+                    if (validCodeSet.contains((code = ts1.getCode() + ts2.getCode() + ts3.getCode()))) {
+                        matchStats.add(0, matchStats.remove(1)); // 交换 12位置
+                        matchStats.add(ts3);
+                        return combineMatch(code, ts1, ts2, ts3);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private String combineMatch(String code, TestMarjorDto.TestMatchStat ts1, TestMarjorDto.TestMatchStat ts2, TestMarjorDto.TestMatchStat ts3) {
+        ts1.setMatched(true);
+        ts2.setMatched(true);
+        ts3.setMatched(true);
+        return code;
+    }
+
+    private void buildPermutation(TestMarjorDto.TestMatchStat[] matchStats, Integer matchCount, List<TestMarjorDto.TestMatchStat> baseStats,
+                                  Set<Integer> usedIndexSet, List<List<TestMarjorDto.TestMatchStat>> permutaionList) {
+        boolean lastCount = matchCount == baseStats.size() + 1;
+        for (int i = 0; i < matchStats.length; i++) {
+            if (usedIndexSet.add(i)) {
+                List<TestMarjorDto.TestMatchStat> foundList = Lists.newArrayList(baseStats);
+                foundList.add(matchStats[i]);
+                if (lastCount) {
+                    permutaionList.add(foundList);
+                } else {
+                    buildPermutation(matchStats, matchCount, foundList, usedIndexSet, permutaionList);
+                }
+                usedIndexSet.remove(i);
+            }
+        }
+    }
+
+    /**
+     * 从除列中删除指定位置后的,并根据count分成两组
+     * @param matchStats
+     * @param index
+     * @param matchCount
+     * @param needMatchStats
+     * @return
+     */
+    private List<TestMarjorDto.TestMatchStat> splitStats(List<TestMarjorDto.TestMatchStat> matchStats, Integer index, Integer matchCount, List<TestMarjorDto.TestMatchStat> needMatchStats) {
+        List<TestMarjorDto.TestMatchStat> remainMatchStats = Lists.newArrayList();
+        for (int i = index; i < matchStats.size(); ) {
+            TestMarjorDto.TestMatchStat ts = matchStats.remove(i);
+            if (ts.getCount().equals(matchCount)) {
+                needMatchStats.add(ts);
+            } else {
+                remainMatchStats.add(ts);
+            }
+        }
+        return remainMatchStats;
+    }
+
+    public void updateTestAnswers(List<QuestionDto> questions, Long examineeId) {
+        SyTestExaminee examinee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+
+        SyTestAnswers answerCond = new SyTestAnswers();
+        answerCond.setExamineeId(examineeId);
+        List<SyTestAnswers> answersList = syTestAnswersMapper.selectSyTestAnswersList(answerCond);
+        Map<Long, SyTestAnswers> oldQuestionAnswersMap = answersList.stream().collect(Collectors.toMap(SyTestAnswers::getQuestionId, Function.identity()));
+        Map<Long, QuestionDto> newQuestionAnswerMap = questions.stream().collect(Collectors.toMap(QuestionDto::getQuestionId, Function.identity()));
+
+        SyTestQuestion questionCond = new SyTestQuestion();
+        questionCond.setPaperId(examinee.getPaperId());
+        List<SyTestQuestion> stdQuestionList = syTestQuestionMapper.selectSyTestQuestionList(questionCond);
+
+        SyTestAnswers mxjbAnswersCond = new SyTestAnswers();
+        stdQuestionList.forEach(t -> {
+            SyTestAnswers oldAnswer = oldQuestionAnswersMap.remove(t.getQuestionId());
+            QuestionDto newAnswer = newQuestionAnswerMap.get(t.getQuestionId());
+            if (null != newAnswer) {
+                mxjbAnswersCond.setAnswer(newAnswer.getAnswer() + "$@" + StringUtils.join(newAnswer.getAttachments(), "$@"));
+                mxjbAnswersCond.setDuration(newAnswer.getDuration());
+            } else {
+                mxjbAnswersCond.setAnswer(null);
+                mxjbAnswersCond.setDuration(-1L);
+            }
+            if (null != oldAnswer) {
+                mxjbAnswersCond.setAnswerId(oldAnswer.getAnswerId());
+                mxjbAnswersCond.setExamineeId(null);
+                mxjbAnswersCond.setExamineeType(null);
+                mxjbAnswersCond.setQuestionId(null);
+                mxjbAnswersCond.setSeq(null);
+                syTestAnswersMapper.updateSyTestAnswers(mxjbAnswersCond);
+            } else {
+                mxjbAnswersCond.setAnswerId(null);
+                mxjbAnswersCond.setExamineeId(examineeId);
+                mxjbAnswersCond.setExamineeType(MxjbContants.ExamineeTypeTest);
+                mxjbAnswersCond.setQuestionId(t.getQuestionId());
+                mxjbAnswersCond.setSeq(t.getSeq());
+                syTestAnswersMapper.insertSyTestAnswers(mxjbAnswersCond);
+            }
+        });
+        if (oldQuestionAnswersMap.size() > 0) {
+            List<Long> oldIds = oldQuestionAnswersMap.values().stream().map(SyTestAnswers::getAnswerId).collect(Collectors.toList());
+            syTestAnswersMapper.deleteSyTestAnswersByIds(oldIds.toArray(new Long[oldIds.size()]));
+        }
+    }
+
+    public PaperDto loadPaperByTestMajorTypeId(Integer testType) {
+        Long paperId = testType.longValue();
+
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        SyTestExaminee mxjbExaminee = getLatestExaminee(testType, sysUser.getCode());
+        SyTestPaper syTestPaper = syTestPaperMapper.selectSyTestPaperById(paperId.longValue());
+        if (null != mxjbExaminee && (mxjbExaminee.getState() < MxjbContants.ExamineeStatusPublish)) { // 未测试完成的先继续测试
+            PaperDto paperDto = loadPaperDto(syTestPaper, mxjbExaminee, true);
+            if (MxjbContants.ExamineeStatusExamine.equals(mxjbExaminee.getState())) {
+                examUtilService.updateTempAnswerAndSave(paperDto, mxjbExaminee.getExamineeId(), MxjbContants.ExamineeTypeTest);
+            }
+            return paperDto;
+        }
+        if (null != mxjbExaminee) {
+            if (4 != testType && !isSelectCompleted(mxjbExaminee)) { // 测试完成但未选择的先选择
+                throw new RuntimeException("请选选择专业类,完成本轮测试, 再进行下一轮");
+            }
+            // 关闭上一轮的测试,进入下一轮
+            SyTestExaminee examineeUpdate = new SyTestExaminee();
+            examineeUpdate.setExamineeId(mxjbExaminee.getExamineeId());
+            examineeUpdate.setState(MxjbContants.ExamineeStatusClose);
+            syTestExamineeMapper.updateSyTestExaminee(examineeUpdate);
+        }
+        SyTestExaminee examineeCond = new SyTestExaminee();
+        PaperDto paperDto = loadPaperDto(syTestPaper, null, false);
+        if (MxjbContants.ExamineeStatusSign.equals(paperDto.getState())) {
+            paperDto.setRemaining(paperDto.getMode() * 60);
+            paperDto.setMinutes(paperDto.getRemaining() / 60);
+            paperDto.setState(MxjbContants.ExamineeStatusExamine);
+            examineeCond.setPaperId(paperId);
+            examineeCond.setCustomerCode(sysUser.getCode());
+            examineeCond.setState(paperDto.getState());
+            examineeCond.setBeginTime(paperDto.getBeginTime());
+            examineeCond.setEndTime(paperDto.getEndTime());
+            examineeCond.setScoreLevel("W");
+            examineeCond.setScore(0L);
+            examineeCond.setScoreRate(0.0);
+            examineeCond.setRanking(0L);
+            syTestExamineeMapper.insertSyTestExaminee(examineeCond);
+            paperDto.setExamineeId(examineeCond.getExamineeId());
+        }
+        paperDto.getQuestions().get(0).setCurrent(true);
+        return paperDto;
+    }
+
+
+    public PaperDto loadPaperByExaminerId(Long examineeId) {
+        SyTestExaminee mxjbExaminee = syTestExamineeMapper.selectSyTestExamineeById(examineeId);
+        if (null == mxjbExaminee) {
+            throw new RuntimeException("无测试人员");
+        }
+        SyTestPaper syTestPaper = syTestPaperMapper.selectSyTestPaperById(mxjbExaminee.getPaperId());
+        PaperDto paperDto = loadPaperDto(syTestPaper, mxjbExaminee, true);
+        return paperDto;
+    }
+
+    private PaperDto loadPaperDto(SyTestPaper syTestPaper, SyTestExaminee examinee, boolean loadParse) {
+        PaperDto paperDto = new PaperDto();
+        SyTestQuestion cond = new SyTestQuestion();
+        cond.setPaperId(syTestPaper.getPaperId());
+        List<SyTestQuestion> questionsList = syTestQuestionMapper.selectSyTestQuestionList(cond);
+        paperDto.setExamineeType(MxjbContants.ExamineeTypeTestValue);
+        paperDto.setName(syTestPaper.getName());
+        paperDto.setPaperId(syTestPaper.getPaperId());
+        paperDto.setScoringType(MxjbContants.ScoringTypeAuto);
+        paperDto.setPublish(true);
+        paperDto.setMode(99*60L); // TODO 竞赛要设置时间
+        paperDto.setEvaluationState(MxjbContants.EvaluationStatusOpen);
+        paperDto.setBeginTime(new Date());
+        paperDto.setEndTime(DateUtils.addMinutes(paperDto.getBeginTime(), paperDto.getMode().intValue()));
+        if (null == examinee) { // 没有考生
+            paperDto.setMinutes(0L);
+            paperDto.setRemaining(0L);
+            paperDto.setState(MxjbContants.ExamineeStatusSign);
+        } else {
+            paperDto.setState(examinee.getState());
+            if (examinee.getState() >= MxjbContants.ExamineeStatusPublish) {
+                paperDto.setBeginTime(examinee.getBeginTime());
+                paperDto.setEndTime(examinee.getEndTime());
+                paperDto.setRemaining(0L);
+            } else {
+                paperDto.setRemaining(paperDto.getMode() * 60);
+            }
+            paperDto.setMinutes(paperDto.getRemaining() / 60);
+            paperDto.setExamineeId(examinee.getExamineeId());
+        }
+        final Map<Long, SyTestAnswers> questionAnswersMap;
+        if(paperDto.getState() >= MxjbContants.ExamineeStatusExamine) {
+            SyTestAnswers answerCond = new SyTestAnswers();
+            answerCond.setExamineeId(examinee.getExamineeId());
+            answerCond.setExamineeType(MxjbContants.ExamineeTypeTest);
+            questionAnswersMap = syTestAnswersMapper.selectSyTestAnswersList(answerCond).stream().collect(Collectors.toMap(SyTestAnswers::getQuestionId, Function.identity()));
+        } else {
+            questionAnswersMap = null;
+        }
+
+        Boolean fillParseAndAnswer = loadParse || MxjbContants.ExamineeStatusPublish.equals(paperDto.getState()) // 自阅卷或发布时可显示成绩
+                || MxjbContants.ExamineeStatusReview.equals(paperDto.getState());
+
+        List<QuestionDto> questionDtoList = paperDto.getQuestions();
+        questionsList.forEach(t -> {
+            QuestionDto dto = examUtilService.toQuestionDto(t, fillParseAndAnswer, questionAnswersMap);
+            dto.setSeq(questionDtoList.size() + 1L);
+            questionDtoList.add(dto);
+        });
+        return paperDto;
+    }
+
+    public boolean isSelectCompleted(SyTestExaminee mxjbExaminee) {
+        boolean noMatch = false;
+        if (StringUtils.isNotBlank(mxjbExaminee.getStats())) {
+            TestMarjorDto.TestStats testStat = null;
+            try {
+                testStat = om.readValue(mxjbExaminee.getStats(), TestMarjorDto.TestStats.class);
+                noMatch = testStat.isNoMatch();
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+            }
+        }
+        return noMatch || isSelectCompleted(mxjbExaminee.getExamineeId());
+    }
+
+    private boolean isSelectCompleted(Long examineeId) {
+        SyTestSelectCategory selectCategoryCond = new SyTestSelectCategory();
+        selectCategoryCond.setExamineeId(examineeId);
+        boolean selectCompleted = CollectionUtils.isNotEmpty(syTestSelectCategoryMapper.selectSyTestSelectCategoryList(selectCategoryCond));
+        return selectCompleted;
+    }
+
+    private SyTestExaminee getLatestExaminee(Integer testType, String customerCode) {
+        Long paperId = testType.longValue();
+        Map cond = Maps.newHashMap();
+        cond.put("paperId", paperId);
+        cond.put("customerCode", customerCode);
+        cond.put("cnt", 1);
+        List<SyTestExaminee> existList = syTestExamineeMapper.selectSyTestExamineeLatest(cond);
+        return CollectionUtils.isNotEmpty(existList) ? existList.get(0) : null;
+    }
+
+}

+ 1172 - 0
ie-admin/src/main/java/com/ruoyi/web/service/VoluntaryService.java

@@ -0,0 +1,1172 @@
+package com.ruoyi.web.service;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONException;
+import com.alibaba.fastjson2.JSONObject;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.ie.domain.*;
+import com.ruoyi.ie.mapper.*;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.syzy.domain.BBusiWishUniversities;
+import com.ruoyi.syzy.domain.BBusiWishUniversitiesProfession;
+import com.ruoyi.syzy.dto.UniversityDetailDTO;
+import com.ruoyi.syzy.mapper.BBusiWishRecordsMapper;
+import com.ruoyi.syzy.mapper.BBusiWishUniversitiesMapper;
+import com.ruoyi.syzy.mapper.BBusiWishUniversitiesProfessionMapper;
+import com.ruoyi.util.PageUtil;
+import com.ruoyi.web.domain.Constant;
+import com.ruoyi.web.domain.VoluntaryDto;
+import io.swagger.annotations.ApiParam;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.lang3.tuple.MutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Service
+public class VoluntaryService {
+    private ObjectMapper mapper = new ObjectMapper();
+    private static TypeReference<List<VoluntaryDto.SingleResponse>> wishDetailsTypeReference = new TypeReference<List<VoluntaryDto.SingleResponse>>() { };
+    private static TypeReference<List<VoluntaryDto.AIResponse>> wishAiDetailsTypeReference = new TypeReference<List<VoluntaryDto.AIResponse>>() { };
+    Set<String> NumberTypeSet = Sets.newHashSet(VoluntaryDto.EnumInputType.Number.name(), VoluntaryDto.EnumInputType.Eyesight.name(), VoluntaryDto.EnumInputType.Score.name());
+
+    private final AWishRecordMapper aWishRecordMapper;
+    private final AEnrollScoreMapper aEnrollScoreMapper;
+    private final AEnrollSpecialMapper aEnrollSpecialMapper;
+    private final AMarjorPlanMapper aMarjorPlanMapper;
+    private final AMarjorSubmitMapper aMarjorSubmitMapper;
+    private final AEnrollUniversityMapper aEnrollUniversityMapper;
+    private final BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper;
+    private final BBusiWishUniversitiesProfessionMapper bBusiWishUniversitiesProfessionMapper;
+
+    private final ISysConfigService sysConfigService;
+    private final EnrollRateCalculator enrollRateCalculator;
+    private final BBusiWishRecordsMapper busiWishRecordsMapper;
+
+    public VoluntaryService(AWishRecordMapper aWishRecordMapper, AEnrollScoreMapper aEnrollScoreMapper, AEnrollSpecialMapper aEnrollSpecialMapper, AMarjorPlanMapper aMarjorPlanMapper, AMarjorSubmitMapper aMarjorSubmitMapper, AEnrollUniversityMapper aEnrollUniversityMapper, BBusiWishUniversitiesMapper bBusiWishUniversitiesMapper, BBusiWishUniversitiesProfessionMapper bBusiWishUniversitiesProfessionMapper, ISysConfigService sysConfigService, EnrollRateCalculator enrollRateCalculator, BBusiWishRecordsMapper busiWishRecordsMapper) {
+        this.aWishRecordMapper = aWishRecordMapper;
+        this.aEnrollScoreMapper = aEnrollScoreMapper;
+        this.aEnrollSpecialMapper = aEnrollSpecialMapper;
+        this.aMarjorPlanMapper = aMarjorPlanMapper;
+        this.aMarjorSubmitMapper = aMarjorSubmitMapper;
+        this.aEnrollUniversityMapper = aEnrollUniversityMapper;
+        this.bBusiWishUniversitiesMapper = bBusiWishUniversitiesMapper;
+        this.bBusiWishUniversitiesProfessionMapper = bBusiWishUniversitiesProfessionMapper;
+        this.sysConfigService = sysConfigService;
+        this.enrollRateCalculator = enrollRateCalculator;
+        this.busiWishRecordsMapper = busiWishRecordsMapper;
+        // mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    }
+
+    public Pair<List<UniversityDetailDTO.WishPlan>, List<UniversityDetailDTO.WishSubmit>> getUniversityHistory(Long universityId, String province, String examineType) {
+        AMarjorPlan planCond = new AMarjorPlan();
+        planCond.setUniversityId(universityId);
+        planCond.setExamineeType(examineType);
+        List<UniversityDetailDTO.WishPlan> planList = aMarjorPlanMapper.selectAMarjorPlanList(planCond).stream().map(t -> {
+            UniversityDetailDTO.WishPlan p = new UniversityDetailDTO.WishPlan();
+            p.setId(t.getId());
+            p.setCollegeCode(t.getEnrollCode());
+            p.setYear(String.valueOf(t.getYear()));
+            p.setLevel(t.getLevel());
+            p.setType("");
+            p.setMarjorCode(t.getMajorCode());
+            p.setMarjorName(t.getMajorName());
+            p.setMarjorBelongs(t.getMajorEnrollCode());
+            p.setMarjorDirection(t.getMajorDirection());
+            p.setSpecialProject("");
+            p.setPlanCount(t.getPlanTotal());
+            p.setXuefei(String.valueOf(t.getXuefei()));
+            p.setXuezhi(String.valueOf(t.getLengthOfSchooling()));
+            p.setEnrollFormula(t.getEnrollFormula());
+            p.setGroupsName(t.getMajorGroup());
+            return p;
+        }).collect(Collectors.toList());
+
+        AMarjorSubmit submitCond = new AMarjorSubmit();
+        submitCond.setUniversityId(universityId);
+        submitCond.setEnrollType("初录");
+        submitCond.setExamineeType(examineType);
+        List<UniversityDetailDTO.WishSubmit> submitList = aMarjorSubmitMapper.selectAMarjorSubmitList(submitCond).stream().map(t -> {
+            UniversityDetailDTO.WishSubmit s = new UniversityDetailDTO.WishSubmit();
+            s.setId(t.getId());
+            s.setYear(String.valueOf(t.getYear()));
+            s.setLevel(t.getLevel());
+            s.setType("");
+            s.setMarjorCode("");
+            s.setMarjorName(t.getMajorName());
+            s.setMarjorBelongs("");
+            s.setMarjorDirection(t.getMajorDirection());
+            s.setSpecialProject("");
+            s.setScore(null != t.getScore() ? t.getScore().longValue() : null);
+            s.setSeat(null);
+            s.setNumReal(null != t.getEnrollTotal() ? t.getEnrollTotal().longValue() : null);
+            s.setEnrollFormula(t.getEnrollFormula());
+            s.setGroupsName(t.getMajorGroup());
+            return s;
+        }).collect(Collectors.toList());
+        return Pair.of(planList, submitList);
+    }
+
+    private void setOptionValue(VoluntaryDto.AIRenderRule r, String valueType, String valueRule, String options, Integer correctType, String correctValue, Integer scoreTotal) {
+        r.setEnumInputType(VoluntaryDto.EnumInputType.valueOf(valueType));
+        if (VoluntaryDto.EnumInputType.Score.equals(r.getEnumInputType())) {
+            r.setOptions(new String[]{null == scoreTotal ? "0" : String.valueOf(scoreTotal.intValue())});
+        } else if (StringUtils.isNotBlank(options) && (VoluntaryDto.EnumInputType.Checkbox.equals(r.getEnumInputType())
+                || VoluntaryDto.EnumInputType.Radio.equals(r.getEnumInputType()) || VoluntaryDto.EnumInputType.Picker.equals(r.getEnumInputType()))) {
+            r.setOptions(options.split(","));
+        }
+        // r.setRequired();
+        // 词典选项类选项 // 词典类选项的优先级最高
+        // String dictOptions;
+        // 非词典选项 // 分制类规则,将多分制在此options中返回
+        // String[] options;
+    }
+
+    public Integer getPlanYear(SysUser u) {
+        return 2025;
+    }
+
+    public Integer getSubmitYear() {
+        return 2024;
+    }
+
+    public R<List<VoluntaryDto.AIRenderRule>> getAIRenderRules(VoluntaryDto.AIRenderRequest req) {
+        Map cond = new HashMap();
+        cond.put("year", getPlanYear(SecurityUtils.getLoginUser().getUser()));
+        Set<String> majorCodeSet = null;
+        if (ArrayUtils.isNotEmpty(req.getMajorEnrollCodes()) && StringUtils.isNotBlank(req.getMajorEnrollCodes()[0])) {
+            List<Long> majorEnrollCodes = Arrays.asList(req.getMajorEnrollCodes()).stream().map(t -> NumberUtils.toLong(t, 0L)).collect(Collectors.toList());
+            majorCodeSet = aMarjorPlanMapper.selectMajorCodesByIds(majorEnrollCodes.toArray(new Long[majorEnrollCodes.size()])).stream().collect(Collectors.toSet());
+        }
+        if(CollectionUtils.isNotEmpty(majorCodeSet)) {
+            cond.put("majorCodes", majorCodeSet.toArray(new String[majorCodeSet.size()]));
+        } else if (ArrayUtils.isNotEmpty(req.getMajorCodes()) && StringUtils.isNotBlank(req.getMajorCodes()[0])) {
+            cond.put("majorCodes", req.getMajorCodes());
+        } else if (ArrayUtils.isNotEmpty(req.getMajorTypes()) && StringUtils.isNotBlank(req.getMajorTypes()[0])) {
+            cond.put("majorTypes", req.getMajorTypes());
+        } else if (StringUtils.isNotBlank(req.getMajorCategory())) {
+            cond.put("majorCategory", req.getMajorCategory());
+        }
+        cond.put("universityCode", req.getUniversityCode());
+        return R.ok(findMatchRules(cond, req.getRenderType() == 1, req.getRenderType() == 2));
+    }
+
+    public R<VoluntaryDto.SingleResponse> postSingleResult(VoluntaryDto.SingleRequest req, Boolean isScoreOnly) {
+        VoluntaryDto.MultipleRequest mr = new VoluntaryDto.MultipleRequest();
+        VoluntaryDto.CollegeMajorDto collegeMajor = new VoluntaryDto.CollegeMajorDto();
+        collegeMajor.setCode(req.getUniversityCode());
+        if (StringUtils.isNotBlank(req.getMajorCode())) {
+            collegeMajor.setMajorCodes(Lists.newArrayList(req.getMajorCode()));
+        }
+        if (StringUtils.isNotBlank(req.getMajorEnrollCode())) {
+            collegeMajor.setMajorEnrollCodes(Lists.newArrayList(req.getMajorEnrollCode()));
+        }
+        collegeMajor.setForm(req.getForm());
+
+        mr.setUniversities(Lists.newArrayList(collegeMajor));
+        List<VoluntaryDto.SingleResponse> respList = postMultipleResult(mr, isScoreOnly).getData();
+        if (respList.size() > 0) {
+            Optional<VoluntaryDto.SingleResponse> optionalSingleResponse = respList.stream().filter(t -> StringUtils.isBlank(t.getMajorDirection())).findFirst();
+            if (optionalSingleResponse.isPresent()) {
+                return R.ok(optionalSingleResponse.get());
+            }
+        }
+        return respList.size() > 0 ? R.ok(respList.get(0)) : null;
+    }
+
+    public R<List<VoluntaryDto.SingleResponse>> postMultipleResult(VoluntaryDto.MultipleRequest req, boolean isScoreOnly) {
+        List<VoluntaryDto.SingleResponse> respList = Lists.newArrayList();
+        for (VoluntaryDto.CollegeMajorDto cm : req.getUniversities()) {
+            Map cond = new HashMap();
+            if (CollectionUtils.isNotEmpty(cm.getMajorEnrollCodes()) && StringUtils.isNotBlank(cm.getMajorEnrollCodes().get(0))) {
+                cond.put("majorEnrollCodes", cm.getMajorEnrollCodes()); // TODO 代替majorCodes更精准,只是现在没有,使用planId代替
+            } else if (CollectionUtils.isNotEmpty(cm.getMajorCodes()) && StringUtils.isNotBlank(cm.getMajorCodes().get(0))) {
+                cond.put("majorCodes", cm.getMajorCodes());
+            } else if (StringUtils.isNotBlank(req.getMajorCategory())) {
+                cond.put("majorCategory", req.getMajorCategory());
+            }
+            cond.put("universityCode", StringUtils.trimToEmpty(cm.getCode()));
+            List<VoluntaryDto.AIResponse> responses = findMajorSuggest(getPlanYear(SecurityUtils.getLoginUser().getUser()), getSubmitYear(), cond, cm.getForm(), false, isScoreOnly);
+            if(CollectionUtils.isNotEmpty(responses)) {
+                responses.stream().forEach(t -> respList.addAll(t.getMajorDetails()));
+            }
+        }
+        return R.ok(respList);
+    }
+
+    public TableDataInfo postAIResult(VoluntaryDto.AIRequest req) {
+        VoluntaryDto.AIRequestFilter filter = req.getFilter();
+        Map cond = new HashMap();
+        if (CollectionUtils.isNotEmpty(filter.getMajorTypes()) && StringUtils.isNotBlank(filter.getMajorTypes().get(0))) {
+            cond.put("majorTypes", filter.getMajorTypes());
+        } else if (StringUtils.isNotBlank(req.getMajorCategory())) {
+            cond.put("majorCategory", req.getMajorCategory());
+        }
+        cond.put("universityName", filter.getKeyword());// 院校名称
+        if (CollectionUtils.isNotEmpty(filter.getLevel())) {
+            cond.put("universityLevel", filter.getLevel());// 办学层次
+        }
+        if (CollectionUtils.isNotEmpty(filter.getType())) {
+            cond.put("universityType", filter.getType());// 院校类型
+        }
+        if (CollectionUtils.isNotEmpty(filter.getNatureTypeCN())) {
+            cond.put("universityNatureType", filter.getNatureTypeCN()); // 办学类型
+        }
+        if (CollectionUtils.isNotEmpty(filter.getLocation())) {
+            cond.put("universityLocation", filter.getLocation()); // 院校省份
+        }
+        if (null != filter.getHasClearing()) {
+            cond.put("hasClearing", filter.getHasClearing() ? "补录" : "初录");
+        }
+        List<VoluntaryDto.AIResponse> responses = findMajorSuggest(getPlanYear(SecurityUtils.getLoginUser().getUser()), getSubmitYear(), cond, req.getForm(), true, false);
+        if (CollectionUtils.isEmpty(responses)) {
+            return new TableDataInfo(Lists.newArrayList(), 0);
+        }
+
+        if (null != filter.getEnumPickEmpty()) {
+            responses = responses.stream().filter(t -> filter.getEnumPickEmpty().equals(t.getEnumPickEmpty())).collect(Collectors.toList());
+        } else if (null != filter.getEnumPickType() && !VoluntaryDto.EnumPickType.All.equals(filter.getEnumPickType())) {
+            responses = responses.stream().filter(t -> filter.getEnumPickType().equals(t.getEnumPickType())).collect(Collectors.toList());
+        }
+        Integer start = null != req.getPageNum() ? (req.getPageNum() - 1) * req.getPageSize() : 0;
+        List<VoluntaryDto.AIResponse> finalList = responses.subList(start, Math.min(start + (null == req.getPageSize() ? 10 : req.getPageSize()), responses.size()));
+        TableDataInfo tableDataInfo = new TableDataInfo(finalList, responses.size());
+        tableDataInfo.setCode(200);
+        return tableDataInfo;
+    }
+
+    private List<VoluntaryDto.AIRenderRule> findMatchRules(Map cond, boolean isAi, boolean isScore) {
+        String examType = SecurityUtils.getLoginUser().getUser().getExamType().title(); // TODO MF
+        String gender = "0".equals(SecurityUtils.getLoginUser().getUser().getSex()) ? "男生" : "女生";
+        cond.put("examineeType", examType);
+
+        List<AEnrollScore> enrollScoreList = aEnrollScoreMapper.selectListByRuleCond(cond);
+        List<VoluntaryDto.AIRenderRule> ruleList = Lists.newArrayList();
+        Set<String> existItemSet  = Sets.newHashSet();
+        String vt;
+        VoluntaryDto.AIRenderRule scoreRateRule = null;
+        for (AEnrollScore s : enrollScoreList) { // 跳过统计类和提示类的规则
+            if (null == (vt = s.getValueType()) || vt.startsWith("Stat") || vt.equals("Notice")) {
+                continue;
+            } else if(isScore && !"学考".equals(s.getItemCategory()) && !"校考".equals(s.getItemCategory())) {
+                continue;
+            } else if (vt.equals("ScoreRate")) {
+                if (!isAi) {
+                    vt = "Score";
+                } else if (existItemSet.add("ScoreRate")) {
+                    scoreRateRule = new VoluntaryDto.AIRenderRule();
+                    scoreRateRule.setEnumRuleCategory(VoluntaryDto.EnumRuleCategory.Enroll);
+                    scoreRateRule.setFieldName("ScoreRate");
+                    scoreRateRule.setEnumInputType(VoluntaryDto.EnumInputType.Number);
+                    scoreRateRule.setMin(0);
+                    scoreRateRule.setMax(100);
+                    scoreRateRule.setLabel("技能测试得分率");
+                    scoreRateRule.setDescription("%");
+                    continue;
+                } else {
+                    continue;
+                }
+            }
+            if (!existItemSet.add(s.getItemField())) {
+                continue;
+            }
+            VoluntaryDto.AIRenderRule r = new VoluntaryDto.AIRenderRule();
+            r.setEnumRuleCategory(VoluntaryDto.EnumRuleCategory.Enroll);
+            r.setFieldName(s.getItemField());
+            setOptionValue(r, vt, s.getValueRule(), s.getValueOptional(), s.getCorrectType(), s.getCorrectValue(), s.getScoreTotal());
+            r.setRegex(s.getRegex());
+            r.setLabel(s.getItemName());
+            r.setDescription(s.getDescription());
+            r.setPlaceholder(s.getPlaceholder());
+            r.setTips(s.getTips());
+            r.setDefaultValue(s.getDefaultValue());
+            r.setDotDisable(null != s.getDotDisable() && s.getDotDisable() > 0);
+            r.setKeyboardMode(s.getKeyboardMode()); // number card car,默认number
+            ruleList.add(r);
+        }
+        if (null != scoreRateRule) {
+            ruleList.add(scoreRateRule);
+        }
+        if(isScore) {
+            return ruleList;
+        }
+        List<AEnrollSpecial> enrollSpecialList = Lists.newArrayList();
+        Map<String, AEnrollSpecial> mergeEnrollSpecialMap = Maps.newHashMap();
+        List<AEnrollSpecial> oriErollSpecialList = aEnrollSpecialMapper.selectListByRuleCond(cond);
+        for (AEnrollSpecial s : oriErollSpecialList) {
+            if (VoluntaryDto.EnumRuleType.Readonly.name().equals(s.getItemType()) || StringUtils.isNotBlank(s.getGender()) && !s.getGender().equals(gender)) {
+                continue;
+            }
+            if (null == s.getItemGroup() || !"Checkbox".equals(s.getValueType())) {
+                enrollSpecialList.add(s);
+                continue;
+            }
+            //
+            String key = s.getItemGroup(); // s.getUniversityId() + "_" + s.getMajorName() + "_" + StringUtils.trimToEmpty(s.getMajorDirection()) + "_" + StringUtils.trimToEmpty(s.getGender()) + "_" + s.getItemCategory() + "_" +
+            AEnrollSpecial exist = mergeEnrollSpecialMap.get(key);
+            if (null == exist) {
+                // 修正分组后的名称及字段名
+                String[] groupNameField = StringUtils.split(s.getItemGroup(), "_");
+                s.setValueOptional(s.getItemName());
+                s.setItemName(groupNameField[0]);
+                s.setItemField(groupNameField[groupNameField.length > 1 ? 1 : 0]);
+                mergeEnrollSpecialMap.put(key, s);
+                enrollSpecialList.add(s);
+                continue;
+            }
+            if(StringUtils.isBlank(exist.getValueOptional()) || !exist.getValueOptional().contains(s.getItemName())) {
+                exist.setValueOptional(exist.getValueOptional() + "," + s.getItemName());
+            }
+        }
+        Map<String,String> mutexOptionMap = getMutexOptionMap();
+        for (AEnrollSpecial s : enrollSpecialList) {
+            if (null == (vt = s.getValueType()) || vt.startsWith("Stat") || vt.equals("Notice") || !existItemSet.add(s.getItemField())) {
+                continue;
+            }
+            VoluntaryDto.AIRenderRule r = new VoluntaryDto.AIRenderRule();
+            r.setEnumRuleCategory(VoluntaryDto.EnumRuleCategory.Special);
+            r.setFieldName(s.getItemField());
+            setOptionValue(r, s.getValueType(), s.getValueRule(), s.getValueOptional(), s.getCorrectType(), s.getCorrectValue(), null);
+            if (VoluntaryDto.EnumInputType.Checkbox.equals(r.getEnumInputType())) {
+                r.setMutexOption(mutexOptionMap.get(r.getFieldName()));
+            }
+            r.setRegex(s.getRegex());
+            r.setLabel(s.getItemName());
+            r.setDescription(s.getDescription());
+            r.setPlaceholder(s.getPlaceholder());
+            r.setTips(s.getTips());
+            r.setDefaultValue(s.getDefaultValue());
+            r.setDotDisable(null != s.getDotDisable() && s.getDotDisable() > 0);
+            r.setKeyboardMode(s.getKeyboardMode()); // number card car,默认number
+            ruleList.add(r);
+        }
+        return ruleList;
+    }
+
+    private Map<String, String> getMutexOptionMap() {
+        String mutexOptionConf = sysConfigService.selectConfigByKey("voluntary.rule.mutexOption");
+        Map<String, String> mutexOptionMap = Maps.newHashMap();
+        if (StringUtils.isNotBlank(mutexOptionConf)) {
+            try {
+                JSONArray.parseArray(mutexOptionConf).forEach(l -> {
+                    JSONObject lo = (JSONObject) l;
+                    mutexOptionMap.put(lo.getString("n"), lo.getString("m"));
+                });
+            } catch (JSONException e) {
+
+            }
+        }
+        return mutexOptionMap;
+    }
+
+    // TODO 同一专业号有多个专业方向
+    private List<VoluntaryDto.AIResponse> findMajorSuggest(Integer planYear, Integer submitYear, Map cond, Map<String, String> paramMap, boolean isAi, boolean isScoreOnly) {
+        String examType = SecurityUtils.getLoginUser().getUser().getExamType().title(); // TODO MF
+        String gender = "0".equals(SecurityUtils.getLoginUser().getUser().getSex()) ? "男生" : "女生";
+        cond.remove("year");
+        cond.put("examineeType", examType);
+        Map<Long, List<AMarjorPlan>> currUniversityMajorPlansMap = Maps.newHashMap();
+        Map<Long, List<AMarjorPlan>> historyUniversityMajorPlansMap = Maps.newHashMap();
+        List<Map<Long, List<AMarjorPlan>>> bothPlanList = Lists.newArrayList(currUniversityMajorPlansMap, historyUniversityMajorPlansMap);
+        Set<String> existSet = Sets.newHashSet();
+        for (AMarjorPlan mp : aMarjorPlanMapper.selectListByRuleCond(cond)) {
+            String key = mp.getUniversityId() + mp.getMajorName() + StringUtils.trimToEmpty(mp.getMajorDirection()) + mp.getYear(); // TODO 考虑为什么会重的问题 examType ?
+            if (!existSet.add(key)) {
+                continue;
+            }
+            for(Map<Long, List<AMarjorPlan>> tmpMap : bothPlanList) {
+                if (!mp.getYear().equals(planYear) && tmpMap == currUniversityMajorPlansMap) {
+                    continue;
+                }
+                List<AMarjorPlan> tmpMpList = tmpMap.get(mp.getUniversityId());
+                if (null == tmpMpList) {
+                    tmpMpList = Lists.newArrayList(mp);
+                    tmpMap.put(mp.getUniversityId(), tmpMpList);
+                } else {
+                    tmpMpList.add(mp);
+                }
+            }
+        }
+        Map<Long, List<BBusiWishUniversitiesProfession>> universityMajorsMap = Maps.newHashMap();
+        // 有计划以计划为准的输出,无计划时则以所有专业输出为准
+        Map<Long, BBusiWishUniversities> universitiesMap = null;
+        if (MapUtils.isEmpty(currUniversityMajorPlansMap)) {
+            Map<String, List<BBusiWishUniversitiesProfession>> tmpUniversityMajorsMap = bBusiWishUniversitiesProfessionMapper.selectListByRuleCond(cond).stream().collect(Collectors.groupingBy(BBusiWishUniversitiesProfession::getCollegeCode));
+            if (MapUtils.isNotEmpty(tmpUniversityMajorsMap)) {
+                Map uCond = Maps.newHashMap();
+                uCond.put("codes", tmpUniversityMajorsMap.keySet());
+                universitiesMap = Maps.newHashMap();
+                for (BBusiWishUniversities u : bBusiWishUniversitiesMapper.selectBBusiWishUniversitiesListSimpleByMap(uCond)) {
+                    universitiesMap.put(u.getId(), u);
+                    universityMajorsMap.put(u.getId(), tmpUniversityMajorsMap.get(u.getCode()));
+                }
+            }
+        } else {
+            cond.put("universityIds", currUniversityMajorPlansMap.keySet());
+            cond.put("examType", Constant.EXAM_TYPE_PG);
+            Map<String, List<BBusiWishUniversitiesProfession>> tmpUniversityMajorsMap = bBusiWishUniversitiesProfessionMapper.selectListByRuleCond(cond).stream().collect(Collectors.groupingBy(BBusiWishUniversitiesProfession::getCollegeCode));
+            cond.remove("universityIds");
+
+            if (MapUtils.isNotEmpty(tmpUniversityMajorsMap)) {
+                Map uCond = new HashMap();
+                uCond.put("ids", currUniversityMajorPlansMap.keySet());
+                universitiesMap = Maps.newHashMap();
+                for (BBusiWishUniversities u : bBusiWishUniversitiesMapper.selectBBusiWishUniversitiesListSimpleByIds(uCond)) {
+                    universitiesMap.put(u.getId(), u);
+                    universityMajorsMap.put(u.getId(), tmpUniversityMajorsMap.get(u.getCode()));
+                }
+            }
+        }
+        // 无计划,也无院校专业
+        if(MapUtils.isEmpty(universitiesMap)) {
+            return Lists.newArrayList();
+        }
+        // 查询涉及的规则
+        cond.put("year", planYear);
+        cond.put("universityIds", universitiesMap.keySet());
+        Map<Long, List<AEnrollScore>> universityScoreListMap = aEnrollScoreMapper.selectListByRuleCond(cond).stream().collect(Collectors.groupingBy(AEnrollScore::getUniversityId));
+        Map<Long, List<AEnrollSpecial>> universitySpecialListMap = aEnrollSpecialMapper.selectListByRuleCond(cond).stream().collect(Collectors.groupingBy(AEnrollSpecial::getUniversityId));
+        cond.remove("universityIds");
+        cond.remove("year");
+
+        Map<String, String> mutexOptionMap = getMutexOptionMap();
+        List<VoluntaryDto.AIResponse> aiRespList = Lists.newArrayList();
+        for (Long universityId : universitiesMap.keySet()) {
+            List<AEnrollScore> scoreList = universityScoreListMap.get(universityId); // 分数条件
+            List<AEnrollSpecial> specialList = universitySpecialListMap.get(universityId);// 专项条件
+            List<AMarjorPlan> currPlanList = currUniversityMajorPlansMap.get(universityId);// 当年计划
+            List<AMarjorPlan> historyPlanList = historyUniversityMajorPlansMap.get(universityId);// 历年计划
+            BBusiWishUniversities u = universitiesMap.get(universityId);// 院校
+            List<BBusiWishUniversitiesProfession> professionList = universityMajorsMap.get(u.getId()); // 所有专业
+            // 院校变更标志
+            AEnrollUniversity euCond = new AEnrollUniversity();
+            euCond.setUniversityId(u.getId());
+            euCond.setYear(planYear);
+            List<AEnrollUniversity> enrollUniversityList = aEnrollUniversityMapper.selectAEnrollUniversityList(euCond);
+            // 院校历史情况
+            AMarjorSubmit submitCond = new AMarjorSubmit();
+            submitCond.setExamineeType(examType);
+            submitCond.setUniversityId(universityId);
+            List<AMarjorSubmit> submitList = aMarjorSubmitMapper.selectAMarjorSubmitList(submitCond);
+
+            VoluntaryDto.AIResponse aiResp = new VoluntaryDto.AIResponse();
+            aiResp.setUniversity(u);
+
+            VoluntaryDto.SingleResponse sr;
+            List<VoluntaryDto.SingleResponse> singleResponseList = Lists.newArrayList();
+            if (CollectionUtils.isNotEmpty(currPlanList)) {
+                aiResp.setEnrollCode(currPlanList.get(0).getEnrollCode());
+                Map<String, BBusiWishUniversitiesProfession> professionMap = null == professionList ? Maps.newHashMap() : professionList.stream().collect(Collectors.toMap(BBusiWishUniversitiesProfession::getName, Function.identity()));
+                for (AMarjorPlan mp : currPlanList) {
+                    if (null != (sr = buildSingleResponse(submitYear, planYear, gender, mp, professionMap.get(mp.getMajorName()), enrollUniversityList, paramMap, u,
+                            historyPlanList, submitList, scoreList, specialList, mutexOptionMap, isAi, isScoreOnly))) {
+                        singleResponseList.add(sr);
+                    }
+                }
+            } else {
+                for (BBusiWishUniversitiesProfession prof : professionList) {
+                    if (null != (sr = buildSingleResponse(submitYear, planYear, gender, null, prof, enrollUniversityList, paramMap, u,
+                            historyPlanList, submitList, scoreList, specialList, mutexOptionMap, isAi, isScoreOnly))) {
+                        singleResponseList.add(sr);
+                    }
+                }
+            }
+
+            aiResp.setMajorDetails(singleResponseList);
+            sortAndExtractEnroll(singleResponseList, aiResp);
+            aiRespList.add(aiResp);
+        }
+        Collections.sort(aiRespList, new Comparator<VoluntaryDto.AIResponse>() {
+            @Override
+            public int compare(VoluntaryDto.AIResponse o1, VoluntaryDto.AIResponse o2) {
+                Integer d1 = o1.getEnrollRate();
+                Integer d2 = o2.getEnrollRate();
+                d1 = null == d1 ? 999 : (d1 == 0 ? 998 : d1);
+                d2 = null == d2 ? 999 : (d2 == 0 ? 998 : d2);
+                return d1.compareTo(d2);
+            }
+        });
+        return aiRespList;
+    }
+
+    private VoluntaryDto.SingleResponse buildSingleResponse(Integer submitYear, Integer planYear, String gender, AMarjorPlan currPlan, BBusiWishUniversitiesProfession prof,
+                                                          List<AEnrollUniversity> enrollUniversityList, Map<String, String> paramMap, BBusiWishUniversities u,
+                                                          List<AMarjorPlan> historyPlanList, List<AMarjorSubmit> submitList,
+                                                          List<AEnrollScore> scoreList, List<AEnrollSpecial> specialList, Map<String,String> mutexOptionMap, boolean isAi, boolean isScoreOnly) {
+        if (null != submitList) {
+            Collections.sort(submitList, new Comparator<AMarjorSubmit>() {
+                @Override
+                public int compare(AMarjorSubmit o1, AMarjorSubmit o2) {
+                    int iRet;
+                    if (0 != (iRet = o1.getYear().compareTo(o2.getYear()))) {
+                        return -iRet;
+                    }
+                    if (0 != (iRet = o1.getMajorName().compareTo(o2.getMajorName()))) {
+                        return -iRet;
+                    }
+                    if (null == o1.getMajorDirection()) {
+                        if (null != o2.getMajorDirection()) {
+                            return 1;
+                        }
+                        return 0;
+                    } else if (null == o2.getMajorDirection()) {
+                        return -1;
+                    }
+                    return 0;
+                }
+            });
+        } else {
+            submitList = Lists.newArrayList();
+        }
+        if (null != historyPlanList) {
+            Collections.sort(historyPlanList, new Comparator<AMarjorPlan>() {
+                @Override
+                public int compare(AMarjorPlan o1, AMarjorPlan o2) {
+                    int iRet;
+                    if (0 != (iRet = o1.getYear().compareTo(o2.getYear()))) {
+                        return -iRet;
+                    }
+                    if (0 != (iRet = o1.getMajorName().compareTo(o2.getMajorName()))) {
+                        return -iRet;
+                    }
+                    if (null == o1.getMajorDirection()) {
+                        if (null != o2.getMajorDirection()) {
+                            return 1;
+                        }
+                        return 0;
+                    } else if (null == o2.getMajorDirection()) {
+                        return -1;
+                    }
+                    return o1.getMajorDirection().compareTo(o2.getMajorDirection());
+                }
+            });
+        } else {
+            historyPlanList = Lists.newArrayList();
+        }
+
+        // 录取线
+        List<VoluntaryDto.MajorClearingHistory> clearings = Lists.newArrayList(); // 补录情况
+        List<VoluntaryDto.MajorEnrollHistory> histories = Lists.newArrayList(); // 初录情况
+
+        List<VoluntaryDto.MajorEnrollRule> ruleMatchList = Lists.newArrayList();
+        List<VoluntaryDto.MajorEnrollRule> improveList = Lists.newArrayList();
+        Double tmpValue; // 总分,得分
+        AMarjorSubmit lastSubmit = null;
+        Boolean typeChange = false;
+        VoluntaryDto.FormulaScoreStat formulaScoreStat = new VoluntaryDto.FormulaScoreStat();
+        String needMajor = StringUtils.trimToEmpty(null != currPlan ? currPlan.getMajorName() : (null != prof ? prof.getName() : ""));
+        String needDirect = StringUtils.trimToEmpty(null != currPlan ? currPlan.getMajorDirection() : "");
+        String needMajorDirect = needMajor + needDirect;
+        // 判断是否政策变化情况
+        List<AEnrollUniversity> validEuList = enrollUniversityList.stream().filter(t -> t.getMajorNames().contains(needMajor)).collect(Collectors.toList());
+        typeChange = CollectionUtils.isNotEmpty(validEuList) && new Integer(1).equals(validEuList.get(0).getTypeChange());
+
+        Set<String> existSet = Sets.newHashSet();
+        existSet.clear();
+        List<AMarjorSubmit> validSubmitList = Lists.newArrayList();
+        if (null != submitList) {
+            for (AMarjorSubmit s : submitList) { // 历年录取通过专业分来判断差别
+                String majorNameDirect = s.getMajorName() + StringUtils.trimToEmpty(s.getMajorDirection());
+                if (needMajorDirect.equals(majorNameDirect) && existSet.add(needMajorDirect + s.getYear() + s.getEnrollType())) {
+                    validSubmitList.add(s);
+                    if (s.getYear().equals(submitYear) && s.getEnrollType().equals("初录") ) {
+                        lastSubmit = s; // TODO 初录,补录与分数线的关系 要处理
+                    }
+                }
+            }
+        }
+        // 分项规则检查
+        existSet.clear();
+        Double currTotal = null;
+        if (null != scoreList) {
+            Double inputScoreRate = NumberUtils.toInt(paramMap.get("ScoreRate"), 0) / 100.0;
+            List<AEnrollScore> fEnrollScoreList = Lists.newArrayList();
+            String inclMajorDirection = needMajor + "(" + needDirect + ")";
+            String exclMajorDirection = needMajor + "(";
+            Map<Boolean, List<AEnrollScore>> majorEnrollScoresMap = scoreList.stream().collect(Collectors.groupingBy(t -> StringUtils.isNotBlank(t.getMajorNames())));
+
+            List<AEnrollScore> tmpEnrollScoreList = majorEnrollScoresMap.get(Boolean.TRUE);
+            AEnrollScore skillEnrollScore = null;
+            if (null != tmpEnrollScoreList) {
+                for (AEnrollScore r : tmpEnrollScoreList) {
+                    if (StringUtils.isNotBlank(r.getMajorNames())) {
+                        if (!r.getMajorNames().contains(needMajor)) {
+                            continue;
+                        }
+                        if (StringUtils.isNotBlank(r.getMajorDirections())) { // 有单独的方向时,分开判断
+                            if (StringUtils.isBlank(needDirect) || !r.getMajorDirections().contains(needDirect)) {
+                                continue;
+                            }
+                        } else if (StringUtils.isNotBlank(needDirect)) { // 有direct要求时,这里需要有 "<专业>(<方向>)
+                            if (!r.getMajorNames().contains(inclMajorDirection)) {
+                                continue;
+                            }
+                        } else if (r.getMajorNames().contains(exclMajorDirection)) { // 当无direct要求时,这时不能有 "<专业>("
+                            continue;
+                        }
+                    }
+                    Boolean isSkillScore = isScoreOnly && r.getItemType().equals("ScoreSkill");
+                    if(isSkillScore) {
+                        skillEnrollScore = r;
+                    }
+                    appendScoreRule(formulaScoreStat, existSet, r, fEnrollScoreList, isAi, paramMap, inputScoreRate, isSkillScore);
+                }
+            }
+            if (fEnrollScoreList.size() == 0 && null != (tmpEnrollScoreList = majorEnrollScoresMap.get(Boolean.FALSE))) {
+                for (AEnrollScore r : tmpEnrollScoreList) {
+                    Boolean isSkillScore = isScoreOnly && r.getItemType().equals("ScoreSkill");
+                    if(isSkillScore) {
+                        skillEnrollScore = r;
+                    }
+                    appendScoreRule(formulaScoreStat, existSet, r, fEnrollScoreList, isAi, paramMap, inputScoreRate, isSkillScore);
+                }
+            }
+
+            boolean isSameYear = null == lastSubmit || planYear.equals(lastSubmit.getYear());
+            currTotal = formulaScoreStat.getTypeValue("StatRateScore", "", false);
+            for (AEnrollScore r : fEnrollScoreList) {
+                VoluntaryDto.MajorEnrollRule mr = buildEnrollRule(r);
+                mr.setContent(StringUtils.isNotBlank(r.getEqualFormula()) ? r.getEqualFormula() : r.getEnrollFormula());
+                if(isScoreOnly) {
+                    String vt;
+                    if (null != (vt = r.getValueType()) && vt.startsWith("Score")) {
+                        improveList.add(mr);
+                        mr.setCategory(VoluntaryDto.EnumRuleCategory.Enroll);
+                        mr.setType(VoluntaryDto.EnumRuleType.ScoreTotal);
+                        // mr.setValue(null != currTotal ? String.valueOf(currTotal.intValue()) : "");
+                        if (null == lastSubmit || null == lastSubmit.getScore() || null == lastSubmit.getScoreTotal() && !isSameYear) {
+                            mr.setFailedMessage("缺历史录取数据");
+                            continue;
+                        }
+                        mr.setYear(lastSubmit.getYear());
+                        if (typeChange) {
+                            mr.setFailedMessage("规则变更,无法计算");
+                            continue;
+                        } else if (!formulaScoreStat.isScoreValid() && (!isAi || inputScoreRate <= 0.0)) {
+                            mr.setFailedMessage("用户没有填写完整");
+                            continue;
+                        }
+                        Integer validScoreTotal = isSameYear ? formulaScoreStat.getAllTotal() : lastSubmit.getScoreTotal();
+                        Double currScoreRate = isSameYear ? 1.0 : formulaScoreStat.getAllTotal() * 1.0 / lastSubmit.getScoreTotal(); // 去年的分转成今年的分
+                        EnrollRateCalculator.RateLevel rl;
+                        if (null != lastSubmit.getScore() && currTotal != null && null != (rl = enrollRateCalculator.satisfy(validScoreTotal, lastSubmit.getScore(), currTotal / currScoreRate))) { // 按去年标准算概率
+                            Double skillRate = Double.parseDouble(skillEnrollScore.getValueRule());
+                            mr.setValue(String.valueOf(Math.round(Math.round(lastSubmit.getScore() * currScoreRate) - currTotal) / skillRate));
+                            mr.setValid(true);
+                        } else {
+                            mr.setFailedMessage("没有对应计算条件");
+                            continue;
+                        }
+                    } else {
+                        ruleMatchList.add(mr);
+                        mr.setValid(true);
+                    }
+                    continue;
+                }
+                ruleMatchList.add(mr);
+                String vt;
+                if (null == (vt = r.getValueType())) { // 无值时相当于直接通过
+                    mr.setValid(true);
+                } else if (null != (vt = r.getValueType()) && vt.startsWith("Score")) {
+                    mr.setValue(null != currTotal ? String.valueOf(currTotal.intValue()) : "");
+                    if (null == lastSubmit || null == lastSubmit.getScore() || null == lastSubmit.getScoreTotal() && !isSameYear) {
+                        mr.setFailedMessage("缺历史录取数据");
+                        continue;
+                    } else if (typeChange) {
+                        mr.setFailedMessage("规则变更,无法计算");
+                        continue;
+                    } else if (!formulaScoreStat.isValid() && (!isAi || inputScoreRate <= 0.0)) {
+                        mr.setFailedMessage("用户没有填写完整");
+                        continue;
+                    }
+                    mr.setValid(true);
+
+                    Integer validScoreTotal = isSameYear ? formulaScoreStat.getAllTotal() : lastSubmit.getScoreTotal();
+                    Double currScoreRate = isSameYear ? 1.0 : formulaScoreStat.getAllTotal() * 1.0 / lastSubmit.getScoreTotal(); // 去年的分转成今年的分
+                    Double currScore = null;
+                    if (null != lastSubmit.getCulturalScore()) {
+                        if (null == (currScore = formulaScoreStat.getTypeValue("ScoreSingle", "", false))) {
+                            currScore = formulaScoreStat.getTypeValue("ScoreBase", "", false);
+                        }
+                        if (null != currScore && currScore < lastSubmit.getCulturalScore() * currScoreRate) {
+                            // mr.setValid(false);
+                            mr.setMissingValue(String.valueOf(Math.round(lastSubmit.getCulturalScore() * currScoreRate - currScore)));
+                            mr.setFailedMessage("校考分低于分数线");
+                        }
+                    }
+                    if (null != lastSubmit.getProfScore() && null != (currScore = formulaScoreStat.getTypeValue("ScoreSkill", "", false)) && currScore < lastSubmit.getProfScore() * currScoreRate) {
+                        // mr.setValid(false);
+                        mr.setMissingValue(String.valueOf(Math.round(lastSubmit.getProfScore() * currScoreRate - currScore)));
+                        mr.setFailedMessage("技能测试分低于分数线");
+                    }
+                    // 检查总分,学考(文化),专业(技能)
+                    EnrollRateCalculator.RateLevel rl;
+                    if (null != lastSubmit.getScore() && currTotal != null && null != (rl = enrollRateCalculator.calSchoolEnrollRate(validScoreTotal, lastSubmit.getScore(), currTotal / currScoreRate))) { // 按去年标准算概率
+                        mr.setRl(rl);
+                        mr.setEnrollRate(rl.rate);
+                        mr.setEnrollRateText(rl.typeLabel);
+                        mr.setEnumPickType(rl.type);
+                        if (mr.getValid() && currTotal < lastSubmit.getScore() * currScoreRate) {
+                            // mr.setValid(false);
+                            mr.setMissingValue(String.valueOf(Math.round(lastSubmit.getScore() * currScoreRate - currTotal))); // 按今年的算差值
+                            mr.setFailedMessage("综合分低于分数线");
+                        }
+                    }
+                } else { // 专项符合型
+                    mr.setValid(processPassCheck(paramMap, formulaScoreStat, r.getValueRule(), mr, r.getValueType(), r.getItemField(), r.getValuePassRule()));
+                }
+            }
+        }
+        if (null != specialList) {
+            existSet.clear();
+            List<AEnrollSpecial> validSpecialList = Lists.newArrayList();
+            Map<String, Pair<String, Set<String>>> groupMutexOptionsMap = Maps.newHashMap();
+            for (AEnrollSpecial r : specialList) {
+                if (StringUtils.isNotBlank(r.getGender()) && !r.getGender().equals(gender)
+                        || StringUtils.isNotBlank(r.getMajorName()) && !Sets.newHashSet(r.getMajorName().split(",")).contains(needMajor)
+                        || StringUtils.isNotBlank(r.getMajorDirection()) && !r.getMajorDirection().equals(needDirect)) {
+                    continue;
+                }
+                if(r.getValueType().startsWith("StatBMI")) { // TODO 身高体重的名称需要是固定的
+                    String height = paramMap.get("身高");
+                    String weight = paramMap.get("体重");
+                    Integer iHeight;
+                    if (StringUtils.isNotBlank(height) && StringUtils.isNotBlank(weight) && (iHeight = NumberUtils.toInt(height, 0)) > 0) {
+                        paramMap.put(r.getItemField(), String.valueOf(NumberUtils.toInt(height, 0) * 100 / iHeight));
+                    }
+                } else if (null != r.getItemGroup() && "Checkbox".equals(r.getValueType())) {
+                    Pair<String, Set<String>> p = getGroupOptions(groupMutexOptionsMap, mutexOptionMap, paramMap, r.getItemGroup());
+                    if (p.getRight().size() == 0) { // 无值时
+                        paramMap.put(r.getItemField(), null);
+                    } else if (StringUtils.isNotBlank(p.getLeft()) && p.getRight().contains(p.getLeft())) { // 选中互斥时全是否
+                        paramMap.put(r.getItemField(), !p.getLeft().contains("无") ? "[\"是\"]" : "[\"否\"]");
+                    } else {
+                        paramMap.put(r.getItemField(), p.getRight().contains(r.getItemName()) ? "[\"是\"]" : "[\"否\"]");
+                    }
+                }
+                validSpecialList.add(r);
+            }
+            for (AEnrollSpecial r : validSpecialList) {
+                VoluntaryDto.MajorEnrollRule mr = new VoluntaryDto.MajorEnrollRule();
+                mr.setCategory(VoluntaryDto.EnumRuleCategory.Special);
+                mr.setContent(r.getEnrollFormula());
+                mr.setDescription(r.getComment());
+                ruleMatchList.add(mr);
+                if(isScoreOnly) {
+                    mr.setType(VoluntaryDto.EnumRuleType.Readonly);
+                    mr.setValid(true);
+                    continue;
+                }
+                mr.setType(itemType2RuleType(r.getItemType(), r.getValueType(), false));
+                if (VoluntaryDto.EnumRuleType.Readonly.equals(mr.getType())) {
+                    mr.setValid(true);
+                } else {
+                    mr.setValid(processPassCheck(paramMap, formulaScoreStat, r.getValueRule(), mr, r.getValueType(), r.getItemField(), r.getValuePassRule()));
+                }
+            }
+        }
+        existSet.clear();
+        Map<String, AMarjorPlan> historyPlanMap = historyPlanList.stream().collect(Collectors.toMap(t -> t.getYear() + t.getMajorName() + StringUtils.trimToEmpty(t.getMajorDirection()), Function.identity()));
+        for (AMarjorSubmit s : validSubmitList) {
+            if ("初录".equals(s.getEnrollType())) {
+                VoluntaryDto.MajorEnrollHistory h = new VoluntaryDto.MajorEnrollHistory();
+                h.setYear(s.getYear());
+                h.setScore(null != s.getScore() ? String.valueOf(s.getScore()) : ""); // TODO 240301 直接输出录取分
+                /*if (null != currTotal && null != s.getScore()) {
+                    Double diff = currTotal - s.getScore();
+                    h.setScore(diff > 0 ? "+" + diff.intValue() : "" + diff.intValue()); // TODO 需要一个分来比较,综合分?
+                } else {
+                    h.setScore("");
+                }*/
+                AMarjorPlan p = historyPlanMap.get(s.getYear() + s.getMajorName() + StringUtils.trimToEmpty(s.getMajorDirection()));
+                if(null != p) {
+                    h.setPlan(p.getPlanTotal()); // TODO 估计输出录取人数 23.12.26 分开输入录取和计划 24.12.27
+                } else {
+                    // System.out.println(s.getId());
+                }
+                h.setEnroll(s.getEnrollTotal());
+                histories.add(h);
+            } else {
+                /*VoluntaryDto.MajorClearingHistory c = new VoluntaryDto.MajorClearingHistory();
+                c.setYear(s.getYear());
+                if (null != currTotal && null != s.getScore()) {
+                    Double diff = currTotal - s.getScore();
+                    c.setScore(diff > 0 ? "+" + diff.intValue() : "" + diff.intValue()); // TODO 需要一个分来比较,综合分?
+                } else {
+                    c.setScore("");
+                }
+                c.setRealNum(s.getEnrollTotal());
+                clearings.add(c);*/ // TODO 240301 关闭补录
+            }
+        }
+
+        VoluntaryDto.SingleResponse sr = new VoluntaryDto.SingleResponse();
+        sr.setUniversityCode(u.getCode());
+        sr.setMajorCode(null != prof ? prof.getCode() : "");
+        sr.setMajorName(needMajor);
+        sr.setMajorDirection(needDirect);
+        // 查找最高的专业 EnrollRule 为本专业的,理论上只有一个合并rule有此值
+        EnrollRateCalculator.RateLevel lastRl = null;
+        boolean specialValid = true, specialPass = true;
+        for (VoluntaryDto.MajorEnrollRule er : ruleMatchList) {
+            if (null != er.getRl() && (null == lastRl || er.getRl().getRate() < lastRl.getRate())) {
+                lastRl = er.getRl();
+            }
+            if ((specialPass || specialValid) && VoluntaryDto.EnumRuleType.Special.equals(er.getType())) {
+                if (null == er.getValid()) {
+                    specialValid = false;
+                } else if (!er.getValid()) {
+                    specialPass = false;
+                }
+            }
+        }
+        if (!specialPass) {
+            sr.setEnrollRate(0);
+            sr.setEnrollRateText("零概率");
+            sr.setEnumPickEmpty(VoluntaryDto.EnumPickEmpty.EnrollPass);
+        } else if (null != lastRl && specialValid) {
+            sr.setEnumPickType(lastRl.getType());
+            sr.setEnrollRate(lastRl.getRate());
+            sr.setEnrollRateText(lastRl.getTypeLabel());
+        } else {
+            sr.setEnrollRateText("无概率");
+            sr.setEnumPickEmpty(VoluntaryDto.EnumPickEmpty.New);
+        }
+        if (null != currPlan) {
+            sr.setMajorGroup(currPlan.getMajorGroup()); // 没有MajorEnrollId, 暂时用这个
+            sr.setMajorEnrollCode(StringUtils.isBlank(currPlan.getMajorEnrollCode()) ? String.valueOf(currPlan.getId()) : currPlan.getMajorEnrollCode());
+            sr.setTips(null != currPlan ? currPlan.getComment() : "");
+        }
+        sr.setHistories(histories);
+        sr.setClearings(clearings);
+        sr.setRules(ruleMatchList);
+        sr.setImproves(improveList);
+        sr.setScore(currTotal);
+        if (!isScoreOnly && null != lastSubmit && null != lastSubmit.getScore() && null != currTotal && currTotal < lastSubmit.getScore()) {
+            Double diffValue = lastSubmit.getScore() - currTotal;
+            sr.setImproves(formulaScoreStat.getImproveScore(diffValue));
+        }
+        return sr;
+    }
+
+    private void appendScoreRule(VoluntaryDto.FormulaScoreStat formulaScoreStat, Set<String> existSet, AEnrollScore r, List<AEnrollScore> fEnrollScoreList,
+                                 Boolean isAi, Map<String, String> paramMap, Double inputScoreRate, Boolean isSkillScore) {
+        String vt, iv;
+        Double currScore = null;
+        Double scoreRate = null;
+        Double tmpValue; // 总分,得分
+        if (null != (vt = r.getValueType()) && vt.startsWith("Score")) { //需要合并Score型(ScoreSingle, ScoreBase, ScoreSkill)的各类成绩, 其他为符合条件
+            if (existSet.add(r.getEnrollFormula())) { // 合并规则只留第一个
+                if (!existSet.add("_ScoreFormula_")) {
+                    existSet.remove(r.getEnrollFormula());
+                    return;
+                }
+                fEnrollScoreList.add(r);
+            }
+            if (null != r.getValueRule() && (scoreRate = Double.parseDouble(r.getValueRule())) <= 0.0) {
+                scoreRate = 1.0;
+            }
+            formulaScoreStat.addTypeTotal(r.getScoreTotal());
+            if (isAi && vt.equals("ScoreRate")) { // 根据得分率及总分计算分值
+                currScore = isSkillScore ? 0.0 : r.getScoreTotal() * inputScoreRate;
+                formulaScoreStat.addGroup(r.getItemGroup(), currScore, r.getScoreTotal().doubleValue(), scoreRate);
+                formulaScoreStat.addType(r.getItemType(), r.getItemField(), currScore, scoreRate);
+            } else {
+                currScore = (!isSkillScore && StringUtils.isNotBlank((iv = paramMap.get(r.getItemField())))) ? NumberUtils.toDouble(iv, 0.0) : 0.0;
+                formulaScoreStat.addGroup(r.getItemGroup(), currScore, r.getScoreTotal().doubleValue(), scoreRate);
+                formulaScoreStat.addType(r.getItemType(), r.getItemField(), currScore, scoreRate);
+            }
+        } else {
+            fEnrollScoreList.add(r);
+        }
+    }
+
+    private VoluntaryDto.MajorEnrollRule buildEnrollRule(AEnrollScore r) {
+        VoluntaryDto.MajorEnrollRule mr = new VoluntaryDto.MajorEnrollRule();
+        mr.setCategory(VoluntaryDto.EnumRuleCategory.Enroll);
+        mr.setType(itemType2RuleType(r.getItemType(), r.getValueType(), true)); // 学考 ScoreSingle 强转为 ScoreTotal, ScoreBase 校考, ScoreSkill 技能, Special 专项
+        mr.setContent(r.getEnrollFormula());
+        mr.setImproveType(r.getItemCategory());
+        mr.setDescription(r.getComment());
+        return mr;
+    }
+    private Pair<String, Set<String>> getGroupOptions(Map<String, Pair<String, Set<String>>> groupOptionsMap, Map<String, String> mutexOptionMap, Map<String, String> paramMap, String itemGroup) {
+        String[] groupNameField = StringUtils.split(itemGroup, "_");
+        String groupFieldName = groupNameField[groupNameField.length > 1 ? 1 : 0];
+        Pair<String, Set<String>> pair = groupOptionsMap.get(groupFieldName);
+        if(null == pair) {
+            String iv = paramMap.get(groupFieldName);
+            Set<String> options = StringUtils.isNotBlank(iv) ? JSONArray.parseArray(iv).stream().map(t -> (String) t).collect(Collectors.toSet()) : Sets.newHashSet();
+            pair = new MutablePair<>(mutexOptionMap.get(groupFieldName), options);
+            groupOptionsMap.put(groupFieldName, pair);
+        }
+        return pair;
+    }
+
+    private Boolean processPassCheck(Map<String, String> paramMap, VoluntaryDto.FormulaScoreStat stat, String valueRule,
+                                          VoluntaryDto.MajorEnrollRule mr, String valueType, String itemField, String passRule) {
+        Boolean matched = null;
+        Double tmpValue;
+        boolean isStat = null != valueType && valueType.startsWith("Stat");
+        if (isStat || NumberTypeSet.contains(valueType)) {
+            Double v = null;
+            String iv;
+            if (isStat) { // TODO 统计分
+                v = stat.getTypeValue(valueType, itemField, "0".equals(valueRule));
+            } else if(StringUtils.isNotBlank(passRule) && StringUtils.isNotBlank((iv = paramMap.get(itemField)))
+                    && (tmpValue = NumberUtils.toDouble(iv, 0.0)) > 0.01) {
+                v = tmpValue;
+            }
+            if (v != null) {
+                String[] range = passRule.split("-");
+                if (StringUtils.isNotBlank(range[0]) && (tmpValue = NumberUtils.toDouble(range[0], 0.0)) > 0.01 && v < tmpValue) {
+                    matched = false;
+                    mr.setFailedMessage("小于最大值");
+                } else if (range.length > 1 && StringUtils.isNotBlank(range[1]) && (tmpValue = NumberUtils.toDouble(range[1], 0.0)) > 0.01 && v > tmpValue) {
+                    matched = false;
+                    mr.setFailedMessage("超过最大值");
+                } else {
+                    matched = true;
+                }
+            } else {
+                mr.setFailedMessage("未输入参数");
+            }
+        } else if ("Radio".equals(valueType)) {
+            String iv = paramMap.get(itemField);
+            if (StringUtils.isNotBlank(iv)) {
+                if (!(matched = StringUtils.isNotBlank(passRule) && passRule.contains(iv))) {
+                    mr.setFailedMessage("条件不满足");
+                }
+            } else {
+                mr.setFailedMessage("未输入参数");
+            }
+        } else if ("Checkbox".equals(valueType) || "Picker".equals(valueType)) {
+            String iv = paramMap.get(itemField);
+            if(StringUtils.isBlank(passRule)) {
+                if (!(matched = StringUtils.isBlank(iv))) {
+                    mr.setFailedMessage("选择条件不满足1");
+                }
+            } else if (StringUtils.isNotBlank(iv)) {
+                List<String> valueList = Arrays.asList(passRule.split(","));
+                List<String> inputList = JSONArray.parseArray(iv).stream().map(t -> (String) t).collect(Collectors.toList());
+                if (!(matched = CollectionUtils.intersection(valueList, inputList).size() == valueList.size())) {
+                    mr.setFailedMessage("选择条件不满足2");
+                }
+            }
+        }
+        return matched;
+    }
+    private void sortAndExtractEnroll(List<VoluntaryDto.SingleResponse> singleResponseList, VoluntaryDto.AIResponse aiResp) {
+        if (CollectionUtils.isEmpty(singleResponseList)) {
+            aiResp.setEnrollRate(null);
+            aiResp.setEnrollRateText("无概率");
+            aiResp.setEnumPickEmpty(VoluntaryDto.EnumPickEmpty.EnrollPass);
+        }
+        Collections.sort(singleResponseList, new Comparator<VoluntaryDto.SingleResponse>() {
+            @Override
+            public int compare(VoluntaryDto.SingleResponse o1, VoluntaryDto.SingleResponse o2) {
+                Integer d1 = o1.getEnrollRate();
+                Integer d2 = o2.getEnrollRate();
+                if (null == d1) {
+                    if (null != d2) {
+                        return 1;
+                    }
+                    return 0;
+                } else if (null == d2) {
+                    return -1;
+                }
+                return -d1.compareTo(d2);
+            }
+        });
+        for (VoluntaryDto.SingleResponse sr : singleResponseList) {
+            aiResp.setEnrollRate(sr.getEnrollRate());
+            aiResp.setEnrollRateText(sr.getEnrollRateText());
+            aiResp.setEnumPickType(sr.getEnumPickType());
+            aiResp.setEnumPickEmpty(sr.getEnumPickEmpty());
+            break;
+        }
+    }
+
+    public R<VoluntaryDto.VoluntaryConfig> getVoluntaryConfig() { // 如果有填报配置相关,放在这里
+        VoluntaryDto.VoluntaryConfig resp = new VoluntaryDto.VoluntaryConfig();
+        return R.ok(resp);
+    }
+
+    public List<JSONObject> getVoluntaryList(@ApiParam @RequestParam VoluntaryDto.EnumVoluntaryType type) { // 我的志愿表 // 后台填充快照缺省
+        AWishRecord cond = new AWishRecord();
+        cond.setUserId(SecurityUtils.getLoginUser().getUserId());
+        if (null != type) {
+            cond.setType(type.name());
+        }
+        cond.setStatus(1);
+        List<AWishRecord> aWishRecordList = aWishRecordMapper.selectAWishRecordList(cond);
+        List<JSONObject> dtoList = Lists.newArrayList();
+        aWishRecordList.stream().forEach(t -> {
+            dtoList.add(toModel(t));
+        });
+        return PageUtil.getDtoListWithPageable(aWishRecordList, dtoList);
+    }
+
+    public R<Long> submitVoluntary(JSONObject model) { // 填报 // 前端+后台按需要剔除一些不需快照的信息(目前主要是院校信息)
+        SysUser user = SecurityUtils.getLoginUser().getUser();
+        AWishRecord wishRecord = new AWishRecord();
+        VoluntaryDto.User userSnapshot = new VoluntaryDto.User();
+        userSnapshot.setName(user.getNickName());
+        userSnapshot.setSex(user.getSex());
+        userSnapshot.setExamType(user.getExamType().title()); // TODO MF
+        userSnapshot.setProvinceName(user.getLocation());
+
+        Long modelId = null;
+        String modelName = null;
+        Integer year = null;
+        VoluntaryDto.EnumVoluntaryType type = null;
+        try {
+            modelId = model.getLong("id");
+            modelName = model.getString("name");
+            year = model.getInteger("year");
+            type = VoluntaryDto.EnumVoluntaryType.valueOf(model.getString("voluntaryType"));
+            wishRecord.setBatchName(model.getString("batchName"));
+            wishRecord.setRequest(model.getString("request"));
+            wishRecord.setDetails(model.getString("details"));
+            wishRecord.setUserSnapshot(mapper.writeValueAsString(userSnapshot));
+        } catch (JsonProcessingException e) {
+            return R.fail("格式错误: " + modelId);
+        }
+        wishRecord.setYear(null == year ? getPlanYear(user) : year);
+        wishRecord.setType(type.name());
+        if (null != modelId) {
+            wishRecord.setUpdateTime(new Date());
+            wishRecord.setId(modelId);
+            wishRecord.setName(modelName);
+            wishRecord.setUpdateTime(new Date());
+            aWishRecordMapper.updateAWishRecord(wishRecord);
+        } else {
+            wishRecord.setCreateTime(new Date());
+            wishRecord.setSeq(getWishSeq(wishRecord.getYear(), wishRecord.getType()));
+            wishRecord.setName(StringUtils.isNotBlank(modelName) ? modelName : ("AI".equals(wishRecord.getType()) ? "AI志愿" : "模拟志愿") + wishRecord.getSeq());
+            wishRecord.setUserId(user.getUserId());
+            wishRecord.setStatus(1);
+            aWishRecordMapper.insertAWishRecord(wishRecord);
+        }
+        return R.ok(wishRecord.getId());
+    }
+
+    private Integer getWishSeq(Integer year, String type) {
+        AWishRecord cond = new AWishRecord();
+        cond.setUserId(SecurityUtils.getLoginUser().getUserId());
+        cond.setYear(year);
+        cond.setType(type);
+        List<AWishRecord> aWishRecordList = aWishRecordMapper.selectAWishRecordList(cond);
+        Integer newSeq = CollectionUtils.isNotEmpty(aWishRecordList) ? aWishRecordList.get(0).getSeq() + 1 : 1;
+        return newSeq;
+    }
+
+    public void obsoleteWishRecord(Long userId) {
+        aWishRecordMapper.updateObsoleteByUser(userId);
+        busiWishRecordsMapper.updateObsoleteByUser(userId);
+    }
+
+    public R<JSONObject> getVoluntary(Long id) { // 志愿表详情 // 后台填充快照缺省
+        SysUser user = SecurityUtils.getLoginUser().getUser();
+        AWishRecord wishRecord = aWishRecordMapper.selectAWishRecordById(id);
+        if (null == wishRecord || !SecurityUtils.getUserId().equals(wishRecord.getUserId())) {
+            return R.fail("错误id");
+        }
+        if (null == wishRecord) {
+            throw new ServiceException("无此志愿id");
+        } else if (!user.getUserId().equals(wishRecord.getUserId())) {
+            throw new ServiceException("无此志愿id号");
+        }
+        JSONObject resp = toModel(wishRecord);
+        return R.ok(resp);
+    }
+
+    private JSONObject toModel(AWishRecord wishRecord) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        JSONObject model = new JSONObject();
+        model.put("id", wishRecord.getId());
+        model.put("year", wishRecord.getYear());
+        model.put("voluntaryType", VoluntaryDto.EnumVoluntaryType.valueOf(wishRecord.getType()));
+        model.put("name", wishRecord.getName());
+        model.put("batchName", wishRecord.getBatchName());
+        model.put("createTime", sdf.format(wishRecord.getCreateTime()));
+        try {
+            model.put("userSnapshot", mapper.readValue(wishRecord.getUserSnapshot(), VoluntaryDto.User.class));
+            model.put("request", JSONObject.parseObject(wishRecord.getRequest()));
+            model.put("details", JSONArray.parseArray(wishRecord.getDetails()));
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+        }
+        return model;
+    }
+
+    private VoluntaryDto.SingleResponse buildTestResp() {
+        String json = sysConfigService.selectConfigByKey("demo.voluntary.single");
+        if (StringUtils.isNotBlank(json)) {
+            try {
+                return mapper.readValue(json, VoluntaryDto.SingleResponse.class);
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+            }
+        }
+        return new VoluntaryDto.SingleResponse();
+    }
+
+    private VoluntaryDto.AIResponse buildAiResp() {
+        String json = sysConfigService.selectConfigByKey("demo.voluntary.ai");
+        if (StringUtils.isNotBlank(json)) {
+            try {
+                VoluntaryDto.AIResponse resp = mapper.readValue(json, VoluntaryDto.AIResponse.class);
+                resp.setMajorDetails(Lists.newArrayList(buildTestResp()));
+                return resp;
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+            }
+        }
+        return new VoluntaryDto.AIResponse();
+    }
+
+    private VoluntaryDto.VoluntaryModel buildModel() {
+        String json = sysConfigService.selectConfigByKey("demo.voluntary.model");
+        if(StringUtils.isNotBlank(json)) {
+            try {
+                VoluntaryDto.VoluntaryModel model = mapper.readValue(json, VoluntaryDto.VoluntaryModel.class);
+                model.setDetails(Lists.newArrayList(buildAiResp()));
+                return model;
+            } catch (JsonProcessingException e) {
+                e.printStackTrace();
+            }
+        }
+        return new VoluntaryDto.VoluntaryModel();
+    }
+
+    private VoluntaryDto.EnumRuleType itemType2RuleType(String itemType, String valueType, boolean foreSingleTotal) {
+        if ("Notice".equals(valueType)) {
+            return VoluntaryDto.EnumRuleType.Readonly;
+        } else if (foreSingleTotal && VoluntaryDto.EnumRuleType.ScoreSingle.name().equals(itemType)) {
+            return VoluntaryDto.EnumRuleType.ScoreTotal;
+        }
+        return VoluntaryDto.EnumRuleType.valueOf(itemType);
+    }
+}

+ 95 - 0
ie-admin/src/main/java/com/ruoyi/web/util/IosVerifyUtil.java

@@ -0,0 +1,95 @@
+package com.ruoyi.web.util;
+
+import javax.net.ssl.*;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Locale;
+
+/**
+ * 苹果IAP内购验证工具类
+ */
+public class IosVerifyUtil {
+    private static class TrustAnyTrustManager implements X509TrustManager {
+        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        }
+        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        }
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[] {};
+        }
+    }
+    private static class TrustAnyHostnameVerifier implements HostnameVerifier {
+        public boolean verify(String hostname, SSLSession session) {
+            return true;
+        }
+    }
+    private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
+    private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";
+    /**
+     * 苹果服务器验证
+     *
+     * @param receipt
+     * 账单
+     * @url 要验证的地址
+     * @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
+     *
+     */
+    public static String buyAppVerify(String receipt,int type) {
+        //环境判断 线上/开发环境用不同的请求链接
+        String url = "";
+        if(type==0){
+            url = url_sandbox; //沙盒测试
+        }else{
+            url = url_verify; //线上测试
+        }
+        //String url = EnvUtils.isOnline() ?url_verify : url_sandbox;
+        try {
+            SSLContext sc = SSLContext.getInstance("SSL");
+            sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
+            URL console = new URL(url);
+            HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
+            conn.setSSLSocketFactory(sc.getSocketFactory());
+            conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("content-type", "text/json");
+            conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
+            conn.setDoInput(true);
+            conn.setDoOutput(true);
+            BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
+            String str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}");//拼成固定的格式传给平台
+            hurlBufOus.write(str.getBytes());
+            hurlBufOus.flush();
+            InputStream is = conn.getInputStream();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+            String line = null;
+            StringBuffer sb = new StringBuffer();
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception ex) {
+            System.out.println("苹果服务器异常");
+            ex.printStackTrace();
+        }
+        return null;
+    }
+    /**
+     * 用BASE64加密
+     *
+     * @param str
+     * @return
+     */
+    public static String getBASE64(String str) {
+        byte[] b = str.getBytes();
+        String s = null;
+        if (b != null) {
+            s = new sun.misc.BASE64Encoder().encode(b);
+        }
+        return s;
+    }
+}

+ 110 - 0
ie-admin/src/main/java/com/ruoyi/web/util/MentalHealthExporter.java

@@ -0,0 +1,110 @@
+package com.ruoyi.web.util;
+
+import com.google.common.collect.Maps;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.springframework.http.MediaType;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MentalHealthExporter {
+
+    public static void export(HttpServletResponse response, Long examineeId, Map<String, Integer> categoryScoreMap) {
+        if (response == null) {
+            response.setStatus(404);
+            return;
+        }
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+            String realFileName = examineeId + "_" + sdf.format(new Date()) + ".docx";
+
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            String text = IOUtils.toString(MentalHealthExporter.class.getResourceAsStream("/templates/mht_report.xml"), "utf8");
+            Map<String, String> valueMap = Maps.newHashMap();
+            String newText = replaceChooseValue(text, categoryScoreMap, valueMap);
+            String finalText = replaceVarValue(newText, valueMap);
+            // OutputStream os = new FileOutputStream("D:\\data\\wish_output.xls");
+            PrintWriter pw = new PrintWriter(response.getOutputStream());
+            pw.write(finalText);
+            pw.flush();
+        } catch (UnsupportedEncodingException e) {
+            response.setStatus(404);
+        } catch (IOException e) {
+            response.setStatus(404);
+        }
+        return;
+    }
+
+    public static void test(Long examineeId, Map<String, Integer> categoryScoreMap) throws IOException {
+        String text = IOUtils.toString(MentalHealthExporter.class.getResourceAsStream("/templates/mht_report.xml"), "utf8");
+        Map<String, String> valueMap = Maps.newHashMap();
+        String newText = replaceChooseValue(text, categoryScoreMap, valueMap);
+        return;
+    }
+
+
+    private static String replaceChooseValue(String text, Map<String, Integer> categoryScoreMap, Map<String, String> valueMap) {
+        StringBuffer sb = new StringBuffer();
+        boolean needXml = true;
+        boolean needPart = true;
+        String processingTag = "";
+        String[] partList = text.split("<!-- | -->");
+        for (String s : partList) {
+            if (s.contains("<")) { // xml
+                if (needXml && needPart) {
+                    sb.append(s);
+                }
+                continue;
+            }
+            String[] datas = s.split("=");
+            if (datas.length == 1) {
+                if (datas[0].equals(processingTag)) {
+                    needXml = true;
+                } else {
+                    needXml = !"效度量表".equals(datas[0]);
+                    processingTag = datas[0];
+                }
+                needPart = true;
+                continue;
+            }
+            Integer score = categoryScoreMap.get(datas[0]);
+            if (null == score) {
+                needPart = false;
+                continue;
+            }
+            String[] scores = datas[1].split("-", -1);
+            needPart = (StringUtils.isBlank(scores[0]) || NumberUtils.toInt(scores[0]) <= score) && (StringUtils.isBlank(scores[1]) || score <= NumberUtils.toInt(scores[1]));
+            if (needPart) {
+                valueMap.put(datas[0], StringUtils.isBlank(scores[0]) ? "006400" : (StringUtils.isBlank(scores[1]) ? "FF0000" : "000000"));
+            }
+            continue;
+        }
+        return sb.toString();
+    }
+
+    private static String replaceVarValue(String text, Map<String, String> valueMap) {
+        String pattern = "(\\$\\{(.+?)\\})";
+        Pattern p = Pattern.compile(pattern);
+        Matcher m = p.matcher(text);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String key = m.group(1);
+            String vk = key.substring(2, key.length() - 1);
+            String value = valueMap.get(vk);
+            m.appendReplacement(sb, null == value ? "" : value);
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+}

+ 164 - 0
ie-admin/src/main/java/com/ruoyi/web/util/VolunteerExporter.java

@@ -0,0 +1,164 @@
+package com.ruoyi.web.util;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.syzy.domain.BBusiWishRecords;
+import com.ruoyi.syzy.domain.BBusiWishUniversities;
+import com.ruoyi.syzy.domain.BBusiWishUniversitySubmitRecruitPlan;
+import com.ruoyi.syzy.dto.SubmitWishRecordDTO;
+import com.ruoyi.web.domain.Constant;
+import com.ruoyi.web.domain.ZytbDto;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.jxls.common.Context;
+import org.jxls.util.JxlsHelper;
+import org.springframework.http.MediaType;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class VolunteerExporter {
+    public static void export(String location, BBusiWishRecords record, SubmitWishRecordDTO.SubmitWishRecordDetail detail, Integer currYear, Integer currSubmitYear,
+                              Integer seat, List<ZytbDto.ZytbVolunteerRes> volunteerResList, HttpServletResponse response) {
+        if (volunteerResList == null) {
+            response.setStatus(404);
+            return;
+        }
+        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
+        try {
+            String course = Constant.EXAM_TYPE_ZG.equals(sysUser.getExamType()) ? detail.getMode()
+                    : Arrays.asList(detail.getMode().split(",")).stream().map(t -> t.substring(0, 1)).collect(Collectors.joining());
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+            String score = String.valueOf(Math.round(detail.getScore()));
+            String realFileName = detail.getYear() + "_" + location + "_" + record.getBatchName() + "_" + score + "分_"
+                    + "_" + course + "_" + record.getName() + "_" + sdf.format(new Date()) + ".xlsx";
+            // _湖南_本科_590分_物化生_志愿表7_20230221181347.xlsx
+            InputStream is = VolunteerExporter.class.getResourceAsStream("/templates/wish_template.xlsx");
+            Context context = new Context();
+            // 志愿表7  湖南    女  本科  590分  -位  物/化/生
+            String title = record.getName() + "  " + location + "    " + normalSex(sysUser.getSex()) + "  " + record.getBatchName() + "  "
+                    + score + "分  " + (null != seat ? seat : "-") + "位  " + course;
+            context.putVar("title", title);
+            context.putVar("planTitle", detail.getYear() + "(招生计划)");
+            context.putVar("years", Lists.newArrayList(String.valueOf(currSubmitYear), String.valueOf(currSubmitYear - 1), String.valueOf(currSubmitYear - 2)));
+            context.putVar("records", buildData(detail.getBatch().getHighlightMajorIds(), volunteerResList));
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            // OutputStream os = new FileOutputStream("D:\\data\\wish_output.xls");
+            JxlsHelper.getInstance().processTemplate(is, response.getOutputStream(), context);
+        } catch (UnsupportedEncodingException e) {
+            response.setStatus(404);
+        } catch (Exception e) {
+            response.setStatus(404);
+        }
+        return;
+    }
+
+    private static List<VolunteerRes> buildData(List<String> highlightMajorIds, List<ZytbDto.ZytbVolunteerRes> volunteerResList) {
+        List<VolunteerRes> dataList = Lists.newArrayList();
+        StringBuilder sb = new StringBuilder();
+        Set<String> highlightMajorIdSet = null != highlightMajorIds ? Sets.newHashSet(highlightMajorIds) : Sets.newHashSet();
+        for (ZytbDto.ZytbVolunteerRes zytbVolunteer : volunteerResList) {
+            VolunteerRes res = new VolunteerRes();
+            BBusiWishUniversities u = zytbVolunteer.getUniversity();
+            BBusiWishUniversitySubmitRecruitPlan rp = zytbVolunteer.getRecruitPlan();
+            dataList.add(res);
+            res.setSeq(dataList.size());
+            res.setEnrollRatio(zytbVolunteer.getEnrollRatio() + "%");
+            res.setCollegeCode(rp.getCollegeCode());
+            sb.setLength(0);
+            sb.append(u.getName());
+            if (StringUtils.isNotBlank(rp.getGroup())) {
+                sb.append("(").append(rp.getGroup()).append(")");
+            }
+            if (StringUtils.isNotBlank(rp.getSpecialProject())) {
+                sb.append("(").append(rp.getSpecialProject()).append(")");
+            }
+            res.setUniversityName(sb.toString());
+            res.setLevel(StringUtils.join(u.getFeatures().split(","), "\n"));
+            res.setLocation(u.getLocation() + "/" + u.getCityName());
+            res.setType(u.getType());
+            res.setNatureType(u.getNatureTypeCN());
+            res.setRank(null!=u.getRanking() ? (0==u.getRanking()?null:u.getRanking()) : null);
+            res.setCourse(rp.getCourse());
+
+            List<Major> majors = Lists.newArrayList();
+            for (ZytbDto.ZytbVoluntaryMarjorDetail md : zytbVolunteer.getMarjorList()) {
+                Major m = new Major();
+                m.setEnrollRatio(md.getEnrollRatio() + "%");
+                m.setMarjorId(StringUtils.trimToEmpty(md.getMarjorId()));
+                m.setMarjorMatched(highlightMajorIdSet.contains(m.getMarjorId()));
+                m.setMarjorName((StringUtils.isNotBlank(md.getMarjorBelongs()) ? md.getMarjorName() + "(" + md.getMarjorBelongs() + ")" : md.getMarjorName()) + StringUtils.trimToEmpty(md.getMarjorDirection()));
+                m.setPlanCount(md.getPlanCount());
+                m.setXuefei(md.getXuefei());
+                m.setXuezhi(md.getXuezhi());
+                List<MajorSubmit> hiList = Arrays.stream(md.getHistories()).map(t ->
+                        new MajorSubmit(normal(t.getNumReal()), normal(t.getLineDiff()), normal(t.getScore()), normal(t.getSeat()))).collect(Collectors.toList());
+                m.setHistories(hiList);
+                majors.add(m);
+            }
+            res.setMajors(majors);
+        }
+        return dataList;
+    }
+
+    public static String normalSex(String v) {
+        return "0".equals(v) ? "男" : ("1".equals(v) ? "女" : "-");
+    }
+
+    public static String normal(Long v) {
+        return null == v || v.equals(0L) ? "-" : String.valueOf(v);
+    }
+    public static String normal(String v) {
+        return StringUtils.isBlank(v) ? "-" : v;
+    }
+
+    @Data
+    public static class VolunteerRes {
+        Integer seq;
+        String enrollRatio;
+        String collegeCode;
+        String universityName;
+        String level;
+        String location;
+        String type;
+        String natureType;
+        Integer rank;
+        String course;
+        List<Major> majors;
+    }
+
+    @Data
+    public static class Major {
+        String enrollRatio;
+        String marjorId;
+        Boolean marjorMatched;
+        String marjorName;
+        Integer planCount;
+        String xuefei;
+        String xuezhi;
+        List<MajorSubmit> histories;
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MajorSubmit {
+        String numReal;
+        String diff;
+        String score;
+        String seat;
+    }
+}

+ 5 - 0
ie-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -428,6 +428,11 @@ public class SysUser extends BaseEntity
         this.cardId = cardId;
     }
 
+
+    public String getCode() {
+        return userId.toString();
+    }
+
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

+ 1 - 1
ie-system/src/main/java/com/ruoyi/sy/domain/SyMajorCareerProspects.java

@@ -1,6 +1,6 @@
 package com.ruoyi.sy.domain;
 
-import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson2.JSONArray;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;

+ 1 - 3
ie-system/src/main/java/com/ruoyi/sy/domain/SyMajorOverview.java

@@ -1,8 +1,6 @@
 package com.ruoyi.sy.domain;
 
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import org.apache.commons.lang3.StringUtils;
+import com.alibaba.fastjson2.JSONArray;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/sy/domain/SyVocationalPostDetail.java

@@ -1,6 +1,6 @@
 package com.ruoyi.sy.domain;
 
-import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson2.JSONArray;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;

+ 2 - 0
ie-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java

@@ -10,6 +10,8 @@ import com.ruoyi.common.core.domain.entity.SysDictData;
  */
 public interface ISysDictDataService
 {
+    public List<SysDictData> selectDictDataByType(String dictType);
+
     /**
      * 根据条件分页查询字典数据
      * 

+ 5 - 0
ie-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java

@@ -19,6 +19,11 @@ public class SysDictDataServiceImpl implements ISysDictDataService
     @Autowired
     private SysDictDataMapper dictDataMapper;
 
+    @Override
+    public List<SysDictData> selectDictDataByType(String dictType) {
+        return dictDataMapper.selectDictDataByType(dictType);
+    }
+
     /**
      * 根据条件分页查询字典数据
      * 

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/criteria/MarjorTdxCriteria.java

@@ -1,6 +1,6 @@
 package com.ruoyi.syzy.criteria;
 
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import org.apache.commons.lang3.builder.ToStringBuilder;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/criteria/UniversityTdxCriteria.java

@@ -1,6 +1,6 @@
 package com.ruoyi.syzy.criteria;
 
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/domain/BBusiWishUniversities.java

@@ -1,6 +1,6 @@
 package com.ruoyi.syzy.domain;
 
-import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson2.JSONArray;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.ruoyi.common.annotation.Excel;
 import com.ruoyi.common.core.domain.BaseEntity;

+ 3 - 3
ie-system/src/main/java/com/ruoyi/syzy/domain/BBusiWishUniversitiesSubject.java

@@ -1,7 +1,7 @@
 package com.ruoyi.syzy.domain;
 
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
@@ -83,7 +83,7 @@ public class BBusiWishUniversitiesSubject extends BaseEntity
 
     public JSONArray getSubjects()
     {
-        return JSONObject.parseArray(subjects);
+        return JSONArray.parseArray(subjects);
     }
     public void setSubjectCount(Long subjectCount) 
     {

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/mapper/BCustomerMarjorsMapper.java

@@ -2,7 +2,7 @@ package com.ruoyi.syzy.mapper;
 
 import java.util.List;
 
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.syzy.domain.BCustomerMarjors;
 
 /**

+ 0 - 1
ie-system/src/main/java/com/ruoyi/syzy/mapper/BCustomerUniversitiesMapper.java

@@ -2,7 +2,6 @@ package com.ruoyi.syzy.mapper;
 
 import java.util.List;
 
-import com.alibaba.fastjson.JSONObject;
 import com.ruoyi.syzy.domain.BCustomerUniversities;
 
 /**

+ 0 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/IBBusiWishUniversitiesService.java

@@ -3,7 +3,6 @@ package com.ruoyi.syzy.service;
 import java.util.List;
 import java.util.Map;
 
-import com.alibaba.fastjson.JSONObject;
 import com.ruoyi.syzy.domain.BBusiWishUniversities;
 import com.ruoyi.syzy.domain.BBusiWishUniversitiesProfession;
 import com.ruoyi.syzy.dto.UniversityDetailDTO;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/IBCustomerMarjorsService.java

@@ -2,7 +2,7 @@ package com.ruoyi.syzy.service;
 
 import java.util.List;
 
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.syzy.domain.BCustomerMarjors;
 
 /**

+ 0 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/IBCustomerUniversitiesService.java

@@ -2,7 +2,6 @@ package com.ruoyi.syzy.service;
 
 import java.util.List;
 
-import com.alibaba.fastjson.JSONObject;
 import com.ruoyi.syzy.domain.BCustomerUniversities;
 
 /**

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/impl/BBusiHollandRecordsServiceImpl.java

@@ -14,7 +14,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;

+ 0 - 2
ie-system/src/main/java/com/ruoyi/syzy/service/impl/BBusiWishUniversitySubmitMarjorsServiceImpl.java

@@ -1,12 +1,10 @@
 package com.ruoyi.syzy.service.impl;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-import com.alibaba.fastjson.JSONArray;
 import com.ruoyi.system.service.ISysConfigService;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/impl/BCustomerMarjorsServiceImpl.java

@@ -7,7 +7,7 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.syzy.domain.BCustomerMarjors;
 import com.ruoyi.syzy.mapper.BCustomerMarjorsMapper;

+ 1 - 1
ie-system/src/main/java/com/ruoyi/syzy/service/impl/BCustomerUniversitiesServiceImpl.java

@@ -2,7 +2,7 @@ package com.ruoyi.syzy.service.impl;
 
 import java.util.List;
 
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.common.utils.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;

+ 2 - 2
ie-system/src/main/java/com/ruoyi/syzy/utils/BatchUtil.java

@@ -8,7 +8,7 @@ import java.util.Map;
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.StringUtils;
 
-import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson2.JSONObject;
 
 public class BatchUtil {
     private static Map<Integer, String> typeNames = new LinkedHashMap<>(5);
@@ -26,7 +26,7 @@ public class BatchUtil {
             if (StringUtils.isBlank(row.getValue())) {
                 return;
             }
-            JSONObject obj = new JSONObject(true);
+            JSONObject obj = new JSONObject();
             obj.put("label", row.getValue());
             obj.put("value", row.getKey());
             list.add(obj);

+ 6 - 0
pom.xml

@@ -42,6 +42,12 @@
     <dependencyManagement>
         <dependencies>
 
+            <dependency>
+                <groupId>org.jxls</groupId>
+                <artifactId>jxls-poi</artifactId>
+                <version>2.12.0</version>
+            </dependency>
+
             <!-- 覆盖SpringFramework的依赖配置-->
             <dependency>
                 <groupId>org.springframework</groupId>