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.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; 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 { @Data @ApiModel public static class VoluntaryRecord extends RenderRequest { Long id; String universityLogo; Long universityId; String universityName; Integer rank; List majors = Lists.newArrayList(); } @Data @ApiModel public static class VoluntaryMajorRecord extends RenderRequest { Long majorId; String majorName; String majorAncestors; String majorGroup; Integer rank; } @Data @ApiModel public static class RenderRequest { @ApiModelProperty("0 render; 2. Skill") int renderType; @ApiModelProperty("院校专业") Long majorId; @ApiModelProperty("院校ID") Long universityId; } @Data @ApiModel @AllArgsConstructor @NoArgsConstructor public static class RenderRule { @ApiModelProperty("类型") String category; @ApiModelProperty("描述") String content; @ApiModelProperty("规则列表") List details = Lists.newArrayList(); } @Data @ApiModel public static class RenderMajorResult extends RenderRequest { @ApiModelProperty EnumPickType enumPickType; @ApiModelProperty Integer enrollRate; // 100进制,按原优志愿,为null表示无概率 @ApiModelProperty String enrollRateText; // 概率描述,按原优志愿 @ApiModelProperty List histories = Lists.newArrayList(); @ApiModelProperty RenderMajorSkill skill; } @Data @ApiModel public static class RenderMajorHistory { @ApiModelProperty Integer year; // 年份 @ApiModelProperty String score; // 最低分 @ApiModelProperty Integer plan; // 计划人数 @ApiModelProperty Integer enroll; // 录取人数 @ApiModelProperty Integer diff; // 负表示低于录取分;正数表示高于录取分 @ApiModelProperty String ruleContent; // 完整的说明 @ApiModelProperty String application; // 报名人数比值 @ApiModelProperty String admission; // 计划人数比值 } @Data @ApiModel public static class RenderMajorSkill { @ApiModelProperty Integer year; // 年份 @ApiModelProperty String cultureScore; // 文化得分 @ApiModelProperty String cultureRule; // 文化规则 @ApiModelProperty String enrollScore; // 录取分 @ApiModelProperty String skillScore; // 反向测技能分 @ApiModelProperty String diff; // 负数表示低于skillScore,正数高于 } // 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 Boolean readonly; } @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 form; } // 公共 @Data @ApiModel public static class VoluntaryModel { @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 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 form = Maps.newHashMap(); } @Data @ApiModel public static class MultipleRequest { // TODO: 院校查询也需要支持此参数,来查询包含此专业大类的院校 // TODO: 先暂定限制大类,可能需要限制专业细分2级分类 @ApiModelProperty String majorCategory; // 因为多选时想尽量让规则重合,所以限制专业大类 @ApiModelProperty List universities; // 多个学校,先定2个,会受分数类型限制 } @Data @ApiModel public static class AIRequest extends MultipleRequest { @ApiModelProperty Integer pageNum; @ApiModelProperty Integer pageSize; // 推荐列表筛选条件 // 筛选条件因为可以不保存,所以单独提取出来 @ApiModelProperty AIRequestFilter filter = new AIRequestFilter(); // TODO: 会尽快统计出来一些需要额外补充的信息 @ApiModelProperty Map form = Maps.newHashMap(); } @Data @ApiModel public static class CollegeMajorDto { @ApiModelProperty String code; // university code @ApiModelProperty List majorCodes; // major codes in university @ApiModelProperty List majorEnrollCodes; // major enroll codes in university @ApiModelProperty Map form = Maps.newHashMap(); } @Data @ApiModel public static class AIRequestFilter { @ApiModelProperty List 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 level; // 办学层次 @ApiModelProperty List type; // 院校类型 @ApiModelProperty List natureTypeCN; // 办学类型 @ApiModelProperty List 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 histories; // 历年数据 @ApiModelProperty List clearings; // 补录数据 @ApiModelProperty List rules; // 所有结论,前端可根据enumRuleType展示 List 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 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 validTotalSet = Sets.newHashSet("ScoreBase","ScoreSingle","ScoreSkill"); Map>> groupItemListMap = Maps.newHashMap(); // 提分项目的 项目原始分 OriScore, 项目总分 Total,项目得分率 Rate Map> groupTypeStatMap = Maps.newHashMap(); // 提分类型的 得分 Score,满分合计 Total Double groupAllTotal = 0.0; // 提分项满分合计 Map> typeKeyScoreMap = Maps.newHashMap(); // 类型项目的 得分 Score 及 原始分 OriScore Map> typeScoreMap = Maps.newHashMap(); // 类型的 得分 Score 及 原始分 Total MutablePair 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 p = typeKeyScoreMap.get(dataKey); return null != p ? (isOri ? p.getRight() : p.getLeft()) : null; } else if ("StatSkill".equals(valueType)) { // 明细分 String dataKey = "ScoreSkill_" + itemName; MutablePair 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 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> 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 getImproveScore(Double diffValue) { List 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 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> itemList, Double missRateScore) { Double statTotal = itemList.stream().mapToDouble(t -> t.getMiddle()).sum(); Double oriTotal = 0.0; for (Triple t : itemList) { oriTotal += Math.min(t.getMiddle() - t.getLeft(), missRateScore * t.getMiddle() / statTotal / t.getRight()); } return Math.round(oriTotal * 10) / 10.0; } private void merge(Map> pairMap, String key, Double value, Double oriValue) { MutablePair 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); } } } }