feat(algorithm): 优化线性规划引擎的负载均衡逻辑

在负载均衡计算中引入历史任务数据,改进平均工作负载计算方式并修复相关语法错误。
This commit is contained in:
Developer 2025-09-23 01:32:11 +08:00
parent 2b3f9acdce
commit caba7e68b3
7 changed files with 444 additions and 49 deletions

View File

@ -23,7 +23,28 @@
"Bash(dotnet run:*)",
"Bash(mkdir:*)",
"Bash(git restore:*)",
"Bash(ls:*)"
"Bash(ls:*)",
"WebSearch",
"Bash(where claude)",
"Bash(claude mcp --help)",
"Bash(claude mcp list)",
"Bash(claude mcp add context7 -- npx -y @upstash/context7-mcp --api-key ctx7sk-a7e77440-3e75-4b24-9462-642a8c69fec2)",
"Read(/C:\\Users\\Asoka\\.claude/**)",
"Read(/C:\\Users\\Asoka\\.claude/**)",
"Bash(npx -y @modelcontextprotocol/server-memory)",
"Bash(claude mcp add memory -- npx -y @modelcontextprotocol/server-memory)",
"Read(/C:\\GitCode\\paiban\\.claude/**)",
"Read(/C:\\Users\\Asoka\\.claude/**)",
"Read(/C:\\Users\\Asoka\\.claude/**)",
"Read(/C:\\Users\\Asoka\\.claude/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api.Contracts\\Services\\Integration/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api\\Services\\Integration/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api\\Services\\Integration/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api.Contracts\\Services\\Integration/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api.Contracts\\Domain\\Work/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api\\Services\\Work/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api\\Services\\Work/**)",
"Read(/C:\\GitCode\\paiban\\NPP.SmartSchedue.Api.Contracts\\Domain\\Work/**)"
],
"deny": []
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -10,12 +10,9 @@
</component>
<component name="ChangeListManager">
<list default="true" id="635f35ee-190e-4c9a-82f7-ee1c8f1bea95" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.NPP.SmartSchedue/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.NPP.SmartSchedue/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.NPP.SmartSchedue/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.NPP.SmartSchedue/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ExecutiveSummary.md" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/GeneticAlgorithmPerformanceAnalysis.md" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/GeneticAlgorithmTestPlan.md" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.xml" beforeDir="false" afterPath="$PROJECT_DIR$/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/PerformanceOptimizationRecommendations.md" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -31,7 +28,7 @@
<entry key="$PROJECT_DIR$/../../.." value="dev_schedule_2" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../../.." />
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitLabMergeRequestFiltersHistory">{
&quot;lastFilter&quot;: {
@ -78,30 +75,30 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
".NET 启动设置配置文件.NPP.SmartSchedue.Host.executor": "Debug",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
"RunOnceActivity.git.unshallow": "true",
"ToolWindow.Debug.ShowToolbar": "false",
"git-widget-placeholder": "dev__schedule",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
"settings.editor.splitter.proportion": "0.1840796",
"ts.external.directory.path": "C:\\GitCodeSpace\\SPMS.Portal\\zhontai.ui.admin.vue3\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true",
"附加到进程.1596:NPP.SmartSchedue.Host.executor": "Debug",
"附加到进程.19008:NPP.SmartSchedue.Host.executor": "Debug",
"附加到进程.28664:NPP.SmartSchedue.Host.executor": "Debug"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;.NET 启动设置配置文件.NPP.SmartSchedue.Host.executor&quot;: &quot;Debug&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;ToolWindow.Debug.ShowToolbar&quot;: &quot;false&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;editor.preferences.fonts.default&quot;,
&quot;settings.editor.splitter.proportion&quot;: &quot;0.1840796&quot;,
&quot;ts.external.directory.path&quot;: &quot;C:\\GitCodeSpace\\SPMS.Portal\\zhontai.ui.admin.vue3\\node_modules\\typescript\\lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;,
&quot;附加到进程.1596:NPP.SmartSchedue.Host.executor&quot;: &quot;Debug&quot;,
&quot;附加到进程.19008:NPP.SmartSchedue.Host.executor&quot;: &quot;Debug&quot;,
&quot;附加到进程.28664:NPP.SmartSchedue.Host.executor&quot;: &quot;Debug&quot;
}
}]]></component>
<component name="RunManager" selected=".NET 启动设置配置文件.NPP.SmartSchedue.Host">
}</component>
<component name="RunManager" selected=".NET Launch Settings Profile.NPP.SmartSchedue.Host">
<configuration name="NPP.SmartSchedue.Host" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/NPP.SmartSchedue.Host/NPP.SmartSchedue.Host.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
@ -113,6 +110,7 @@
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" />
<method v="2">
<option name="Build" />
</method>
@ -128,17 +126,11 @@
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
<deployment type="dockerfile">
<settings />
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
<configuration name="NPP.SmartSchedue.Host/Dockerfile" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
@ -150,6 +142,13 @@
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
<deployment type="dockerfile">
<settings />
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
@ -179,6 +178,7 @@
<workItem from="1756084492195" duration="43899000" />
<workItem from="1756185335516" duration="36937000" />
<workItem from="1756289004055" duration="1442000" />
<workItem from="1756998539606" duration="775000" />
</task>
<servers />
</component>

