案例 38:校园奖学金/评优/助学金资格评定
本案例对应 USE-CASES.md 场景 38(诉求 48:校园奖学金/评优/助学金资格评定),属于教育校园领域。一套规则同时承接国家级奖助、学业奖学金、助学金、单项奖学金四类申请:规则脚本负责硬资格、三维评分、等级映射与金额定额,Service 负责困难生身份、助学金成绩线等政策兜底,输出获奖等级、奖励金额、附加荣誉与落选原因。
学生申请要素由 ScholarshipApplication 承载:申请类型 applyType 分为 NATIONAL 国家级奖助、ACADEMIC 学业奖学金、NEED_BASED 助学金、INDIVIDUAL 单项奖学金;学业数据包括加权平均分、年级排名与年级总人数;资格数据包括不及格学分数、是否有纪律处分;科研与表现数据包括竞赛级别(国家级/省级/校级/无)、是否发表学术论文、是否志愿服务之星;助学金申请还需家庭经济情况(HARDSHIP 特困 / DIFFICULT 困难 / NORMAL 一般)。
评定分两段:先是硬资格门控,存在不及格学分或考核期内有纪律处分直接丧失参评资格,不进入评分;通过门控后计算综合测评得分——成绩分(60 分制,均分 95/90/85/80/75 对应 60/54/46/38/28)、排名分(25 分制,年级前 10%/20%/30%/50% 对应 25/20/13/7)、科研竞赛分(15 分制,国家级竞赛或论文 15、省级 10、校级 5),三项相加为综合测评总分。
等级映射按申请类型分流:国奖类总分 85 以上国家奖学金(8000 元)、70 以上国家励志奖学金(5000 元);学业类 85/65/50 分别对应一等(3000)、二等(2000)、三等(1000);助学金按家庭经济情况定特困档(3000)与困难档(2000);单项类按成果给科研创新(国家级竞赛或论文,1500)、省级竞赛(800)、志愿服务之星(500)。发表论文且获奖者加授「科研创新标兵」荣誉,志愿服务之星加授同名荣誉。
Service 层承担三条脚本之外的政策兜底:国家励志奖学金只面向家庭经济困难学生,综合分达标但家庭情况一般的学生改判落选并说明原因;助学金要求家庭经济情况非一般且综合测评不低于 25 分;单项奖学金若没有任何竞赛、论文或志愿成果则落选。决策结果 ScholarshipDecision 输出是否具备资格、综合测评分、获奖等级、奖励金额、落选原因与附加荣誉列表。
- 奖助项目多、口径各异:国奖、国励志、学业三等、助学金两档、三类单项奖共用一套申请入口,金额线与等级线复杂,人工评审容易错档错额。
- 资格门槛必须前置且不容变通:挂科与纪律处分是一票否决项,不能因为分数高就破格,硬门槛与评分必须严格分两段。
- 同一分数在不同奖项下结论不同:国家励志奖学金限困难生,助学金既看家庭情况又看成绩基本线,身份条件与分数条件交叉,纯查表无法表达。
- 排名百分比计算存在边界争议:前 10%、前 20% 要用排名与总人数实时比较,浮点除法在临界名次上容易产生口径纠纷。
- 政策调整频繁且时效性强:奖金额度、分数线每年随学校政策与财政文件调整,评审季时间紧,硬编码改动风险大。
- 评审结果需要可解释:学生申诉时必须能给出综合测评分构成、落选具体原因,而不是只有一个最终结论。
| 组成 | 文件 | 职责 |
|---|---|---|
| 入参模型 | model/ScholarshipApplication.java |
申请类型、学业数据、硬资格标志、竞赛论文志愿与家庭经济情况 |
| 决策模型 | model/ScholarshipDecision.java |
资格结论、综合测评分、获奖等级、金额、落选原因、附加荣誉 |
| 规则接口 | ScholarshipRules.java |
6 个规则方法:门控、三维评分、等级、金额 |
| 规则脚本 | resources/demo/scholarship.xml |
成绩档位、排名百分位、科研分值、等级线与金额表全部外置 |
| 编排服务 | service/ScholarshipService.java |
硬门控短路、三维汇总、困难生/成绩线/单项成果二次校验、荣誉组装 |
| 测试 | ScholarshipServiceTest.java |
6 组评定形态,含 12 名学生的多档对照 |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 入参(@Param) |
规则口径 | 返回 |
|---|---|---|---|
eligibilityOk |
failedCredits、violation |
无不及格学分且无纪律处分才具备参评资格 | boolean |
academicScore |
average |
均分 ≥95 60 / ≥90 54 / ≥85 46 / ≥80 38 / ≥75 28 / 其余 0 |
int 成绩分 |
rankScore |
rank、total |
前 10% 25 / 前 20% 20 / 前 30% 13 / 前 50% 7 / 其余 0 | int 排名分 |
researchScore |
competition、paper |
国家级竞赛或发表论文 15 / 省级竞赛 10 / 校级竞赛 5 / 其余 0 | int 科研分 |
awardLevel |
type、score、economic、competition、paper、volunteer |
国奖类 ≥85 NATIONAL_SPECIAL、≥70 NATIONAL_ENDEAVOR;学业类 ≥85/≥65/≥50 为一/二/三等;助学类按 HARDSHIP/DIFFICULT 定档;单项类国家级竞赛或论文 INNOVATION、省级竞赛 COMP_PROVINCIAL、志愿之星 VOLUNTEER;其余 NONE |
String 等级 |
amountOf |
level |
各等级定额:国奖 8000 / 国励志 5000 / 学业一等 3000、二等 2000、三等 1000 / 特困助学金 3000、困难 2000 / 科研创新 1500 / 省级竞赛 800 / 志愿 500 | int 金额 |
将下列文件按包结构放入 JQuick-Java 测试工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 demo/service/,XML 放入 src/test/resources/demo/。放入后执行:
mvn test -Dtest=ScholarshipServiceTestScholarshipApplication.java
Section titled “ScholarshipApplication.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 学生奖学金/助学金申请要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class ScholarshipApplication {
/** * 学号 */ private String studentId;
private String studentName;
/** * 申请类型:NATIONAL 国家级奖助 / ACADEMIC 学业奖学金 / * NEED_BASED 助学金 / INDIVIDUAL 单项奖学金 */ private String applyType;
/** * 加权平均分(0-100) */ private int averageScore;
/** * 年级排名 */ private int rankInGrade;
/** * 年级总人数 */ private int gradeTotal;
/** * 不及格学分数 */ private int failedCredits;
/** * 是否有纪律处分 */ private boolean disciplineViolation;
/** * 竞赛级别:NATIONAL 国家级 / PROVINCIAL 省级 / SCHOOL 校级 / NONE 无 */ private String competitionLevel;
/** * 是否发表学术论文 */ private boolean publishedPaper;
/** * 是否志愿服务之星 */ private boolean volunteerStar;
/** * 家庭经济情况:HARDSHIP 特困 / DIFFICULT 困难 / NORMAL 一般 */ private String familyEconomicLevel;}ScholarshipDecision.java
Section titled “ScholarshipDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 奖学金评定决策 */@Datapublic class ScholarshipDecision {
/** * 学号 */ private String studentId;
/** * 是否具备参评资格 */ private boolean eligible;
/** * 综合测评得分 */ private int evaluationScore;
/** * 获奖等级: * NATIONAL_SPECIAL 国家奖学金 / NATIONAL_ENDEAVOR 国家励志奖学金 / * FIRST/SECOND/THIRD 学业一/二/三等 / * FIRST_GRANT/SECOND_GRANT 特困/困难助学金 / * INNOVATION 科研创新单项 / COMP_PROVINCIAL 省级竞赛单项 / * VOLUNTEER 志愿服务单项 / NONE 未获奖 */ private String scholarshipLevel;
/** * 奖励金额(元) */ private int rewardAmount;
/** * 退回/落选原因 */ private String rejectReason;
/** * 附加荣誉 */ private List<String> honors = new ArrayList<String>();
public void addHonor(String honor) { this.honors.add(honor); }}ScholarshipRules.java
Section titled “ScholarshipRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 38:硬资格门控、学业/排名/科研分项与奖助等级规则 */public interface ScholarshipRules {
/** * 参评硬资格:无不及格学分且无纪律处分 */ boolean eligibilityOk(@Param("failedCredits") int failedCredits, @Param("violation") boolean violation);
/** * 成绩分(60 分制):≥95 60 / ≥90 54 / ≥85 46 / ≥80 38 / ≥75 28 / 其余 0 */ int academicScore(@Param("average") int average);
/** * 排名分(25 分制):前 10% 25 / 前 20% 20 / 前 30% 13 / 前 50% 7 / 其余 0 */ int rankScore(@Param("rank") int rank, @Param("total") int total);
/** * 科研竞赛分(15 分制):国家级竞赛或论文 15 / 省级 10 / 校级 5 */ int researchScore(@Param("competition") String competition, @Param("paper") boolean paper);
/** * 奖助等级: * 国奖类 ≥85 国家奖学金、≥70 国家励志(困难生由 Service 二次校验); * 学业类 ≥85 一等、≥65 二等、≥50 三等; * 助学类 HARDSHIP 特困档、DIFFICULT 困难档; * 单项类国家竞赛/论文创新 1500、省级竞赛 800、志愿之星 500 */ String awardLevel(@Param("type") String type, @Param("score") int score, @Param("economic") String economic, @Param("competition") String competition, @Param("paper") boolean paper, @Param("volunteer") boolean volunteer);
/** * 各等级对应金额 */ int amountOf(@Param("level") String level);}scholarship.xml
Section titled “scholarship.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.ScholarshipRules">
<java name="eligibilityOk" returnClass="boolean"> <![CDATA[ boolean def eligibilityOk(int:failedCredits, boolean:violation) { if (failedCredits > 0) { return false; } else if (violation) { return false; } else { return true; } } ]]> </java>
<java name="academicScore" returnClass="int"> <![CDATA[ int def academicScore(int:average) { if (average >= 95) { return 60; } else if (average >= 90) { return 54; } else if (average >= 85) { return 46; } else if (average >= 80) { return 38; } else if (average >= 75) { return 28; } else { return 0; } } ]]> </java>
<java name="rankScore" returnClass="int"> <![CDATA[ int def rankScore(int:rank, int:total) { if (rank * 100 < total * 10) { return 25; } else if (rank * 100 < total * 20) { return 20; } else if (rank * 100 < total * 30) { return 13; } else if (rank * 100 < total * 50) { return 7; } else { return 0; } } ]]> </java>
<java name="researchScore" returnClass="int"> <![CDATA[ int def researchScore(str:competition, boolean:paper) { if (competition == "NATIONAL") { return 15; } else if (paper) { return 15; } else if (competition == "PROVINCIAL") { return 10; } else if (competition == "SCHOOL") { return 5; } else { return 0; } } ]]> </java>
<java name="awardLevel" returnClass="java.lang.String"> <![CDATA[ str def awardLevel(str:type, int:score, str:economic, str:competition, boolean:paper, boolean:volunteer) { if (type == "NATIONAL") { if (score >= 85) { return "NATIONAL_SPECIAL"; } else if (score >= 70) { return "NATIONAL_ENDEAVOR"; } else { return "NONE"; } } else if (type == "ACADEMIC") { if (score >= 85) { return "FIRST"; } else if (score >= 65) { return "SECOND"; } else if (score >= 50) { return "THIRD"; } else { return "NONE"; } } else if (type == "NEED_BASED") { if (economic == "HARDSHIP") { return "FIRST_GRANT"; } else if (economic == "DIFFICULT") { return "SECOND_GRANT"; } else { return "NONE"; } } else { if (competition == "NATIONAL") { return "INNOVATION"; } else if (paper) { return "INNOVATION"; } else if (competition == "PROVINCIAL") { return "COMP_PROVINCIAL"; } else if (volunteer) { return "VOLUNTEER"; } else { return "NONE"; } } } ]]> </java>
<java name="amountOf" returnClass="int"> <![CDATA[ int def amountOf(str:level) { if (level == "NATIONAL_SPECIAL") { return 8000; } else if (level == "NATIONAL_ENDEAVOR") { return 5000; } else if (level == "FIRST") { return 3000; } else if (level == "SECOND") { return 2000; } else if (level == "FIRST_GRANT") { return 3000; } else if (level == "SECOND_GRANT") { return 2000; } else if (level == "INNOVATION") { return 1500; } else if (level == "COMP_PROVINCIAL") { return 800; } else if (level == "VOLUNTEER") { return 500; } else if (level == "THIRD") { return 1000; } else { return 0; } } ]]> </java>
</javas>ScholarshipService.java
Section titled “ScholarshipService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.ScholarshipRules;import com.github.paohaijiao.demo.model.ScholarshipApplication;import com.github.paohaijiao.demo.model.ScholarshipDecision;
/** * 场景 38:校园奖学金/评优/助学金资格评定 */public class ScholarshipService {
/** * 助学金最低综合测评分(成绩基本要求) */ private static final int NEED_BASED_MIN_SCORE = 25;
private final ScholarshipRules rules;
public ScholarshipService(ScholarshipRules rules) { this.rules = rules; }
public ScholarshipDecision evaluate(ScholarshipApplication a) { ScholarshipDecision d = new ScholarshipDecision(); d.setStudentId(a.getStudentId());
if (!rules.eligibilityOk(a.getFailedCredits(), a.isDisciplineViolation())) { d.setEligible(false); d.setScholarshipLevel("NONE"); d.setRewardAmount(0); if (a.getFailedCredits() > 0) { d.setRejectReason("存在 " + a.getFailedCredits() + " 学分不及格课程,不具备奖助参评资格"); } else { d.setRejectReason("考核期内存在纪律处分,不具备奖助参评资格"); } return d; } d.setEligible(true);
int score = rules.academicScore(a.getAverageScore()) + rules.rankScore(a.getRankInGrade(), a.getGradeTotal()) + rules.researchScore(a.getCompetitionLevel(), a.isPublishedPaper()); d.setEvaluationScore(score);
String level = rules.awardLevel(a.getApplyType(), score, a.getFamilyEconomicLevel(), a.getCompetitionLevel(), a.isPublishedPaper(), a.isVolunteerStar());
if ("NATIONAL_ENDEAVOR".equals(level) && "NORMAL".equals(a.getFamilyEconomicLevel())) { d.setScholarshipLevel("NONE"); d.setRewardAmount(0); d.setRejectReason("综合测评达国家励志奖学金线,但该奖项仅面向家庭经济困难学生"); return d; } if ("NEED_BASED".equals(a.getApplyType())) { if ("NORMAL".equals(a.getFamilyEconomicLevel())) { d.setScholarshipLevel("NONE"); d.setRewardAmount(0); d.setRejectReason("家庭经济情况为一般,不符合助学金申请条件"); return d; } if (score < NEED_BASED_MIN_SCORE) { d.setScholarshipLevel("NONE"); d.setRewardAmount(0); d.setRejectReason("综合测评 " + score + " 分未达助学金成绩基本线 " + NEED_BASED_MIN_SCORE + " 分"); return d; } } if ("INDIVIDUAL".equals(a.getApplyType()) && "NONE".equals(level)) { d.setScholarshipLevel("NONE"); d.setRewardAmount(0); d.setRejectReason("无国家级/省级竞赛、论文或志愿服务之星等单项成果"); return d; }
d.setScholarshipLevel(level); d.setRewardAmount(rules.amountOf(level)); if (a.isPublishedPaper() && !"NONE".equals(level)) { d.addHonor("科研创新标兵"); } if (a.isVolunteerStar()) { d.addHonor("志愿服务之星"); } return d; }}ScholarshipServiceTest.java
Section titled “ScholarshipServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.ScholarshipApplication;import com.github.paohaijiao.demo.model.ScholarshipDecision;import com.github.paohaijiao.demo.service.ScholarshipService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 38:校园奖学金/助学金评定 * <p> * 6 组形态:国家奖学金标杆、国家励志(困难生限定)、学业一二三档、 * 助学金困难分级与一般家庭驳回、单项奖学金、挂科/处分资格门控。 */public class ScholarshipServiceTest {
private ScholarshipService service;
@Before public void setUp() { ScholarshipRules rules = JQuickJava.create() .createApi(ScholarshipRules.class, "demo/scholarship.xml"); this.service = new ScholarshipService(rules); }
/** * 均分 96、年级前 1.5%、国赛获奖加论文 → 国家奖学金 8000 */ @Test public void shouldAwardNationalSpecialScholarship() { ScholarshipApplication a = base("S001", "NATIONAL", "NORMAL"); a.setAverageScore(96); a.setRankInGrade(3); a.setGradeTotal(200); a.setCompetitionLevel("NATIONAL"); a.setPublishedPaper(true);
ScholarshipDecision d = service.evaluate(a);
Assert.assertTrue(d.isEligible()); Assert.assertEquals(100, d.getEvaluationScore()); Assert.assertEquals("NATIONAL_SPECIAL", d.getScholarshipLevel()); Assert.assertEquals(8000, d.getRewardAmount()); Assert.assertTrue(d.getHonors().contains("科研创新标兵")); }
/** * 88 分前 12.5% 校赛 → 综合 71 分,困难生获国家励志奖学金 */ @Test public void shouldAwardNationalEndeavorToNeedyStudent() { ScholarshipApplication a = base("S002", "NATIONAL", "DIFFICULT"); a.setAverageScore(88); a.setRankInGrade(25); a.setGradeTotal(200); a.setCompetitionLevel("SCHOOL");
ScholarshipDecision d = service.evaluate(a);
Assert.assertEquals(71, d.getEvaluationScore()); Assert.assertEquals("NATIONAL_ENDEAVOR", d.getScholarshipLevel()); Assert.assertEquals(5000, d.getRewardAmount()); }
/** * 学业奖学金:95 分前 5% 一等、85 分前 15% 二等、80 分前 25% 三等 */ @Test public void shouldAwardAcademicScholarshipByThreeTiers() { ScholarshipApplication first = base("S003", "ACADEMIC", "NORMAL"); first.setAverageScore(95); first.setRankInGrade(10); first.setGradeTotal(200); ScholarshipDecision d1 = service.evaluate(first); Assert.assertEquals(85, d1.getEvaluationScore()); Assert.assertEquals("FIRST", d1.getScholarshipLevel()); Assert.assertEquals(3000, d1.getRewardAmount());
ScholarshipApplication second = base("S004", "ACADEMIC", "NORMAL"); second.setAverageScore(85); second.setRankInGrade(30); second.setGradeTotal(200); ScholarshipDecision d2 = service.evaluate(second); Assert.assertEquals(66, d2.getEvaluationScore()); Assert.assertEquals("SECOND", d2.getScholarshipLevel()); Assert.assertEquals(2000, d2.getRewardAmount());
ScholarshipApplication third = base("S005", "ACADEMIC", "NORMAL"); third.setAverageScore(80); third.setRankInGrade(50); third.setGradeTotal(200); ScholarshipDecision d3 = service.evaluate(third); Assert.assertEquals(51, d3.getEvaluationScore()); Assert.assertEquals("THIRD", d3.getScholarshipLevel()); Assert.assertEquals(1000, d3.getRewardAmount()); }
/** * 助学金:特困 3000、困难 2000、一般家庭驳回 */ @Test public void shouldGrantNeedBasedAidByEconomicLevel() { ScholarshipApplication hardship = base("S006", "NEED_BASED", "HARDSHIP"); hardship.setAverageScore(75); hardship.setRankInGrade(100); hardship.setGradeTotal(200); ScholarshipDecision d1 = service.evaluate(hardship); Assert.assertEquals(28, d1.getEvaluationScore()); Assert.assertEquals("FIRST_GRANT", d1.getScholarshipLevel()); Assert.assertEquals(3000, d1.getRewardAmount());
ScholarshipApplication difficult = base("S007", "NEED_BASED", "DIFFICULT"); difficult.setAverageScore(75); difficult.setRankInGrade(100); difficult.setGradeTotal(200); ScholarshipDecision d2 = service.evaluate(difficult); Assert.assertEquals("SECOND_GRANT", d2.getScholarshipLevel()); Assert.assertEquals(2000, d2.getRewardAmount());
ScholarshipApplication normal = base("S008", "NEED_BASED", "NORMAL"); normal.setAverageScore(90); normal.setRankInGrade(5); normal.setGradeTotal(200); ScholarshipDecision d3 = service.evaluate(normal); Assert.assertEquals("NONE", d3.getScholarshipLevel()); Assert.assertTrue(d3.getRejectReason().contains("不符合助学金申请条件")); }
/** * 单项:国赛创新奖 1500;志愿服务之星 500 */ @Test public void shouldAwardIndividualScholarship() { ScholarshipApplication comp = base("S009", "INDIVIDUAL", "NORMAL"); comp.setCompetitionLevel("NATIONAL"); ScholarshipDecision d1 = service.evaluate(comp); Assert.assertEquals("INNOVATION", d1.getScholarshipLevel()); Assert.assertEquals(1500, d1.getRewardAmount());
ScholarshipApplication volunteer = base("S010", "INDIVIDUAL", "NORMAL"); volunteer.setVolunteerStar(true); ScholarshipDecision d2 = service.evaluate(volunteer); Assert.assertEquals("VOLUNTEER", d2.getScholarshipLevel()); Assert.assertEquals(500, d2.getRewardAmount()); Assert.assertTrue(d2.getHonors().contains("志愿服务之星")); }
/** * 挂科或处分 → 直接丧失参评资格 */ @Test public void shouldRejectFailedOrDisciplinedApplicant() { ScholarshipApplication failed = base("S011", "ACADEMIC", "NORMAL"); failed.setAverageScore(96); failed.setFailedCredits(3); ScholarshipDecision d1 = service.evaluate(failed); Assert.assertFalse(d1.isEligible()); Assert.assertEquals("NONE", d1.getScholarshipLevel()); Assert.assertTrue(d1.getRejectReason().contains("3 学分不及格"));
ScholarshipApplication violated = base("S012", "ACADEMIC", "NORMAL"); violated.setAverageScore(96); violated.setDisciplineViolation(true); ScholarshipDecision d2 = service.evaluate(violated); Assert.assertFalse(d2.isEligible()); Assert.assertTrue(d2.getRejectReason().contains("纪律处分")); }
private ScholarshipApplication base(String id, String type, String economic) { ScholarshipApplication a = new ScholarshipApplication(); a.setStudentId(id); a.setStudentName("测试学生"); a.setApplyType(type); a.setAverageScore(70); a.setRankInGrade(100); a.setGradeTotal(200); a.setFailedCredits(0); a.setDisciplineViolation(false); a.setCompetitionLevel("NONE"); a.setPublishedPaper(false); a.setVolunteerStar(false); a.setFamilyEconomicLevel(economic); return a; }}在工程根目录执行:
mvn test -Dtest=ScholarshipServiceTest测试覆盖 6 组评定形态,共构造 12 名学生申请,断言与源码逐一对应:
| 测试方法 | 业务形态 | 关键断言 |
|---|---|---|
shouldAwardNationalSpecialScholarship |
均分 96、年级前 1.5%、国赛加论文 | 具备资格;综合分 100(60+25+15);NATIONAL_SPECIAL;8000 元;荣誉含「科研创新标兵」 |
shouldAwardNationalEndeavorToNeedyStudent |
88 分、前 12.5%、校赛、困难家庭 | 综合分 71(46+20+5);NATIONAL_ENDEAVOR;5000 元 |
shouldAwardAcademicScholarshipByThreeTiers |
学业奖三名学生分档 | 95 分前 5% 综合 85 → FIRST 3000;85 分前 15% 综合 66 → SECOND 2000;80 分前 25% 综合 51 → THIRD 1000 |
shouldGrantNeedBasedAidByEconomicLevel |
助学金三名学生:特困、困难、一般 | 特困综合 28 → FIRST_GRANT 3000;困难 → SECOND_GRANT 2000;一般家庭(综合 79)仍 NONE,原因含「不符合助学金申请条件」 |
shouldAwardIndividualScholarship |
单项奖:国赛与志愿服务之星 | 国赛 → INNOVATION 1500;志愿之星 → VOLUNTEER 500,荣誉含「志愿服务之星」 |
shouldRejectFailedOrDisciplinedApplicant |
挂科 3 学分与纪律处分 | 两者均 eligible=false、等级 NONE;原因分别含「3 学分不及格」「纪律处分」 |
- 硬门槛前置两段式:
eligibilityOk不通过时 Service 直接返回,根本不计算综合分,挂科与处分的一票否决结论不会被高分逆转,且按否决项给出差异化原因。 - 一方法一口径、三维独立打分:成绩、排名、科研各是一个规则方法,分值上限 60/25/15 互不干扰;等级映射与金额定额再分两个方法,评审争议时可逐维回溯。
- 交叉相乘计算排名百分位:
rankScore用rank * 100 < total * 10/20/30/50判定前 10%/20%/30%/50%,全程整数比较,规避引擎整数除法不提升浮点带来的临界名次误差。 - Service 政策二次校验:国家励志奖学金的困难生限定、助学金的家庭情况与 25 分成绩基本线、单项奖无成果落选,都在 Service 对脚本等级做改判,规则脚本只承载通用分数线与金额表。
- 规则外置热更新:成绩档位、排名比例、奖金额度全部写在 XML,学校年度调整资助政策时改脚本重建代理即可,评审季无需重新打包学生系统。
- 代理复用、ASM、Java 8 信创友好:
@Before中一次创建ScholarshipRules代理服务全部用例;动态代理 + ASM 无重型容器依赖,可在 JDK 8 与国产化 JDK 的教务/学工系统中直接嵌入。