hare8999@163.com 3 years ago
parent
commit
cc923e6fc4

+ 229 - 0
doc/Mind/ElectiveEngine.cs

@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace mxdemo.Mind
+{
+    #region models
+
+    /// <summary>
+    /// 选科组合
+    /// </summary>
+    public class SubjectGroup
+    {
+        public int Limitation { get; set; } // 人数限制
+        public string Name { get; set; }
+        public string Code { get; set; }
+        public long Id { get; set; }
+    }
+
+    /// <summary>
+    /// 志愿
+    /// </summary>
+    public class Preference
+    {
+        public int Rank { get; set; } // 第1、2、3志原 越小优先级越高
+        public long GroupId {get;set;}
+        public long StudentId { get; set; }
+        public long Id { get; set; }
+    }
+
+    /// <summary>
+    /// 得分情况
+    /// </summary>
+    public class GroupScore
+    {
+        public decimal Score { get; set; }
+        public long StudentId { get; set; }
+        public long GroupId { get; set; }
+    }
+
+    /// <summary>
+    /// 学生信息
+    /// </summary>
+    public class Student
+    {
+        public long Id { get; set; }
+        public string No { get; set; }
+        public string Name { get; set; }
+    }
+
+    public class ElectiveElement
+    {
+        public Student Student { get; set; }
+        public Preference Preference { get; set; }
+        public SubjectGroup Group { get; set; }
+
+        // 计算属性
+        public int PreferenceRank { get; set; } // 志愿排名
+        public int GroupRank { get; set; }  // 组合排名,即成绩排名(组合全校排名),同样越小优先级越高
+    }
+
+    #endregion
+
+    public class ElectiveResult
+    {
+        public ElectiveResult(List<Student> students)
+        {
+            this.UnmatchedStudents = students;
+        }
+
+        public List<Student> MatchedStudents { get; set; }
+            = new List<Student>();
+        public Dictionary<SubjectGroup, List<ElectiveElement>> MatchedDetails { get; set; }
+            = new Dictionary<SubjectGroup, List<ElectiveElement>>();
+        public List<Student> UnmatchedStudents { get; set; }
+
+        public void AppendMatched(ElectiveElement element)
+        {
+            // 应该还有更多细节需要考虑
+            var appendStudent = element.Student;
+            if (MatchedStudents.Contains(appendStudent)) return;
+
+            if (UnmatchedStudents.Contains(appendStudent))
+                RemoveUnmatched(appendStudent);
+
+            var appendGroup = MatchedDetails.ContainsKey(element.Group)
+                ? MatchedDetails[element.Group]
+                : new List<ElectiveElement>();
+            if (appendGroup.Count < element.Group.Limitation)
+            {
+                MatchedDetails[element.Group] = appendGroup;
+                AddMatchedAndDetails(element);
+            }
+            else
+            {
+                AddUnmatched(appendStudent);
+            }
+        }
+
+        private void AddMatchedAndDetails(ElectiveElement element)
+        {
+            // 应该还有更多细节需要考虑
+            MatchedDetails[element.Group].Add(element);
+            MatchedStudents.Add(element.Student);
+        }
+
+        private void AddUnmatched(Student student)
+        {
+            // 应该还有更多细节需要考虑
+            UnmatchedStudents.Add(student);
+        }
+
+        private void RemoveUnmatched(Student student)
+        {
+            // 应该还有更多细节需要考虑
+            UnmatchedStudents.Remove(student);
+        }
+    }
+
+    public interface IElectiveStrategy
+    {
+        /// <summary>
+        /// 从开放选科中取出按优先级分组的选科
+        /// </summary>
+        /// <returns></returns>
+        List<List<SubjectGroup>> GetAllPrioritizedGroups();
+
+        /// <summary>
+        /// 参与这次运算的志愿优先级集合
+        /// </summary>
+        /// <returns></returns>
+        List<List<int>> GetAllPrioritizedPreferenceRanks();
+
+        /// <summary>
+        /// 参与这次运算的所有学生
+        /// </summary>
+        /// <returns></returns>
+        List<Student> GetAllStudents();
+    }
+
+    public abstract class ElectiveEngine: IComparer<ElectiveElement>
+    { 
+        protected IElectiveStrategy ElectiveStrategy { get; set; }
+
+
+        protected ElectiveEngine(IElectiveStrategy electiveStrategy)
+        {
+            ElectiveStrategy = electiveStrategy;
+        }
+
+        public ElectiveResult Execute()
+        {
+            var prioritizedGroups = ElectiveStrategy.GetAllPrioritizedGroups();
+            var prioritizedPreferenceRanks = ElectiveStrategy.GetAllPrioritizedPreferenceRanks();
+            var allStudents = ElectiveStrategy.GetAllStudents();
+            var electiveResult = new ElectiveResult(allStudents);
+
+            foreach(var groups in prioritizedGroups)
+            {
+                foreach(var ranks in prioritizedPreferenceRanks)
+                {
+                    var students = electiveResult.UnmatchedStudents;
+                    var elements = BuildElectiveElements(students, groups, ranks);
+                    ExecuteCore(elements, electiveResult);
+                }
+            }
+            return electiveResult;
+        }
+
+        private void ExecuteCore(List<ElectiveElement> elements, ElectiveResult context)
+        {
+            // 核心方法优先级排序, 已经按期望排好了录取顺序
+            elements.Sort(this);  // this.IComparer<ElectiveElement>.Compare
+            // 接下来考虑每个组合容量的问题
+            foreach(var element in elements)
+            {
+                context.AppendMatched(element);
+            }
+        }
+
+        protected virtual List<ElectiveElement> BuildElectiveElements(List<Student> students, List<SubjectGroup> groups, List<int> ranks)
+        {
+            // 根据组合与志愿 生成不同的选科元素
+            // 可以按计算需要,相互挂一些引用,方便计算过程中的删除操作
+            // TODO: 如果引入意向专业,也可以改写这个生成的逻辑
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// 优先级比较
+        /// </summary>
+        /// <param name="x"></param>
+        /// <param name="y"></param>
+        /// <returns></returns>
+        public abstract int Compare(ElectiveElement x, ElectiveElement y);
+    }
+
+    public class PreferenceAheadElectiveEngine : ElectiveEngine
+    {
+        public PreferenceAheadElectiveEngine(IElectiveStrategy electiveStrategy)
+            :base(electiveStrategy)
+        {
+
+        }
+
+        public override int Compare(ElectiveElement x, ElectiveElement y)
+        {
+            // TODO: 容错处理
+            if (x.PreferenceRank == y.PreferenceRank) return y.GroupRank - x.GroupRank;
+            return y.PreferenceRank - x.PreferenceRank;
+        }
+    }
+
+    public class ScoreAheadElectiveEngine : ElectiveEngine
+    {
+        public ScoreAheadElectiveEngine(IElectiveStrategy electiveStrategy)
+            : base(electiveStrategy)
+        {
+
+        }
+
+        public override int Compare(ElectiveElement x, ElectiveElement y)
+        {
+            // TODO: 容错处理
+            if (x.GroupRank == y.GroupRank) return y.PreferenceRank - x.PreferenceRank;
+            return y.GroupRank - x.GroupRank;
+        }
+    }
+}

+ 486 - 0
doc/Mind/ElectiveGeneration.cs

@@ -0,0 +1,486 @@
+using System;
+using System.Collections.Generic;
+using static mxdemo.Mind.IStudentClassDispatchService;
+
+namespace mxdemo.Mind
+{
+    // 选科数据与决策会有很多重合的地方
+    public enum EnumElectiveGeneration
+    {
+        Init = 0, // 初始化态,选科正式开启前的状态,开启之后往下一代推进
+        Primary = 1, // 初选(正常报名)阶段,初选时间结束时往下一代推进 (先暂定进入PrimaryDM之前选科设置还可以修改,之后禁止,防止扰乱数据)
+        PrimaryDM, // (PrimaryDecisionMaking)初选录取阶段,应用决策之后,开启补录报名(时间段)并往下一代推进
+        BackTracking, // 补录报名时间结束后往下一代推进
+        BackTrackingDM, // 调剂OR提醒,如果为直接调剂,则跳过FinalAdjust,如果为提醒(设时间段)并往下一代推进
+        FinalAdjust, // 调剂报名期间
+        FinalAdjustDM, // 调剂决策,这个阶段会最终定版
+        RankBalance, // 可选步骤,行为类似强制调剂
+        // Dispatch, // 分班?分班是否要进入此结构?貌似结构不太统一!应该还是要分出去做
+        Terminate // 最终代,选科流程数据全部封存
+    }
+
+    public enum EnumElectiveOperation
+    {
+        None = 0,
+        Canceled,
+        Disabled,
+        Optional,
+        Rejected,
+        Approved,
+        Forced
+    }
+
+    // 待思考问题:
+    /*
+    1 哪个结点开始不允许修改选科设置
+    2 未操作志愿、未选完志愿、未响应决策的学生是否需要区分显示
+    3 补录决策时,是否存在部分调剂、部分提醒的情况?
+    4 调剂行为定义:直接修改学生最终录取组合,触发发签名
+     */
+
+    #region models
+
+    /// <summary>
+    /// Generation init时生成的缓存数据,方便后续计算
+    /// 生成开放组合的数据即可
+    /// 每次修改选科设置(在允许范围内修改)需要刷新
+    /// </summary>
+    public class ElectiveStudentScore
+    {
+        long id;
+        long roundId;
+
+        long studentId;
+        long groupId;
+
+        decimal scoreTotal; // 9科成绩,看是否有必要存
+        decimal scoreGroup; // 6科成绩
+        int rankTotal; // 9科全校排名,看是否有必要存
+        int rankGroup; // 6科全校排名
+    }
+
+    /// <summary>
+    /// 每一代都为体现学生的选科数据变化,任何操作都可以被记录下来,只是最终哪条生效的问题
+    /// 当然也可以根据实际情况减少部分记录,类下面的注解将讨论这些问题
+    /// </summary>
+    public class ElectiveGenerationFlowData
+    {
+        long id;
+        long roundId;
+
+        long studentId;
+        long groupId;
+
+        EnumElectiveGeneration generation;
+        EnumElectiveOperation operation;
+        long operateBy;
+        DateTime operateTime;
+        int rank; // Primary中可能存在多志愿,用来控制此阶段的志愿优先级,如第1志愿=0,第2志愿=1,第三志愿=2
+
+        string remark;
+        // 以下2个字段用来挂载生命周期关键引用对象,具体每个环节挂什么,需要再斟酌。
+        string refType;
+        long refId;
+    }
+    public class ElectiveGenerationFlowLog : ElectiveGenerationFlowData
+    {
+        // + 区分ElectiveGenerationFlowData的字段
+    }
+
+    public class RoundForFlow : mxdemo.Mind.IStudentClassDispatchService.Round // Round到时候合并在一起
+    {
+        bool AllMatched; // 所有学生匹配完毕,可以开始推进排名均衡或者分班
+        EnumElectiveGeneration currentGeneration; // 内部字段
+    }
+
+    // 1 初选决策时,需要进行设置
+    // 2 补录决策时,需要进行设置,此时要根据具体数据来定
+    //   如果所有学生都已经匹配完毕(Approved,Forced都属性已匹配),则可以推进排名均衡或者分班
+    //   如果还有未匹配的,则推进到FinalAdjust
+    public class FlowPushSetting
+    {
+        long id;
+        long roundId;
+        DateTime begin;
+        DateTime end;
+        bool onlyRecommand; // 仅允许学生选择推荐组合, 否则允许学生填报有空缺的组合
+        bool onlyAgree; // 调剂阶段,只允许学生选择同意, 否则允许学生拒绝
+    }
+
+    public class ElectiveEsign
+    {
+        long id;
+        long roundId;
+        long flowDataId; // 对应哪条匹配数据(录取或者调剂)【匹配算法结果表也可如此挂载flowDataId,这样可以追溯匹配原因】
+
+        string esignPath; // 签字保存图片
+        bool actived; // 有效标记,可以考虑:如果学生需要签名且未签名时,如果再遇到新的调剂时,旧的签名还要继续么?我觉得应该是不需要的
+        // 可能还有其它字段
+    }
+
+    /*  
+     总则:ElectiveGenerationFlowData记录的是每一代数据的最终结果,如果想要更精细的记录,可以结合ElectiveGenerationFlowLog
+    1 Generation.Init 可有可无,不需要生成初始数据?
+
+    2 Generation.Primary
+        报名时:
+        ADD operation=Approved
+
+        取消时:
+        UPDATE operation=Cancel 
+
+        排名挤出时-被挤出人:
+        UPDATE operation=Disabled, remark = '排名挤出'
+        refType = 'ElectiveGenerationFlowData'
+        refId = ElectiveGenerationFlowData.id // 报名解发挤出那条记录的ElectiveGenerationFlowData.id
+
+        取消时恢复补挤出的人(只有当被影响的人还有剩余报名次数的时候才自动恢复,因为期间他可能自己又补填了一个志愿):
+        QueryBy refId
+        UPDATE operation=Approved WHEN COUNT(operation=Approved)<setting.PreferenceCount
+        remark = ''
+        refType = ''
+        refId = 0 // 取消对应记录的ID
+
+        Primary中学生的有效报名数据:operation=Approved
+
+    3 Generation.PrimaryDM
+        目标:启用算法,给每个学生生成一条决策数据,表示初选录取情况(算法见ElectiveEngine草稿)
+
+        录取:
+        ADD operation=Approved
+        remark = 录取说明?
+        refType = 'ElectiveEsign'
+        refId = ElectiveEsign.id // 生成一条待签记录
+        // 这样的话签名与选科的代数据就可分割开了,具体说是否一定要学生签名才能推进下一代?
+        // 我看未必,因为学校不可能因为个别钉子户就暂停教务
+        
+        未录取,但自选专业满足
+        ADD operation=Optional, groupId=推荐组合
+        remark = 未录取原因
+        未录取,也无自选专业满足
+        ADD operation=Rejected, groupId=推荐组合
+        remark = 未录取原因
+
+        PrimaryDM中 operation=[Optional,Rejected]的学生,即是一代补录流程的目标学生
+
+    4 Generation.BackTracking
+        操作逻辑重用Generation.Primary
+
+    5 Generation.BackTrackingDM
+        操作逻辑重用Generation.PrimaryDM,
+        计算差别:仅1个志愿,但还是可以引用自选专业,因为学生在此期间可能更改了自选专业
+
+        操作差别:校长在推动下一代流程时,有可能会开始手动调剂
+        UPDATE operation=Forced  // Forced操作等同于Approved录取状态,即告诉学生被调剂了,签字同意即可(即也挂ref钩子)
+
+    6 Generation.FinalAdjust
+        可选步骤,有可能跳过,如未跳过,则是调剂报名
+        同意:前端会强制签名,所以这条在保存时可以直接挂钩ElectiveEsign
+        ADD operation=Approved, refType='ElectiveEsign', refId=ElectiveEsign.id
+        拒绝:
+        ADD operation=Rejected, remark=不同意原因
+        附:没有记录则是未参与的
+
+    7 Generation.FinalAdjustDM
+        针对未参与的或operation=Rejected, 校长会在此环节强制调剂,以达到最终定版的目的
+        ADD operation=Forced, refType='ElectiveEsign', refId=0 // 待签名
+
+    8 Generation.RankBalance
+        可选步骤,也是产生类似Generation.FinalAdjustDM数据逻辑,只是学校出发的角度不一样
+        ADD operation=Forced, refType='ElectiveEsign', refId=0 // 待签名
+
+    9 Generation.Dispatch 待定应该要分出去单独做
+
+    10 Generation.Terminate
+        不产生数据,但需要记录此状态,表示选科数据封存(记录在round实例中)
+     */
+
+    #endregion
+
+    public interface IElectiveService
+    {
+        #region getElectiveSummary
+        /// <summary>
+        /// 选科过程中的统计数据值,除了要展示之外还需要做查询支持
+        /// </summary>
+        public class ElectiveSummaryValue
+        {
+            // 展示字段
+            int groupId; // 组合ID,没有组合为0比如未参与报名人数(实际只有两种统计数据,针对某个组合的统计和不针对组合的统计)
+            int value; // 数值
+            string color; // 支持'R','G','B','',或者'#FA1234'等hexString
+            bool bold; // 加粗,重点展示
+            bool star; // 带*,重点展示
+
+            // 反向查询代码 将统计逻辑的条件关键字放入此代码,回传后台能反向解析并恢复明细
+            // 想法:让统计全部基于ElectiveGenerationFlowData,形成一种或多种QueryDto{roundId:'',generation:'',groupId:''...}等, 序列化实际查询条件
+            string queryCode;
+            bool disabled; // 不支持查询
+        }
+
+        /// <summary>
+        /// 查询的某一列
+        /// </summary>
+        public class ElectiveSummaryCategory
+        {
+            string category; // 身份标识,类似字段名/列名,可能被前端用来定制渲染模板
+            string displayName; // 显示名称
+
+            ElectiveSummaryValue[] values;
+        }
+
+        public interface IElectiveGenerationSummary
+        {
+            int roundId { get; set; } // 选科轮次
+            EnumElectiveGeneration generation { get; set; } // 进程代
+        }
+
+        /// <summary>
+        /// 进程代的统计结果从primary开始出数据
+        /// </summary>
+        public class ElectivePrimarySummary : IElectiveGenerationSummary
+        {
+            ElectiveSummaryCategory[][] categories; // 是多维数组,以应对多志愿的情况
+
+            public int roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public EnumElectiveGeneration generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        /// <summary>
+        /// 非Primary非DM的某一代的统计结果
+        /// </summary>
+        public class ElectiveGenerationSummary: IElectiveGenerationSummary
+        {
+            /// <summary>
+            /// 对某一代的统计项目,比如在初选决策、补录报名的统计等,下面接口会细述
+            /// </summary>
+            ElectiveSummaryCategory[] categories;
+
+            int IElectiveGenerationSummary.roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            EnumElectiveGeneration IElectiveGenerationSummary.generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        public class ElectiveGenerationDMSummary: ElectiveGenerationSummary
+        {
+            /// <summary>
+            /// 迭加到某决策代的统计项目,比如当前选科的总录取人数、待签人数、已签人数等,下面接口会细述
+            /// </summary>
+            ElectiveSummaryCategory[] accumulates;
+        }
+
+        /// <summary>
+        /// 获取选科进程的统计数据,一直汇总到当前进程代
+        /// </summary>
+        /// <returns></returns>
+        IElectiveGenerationSummary[] getElectiveSummary();
+        // 下面开始按代来描述ElectiveGenerationSummary需要返回的具体内容
+        /*  一、ElectivePrimarySummary,所需ElectiveSummaryCategory->{category:'',displayName:''}定义如下:
+         *  // hht.22/4/10 这个属性不要了 {'expectedCount','设置人数'},  -- 选科设置的人数, 为方便输出与图表展示,可能后续会有重复输出
+         *  {'actualCount','实际人数'},   -- 填报第X志愿的人数
+         *  {'unfinishedCount','未完成人数'},  -- 未填报第X志愿的人数(根据ElectiveGenerationFlowData实际Approved&Rank情况统计即可,因为如果报名还在进行中,本身就是动态变化的东西)
+         *  按第一志愿、第二志愿、...的顺序输出数据
+         *  
+         *  二、ElectiveGenerationDMSummary.accumulates 
+         *  仅决策代需要统计此数据,决策代[PrimaryDM BackTrackingDM FinalAdjustDM RankBalance]
+         *  {'expectedCount', '设置人数'} -- 选科设置人数
+         *  {'approvedCount', '正常录取人数'} -- 到此阶段为止,正常录取人数
+         *  {'forcedCount', '调剂录取人数'} -- 到此阶段为止,调剂录取人数
+         *  {'esignedCount', '已签人数'} -- 到此阶段为止,触发的有效签名统计
+         *  {'esigningCount', '待签人数'} -- 到此阶段为止,阶段触发有效的应签未签统计
+         *  
+         *  三、ElectiveGenerationSummary.categories 只统计某代的数据
+         *  A PrimaryDM
+         *  {'indicateCount','指标'} -- 正负表示超过设置的人数,负数表示低于设置的人数
+         *  {'approvedCount, '录取人数'} -- 正常录取人数
+         *  {'adjustCount','调剂人数'} -- 未录取人数,理论上应该<=指标的绝对值
+         *  {'matchedCount','可调剂人数'} -- 未录取可被调剂的人(志愿不满足但自选专业满足)
+         *  {'nonmatchedCount','不可调剂人数'} -- 未录取不可被调剂的人(志愿与自选专业均不满足)
+         *  
+         *  B BackTracking FinalAdjust
+         *  {'matchedApproved','可调剂已填人数'} -- 统计有效补录报名
+         *  {'matchedRejected','可调剂已拒绝人数'} -- 统计拒绝报名的人数
+         *  {'matchedNonaction','可调剂未完成人数'} -- 统计未参与已经失效的报名人数(因为有可能被排名挤出)
+         *  {'nonmatchedApproved','不可调剂已填人数'}
+         *  {'nonmatchedRejected','不可调剂已拒绝人数'}
+         *  {'nonmatchedNonaction','不可调剂未完成人数'}
+         *  
+         *  C BackTrackingDM FinalAdjustDM RankBalance
+         *  {'approvedCount','补录录取人数'} -- 正常录取 [BackTrackingDM FinalAdjustDM]
+         *  {'forcedCount','调剂录取人数'} -- 强制调剂录取 [BackTrackingDM FinalAdjustDM RankBalance]
+         *  {'matchedCount','可调剂人数'} -- 正常录取 [BackTrackingDM]
+         *  {'nonmatchedCount','不可调剂人数'} -- 正常录取 [BackTrackingDM] 
+         *  
+        */
+        #endregion
+
+        #region getElectiveSummaryDetail 汇总明细清单
+
+        /// <summary>
+        /// 明细肯定是通过 某个组合某个学生 来呈现的
+        /// </summary>
+        public interface IElectiveGengerationDetail
+        {
+            long id { get; set; }
+            EnumElectiveGeneration generation { get; set; } 
+            string category { get; set; } // ElectiveSummaryCategory.category
+            int roundId { get; set; }
+            int studentId { get; set; }
+            string studentName { get; set; }
+            int classId { get; set; }
+            string className { get; set; }
+            int groupId { get; set; }
+            string groupName { get; set; }
+        }
+
+        /// <summary>
+        /// 此类相当于ElectiveGenerationFlowData的DTO,将字段转义为UI需要的内容
+        /// 明细的学生组合信息, 主要包含报名信息;
+        /// 还有一部分自选专业的信息,需要借助其它接口
+        /// </summary>
+        public class ElectiveGenerationDetail: IElectiveGengerationDetail
+        {
+            int rankInTime { get; set; } // 组合即时排名
+            int rankInGroup { get; set; } // 组合全校排名
+            int rankInGrade { get; set; } // 总分全校排名
+            int rankPreference { get; set; } // 志愿RANK 第几志愿
+            string OperateTime { get; set; } // 报名时间
+
+            public long id { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string category { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public EnumElectiveGeneration generation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int roundId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int studentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string studentName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int classId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string className { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public int groupId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+            public string groupName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+        }
+
+        /// <summary>
+        /// 录取明细(包含approved正常录取;forced强制调剂录取),当前组合为录取项
+        /// </summary>
+        public class ElectiveGenerationEnrollDetail: ElectiveGenerationDetail
+        {
+            ElectiveGenerationDetail[] otherEnlist; // 非录取报名清单
+            Dictionary<long, string> failedReasons; // 如果有未录取原因,填充在此MAP{key: detailId, value: failed reason}中
+            bool forced; // 是否属于强制调剂
+            bool esigned; // 是否已经签名
+        }
+
+        /// <summary>
+        /// 未录取明细,当前组合是推荐项
+        /// </summary>
+        public class ElectiveGenerationDisenrollDetail: ElectiveGenerationDetail
+        {
+            ElectiveGenerationDetail[] enlist; // 之前的报名清单(肯定都没有录取)
+            Dictionary<long, string> failedReasons; // 如果有未录取原因,填充在此MAP中
+            bool agreed; // 同意此组合
+            bool disagreed; // 不同意此组合
+            bool disagreeReason; // 不同意的原因
+            ElectiveGenerationDetail other; // 即没有同意,也没有不同意,选了其它组合
+            bool esigned; // 是否已经签名
+        }
+
+        /// <summary>
+        /// 初选报名搭配使用,展示其它可报组合情况
+        /// </summary>
+        public class ElectiveGenerationOptionalMajor : ElectiveOptionalMajor
+        {
+            int bestMatchedGroupId; // 最佳匹配
+        }
+
+        /// <summary>
+        /// Map: {key: studentid, value: ElectiveGenerationOptionalMajor}
+        /// </summary>
+        /// <param name="studentIds"></param>
+        /// <returns>批量取学生自选专业匹配情况</returns>
+        Dictionary<int, ElectiveGenerationOptionalMajor> getGenerationOptionalMajorsBatch(int[] studentIds);
+
+        /// <summary>
+        /// 通过summary中输出的queryCode反查明细
+        /// </summary>
+        /// <param name="queryCode"></param>
+        /// <returns></returns>
+        List<IElectiveGengerationDetail> getElectiveGengerationDetails(string queryCode);
+
+        #endregion
+
+        #region decision-making
+
+        /// <summary>
+        /// 匹配算法,算法名称代表的具体逻辑请看注解(不要顾名思义)
+        /// </summary>
+        public enum EnumElectiveDMAlgorithm
+        {
+            /// <summary>
+            /// 按成绩:
+            /// ElectiveEngine中,相当于[未录学生]/[组合*ALL]/[志愿*ALL]一并运行,排序时排名优先;
+            /// 以上逻辑循环迭代,每次只补空缺组合,直至未录学生=0。
+            /// 第1次迭代之后运算的学生均属于可调剂名单,不会有不可调剂名单
+            /// </summary>
+            RankFirst,
+
+            /// <summary>
+            /// 按专业:
+            /// ElectiveEngine中,相当于[未录学生]/[组合*ALL]/[志愿1][志愿2,志愿3][自选专业伪志愿]/三波运行,排序时排名优先;
+            /// 以上逻辑循环迭代,每次只补空缺组合,直至伪志愿运算结束。
+            /// [志愿1][志愿2,志愿3]迭代的录取学生为正常录取;自选专业伪志愿为可调剂名单;运行结束后的未录学生为不可调剂名单。
+            /// </summary>
+            MajorFirst, // 
+        }
+
+        /// <summary>
+        /// 执行选科匹配算法。为减少错误,对DM代数据全删全加可能好点。
+        /// </summary>
+        /// <param name="algorithm"></param>
+        void applyElectiveDMAlgorithm(EnumElectiveDMAlgorithm algorithm);
+
+        #endregion
+
+        #region forced
+
+        /// <summary>
+        /// 决策阶段,强制调剂录取
+        /// </summary>
+        /// <param name="studentId"></param>
+        /// <param name="groupId"></param>
+        void enrollByForce(long studentId, int groupId);
+
+        /// <summary>
+        /// 取消强制调剂操作
+        /// </summary>
+        /// <param name="id">ElectiveGenerationFlowData.id</param>
+        void cancelEnrollByForce(long id);       
+
+        #endregion
+
+        #region generation推进
+
+        /// <summary>
+        /// 决策完毕时,推进下一代进行
+        /// </summary>
+        /// <param name="setting"></param>
+        void pushGenerationSetting(FlowPushSetting setting);
+
+        /// <summary>
+        /// 如果在所有学生全部录取的情况,可以在任意决策结点跳转至排名均衡
+        /// </summary>
+        void jumpGenerationRankBalance();
+
+        /// <summary>
+        /// 如果在所有学生全部录取的情况,可以在任意决策结点跳转至终止态,封存数据
+        /// </summary>
+        void terminateGeneration();
+
+        /// <summary>
+        /// 在任意报名阶段,如果校长发现数据已经完全OK,则可以强制推进进程,提前进入决策
+        /// </summary>
+        void flushIntoGenerationDM();
+
+        #endregion
+
+        #region 排名均衡(略)先完成前面的
+        #endregion
+    }
+}

+ 102 - 0
doc/Mind/FacultyEstimate.cs

@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    #region model
+
+    public class FacultyForm
+    {
+        public int subjectId;   // 这个想想「班会、体育、自习」怎么处理?
+        public string subjectName; // 名称
+        public bool isFixed; // 固定师资,用以区分6门合格考与等级考学科,非固定科目需要填写合格考与等级考
+        public bool isOffical; // 正式科目,9科为正式科目,非正式科目提供的校验不一样
+        public int classesCountPrev; // 周课时数-分班前-仅参考用
+        public int teacherCount; // 单科老师数量
+        public int teacherClassesCount; // 单科老师周课时数
+        public int classesCount; // 单科总课时数,前端会提供辅助计算
+        public int levelClassesCount; // 等级考课时数-分班后
+        public int qualifiedClassesCount; // 合格考课时数-分班后
+    }
+
+    public class FacultyResult : FacultyForm
+    {
+        public int levelClassCount; // 等级考班级数
+        public int qualifiedClassCount; // 合格考班级数
+        public int requiredClassesCount; // 所需课时
+        public int actualClassesCount; // 实际课时
+        public int missingClassesCount; // 缺少课时,正数表示缺的,负数表示超的
+        public int missingTeacherCount; // 缺少老师,正负同上
+
+        public bool completed; // 是否计算结束
+    }
+
+    public class GroupSetting
+    {
+        // 略,延用之前的结构内容
+    }
+
+    public class GroupModel
+    {
+        // 后台自己设置标识,前端会原样返回,前端展示与操作只会用到索引
+        public string groupIds;
+        public List<GroupSetting> roundGroups;
+        public List<FacultyResult> faculties;
+    }
+
+    public class ScoreImportConfig // 需要调整结构
+    {
+        #region 不变部分
+        //      {
+        //"params": {},
+        //"roundId": 14,
+        //"schoolId": 31,
+        //"year": 2021,
+        //"name": "第1次选科",
+        //"beginTime": "2022-03-05",
+        //"endTime": "2022-03-20",
+        //"state": "1",
+        //"importWeight":[]
+        #endregion
+
+        #region 变更部分
+        // 新增模型列表
+        public List<GroupModel> groupModels;
+        // 选择其中1个模型
+        public int activeGroupModelIndex; // 构想的是传索引,因为前端会做删除等调整,而且新建时也可能同时创建多个引用,此时无ID能够做出区分
+        // 选志愿的数量,默认为1
+        public int preferenceCount = 1;
+        #endregion
+
+        #region 新增控制配置
+        public bool rankInvisible; // 排名不可见,默认false: 排名可见
+        public bool scoreInvisible; // 分数不可见,默认false: 分数可见
+        public bool enableEsign;  // 开启电子签名,默认false: 不需要
+        public bool enablePaperSign; // 开启电子签名的情况下,是否需要纸质签名(促进学生使用班牌),默认false: 不需要
+        #endregion
+    }
+
+    #endregion
+
+    public interface ISelectedScoreImport
+    {
+        // ScoreImportConfig的结构有变化见上面region
+        ScoreImportConfig getScoreImportConfig(int roundId);
+
+        /// <summary>
+        /// 生成空的师资表格,在校长全部重新填写的时候调用。如果是从已完成的模型复制,则不会调用,前端会自行处理
+        /// </summary>
+        /// <returns></returns>
+        List<FacultyResult> generateFaculties();
+
+        // 计算师资匹配结果
+        // 构想:全量传数据,但后台只挑需要的groupModels[targetIndex]进行计算,计算完毕标记为completed=true
+        // 计算结束,需要标记出groupModels中,completed=true的模型,最优解为isBest=true
+        // 最优解:faculties.sum(f => Math.Abs(f.missingTeacherCount)) 总和最小的GroupModel为最优
+        ScoreImportConfig calculateFaculty(ScoreImportConfig round, int groupModelIndex);
+
+        void saveScoreImportConfig({ round: ScoreImportConfig }); // config结构变了,保存也要注意
+
+        void queryScoreImportDetails({ round: ScoreImportConfig, groupModelIndex: int }); // config结构变了,查询时要指定哪个模型
+    }
+}

+ 136 - 0
doc/Mind/PrimaryElective.cs

@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    #region model
+
+    public class ElectiveSetting
+    {
+
+        //"selectResult": {
+        //  "params": {},
+        //  "roundId": 8,
+        //  "schoolId": 1,
+        //  "year": 2021,
+        //  "name": "第三次",
+        //  "beginTime": "2022-02-16",
+        //  "endTime": "2022-02-17",
+        //  "groupIds": "1,2,3,10,11,12",
+
+        public int preferenceCount; // 选填志愿数量
+
+  //  "groupList": [
+  //    {
+  //      "params": {},
+  //      "groupId": 1,
+  //      "mask": 448,
+  //      "name": "物化生",
+  //      "rank": 1
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 2,
+  //      "mask": 416,
+  //      "name": "物化政",
+  //      "rank": 2
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 3,
+  //      "mask": 400,
+  //      "name": "物化地",
+  //      "rank": 3
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 10,
+  //      "mask": 56,
+  //      "name": "历政地",
+  //      "rank": 10
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 11,
+  //      "mask": 104,
+  //      "name": "历政生",
+  //      "rank": 11
+  //    },
+  //    {
+  //  "params": { },
+  //      "groupId": 12,
+  //      "mask": 88,
+  //      "name": "历生地",
+  //      "rank": 12
+  //    }
+  //  ],
+  //  "state": "1"
+  //},
+  //"allowSelect": false
+
+    }
+
+    // 学生这里无论什么时间得到的都是最终结果,不需要中间快照,以此区分校长的选科数据。
+    // 校长的选科数据,初选时与学生相同;补录时也需要这份数据、但也需要初选与补录的差异部分;排名均衡同理
+    public class ElectiveSelectModel
+    {
+        public int groupId; // 组合
+        public string groupName; // 名称
+        public int classCount; // 班级数
+        public int personCount; // 限制人数
+
+        public int personInTime; // 实时人数
+        public int rankInGroup; // 选科实时排名
+        public int rankInGrade; // 选科全校排名
+
+        public bool allowSelect; // 是否可以报名
+        public string disabledReason; // 不可报名时的原因
+
+        public bool selected;  // 已报名
+        public int selectedRank; // 多志愿时的排序
+    }
+
+    // 自选专业
+    public class ElectiveOptionalMajor
+    {
+        public int collegeId;
+        public string collegeName;
+        // 可能还包含院校的一些其它属性
+
+        public string majorCategoryCode;
+        public string majorCategoryName;
+
+        public Dictionary<string, string> majors;
+
+        public string limitationA; // 选科限制1
+        public string limitationB; // 选科限制2
+
+        public List<int> matchedGroupIds; // 匹配的组合
+    }
+
+    #endregion
+
+    // 本期会涉及很多隐藏需求,前端会去back-ui按页面+按功能设置很多按钮功能,后续配置为权限组来操作
+    // 本期后台输出数据的时候不需要按权限屏蔽数据,全量输出即可,以减少后端开发的工作量
+    // 因为本期要控制隐藏的内容比功能入口、API入口要更精细的粒度,可能是列表的某几列,可能是某些按钮,如果严格API数据规范可能会增加海量的工作。
+    public interface IPrimaryElectiveService
+    {
+        // 旧接口getStudentSelected,返回内容+preferenceCount,志愿数
+        ElectiveSetting getStudentSelected();
+
+        // 学生获取选科状态数据,得到是每个组合最终状态和结果
+        List<ElectiveSelectModel> getPrimaryElectives();
+
+        // 学生获取自选专业及匹配情况
+        List<ElectiveOptionalMajor> getOptionalMajors();
+
+        // 初选报名。这里简化原型里的操作,直接提交,如需排序,调sortElectiveSelected接口,并刷新getPrimaryElectives
+        void submitPrimaryElective(ElectiveSelectModel model);
+
+        // 调整排序。这里简化原型里的操作,并刷新getPrimaryElectives
+        void sortPrimaryElective(ElectiveSelectModel model);
+
+        // 取消报名
+        void cancelPrimaryElective(ElectiveSelectModel model);
+    }
+}

+ 140 - 0
doc/Mind/StudentClassDispatch.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+
+namespace mxdemo.Mind
+{
+    public interface IStudentClassDispatchService
+    {
+        // 1 班级设置 参考校长的class-manage页面
+        //   a 从年级树缓存中查找当前选科学年对应的班级进行选择
+        //   b 允许用户输入不存在的班级,输入完毕后确认是否创建新班级
+        //       附:(创建后务必刷新缓存)(暂不考虑批量创建班级)(创建接口参考class-manage页面)
+
+        // 2 应用班级设置 设置人数及分配模式,得到初步分派的结果
+        // 3 手工微调 将查询组件化,允许加入多个对比查询,作选中操作,执行转移/或者交换操作
+
+        #region model
+
+        /// <summary>
+        /// 选科轮次,仅显示分班关键字段,后续可能还会加选科流程的其它字段
+        /// </summary>
+        public class Round
+        {
+            // 本身的字段
+            int roundId; // 选科轮次ID
+            string groupIds; // 开放组合
+            object[] roundGroups; // 这里包含各组合以及班级数设置
+            // ...
+
+            // 流程控制字段
+            bool allowDispatch; // 标记当前是否可以进行分班逻辑
+            Dictionary<int, int> enrollGroupCount; // 各组合录取人数,按组合汇总
+            bool dispatchCompleted; // 所有学生已经分配完毕
+        }
+
+        /// <summary>
+        /// 班级,仅显示分班关键字段
+        /// </summary>
+        public class Clazz
+        {
+            int classId;
+            string name;
+        }
+
+        public class Student
+        {
+            int studentId;
+            int sex;
+            int rankInGroup; // 组合内排名
+        }
+
+        public class DispatchSetting
+        {
+            int roundId; // 轮次ID
+            int groupId; // 组合ID
+
+            int classId; // 班级
+            int expectedCount; // 期望人数
+            int? actualCount; // 实际人数,应用设置才产生数量
+            int? actualCountInMale; // 实际男生,应用设置才产生数量
+            int? actualCountInFemale; // 实际女生,应用设置才产生数量
+        }
+
+        public class DispatchResult
+        {
+            int roundId; // 轮次ID
+            int groupId; // 组合ID
+
+            int classId; // 班级
+            Student[] students; // 分配的学生
+        }
+
+        public enum EnumDispatchMode
+        {
+            RankBalance, // 按成绩平均分配,默认按这种分配方式,可保证各个班成绩都差不多
+            RankPriority, // 按成绩优先,会导致成绩好的集中到一起
+            Random // 随机
+        }
+
+        /// <summary>
+        /// 查询参数
+        /// </summary>
+        public class DispatchQuery
+        {
+            int roundId;
+            int groupId;
+            int classId;
+            int? sex;
+        }
+        #endregion
+        /// <summary>
+        /// 按学年,次数查询
+        /// ROUND: 如果allowDispatch=false,分班页仅只读不能操作
+        /// </summary>
+        /// <returns></returns>
+        Round GetRoundStatus(int year, int round); // 类似/front/report/getStudentSelected
+
+        /// <summary>
+        /// 获取分班配置
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <returns></returns>
+        DispatchSetting[] GetDispatchSettings(int roundId);
+
+        /// <summary>
+        /// 保存配置
+        /// </summary>
+        /// <param name="settings"></param>
+        void SaveDispatchSettings(DispatchSetting[] settings);
+
+        /// <summary>
+        /// 应用配置,分派学生,按组合执行
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="mode"></param>
+        /// <returns></returns>
+        DispatchResult ApplyDispatchSettings(int roundId, int groupId, EnumDispatchMode mode = EnumDispatchMode.RankBalance);
+
+        /// <summary>
+        /// 分派名单查询,不需要分页,按班展示,启动table排序功能
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="groupIds"></param>
+        /// <returns></returns>
+        DispatchResult GetDispatchResult(DispatchQuery query);
+
+        /// <summary>
+        /// 分派转移,用这个接口实现转移学生或者交换学生(手工操作),后台在操作成功后记得刷新setting中的3个人数汇总字段
+        /// </summary>
+        /// <param name="roundId"></param>
+        /// <param name="students"></param>
+        /// <param name="toClassId"></param>
+        void ExchangeDispatch(int roundId, Student[] students, int toClassId);
+
+        /// <summary>
+        /// 分配完毕后发布,锁定分班流程
+        /// </summary>
+        /// <param name="roundId"></param>
+        void PublishDispatch(int roundId);
+    }
+}