22
AGENTS.md Normal file
View File

@ -0,0 +1,22 @@
# Repository Guidelines
## Project Structure & Module Organization
Source lives under the solution `NPP.SmartSchedue.sln`. `NPP.SmartSchedue.Host` hosts the ASP.NET Core app, with environment settings in `appsettings*.json`, seed scripts in `ConfigCenter` and `InitData`, and static assets in `wwwroot`. Domain services sit in `NPP.SmartSchedue.Api`, organized into `Core` (attributes, constants), `Services` (business logic), and `Repositories` (data access). Shared contracts, DTOs, and interfaces reside in `NPP.SmartSchedue.Api.Contracts`; keep cross-project types there. Tests are in `NPP.SmartSchedue.Tests`, with reusable fixtures in `BaseTest` and service-focused integration suites under `Services`.
## Build, Test, and Development Commands
- `dotnet restore NPP.SmartSchedue.sln` restores all project dependencies.
- `dotnet build NPP.SmartSchedue.sln -c Release` validates compilation; use `-c Debug` during local iteration.
- `dotnet run --project NPP.SmartSchedue.Host` boots the API with the default profile; add `--launch-profile Development` when you need local CAP setups.
- `dotnet test NPP.SmartSchedue.Tests/NPP.SmartSchedue.Tests.csproj --logger "trx;LogFileName=test-results.trx"` exercises the xUnit suites and saves artifacts for PR review.
## Coding Style & Naming Conventions
Target .NET 9 defaults: four-space indentation, braces on new lines, and explicit namespaces. Use `PascalCase` for classes, public members, and DTOs, `camelCase` for locals and private fields, and prefix interfaces with `I` as seen in `Api.Contracts`. Suffix asynchronous methods with `Async`, and keep request/response models in the Contracts project. Run `dotnet format` before committing to enforce spacing and using-order consistency.
## Testing Guidelines
Write xUnit tests alongside existing patterns such as `EquipmentMaintenanceIntegrationTest`. Name new files `{Feature}Test.cs` or `{Feature}IntegrationTest.cs`, group helper logic in `BaseTest`, and rely on `Microsoft.AspNetCore.Mvc.Testing` for host bootstrapping. Aim to cover new service paths and repository edge cases; add mocks via Moq where external systems (CAP, RabbitMQ, MySQL) would otherwise be required.
## Commit & Pull Request Guidelines
Prefer descriptive commits (`feat(scheduling): add notification workflow`) instead of numeric placeholders seen in recent history. Bundle related changes, include migration or config adjustments in the same commit, and reference tracking IDs where relevant. Pull requests should outline the change, list manual or automated test evidence, mention configuration impacts (CAP, FreeSql, message queues), and supply screenshots for any UI updates under `wwwroot`.
## Configuration & Environment Notes
Keep secrets out of `appsettings.json`; use user secrets or environment variables for connection strings and CAP brokers. Document any required RabbitMQ or MySQL endpoints in the PR. When adding assets or scripts, mirror the structure under `ConfigCenter`, `InitData`, and `wwwroot` so the publish targets continue copying them correctly.

View File

