SpringBoot集成OptaPlanner與使用指南
本文基于IBMS項目中的值班排班系統(tǒng)實現(xiàn),詳細介紹如何在Spring Boot項目中集成OptaPlanner,并構(gòu)建復雜的約束求解系統(tǒng)。
OptaPlanner簡介
什么是OptaPlanner?
OptaPlanner 是一個開源的約束滿足問題求解器(Constraint Satisfaction Problem Solver),由JBoss社區(qū)開發(fā)。它能夠:
- ?? 快速找到復雜約束優(yōu)化問題的最優(yōu)或近似最優(yōu)解
- ?? 支持硬約束(必須滿足)和軟約束(盡量滿足)
- ? 提供多種求解算法(局部搜索、遺傳算法、模擬退火等)
- ?? 被廣泛應用于排班、路線規(guī)劃、資源分配等領(lǐng)域
典型應用場景
| 場景 | 描述 | 難度 |
|---|---|---|
| 員工排班 | 在滿足各種約束的情況下分配班次 | 中等 |
| 車隊路線規(guī)劃 | 優(yōu)化配送路線 | 高 |
| 會議日程安排 | 安排會議時間和地點 | 中等 |
| 醫(yī)院值班 | 醫(yī)護人員值班分配 | 高 |
| 考場座位分配 | 考生與考場的最優(yōu)分配 | 中等 |
集成步驟
1. 添加依賴
在 pom.xml 中添加OptaPlanner依賴:
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-core</artifactId>
<version>7.47.0.Final</version>
</dependency>版本選擇建議:
7.x.x- 穩(wěn)定版本,性能好,文檔完善8.x.x- 新功能,更好的API設(shè)計- 建議根據(jù)JDK版本選擇:JDK 8-11 用 7.x,JDK 11+ 用 8.x
- 這里選擇了7.x.x版本是因為項目的JDK版本限制
2. 創(chuàng)建求解配置文件
在 src/main/resources/planner/solverConfig.xml 創(chuàng)建配置文件:
<solver>
<!-- 定義求解的問題類(Solution) -->
<solutionClass>org.jeecg.modules.planner.Roster</solutionClass>
<!-- 定義規(guī)劃實體類 -->
<entityClass>org.jeecg.modules.planner.ShiftAssignment</entityClass>
<!-- 運行模式:FAST_ASSERT(快速調(diào)試)、FULL_ASSERT(完整校驗)、NON_ASSERT(生產(chǎn)環(huán)境) -->
<environmentMode>FAST_ASSERT</environmentMode>
<!-- 定義約束提供者 -->
<scoreDirectorFactory>
<constraintProviderClass>org.jeecg.modules.planner.RosterConstraintProvider</constraintProviderClass>
</scoreDirectorFactory>
<!-- 求解時間限制 -->
<termination>
<secondsSpentLimit>10</secondsSpentLimit>
<bestScoreLimit>0hard/-5000soft</bestScoreLimit>
</termination>
<!-- 構(gòu)造啟發(fā)階段 -->
<constructionHeuristic>
<constructionHeuristicType>FIRST_FIT_DECREASING</constructionHeuristicType>
<entitySorterManner>NONE</entitySorterManner>
<valueSorterManner>NONE</valueSorterManner>
</constructionHeuristic>
<!-- 局部搜索階段 -->
<localSearch>
<acceptor>
<acceptorType>SIMULATED_ANNEALING</acceptorType>
<simulatedAnnealingStartingTemperature>2hard/1000soft</simulatedAnnealingStartingTemperature>
</acceptor>
<forager>
<acceptedCountLimit>5000</acceptedCountLimit>
</forager>
</localSearch>
</solver>3. Spring Boot集成配置
創(chuàng)建 @Configuration 類(可選,用于高級配置):
@Configuration
public class OptaPlannerConfig {
@Bean
public SolverFactory<Roster> solverFactory() {
return SolverFactory.createFromXmlResource("./planner/solverConfig.xml");
}
}核心概念
?? 關(guān)鍵術(shù)語解釋
1.PlanningSolution(規(guī)劃求解方案)
- 代表整個優(yōu)化問題
- 包含規(guī)劃實體集合和問題事實
- 有一個規(guī)劃評分
@PlanningSolution
public class Roster {
// 問題事實:不被規(guī)劃器修改
@ProblemFactProperty
private ConstraintConfig constraintConfig;
// 規(guī)劃實體集合:規(guī)劃器需要分配的值域變量
@PlanningEntityCollectionProperty
private List<ShiftAssignment> shiftAssignmentList;
// 值域范圍:規(guī)劃變量的可選值
@ValueRangeProvider(id = "employeeRange")
private List<Employee> employeeList;
// 規(guī)劃評分:衡量方案質(zhì)量
@PlanningScore
private HardSoftScore score;
}2.PlanningEntity(規(guī)劃實體)
- 代表需要被優(yōu)化分配的對象
- 包含規(guī)劃變量
@PlanningEntity
public class ShiftAssignment {
private Shift shift; // 班次(不變)
@PlanningVariable(valueRangeProviderRefs = "employeeRange")
private Employee employee; // 需要分配的員工
}3.PlanningVariable(規(guī)劃變量)
- 是規(guī)劃器需要分配的值
- 從值域提供者中獲取可選值
@PlanningVariable(valueRangeProviderRefs = "employeeRange") private Employee employee;
4.ConstraintProvider(約束提供者)
- 定義所有約束規(guī)則
- 硬約束:違反會使評分不可行
- 軟約束:違反會降低評分
public class RosterConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[]{
// 硬約束
shiftMustHaveAssignment(factory),
// 軟約束
balanceWorkload(factory)
};
}
}5.Score(評分)
HardSoftScore- 分離硬軟約束評分- 格式:
Xhard/Ysoft(例如:0hard/-500soft) - 評分越高越好,最優(yōu)為
0hard/0soft
實現(xiàn)詳解
1. 定義域模型
Employee(員工)
public class Employee {
private String id; // 員工ID
private String name; // 員工姓名
private EmployeePreference preference; // 員工偏好
private String availableShifts; // 可上班次(逗號分隔)
private String departId; // 所屬部門
public Employee(String id, String name, EmployeePreference preference,
String availableShifts, String departId) {
this.id = id;
this.name = name;
this.preference = preference;
this.availableShifts = availableShifts;
this.departId = departId;
}
}說明:
availableShifts存儲員工可以上的班次,格式為班次ID的逗號分隔列表preference記錄員工個人偏好(如不想上夜班等)
EmployeePreference(員工偏好)
public class EmployeePreference {
private boolean avoidConsecutiveWork; // 不能連續(xù)排班
private boolean avoidWorkday; // 工作日不能排班
private boolean avoidHoliday; // 節(jié)假日不能排班
}
Shift(班次)
public class Shift {
private LocalDate date; // 班次日期
private boolean isHoliday; // 是否為節(jié)假日
private ShiftType type; // 班次類型(早/中/晚)
private boolean isNight; // 是否為夜班
private int requiredEmployees; // 需要的員工數(shù)
}
ShiftType(班次類型枚舉)
public enum ShiftType {
MORNING(1), // 早班
NOON(2), // 中班
NIGHT(3); // 晚班
private final int order;
ShiftType(int order) { this.order = order; }
public int getOrder() { return order; }
}ShiftAssignment(班次分配 - 規(guī)劃實體)
@PlanningEntity
public class ShiftAssignment {
private Shift shift;
@PlanningVariable(valueRangeProviderRefs = "employeeRange")
private Employee employee;
public ShiftAssignment(Shift shift, Employee employee) {
this.shift = shift;
this.employee = employee;
}
}Roster(排班表 - 求解方案)
@PlanningSolution
public class Roster {
@ProblemFactProperty
private ConstraintConfig constraintConfig;
@PlanningEntityCollectionProperty
private List<ShiftAssignment> shiftAssignmentList;
@ValueRangeProvider(id = "employeeRange")
private List<Employee> employeeList;
@PlanningScore
private HardSoftScore score;
private List<Shift> shiftList;
}2. 數(shù)據(jù)準備 - RosterGenerator
@Component
public class RosterGenerator {
@Resource
private IDutyStaffService dutyStaffService;
@Resource
private IDutyHolidayService dutyHolidayService;
@Resource
private IDutyShiftService dutyShiftService;
@Resource
private ISysUserService sysUserService;
public List<Roster> loadRosterFromDB(ConstraintConfig config) {
// 1. 加載所有班次
List<DutyShift> shiftList = dutyShiftService.list();
Map<String, Integer> shiftMap = shiftList.stream()
.collect(Collectors.toMap(DutyShift::getId, DutyShift::getOrder));
config.setShiftMap(shiftMap);
// 2. 獲取值班部門的所有員工
List<String> departIds = config.getDepartIds();
LambdaQueryWrapper<DutyStaff> staffQueryWrapper =
new LambdaQueryWrapper<DutyStaff>().in(DutyStaff::getDepartId, departIds);
List<DutyStaff> staffs = dutyStaffService.list(staffQueryWrapper);
// 3. 生成排班期間內(nèi)的所有班次
Date startDate = config.getStartDate();
Date endDate = config.getEndDate();
LocalDate start = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate end = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
List<DutyHoliday> holidays = dutyHolidayService.list(
new LambdaQueryWrapper<DutyHoliday>()
.between(DutyHoliday::getCalendarDate, startDate, endDate)
);
ArrayList<Shift> shifts = new ArrayList<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
// 根據(jù)配置決定是否跳過特定日期
if (shouldSkipDate(date, holidays, config)) continue;
// 為該日期生成三個班次
boolean isHoliday = isHoliday(date, holidays);
shifts.add(new Shift(date, isHoliday, ShiftType.MORNING, false));
shifts.add(new Shift(date, isHoliday, ShiftType.NOON, false));
shifts.add(new Shift(date, isHoliday, ShiftType.NIGHT, true));
}
// 4. 為每個部門構(gòu)造Roster對象
ArrayList<Roster> rosters = new ArrayList<>();
for (String departId : departIds) {
// 獲取該部門的員工
List<Employee> employeeList = staffs.stream()
.filter(staff -> staff.getDepartId().equals(departId))
.map(staff -> new Employee(
staff.getUserId(),
sysUser.getRealname(),
buildPreference(staff),
staff.getShiftId(),
departId
))
.collect(Collectors.toList());
if (employeeList.isEmpty()) continue;
// 初始分配(隨機輪轉(zhuǎn))
List<ShiftAssignment> assignments = new ArrayList<>();
for (int i = 0; i < shifts.size(); i++) {
int selectedIndex = (i + new Random().nextInt(employeeList.size()))
% employeeList.size();
assignments.add(new ShiftAssignment(
shifts.get(i),
employeeList.get(selectedIndex)
));
}
Roster roster = new Roster();
roster.setConstraintConfig(config);
roster.setEmployeeList(employeeList);
roster.setShiftList(shifts);
roster.setShiftAssignmentList(assignments);
rosters.add(roster);
}
return rosters;
}
}約束定義
RosterConstraintProvider 詳解
@Slf4j
public class RosterConstraintProvider implements ConstraintProvider {
@Override
public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory factory) {
return new Constraint[]{
// 硬約束
shiftMustHaveAssignment(factory), // ① 每個班次必須有員工
mustBeAvailableForShift(factory), // ② 員工只能上可分配班次
avoidConsecutiveShifts(factory), // ③ 避免連續(xù)排班
avoidNightShiftConsecutive(factory), // ④ 避免連續(xù)夜班
// 軟約束
balanceWorkload(factory), // ⑤ 平衡工作量
encourageMultipleEmployees(factory), // ⑥ 鼓勵多員工參與
avoidHoliday(factory), // ⑦ 避免節(jié)假日排班
avoidWorkday(factory), // ⑧ 避免工作日排班
avoidClusteredAssignments(factory) // ⑨ 避免排班集中
};
}
/** ① 硬約束:每個班次必須有員工分配 */
private Constraint shiftMustHaveAssignment(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() == null)
.penalize("班次必須有員工分配", HardSoftScore.ofHard(1));
}
/** ② 硬約束:按照員工可排班班次排班 */
private Constraint mustBeAvailableForShift(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(factory.from(ConstraintConfig.class))
.filter((assignment, cfg) -> {
Employee employee = assignment.getEmployee();
Shift shift = assignment.getShift();
String available = employee.getAvailableShifts();
if (available == null || available.isEmpty()) {
return true; // 無法分配
}
List<Integer> orderList = Arrays.stream(available.split(","))
.map(cfg.getShiftMap()::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return !orderList.contains(shift.getType().getOrder());
})
.penalize("按照員工可排班班次排班", HardSoftScore.ofHard(1));
}
/** ③ 硬約束:避免連續(xù)排班 */
private Constraint avoidConsecutiveShifts(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(ShiftAssignment.class,
Joiners.equal(ShiftAssignment::getEmployee),
Joiners.lessThan(sa -> sa.getShift().getDate()))
.join(factory.from(ConstraintConfig.class))
.filter((a, b, cfg) -> {
// 檢查是否同一員工的連續(xù)排班
boolean isCfgConstraint = cfg.isAvoidConsecutiveShifts();
boolean isPreference = cfg.isAllowPersonalPreference() &&
a.getEmployee().getPreference().isAvoidConsecutiveWork();
if (!isCfgConstraint && !isPreference) return false;
long daysDiff = ChronoUnit.DAYS.between(
a.getShift().getDate(),
b.getShift().getDate()
);
// 同一天相鄰班次或連續(xù)天排班
return (daysDiff == 0 &&
Math.abs(a.getShift().getType().getOrder() -
b.getShift().getType().getOrder()) == 1) ||
(daysDiff == 1 &&
Math.abs(a.getShift().getType().getOrder() -
b.getShift().getType().getOrder()) <= 1);
})
.penalize("避免連續(xù)排班", HardSoftScore.ofHard(1));
}
/** ④ 硬約束:避免連續(xù)夜班 */
private Constraint avoidNightShiftConsecutive(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(ShiftAssignment.class,
Joiners.equal(ShiftAssignment::getEmployee),
Joiners.lessThan(sa -> sa.getShift().getDate()))
.join(factory.from(ConstraintConfig.class))
.filter((a, b, cfg) ->
cfg.isAvoidConsecutiveNightShifts() &&
a.getShift().isNight() &&
b.getShift().isNight()
)
.penalize("避免連續(xù)夜班", HardSoftScore.ofHard(1));
}
/** ⑤ 軟約束:平衡排班量 */
private Constraint balanceWorkload(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.groupBy(ShiftAssignment::getEmployee, ConstraintCollectors.count())
.join(factory.from(ConstraintConfig.class))
.penalize("平衡排班量", HardSoftScore.ofSoft(1000),
(employee, count, cfg) ->
cfg.isBalanceWorkload() ? Math.abs(count - 5) : 0);
}
/** ⑥ 軟約束:鼓勵多員工參與 */
private Constraint encourageMultipleEmployees(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.groupBy(ShiftAssignment::getEmployee)
.reward("鼓勵多員工參與", HardSoftScore.ofSoft(500));
}
/** ⑦ 軟約束:避免節(jié)假日排班 */
private Constraint avoidHoliday(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(factory.from(ConstraintConfig.class))
.filter((sa, cfg) ->
(cfg.isAvoidHoliday() ||
(cfg.isAllowPersonalPreference() &&
sa.getEmployee().getPreference().isAvoidHoliday())) &&
sa.getShift().isHoliday()
)
.penalize("避免節(jié)假日排班", HardSoftScore.ofSoft(1));
}
/** ⑧ 軟約束:避免工作日排班 */
private Constraint avoidWorkday(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(factory.from(ConstraintConfig.class))
.filter((sa, cfg) ->
(cfg.isAvoidWorkdayShifts() ||
(cfg.isAllowPersonalPreference() &&
sa.getEmployee().getPreference().isAvoidWorkday())) &&
!sa.getShift().isHoliday()
)
.penalize("避免工作日排班", HardSoftScore.ofSoft(1));
}
/** ⑨ 軟約束:避免排班過于集中 */
private Constraint avoidClusteredAssignments(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(ShiftAssignment.class,
Joiners.equal(ShiftAssignment::getEmployee),
Joiners.lessThan(sa -> sa.getShift().getDate()))
.join(factory.from(ConstraintConfig.class))
.filter((a, b, cfg) -> {
long daysBetween = ChronoUnit.DAYS.between(
a.getShift().getDate(),
b.getShift().getDate()
);
return daysBetween > 0 && daysBetween <= 2;
})
.penalize("避免排班過于集中", HardSoftScore.ofSoft(5));
}
}約束編寫最佳實踐
1. 命名清晰
// ? 好:清楚表達約束含義
.penalize("避免連續(xù)排班", HardSoftScore.ofHard(1))
// ? 差:模糊不清
.penalize("constraint1", HardSoftScore.ofHard(1))2. 使用過濾優(yōu)先
// ? 好:先過濾,減少計算 .filter(sa -> sa.getEmployee() != null) .penalize(...) // ? 差:后續(xù)在判斷中處理null .penalize(..., (sa, cfg) -> sa.getEmployee() == null ? 0 : penalty)
3. 平衡評分權(quán)重
// 硬約束權(quán)重通常設(shè)為1(表示不可違反) HardSoftScore.ofHard(1) // 軟約束權(quán)重根據(jù)重要性設(shè)置 HardSoftScore.ofSoft(1) // 低優(yōu)先級 HardSoftScore.ofSoft(100) // 中等優(yōu)先級 HardSoftScore.ofSoft(1000) // 高優(yōu)先級
實際應用
PlannerController - API接口
@RestController
@RequestMapping("/planner")
public class PlannerController {
@Resource
private RosterGenerator rosterGenerator;
@PostMapping("/solve")
public Object solve(@RequestBody ConstraintConfig config) {
// 1. 從數(shù)據(jù)庫加載數(shù)據(jù)并生成Roster對象
List<Roster> rosters = rosterGenerator.loadRosterFromDB(config);
// 2. 使用線程池并行求解多個Roster
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Roster>> futures = new ArrayList<>();
for (Roster roster : rosters) {
futures.add(executor.submit(() -> {
// 3. 為每個Roster創(chuàng)建求解器
SolverFactory<Roster> factory = SolverFactory
.createFromXmlResource("./planner/solverConfig.xml");
Solver<Roster> solver = factory.buildSolver();
// 4. 執(zhí)行求解
return solver.solve(roster);
}));
}
// 5. 收集所有求解結(jié)果
ArrayList<Roster> result = new ArrayList<>();
for (Future<Roster> f : futures) {
result.add(f.get());
}
executor.shutdown();
return result;
}
}請求示例
curl -X POST http://localhost:8080/planner/solve \
-H "Content-Type: application/json" \
-d '{
"departIds": ["dept_001", "dept_002"],
"shiftIds": ["shift_001", "shift_002", "shift_003"],
"startDate": "2025-11-01",
"endDate": "2025-11-30",
"avoidConsecutiveShifts": true,
"avoidConsecutiveNightShifts": true,
"allowPersonalPreference": true,
"balanceWorkload": true,
"avoidWorkdayShifts": false,
"avoidHoliday": false,
"hardWeight": 1,
"softWeight": 1
}'
響應示例
[
{
"constraintConfig": {...},
"shiftAssignmentList": [
{
"shift": {
"date": "2025-11-01",
"isHoliday": false,
"type": "MORNING",
"isNight": false
},
"employee": {
"id": "emp_001",
"name": "張三",
"availableShifts": "shift_001,shift_002"
}
}
],
"score": {
"hardScore": 0,
"softScore": -450
}
}
]
性能優(yōu)化
1. 求解配置優(yōu)化
<!-- 延長求解時間以獲得更優(yōu)解 -->
<termination>
<secondsSpentLimit>30</secondsSpentLimit>
<bestScoreLimit>0hard/-3000soft</bestScoreLimit>
</termination>
| 參數(shù) | 含義 | 影響 |
|---|---|---|
secondsSpentLimit | 最長求解時間 | 時間越長解越優(yōu),但響應慢 |
bestScoreLimit | 目標評分 | 達到目標立即停止 |
constructionHeuristicType | 初始解策略 | 影響啟發(fā)階段速度 |
acceptedCountLimit | 每步評估數(shù) | 越大越精確,越慢 |
2. 初始解優(yōu)化
改進 RosterGenerator 中的初始分配策略:
// 當前:隨機輪轉(zhuǎn)
int selectedIndex = (i + random.nextInt(employeeCount)) % employeeCount;
// 改進:考慮員工可用班次
int selectedIndex = selectBestEmployee(shifts.get(i), employeeList, assignments);
private int selectBestEmployee(Shift shift, List<Employee> employees,
List<ShiftAssignment> assignments) {
Employee best = null;
int minAssignments = Integer.MAX_VALUE;
for (Employee emp : employees) {
// 檢查員工是否可以上該班次
if (!canAssignShift(emp, shift)) continue;
// 選擇已分配班次最少的員工
long count = assignments.stream()
.filter(sa -> sa.getEmployee().equals(emp))
.count();
if (count < minAssignments) {
minAssignments = (int) count;
best = emp;
}
}
return best != null ? employees.indexOf(best) : 0;
}3. 約束優(yōu)化
// ? 好:使用Joiners進行高效連接
.join(ShiftAssignment.class,
Joiners.equal(ShiftAssignment::getEmployee),
Joiners.lessThan(sa -> sa.getShift().getDate()))
// ? 差:在filter中進行復雜計算
.filter(a -> assignments.stream()
.filter(b -> b.getEmployee().equals(a.getEmployee()))
.count() > 1)4. 數(shù)據(jù)結(jié)構(gòu)優(yōu)化
// 預計算班次映射,避免重復查詢
Map<String, Integer> shiftMap = shiftList.stream()
.collect(Collectors.toMap(DutyShift::getId, DutyShift::getOrder));
// 預計算節(jié)假日集合,提高查詢速度
Set<LocalDate> holidayDates = holidays.stream()
.map(h -> h.getCalendarDate())
.collect(Collectors.toSet());5. 索引優(yōu)化建議
// 數(shù)據(jù)庫索引建議 CREATE INDEX idx_duty_staff_depart ON duty_staff(depart_id); CREATE INDEX idx_duty_holiday_date ON duty_holiday(calendar_date); CREATE INDEX idx_duty_shift_order ON duty_shift(`order`);
常見問題
Q1: 求解沒有找到可行解?
癥狀: 返回 0hard/-XXsoft 的評分
原因分析:
- 硬約束過多或相互沖突
- 員工人數(shù)過少,無法覆蓋所有班次
- 員工可用班次限制過嚴
解決方案:
- 檢查約束配置
// 減少硬約束 avoidConsecutiveShifts(factory), // 改為軟約束
- 增加員工數(shù)量或擴大可用班次
- 調(diào)整初始解策略
Q2: 求解速度太慢?
癥狀: 求解時間超過預期
優(yōu)化步驟:
// ①減少數(shù)據(jù)量
List<Roster> rosters = generator.loadRosterFromDB(config); // 較大
// ↓
// 改為按部門分批求解
List<Roster> batch1 = generator.loadRosterForDept(config, "dept_001");
List<Roster> batch2 = generator.loadRosterForDept(config, "dept_002");
// ②簡化約束
return new Constraint[]{
shiftMustHaveAssignment(factory), // 保留必需
mustBeAvailableForShift(factory),
// avoidConsecutiveShifts(factory), // 移除非關(guān)鍵
// avoidClusteredAssignments(factory),
};
// ③降低求解時間
<secondsSpentLimit>5</secondsSpentLimit> // 從10秒改為5秒Q3: 評分計算不合理?
癥狀: 明顯不均衡的排班方案
檢查清單:
// 1. 檢查權(quán)重設(shè)置
balanceWorkload(factory)
.penalize(..., HardSoftScore.ofSoft(1000), // 權(quán)重是否合適?
(emp, count, cfg) -> cfg.isBalanceWorkload() ? Math.abs(count - 5) : 0);
// 2. 檢查目標值設(shè)置
Math.abs(count - 5) // 5是否應該是 count/average?
// 3. 檢查約束條件
filter((a, b, cfg) -> cfg.isBalanceWorkload() ? ... : true)
// ↑ 應該返回false,表示不應用該約束改進方案:
// 改用相對差異 int avgCount = totalShifts / employeeCount; return Math.abs(count - avgCount); // 或使用百分位數(shù) double threshold = avgCount * 1.2; // 允許超過20% return count > threshold ? count - threshold : 0;
Q4: 內(nèi)存占用過大?
癥狀: OutOfMemoryError 或響應緩慢
優(yōu)化方案:
// ①減少規(guī)劃實體數(shù)量
// 原:month × 3shifts × departments = 大量ShiftAssignment
// 改:預處理,只保留必要的
// ②使用流式處理
List<ShiftAssignment> assignments = new ArrayList<>();
for (Shift shift : shifts) {
assignments.add(new ShiftAssignment(shift, null)); // 延遲分配
}
// ③分批求解
int batchSize = 100;
for (int i = 0; i < shifts.size(); i += batchSize) {
List<Shift> batch = shifts.subList(i, Math.min(i + batchSize, shifts.size()));
solveBatch(batch);
}Q5: 如何調(diào)試約束?
啟用詳細日志:
logging:
level:
org.optaplanner: DEBUG
org.jeecg.modules.planner: DEBUG
在約束中添加日志:
private Constraint avoidConsecutiveShifts(ConstraintFactory factory) {
return factory.from(ShiftAssignment.class)
.filter(sa -> sa.getEmployee() != null)
.join(ShiftAssignment.class,
Joiners.equal(ShiftAssignment::getEmployee),
Joiners.lessThan(sa -> sa.getShift().getDate()))
.join(factory.from(ConstraintConfig.class))
.filter((a, b, cfg) -> {
boolean violated = /* violation check */;
if (violated) {
log.debug("連續(xù)排班違反: emp={}, date1={}, date2={}",
a.getEmployee().getName(),
a.getShift().getDate(),
b.getShift().getDate());
}
return violated;
})
.penalize("避免連續(xù)排班", HardSoftScore.ofHard(1));
}總結(jié)
核心要點回顧
| 方面 | 要點 |
|---|---|
| 集成 | 添加依賴 → 創(chuàng)建配置文件 → 定義域模型 → 實現(xiàn)約束 |
| 設(shè)計 | PlanningSolution + PlanningEntity + ConstraintProvider |
| 優(yōu)化 | 平衡求解時間和解質(zhì)量,合理設(shè)置權(quán)重 |
| 調(diào)試 | 啟用日志,檢查約束違反情況,驗證評分計算 |
最佳實踐清單
- ? 將硬約束權(quán)重設(shè)為1,軟約束根據(jù)優(yōu)先級調(diào)整
- ? 在filter中盡早過濾null值和不符合條件的數(shù)據(jù)
- ? 使用Joiners進行高效的連接操作
- ? 為約束提供清晰的名稱便于調(diào)試
- ? 使用并行求解加快處理速度
- ? 定期監(jiān)控求解時間和解質(zhì)量
- ? 為初始解提供更好的啟發(fā)式策略
進一步學習資源
到此這篇關(guān)于SpringBoot集成OptaPlanner與使用指南的文章就介紹到這了,更多相關(guān)SpringBoot OptaPlanner使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot項目中使用docker進行遠程部署的實現(xiàn)
本文主要介紹了在Spring Boot項目中使用Docker進行遠程部署,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-01-01
Spring Data Neo4j實現(xiàn)復雜查詢的多種方式
在 Spring Data Neo4j 中,實現(xiàn)復雜查詢可以通過多種方式進行,包括使用自定義查詢、方法命名查詢以及使用 Cypher 查詢語言,以下是詳細介紹,幫助你在 Spring Data Neo4j 中實現(xiàn)復雜查詢,需要的朋友可以參考下2024-11-11

