跳转到内容

案例 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 名学生的多档对照
规则方法 入参(@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/。放入后执行:

Terminal window
mvn test -Dtest=ScholarshipServiceTest
package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 学生奖学金/助学金申请要素
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 奖学金评定决策
*/
@Data
public 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);
}
}
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);
}
<?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>
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;
}
}
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;
}
}

在工程根目录执行:

Terminal window
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 的教务/学工系统中直接嵌入。