@ -207,6 +207,10 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
Dictionary<(long TaskId, long PersonnelId), Variable> decisionVariables)
{
var auxiliaryVars = new Dictionary<string, Variable>();
var historicalWorkloads = BuildHistoricalWorkloadMap(context, out var totalHistoricalTasks);
var avgWorkload = context.AvailablePersonnel.Count > 0
? (double)(context.Tasks.Count + totalHistoricalTasks) / context.AvailablePersonnel.Count
: 0.0;
// 1. 人员工作负载变量 (连续变量,表示每个人员的任务数量)
foreach (var personnel in context.AvailablePersonnel)
@ -216,7 +220,6 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
}
// 2. 负载均衡偏差变量 (支持公平性目标建模)
var avgWorkload = (double)context.Tasks.Count / context.AvailablePersonnel.Count;
foreach (var personnel in context.AvailablePersonnel)
{
// 正偏差变量 (超出平均负载的部分)
@ -224,7 +227,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
auxiliaryVars[$"pos_dev_{personnel.Id}"] = posDeviationVar;
// 负偏差变量 (低于平均负载的部分)
var negDeviationVar = solver.MakeNumVar(0, avgWorkload, $"neg_dev_{personnel.Id}");
var negDeviationVar = solver.MakeNumVar(0, Math.Max(0.0, avgWorkload), $"neg_dev_{personnel.Id}");
auxiliaryVars[$"neg_dev_{personnel.Id}"] = negDeviationVar;
}
@ -832,7 +835,6 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
/// 添加公平性约束 - 对应遗传算法的CalculateFairnessScore和负载均衡机制
/// 包含:负载均衡约束、人员利用率约束、过度集中惩罚约束
/// </summary>
private void AddFairnessConstraints(
Solver solver,
Dictionary<(long TaskId, long PersonnelId), Variable> decisionVars,
Dictionary<string, Variable> auxiliaryVars,
@ -1136,7 +1138,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
solution.BestFitness, assignments.Count, workloadDistribution.Count, qualityMetrics.ConstraintSatisfactionRate);
// 详细的工作负载分析
LogWorkloadAnalysis(workloadDistribution, context.Tasks.Count, context.AvailablePersonnel.Count);
LogWorkloadAnalysis(workloadDistribution, context.Tasks.Count, context.AvailablePersonnel.Count, context);
}
else
{
@ -1163,6 +1165,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
var metrics = new SolutionQualityMetrics();
var violationCount = 0;
var totalChecks = 0;
var historicalWorkloads = BuildHistoricalWorkloadMap(context, out _);
// 检查1任务分配完整性
var completenessRate = assignments.Count > 0 ? (double)assignments.Count / context.Tasks.Count : 0.0;
@ -1195,14 +1198,26 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
metrics.TimeConflictRate = totalChecks > 0 ? (double)timeConflictCount / totalChecks : 0.0;
if (timeConflictCount > 0) violationCount++;
// 检查3负载均衡评估
if (assignments.Any())
// 检查3负载均衡评估纳入历史任务
var currentWorkloadMap = assignments
.GroupBy(a => a.Value)
.ToDictionary(g => g.Key, g => g.Count());
var combinedWorkloads = context.AvailablePersonnel.ToDictionary(
p => p.Id,
p => historicalWorkloads.GetValueOrDefault(p.Id) + currentWorkloadMap.GetValueOrDefault(p.Id));
if (combinedWorkloads.Any())
{
var workloadValues = personnelTimeSlots.Values.Select(v => (decimal)v.Count).ToList();
var workloadValues = combinedWorkloads.Values.Select(v => (decimal)v).ToList();
var giniCoefficient = CalculateGiniCoefficient(workloadValues);
metrics.LoadBalanceScore = Math.Max(0.0, 1.0 - giniCoefficient);
var utilizationRate = (double)personnelTimeSlots.Count / context.AvailablePersonnel.Count;
var utilizedCount = combinedWorkloads.Count(kv => kv.Value > 0);
metrics.PersonnelUtilizationRate = context.AvailablePersonnel.Count > 0
? (double)utilizedCount / context.AvailablePersonnel.Count
: 0.0;
}
metrics.PersonnelUtilizationRate = utilizationRate;
}
@ -1216,7 +1231,6 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
/// <summary>
/// 记录详细的工作负载分析日志
/// </summary>
private void LogWorkloadAnalysis(Dictionary<long, decimal> workloadDistribution, int totalTasks, int totalPersonnel)
{
if (!workloadDistribution.Any()) return;
@ -1309,6 +1323,27 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
#region
private Dictionary<long, int> BuildHistoricalWorkloadMap(GlobalAllocationContext context, out int totalHistoricalTasks)
{
var historicalWorkloads = new Dictionary<long, int>();
totalHistoricalTasks = 0;
foreach (var personnel in context.AvailablePersonnel)
{
var historyCount = 0;
if (context.PersonnelHistoryTasks.TryGetValue(personnel.Id, out var historyTasks) && historyTasks != null)
{
historyCount = historyTasks.Count;
}
historicalWorkloads[personnel.Id] = historyCount;
totalHistoricalTasks += historyCount;
}
return historicalWorkloads;
}
/// <summary>
/// 【深度实现】判断是否为二班或三班 - 集成ShiftService的专业班次信息查询
/// 【业务思考】不再依赖简单的字符串匹配而是通过ShiftService获取准确的班次信息
@ -2409,4 +2444,4 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms
public double ConstraintSatisfactionRate { get; set; }
public List<string> ViolationDetails { get; set; } = new();
}
}
}

