| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- 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<VoluntaryMajorRecord> 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<AIRenderRule> details = Lists.newArrayList();
- }
- @Data
- @ApiModel
- public static class RenderMajorResult extends RenderRequest {
- @ApiModelProperty
- EnumPickType enumPickType;
- @ApiModelProperty
- Integer enrollRate; // 100进制,按原优志愿,为null表示无概率
- @ApiModelProperty
- String enrollRateText; // 概率描述,按原优志愿
- @ApiModelProperty
- List<RenderMajorHistory> 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<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 Math.round(oriTotal * 10) / 10.0;
- }
- 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);
- }
- }
- }
- }
|