311
apply_fairness.ps1 Normal file
View File

@ -0,0 +1,311 @@
$path = "NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs"
$lines = [System.Collections.Generic.List[string]]::new()
$lines.AddRange([System.IO.File]::ReadAllLines($path))
function FindExactIndexInRange($lines, $value, $start, $end) {
for ($i = $start; $i -le $end; $i++) {
if ($lines[$i] -eq $value) { return $i }
}
return -1
}
function FindContainsIndexInRange($lines, $value, $start, $end) {
for ($i = $start; $i -le $end; $i++) {
if ($lines[$i].Contains($value)) { return $i }
}
return -1
}
# --- BuildAuxiliaryVariables ---
$index = $lines.IndexOf(' var auxiliaryVars = new Dictionary<string, Variable>();')
if ($index -ge 0) {
$lines.InsertRange($index + 1, [string[]]@(
' var historicalWorkloads = BuildHistoricalWorkloadMap(context, out var totalHistoricalTasks);',
' var avgWorkload = context.AvailablePersonnel.Count > 0',
' ? (double)(context.Tasks.Count + totalHistoricalTasks) / context.AvailablePersonnel.Count',
' : 0.0;'
))
}
$index = $lines.IndexOf(' var avgWorkload = (double)context.Tasks.Count / context.AvailablePersonnel.Count;')
if ($index -ge 0) { $lines.RemoveAt($index) }
$index = $lines.IndexOf(' var negDeviationVar = solver.MakeNumVar(0, avgWorkload, $"neg_dev_{personnel.Id}");')
if ($index -ge 0) { $lines[$index] = ' var negDeviationVar = solver.MakeNumVar(0, Math.Max(0.0, avgWorkload), $"neg_dev_{personnel.Id}");' }
# --- AddFairnessConstraints ---
$start = $lines.IndexOf(' private void AddFairnessConstraints(')
if ($start -ge 0) {
$brace = 0
for ($end = $start; $end -lt $lines.Count; $end++) {
if ($lines[$end].Contains('{')) { $brace++ }
if ($lines[$end].Contains('}')) { $brace-- }
if ($brace -eq 0) { break }
}
$lines.RemoveRange($start, $end - $start + 1)
$newFairness = @(
' private void AddFairnessConstraints(',
' Solver solver,',
' Dictionary<(long TaskId, long PersonnelId), Variable> decisionVars,',
' Dictionary<string, Variable> auxiliaryVars,',
' GlobalAllocationContext context)',
' {',
' var constraintCount = 0;',
'',
' _logger.LogInformation("⚖️ 开始添加公平性约束...");',
'',
' var totalTasks = context.Tasks.Count;',
' var totalPersonnel = context.AvailablePersonnel.Count;',
' var historicalWorkloads = BuildHistoricalWorkloadMap(context, out var totalHistoricalWorkload);',
' var totalWorkload = totalTasks + totalHistoricalWorkload;',
' var avgWorkload = totalPersonnel > 0 ? (double)totalWorkload / totalPersonnel : 0.0;',
'',
' // 公平性约束1负载均衡偏差计算',
' foreach (var personnel in context.AvailablePersonnel)',
' {',
' var workloadVarKey = $"workload_{personnel.Id}";',
' var posDevVarKey = $"pos_dev_{personnel.Id}";',
' var negDevVarKey = $"neg_dev_{personnel.Id}";',
' var historicalWorkload = historicalWorkloads.GetValueOrDefault(personnel.Id);',
'',
' if (auxiliaryVars.ContainsKey(workloadVarKey) &&',
' auxiliaryVars.ContainsKey(posDevVarKey) &&',
' auxiliaryVars.ContainsKey(negDevVarKey))',
' {',
' // 约束currentWorkload + 历史工作量偏移 = avgWorkload + pos_deviation - neg_deviation',
' var targetWorkload = avgWorkload - historicalWorkload;',
' var balanceConstraint = solver.MakeConstraint(targetWorkload, targetWorkload,',
' $"load_balance_{personnel.Id}");',
'',
' balanceConstraint.SetCoefficient(auxiliaryVars[workloadVarKey], 1);',
' balanceConstraint.SetCoefficient(auxiliaryVars[posDevVarKey], -1);',
' balanceConstraint.SetCoefficient(auxiliaryVars[negDevVarKey], 1);',
' constraintCount++;',
' }',
' }',
'',
' // 公平性约束2最大负载限制 - 防止极端不均衡',
' var maxTotalWorkload = Math.Ceiling(avgWorkload * 1.5);',
' foreach (var personnel in context.AvailablePersonnel)',
' {',
' var workloadVarKey = $"workload_{personnel.Id}";',
' var historicalWorkload = historicalWorkloads.GetValueOrDefault(personnel.Id);',
' if (auxiliaryVars.ContainsKey(workloadVarKey))',
' {',
' var constraint = solver.MakeConstraint(0, Math.Max(0.0, maxTotalWorkload - historicalWorkload),',
' $"max_workload_{personnel.Id}");',
' constraint.SetCoefficient(auxiliaryVars[workloadVarKey], 1);',
' constraintCount++;',
' }',
' }',
'',
' // 公平性约束3最小人员利用率约束 - 确保不会过度集中',
' var minPersonnelUtilization = totalPersonnel == 0',
' ? 0',
' : (totalTasks > 10 ? Math.Max(3, Math.Ceiling(totalPersonnel * 0.4)) : Math.Min((double)totalPersonnel, 2));',
'',
' // 创建人员参与指示变量',
' var participationVars = new List<Variable>();',
' foreach (var personnel in context.AvailablePersonnel)',
' {',
' var participationVar = solver.MakeBoolVar($"participation_{personnel.Id}");',
' participationVars.Add(participationVar);',
'',
' // 如果人员参与则至少分配1个任务',
' var constraint = solver.MakeConstraint(0, double.PositiveInfinity,',
' $"participation_min_{personnel.Id}");',
'',
' constraint.SetCoefficient(participationVar, -1);',
'',
' var personnelTasks = decisionVars.Where(v => v.Key.PersonnelId == personnel.Id);',
' foreach (var kvp in personnelTasks)',
' {',
' constraint.SetCoefficient(kvp.Value, 1);',
' }',
' constraintCount++;',
' }',
'',
' // 最小参与人数约束',
' var minParticipationConstraint = solver.MakeConstraint(minPersonnelUtilization, double.PositiveInfinity,',
' "min_personnel_utilization");',
'',
' foreach (var participationVar in participationVars)',
' {',
' minParticipationConstraint.SetCoefficient(participationVar, 1);',
' }',
' constraintCount++;',
'',
' _logger.LogDebug("✅ 公平性约束添加完成 - 约束数量:{ConstraintCount},平均负载:{AvgWorkload:F2},最大负载:{MaxWorkload:F2},最小参与人数:{MinParticipation}",',
' constraintCount, avgWorkload, maxTotalWorkload, minPersonnelUtilization);',
' }'
)
$lines.InsertRange($start, $newFairness)
}
# Update call to LogWorkloadAnalysis
$index = $lines.IndexOf(' LogWorkloadAnalysis(workloadDistribution, context.Tasks.Count, context.AvailablePersonnel.Count);')
if ($index -ge 0) { $lines[$index] = ' LogWorkloadAnalysis(workloadDistribution, context.Tasks.Count, context.AvailablePersonnel.Count, context);' }
# ValidateSolutionQuality
$index = $lines.IndexOf(' var totalChecks = 0;')
if ($index -ge 0) { $lines.Insert($index + 1, ' var historicalWorkloads = BuildHistoricalWorkloadMap(context, out _);') }
$start = $lines.IndexOf(' // 检查3负载均衡评估')
if ($start -ge 0) {
$lines.RemoveRange($start, 8)
$lines.InsertRange($start, [string[]]@(
' // 检查3负载均衡评估纳入历史任务',
' var currentWorkloadMap = assignments',
' .GroupBy(a => a.Value)',
' .ToDictionary(g => g.Key, g => g.Count());',
'',
' var combinedWorkloads = context.AvailablePersonnel.ToDictionary(',
' p => p.Id,',
' p => historicalWorkloads.GetValueOrDefault(p.Id) + currentWorkloadMap.GetValueOrDefault(p.Id));',
'',
' if (combinedWorkloads.Any())',
' {',
' var workloadValues = combinedWorkloads.Values.Select(v => (decimal)v).ToList();',
' var giniCoefficient = CalculateGiniCoefficient(workloadValues);',
' metrics.LoadBalanceScore = Math.Max(0.0, 1.0 - giniCoefficient);',
'',
' var utilizedCount = combinedWorkloads.Count(kv => kv.Value > 0);',
' metrics.PersonnelUtilizationRate = context.AvailablePersonnel.Count > 0',
' ? (double)utilizedCount / context.AvailablePersonnel.Count',
' : 0.0;',
' }'
))
}
# LogWorkloadAnalysis replacement
$start = $lines.IndexOf(' private void LogWorkloadAnalysis(Dictionary<long, decimal> workloadDistribution, int totalTasks, int totalPersonnel)')
if ($start -ge 0) {
$brace = 0
for ($end = $start; $end -lt $lines.Count; $end++) {
if ($lines[$end].Contains('{')) { $brace++ }
if ($lines[$end].Contains('}')) { $brace-- }
if ($brace -eq 0) { break }
}
$lines.RemoveRange($start, $end - $start + 1)
$newLog = @(
' private void LogWorkloadAnalysis(',
' Dictionary<long, decimal> workloadDistribution,',
' int totalTasks,',
' int totalPersonnel,',
' GlobalAllocationContext context)',
' {',
' var historicalWorkloads = BuildHistoricalWorkloadMap(context, out _);',
'',
' if (!workloadDistribution.Any() && historicalWorkloads.Values.All(value => value == 0))',
' {',
' return;',
' }',
'',
' var combinedWorkloads = context.AvailablePersonnel.ToDictionary(',
' p => p.Id,',
' p => workloadDistribution.GetValueOrDefault(p.Id) + historicalWorkloads.GetValueOrDefault(p.Id));',
'',
' if (!combinedWorkloads.Any())',
' {',
' return;',
' }',
'',
' var combinedValues = combinedWorkloads.Values.Select(value => (decimal)value).ToList();',
' var avgWorkload = combinedValues.Average(value => (double)value);',
' var maxWorkload = combinedValues.Max();',
' var minWorkload = combinedValues.Min();',
' var utilizedCount = combinedWorkloads.Count(kv => kv.Value > 0);',
' var utilizationRate = totalPersonnel > 0 ? (double)utilizedCount / totalPersonnel : 0.0;',
'',
' _logger.LogInformation("📈 工作负载分析(含历史) - 平均:{Avg:F2},最大:{Max},最小:{Min},当前批次任务数:{CurrentTasks},人员利用率:{Util:P2},基尼系数:{Gini:F4}",',
' avgWorkload, maxWorkload, minWorkload, totalTasks, utilizationRate, CalculateGiniCoefficient(combinedValues));',
'',
' var currentDistributionLog = string.Join(", ",',
' workloadDistribution.OrderBy(kv => kv.Key)',
' .Select(kv => $"人员{kv.Key}:{kv.Value}任务"));',
' if (!string.IsNullOrWhiteSpace(currentDistributionLog))',
' {',
' _logger.LogDebug("📊 本次任务负载:{Distribution}", currentDistributionLog);',
' }',
'',
' var historicalDistributionLog = string.Join(", ",',
' historicalWorkloads.OrderBy(kv => kv.Key)',
' .Select(kv => $"人员{kv.Key}:{kv.Value}历史任务"));',
' if (!string.IsNullOrWhiteSpace(historicalDistributionLog))',
' {',
' _logger.LogDebug("🕰️ 历史任务负载:{Distribution}", historicalDistributionLog);',
' }',
'',
' var cumulativeDistributionLog = string.Join(", ",',
' combinedWorkloads.OrderBy(kv => kv.Key)',
' .Select(kv => $"人员{kv.Key}:{kv.Value}累计任务"));',
' _logger.LogDebug("⚖️ 累计任务负载:{Distribution}", cumulativeDistributionLog);',
' }'
)
$lines.InsertRange($start, $newLog)
}
# Insert helper method after #region 辅助方法
$index = $lines.IndexOf(' #region 辅助方法')
if ($index -ge 0) {
$lines.InsertRange($index + 1, [string[]]@(
'',
' private Dictionary<long, int> BuildHistoricalWorkloadMap(GlobalAllocationContext context, out int totalHistoricalTasks)',
' {',
' var historicalWorkloads = new Dictionary<long, int>();',
' totalHistoricalTasks = 0;',
'',
' foreach (var personnel in context.AvailablePersonnel)',
' {',
' var historyCount = 0;',
' if (context.PersonnelHistoryTasks.TryGetValue(personnel.Id, out var historyTasks) && historyTasks != null)',
' {',
' historyCount = historyTasks.Count;',
' }',
'',
' historicalWorkloads[personnel.Id] = historyCount;',
' totalHistoricalTasks += historyCount;',
' }',
'',
' return historicalWorkloads;',
' }',
''
))
}
# CreateBatchIntegrityConstraints modifications
$start = $lines.IndexOf(' private int CreateBatchIntegrityConstraints(')
if ($start -ge 0) {
$brace = 0
for ($end = $start; $end -lt $lines.Count; $end++) {
if ($lines[$end].Contains('{')) { $brace++ }
if ($lines[$end].Contains('}')) { $brace-- }
if ($brace -eq 0) { break }
}
$idx = FindExactIndexInRange $lines ' var totalPersonnel = context.AvailablePersonnel.Count;' $start $end
if ($idx -ge 0) {
$lines.InsertRange($idx + 1, [string[]]@(
' var historicalWorkloads = BuildHistoricalWorkloadMap(context, out var totalHistoricalWorkload);',
' var totalWorkload = totalTasks + totalHistoricalWorkload;',
' var avgWorkload = totalPersonnel > 0 ? (double)totalWorkload / totalPersonnel : 0.0;'
))
$orig = FindExactIndexInRange $lines ' var avgWorkload = (double)totalTasks / totalPersonnel;' $start ($end + 3)
if ($orig -ge 0) { $lines.RemoveAt($orig) }
}
$afterToList = FindExactIndexInRange $lines ' .ToList();' $start $end
if ($afterToList -ge 0) { $lines.Insert($afterToList + 1, ' var historicalWorkload = historicalWorkloads.GetValueOrDefault(personnel.Id);') }
$constraintLine = FindExactIndexInRange $lines ' var constraint = solver.MakeConstraint(' $start $end
if ($constraintLine -ge 0) {
$lines.RemoveRange($constraintLine, 3)
$lines.InsertRange($constraintLine, [string[]]@(
' var lowerBound = Math.Max(0.0, (avgWorkload - maxDeviation) - historicalWorkload);',
' var upperBound = Math.Max(lowerBound, (avgWorkload + maxDeviation) - historicalWorkload);',
' var constraint = solver.MakeConstraint(',
' lowerBound,',
' upperBound,',
' $"workload_balance_{personnel.Id}");'
))
}
}
[System.IO.File]::WriteAllLines($path, $lines)

6
temp_script.ps1 Normal file
View File

@ -0,0 +1,6 @@
$lines = Get-Content -Path "NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs"
for ($i=0; $i -lt $lines.Length; $i++) {
if ($lines[$i] -like '*workload_{personnel.Id}*') {
Write-Host ($i.ToString() + ':' + $lines[$i])
}
}