news 2026/10/18 14:38:59

or-tools人员排班建模实战:CP-SAT求解器与JavaAPI实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
or-tools人员排班建模实战:CP-SAT求解器与JavaAPI实现

简介:面向Java开发者与运筹优化初学者的OR-Tools人员排班建模求解示例包,聚焦IT行业常见的调度与资源配置难点。资源通过Java API,将员工技能匹配、工作时长上限、连续工作日之间至少休息一天等实际约束转化为整数规划模型,并调用求解器配置参数、搜索策略并输出最优排班。压缩包仅3KB,共4个文件,包含1个Java源文件与3个CSV数据表;CSV分别保存班次、各时段需求人数等输入数据,Java源码覆盖模型创建、约束添加、求解调用、结果检查和输出等核心步骤,便于快速理解建模与调用流程。目前已有978人学习下载,适合需要以最小成本上手OR-Tools的开发者。读懂该示例后,可直接套用到门店排班、项目人力分配等场景,并进一步迁移至更复杂的运筹优化问题。

1. 先算清楚再排班:or-tools 人员排班模型到底解决了什么

手工排班最耗神的,不是把格子填满,而是每次有人调休、请假,整条规则链都要重推。基于 or-tools 的人员排班问题建模求解(JavaAPI),本质上就是把人、日期、班次和规则变成布尔变量与线性约束,交给 CP-SAT 引擎做全局搜索,而不是按一个固定套路从周一排到周日。排出来的表,能直接交给值班经理使用,也能在下次有人换班时重算一次。

它解决的不是“生成一张能看的表”,而是“在多条互相咬合的规则下找到可行且更优的表”:每天每个班次必须有人覆盖、一个人一天不能上两个班、夜班不能连续太多、请假的人不能出现在当天表里。适合几十人以内、规则经常变化的轮班场景。手写排班已经让你头疼时,建模求解是代价最低的落地路径,尤其适合团队排班、门店轮班、值班表这类中高频场景。

下面按模型、代码、参数、排查四个角度展开。新手按顺序能跑通一个最小 Java 版本,熟手可以直接跳到第三章参数和第四章坑的位置。

2. 把排班规则翻成 CP-SAT 变量:JavaAPI 建模第一课

2.1 人员排班为什么适合用 CP-SAT 建模

排班问题有一个共同结构:一堆离散选项,一组强约束,一个“差不多就行”的目标。比如员工 A 可以上早班也可以上中班,但不能同一天两个班都上;夜班需要 2 个人,但连续夜班不能超过 3 天。这类问题天然是离散组合问题,不是线性规划能直接套的,贪心算法也很难保证不陷入局部解。

or-tools 里的 CP-SAT 求解器对这类问题非常合适。它对布尔变量和线性约束做了大量预处理、约束传播和启发式搜索,能在解空间中剪掉大量不可能的区间。JavaAPI 下,变量、约束、求解器都是独立的类,建模思路和 Python 版一致,只是语法换成 Java 的强类型写法。如果你已经在用 Maven 管依赖,集成并不复杂。

建模第一步是选定变量粒度。最常见做法是定义一个三维布尔变量:x[员工][天][班次] = 1 表示该员工在该天承担该班次。早班、中班、夜班分别用 0、1、2 表示,休息则不产生任何 1。这个变量一旦定义好,后面所有规则都变成“对这个三维数组求和、比较大小”的约束,这也正是 CP-SAT 最擅长处理的表达式。

硬约束通常长这样:每个员工每天最多一个班次,所以对固定员工和固定天数,把三个班次变量求和,必须小于等于 1。每天每个班次的需求人数是给定的,所以对固定天和固定班次,把所有员工变量求和,必须大于等于需求数。连续夜班限制则用滑动窗口,对连续 N 天窗口里的夜班变量求和,限制一个上限。这三类约束几乎覆盖了 80% 的排班规则核心。

2.2 最小可运行的 JavaAPI 排班程序:从 Maven 依赖到出表

先准备工程。我用的是标准 Maven 结构,只要引入 or-tools 的 Java 依赖即可,不需要额外下载本机动态库包,Loader.loadNativeLibraries()会按当前系统加载对应原生库。

<dependencies> <dependency> <groupId>com.google.ortools</groupId> <artifactId>ortools-java</artifactId> <version>${ortools.version}</version> </dependency> </dependencies>

版本号写在 Maven 的 properties 里,用你项目当前正在使用的稳定版即可。如果公司内网有私服,先让私服同步一次,避免首次拉取超时。

下面是一个可以直接跑通的最小排班程序。20 个员工、7 天、3 个班次,每天需求固定,目标是“满足覆盖的前提下尽量少排人”,避免出现冗余加班。

import com.google.ortools.Loader; import com.google.ortools.sat.CpModel; import com.google.ortools.sat.CpSolver; import com.google.ortools.sat.CpSolverStatus; import com.google.ortools.sat.IntVar; import com.google.ortools.sat.LinearExpr; import com.google.ortools.sat.LinearExprBuilder; public class ShiftSchedulerMinimal { private static final int NUM_EMPLOYEES = 20; private static final int NUM_DAYS = 7; private static final int NUM_SHIFTS = 3; private static final int NIGHT_SHIFT = 2; public static void main(String[] args) { Loader.loadNativeLibraries(); CpModel model = new CpModel(); // x[员工][天][班次],取 1 表示该员工当天上该班次 IntVar[][][] x = new IntVar[NUM_EMPLOYEES][NUM_DAYS][NUM_SHIFTS]; for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { x[e][d][s] = model.newBoolVar("x_" + e + "_" + d + "_" + s); } } } // 硬约束 1:每个员工每天最多一个班次 for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS; d++) { model.addLessOrEqual(LinearExpr.sum(x[e][d]), 1); } } // 硬约束 2:每天每个班次需求覆盖 int[][] demand = new int[NUM_DAYS][NUM_SHIFTS]; for (int d = 0; d < NUM_DAYS; d++) { demand[d][0] = 4; // 早班 4 人 demand[d][1] = 3; // 中班 3 人 demand[d][2] = 2; // 夜班 2 人 } for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { IntVar[] assigned = new IntVar[NUM_EMPLOYEES]; for (int e = 0; e < NUM_EMPLOYEES; e++) { assigned[e] = x[e][d][s]; } model.addGreaterOrEqual(LinearExpr.sum(assigned), demand[d][s]); } } // 硬约束 3:连续 4 天里,夜班数量不超过 3 天 for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS - 3; d++) { IntVar[] window = new IntVar[]{ x[e][d][NIGHT_SHIFT], x[e][d + 1][NIGHT_SHIFT], x[e][d + 2][NIGHT_SHIFT], x[e][d + 3][NIGHT_SHIFT] }; model.addLessOrEqual(LinearExpr.sum(window), 3); } } // 目标:在满足需求的前提下,总排班次数尽量少 LinearExprBuilder obj = LinearExpr.newBuilder(); for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { obj.addTerm(x[e][d][s], 1); } } } model.minimize(obj.build()); CpSolver solver = new CpSolver(); solver.getParameters().setMaxTimeInSeconds(10); solver.getParameters().setNumSearchWorkers(8); solver.getParameters().setLogSearchProgress(true); CpSolverStatus status = solver.solve(model); if (status == CpSolverStatus.OPTIMAL || status == CpSolverStatus.FEASIBLE) { for (int e = 0; e < NUM_EMPLOYEES; e++) { StringBuilder line = new StringBuilder("员工 " + e + ": "); for (int d = 0; d < NUM_DAYS; d++) { String dayInfo = null; for (int s = 0; s < NUM_SHIFTS; s++) { if (solver.value(x[e][d][s]) == 1L) { dayInfo = "D" + d + "班" + s; break; } } line.append(dayInfo == null ? "D" + d + "休 " : dayInfo + " "); } System.out.println(line); } System.out.println("目标值:" + solver.objectiveValue()); } else { System.out.println("未找到可行解,状态:" + status); } } }

代码里两个地方值得说明。LinearExpr.sum(x[e][d])把员工 e 在第 d 天的三个班次变量加起来,整数求和结果小于等于 1,就实现了“每天只能上一种班”。而需求覆盖则是按天、按班次把所有员工变量求和,这和一个固定员工求和的方向不一样,初学者最容易在这里把索引顺序写反。

连续夜班限制我用的是 4 天滑动窗口和小于等于 3,它允许一个员工在连续 4 天内最多上 3 天夜班,但不会出现连续 4 个夜班的极端情况。如果你希望更严格的“连续夜班最多 2 天”,就把窗口长度改成 3,上限改成 2。窗口约束是排班约束里性价比最高的一种,因为它只需要几行代码,就能把最危险的长夜班序列剪掉。

目标函数用LinearExprBuilder逐个累加变量权重,这里每个班次权重都是 1,所以目标值就是总排班人次。在覆盖需求已经满足的前提下,最小化总人次会自然减少多余加班。需求越大,目标值越接近需求总和的低值,这是一种比较稳妥的初始目标写法。

2.3 目标函数:公平性与员工偏好如何加权

上面的最小程序只有一个目标:少排人。真实排班通常还有第二个目标,比如让夜班尽量公平、让员工尽可能排到自己想要的班次。多人排班中,多目标一般通过加权求和变成一个目标,这也是 CP-SAT 建模的常见做法。

比如要给员工偏好加分,可以准备一个preference[员工][天][班次]分数表,然后在目标构造器里把每个班次变量的系数从 1 改成偏好分数。模型会倾向于让每个员工拿到高分班次,而不是单纯追求“人越少越好”。

LinearExprBuilder obj = LinearExpr.newBuilder(); for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { obj.addTerm(x[e][d][s], preference[e][d][s]); } } } model.maximize(obj.build());

偏好分可以这样设计:员工申请休假的班次给负分,非常不想上的班次给 0,想上的班次给 10,最想上的给 20。负分要谨慎,因为负分会让模型“故意不排这个人”来抬高目标值,但覆盖需求硬约束又强制必须排人,最后可能把最不想上的人排去最差的班次。我一般把偏好分数都设为非负,再用目标最大化处理,让“喜欢”比“不喜欢”更重要,而不是用负分惩罚。

夜班公平性的常见做法是给每个员工加一个累加变量,统计一周内夜班次数,然后在目标里对“夜班总次数的最大值”做惩罚。不过这一步会让模型规模明显变大,建议先跑通最小模型,再逐步加公平性目标,不要一上来就把所有诉求塞进目标。

3. 从能跑到跑得快:求解参数、软约束与无解场景的松弛处理

3.1 CP-SAT 求解器参数怎么调:时间预算、并行搜索与日志解读

模型能跑出解,只是第一步。数据量从 20 人涨到 200 人后,求解时间会变得很难看。排班这种组合爆炸问题,参数设置是否合理,可能让求解时间从几十秒变成几分钟,这是最容易被当成“玄学”的部分。

三个参数最值得先调。第一个是setMaxTimeInSeconds,给求解器一个时间上限,避免线上环境被一个排班任务卡死。第二个是setNumSearchWorkers,控制并行搜索线程数。第三个是setLogSearchProgress(true),打开搜索过程日志,观察上下界收敛情况。

solver.getParameters().setMaxTimeInSeconds(30); solver.getParameters().setNumSearchWorkers(8); solver.getParameters().setLogSearchProgress(true);

setMaxTimeInSeconds(30)表示最多搜 30 秒,时间到了以后会把当前找到的最优可行解返回,状态可能是FEASIBLE而不是OPTIMAL。如果你的排班需求是每晚跑一次,时间预算可以放到 120 秒;如果是交互式试算,10 秒以内比较合适。

setNumSearchWorkers(8)不是越大越好。我踩过的典型问题是把 worker 数设成 CPU 逻辑核数的两倍,结果内存占用翻倍,求解速度反而下降。CP-SAT 的并行搜索需要通信和合并子解,worker 太多时调度开销会吃掉收益。一般建议从 4 开始,逐步加到物理核数的一半或全部,观察日志里找到首个可行解的时间。

打开日志后,重点看每行尾部类似best和bound的数字。best是当前最高可行解的目标值,bound是当前推导出来的最优目标边界。两者靠近,说明模型在收敛;如果长时间不动且日志里failures在涨,说明该剪枝的约束已经基本剪完,继续等可能突破不了当前解。这时候不要硬等,应该回去调整约束或目标权重。

3.2 需求覆盖不满导致无解:用短缺变量把硬约束变软

排班最常见的一次“翻车”是:模型返回INFEASIBLE,但你反复检查,每一条规则都合理。原因通常是某一天的某个班次,需求人数相加超过了可用员工数,或者几个硬约束叠加后,剩下的可行域变成了空集。

排班模型必须给“没人可用”留余地。常见做法是把需求覆盖从硬约束改成软约束:允许某天某班次缺人,但缺一个人扣掉一个分数。这样模型永远不会直接无解,它只会尽量压低缺人成本,最后告诉你“缺口最小的方案是什么”。

这里的做法是给每个(天, 班次)增加一个短缺变量,再把“已分配人数 + 短缺人数”强制大于等于需求。短缺人数本身不是固定值,求解器可以选是排人还是接受缺人。

IntVar[][] shortage = new IntVar[NUM_DAYS][NUM_SHIFTS]; for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { shortage[d][s] = model.newIntVar(0, demand[d][s], "shortage_" + d + "_" + s); IntVar[] assigned = new IntVar[NUM_EMPLOYEES + 1]; for (int e = 0; e < NUM_EMPLOYEES; e++) { assigned[e] = x[e][d][s]; } assigned[NUM_EMPLOYEES] = shortage[d][s]; model.addGreaterOrEqual(LinearExpr.sum(assigned), demand[d][s]); } }

短缺变量上限设为需求人数就够了,因为缺的人不可能超过需要的人。这里要注意索引顺序,短缺变量必须和对应的[d][s]对齐,否则会出现“今天的缺口补到明天去”的怪现象。

软约束必须在目标里配上惩罚成本,否则模型不会偏向于少缺人。给短缺变量乘以一个足够大的权重,比如 100,然后和最小化总排班次数一起放进目标:

LinearExprBuilder obj = LinearExpr.newBuilder(); // 缺人惩罚:缺一个扣 100 分 for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { obj.addTerm(shortage[d][s], 100); } } // 冗余加班惩罚:每多排一个人扣 1 分 for (int e = 0; e < NUM_EMPLOYEES; e++) { for (int d = 0; d < NUM_DAYS; d++) { for (int s = 0; s < NUM_SHIFTS; s++) { obj.addTerm(x[e][d][s], 1); } } } model.minimize(obj.build());

权重 100 和 1 的比例意味着:缺一个人相当于少排 100 个人的代价,所以模型会优先保证覆盖,然后才考虑减少加班。实际项目里这个比例要根据业务容忍度调整。如果缺人后果非常严重,可以把权重拉到 1000;如果允许偶尔缺人换取员工正常休息,就降到 10。权重的调整不要一次跨太多档,先把 100:1 跑一轮,再根据结果往上下微调。

4. 排班建模落地避坑:五个高频问题与排查思路

4.1 模型一上来 INFEASIBLE,问题不一定在夜班规则上

现象:写了一个“看起来没什么问题”的模型,求解器却直接返回INFEASIBLE,日志里也没有某一条具体约束报错。

原因:最常见的是覆盖需求总和超过员工总数。比如 20 个员工,每天早班要 10 人、中班要 8 人、夜班要 6 人,一天就至少要 24 人次,但员工只有 20 人,每人每天最多 1 个班次,这就必然无解。第二个常见原因是多个硬约束叠加后互相锁死:比如“每人每天最多一个班次”和“每人每周必须休息一天”本身不冲突,但如果你还加了“每周总班次数必须大于 6”,那就没有剩余空间了。

解决:先做一次最笨的静态校验。遍历每一天,把当天所有班次的需求人数求和,检查有没有超过员工总数。如果没超,再逐个关闭约束测试。关闭一个约束后能出解,说明问题就藏在那条约束和别的约束的交叉处。也可以用 3.2 节的短缺变量把需求覆盖改软,把无解变成带惩罚的可解问题,然后看哪里产生了短缺。

4.2 JavaAPI 返回值静默变成 0:状态不对就取值是白取

现象:代码跑完,控制台打印出来的排班表全是“休”,一个班次都没有,但求解器明明返回了OPTIMAL。

原因:这类问题多半是取值时机不对。solver.value(x[e][d][s])只有在求解成功之后调用才有意义。如果先判断状态是FEASIBLE却因为某个线程异常回到了“未求解”状态,或者模型刚被重建,老求解器的变量引用已经和模型脱节,取值就会得到默认的 0。

解决:取数务必做两层保护。第一层是判断status == OPTIMAL || status == FEASIBLE,否则直接返回。第二层是打印的时候统计一下输出里的总班次数,和需求覆盖总和做对比。总班次数如果明显小于需求总和,说明要么是目标权重导致覆盖没被满足,要么就是你取错了变量数组。我习惯在打印排班表之后再加一行总人次输出,一眼就能看出异常。

4.3 把 0/1 决策变量声明成 IntVar,权重算起来容易翻车

现象:有人图省事,用model.newIntVar(0, 1, "x")代替model.newBoolVar,然后在目标里给这个变量乘以一个权重,结果模型结果总是和预期不一致,甚至出现中间值。

原因:newIntVar(0, 1)在 CP-SAT 里虽然取值范围也是 0 和 1,但它不会被当成布尔变量参与传播。CP-SAT 对布尔变量有专门的强化推理和启发式策略,你把布尔语义弄丢了,求解器在某些场景下需要多花很多搜索才能证明一个很简单的问题。更严重的是,后续如果写出x + x这种表达式,被当成整数变量处理后,模型结构就不清晰了。

解决:所有“上不上这个班”这类二选一决策,一律用model.newBoolVar。类型声明上的小差异,在排班这种大量布尔变量的模型里,不是代码风格问题,而是求解性能问题。

4.4 工人数开得太大,求解反而越来越慢

现象:部署的机器是 16 核,把setNumSearchWorkers(16)设上去,本以为会快一倍,结果求解时间翻了两倍,内存也涨了。

原因:CP-SAT 的并行搜索不是“每个核各搜各的”。它会周期性合并子搜索的状态,worker 越多,合并和通信成本越高。对于中小规模的排班模型,8 个 worker 往往已经接近收益上限;继续加线程,边际收益递减甚至变成负数。

解决:先用默认参数跑一次,记录求解时间;再分别用 2、4、8、16 调一轮。对几十人到几百人的排班,一般 4 到 8 就是甜点区。同时记得配合setMaxTimeInSeconds兜底,别让一个实验任务挂在服务器上过夜。

4.5 直接抄老版本 Java 示例,API 方法名对不上

现象:代码是从网上的 or-tools 旧示例里抄的,Maven 依赖却拉的是新版,编译报错集中在getParameters、setLogSearchProgress、LinearExprBuilder这些调用上。

原因:or-tools 的 Java API 一直在小步演进,比如日志开关、参数入口、部分构造器的命名都有过调整。旧示例不等于当前版本可用的示例。

解决:不要背 API,以你本地 Maven 拉下来的 jar 包为准。最省事的方法是直接查看项目里的ortools-java依赖源码,或者写一个 10 行的最小调用确认当前版本的参数入口。排班建模的难点在约束设计,不在 API 记忆,花十分钟确认版本,比反复编译失败强得多。

5. 用一套校验函数把 or-tools 排班变成内部决策工具

5.1 每次出表前先做一次独立校验

模型给出的结果,不能直接当成最终排班表下发。即使 CP-SAT 返回了OPTIMAL,也只能说明“对这个模型是最优”,不代表变量赋值一定符合业务直觉。我的习惯是写一个独立校验函数,把排班表从求解器里取出来后,再跑一遍所有规则的检查。

校验函数不依赖模型对象,只依赖纯的二维数组schedule[员工][天],元素存班次编号,-1 表示休息。这样可以把“模型”和“业务校验”彻底分开,以后换求解器或换语言都不影响校验逻辑。

private static boolean isScheduleLegal(int[][] schedule, int[][] demand, int maxConsecutiveNights) { int employees = schedule.length; int days = schedule[0].length; // 每天每个班次需求覆盖检查 for (int d = 0; d < days; d++) { int[] count = new int[schedule[0].length > 3 ? schedule[0].length : 3]; for (int e = 0; e < employees; e++) { if (schedule[e][d] >= 0) { count[schedule[e][d]]++; } } for (int s = 0; s < 3; s++) { if (count[s] < demand[d][s]) { return false; } } } // 连续夜班检查 for (int e = 0; e < employees; e++) { int consecutive = 0; for (int d = 0; d < days; d++) { if (schedule[e][d] == 2) { consecutive++; if (consecutive > maxConsecutiveNights) { return false; } } else { consecutive = 0; } } } return true; }

这段代码只做两件事:覆盖需求和连续夜班。你可以继续追加“两个员工不能同时请假”“某类员工不能上夜班”等规则。把校验函数和求解器放进同一个测试类,每次改动模型后跑一遍,能让排班表的返工率显著下降。

5.2 用目标权重的多组试算来定最终规则

排班模型的价值不在第一次跑出结果,而在你可以用同一套代码快速试算多组方案。比如夜班惩罚从 1 调到 5,看夜班分布会不会更均匀;需求覆盖权重从 100 调到 50,看会不会出现个别班次缺人。每次只改一个权重,把排班表和一个简单的评估指标记录下来,几轮对比下来,业务方就能直观理解“规则之间如何取舍”。

我现在的习惯是:先拿 10 人乘 14 天的小数据调通模型,确认每条约束都生效,再放到全量数据上跑。权重每次只动一档,记录目标值和短缺量,不一次性把所有规则塞进去。这个习惯帮我避开了好几次“大模型无解却不知道从哪开始排查”的尴尬。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 14:37:53

大模型辅助论文写作:5天完成高质量初稿的实操流程

先说一个我见过很多次的场景&#xff1a;拿到大模型之后&#xff0c;第一反应是让它直接“帮我写一段正文”&#xff0c;结果生成的内容看起来像模像样&#xff0c;放回论文里全是正确的废话。另一批人又走到另一个极端&#xff0c;坚持全部手写&#xff0c;AI只用来查文献&…

作者头像 李华
网站建设 2026/10/18 14:35:26

IEEE14节点潮流计算MATLAB工程实现与避坑指南

简介&#xff1a;本资源是一份面向电气工程专业本科生及电力系统初学者的MATLAB实践教学文档&#xff0c;聚焦电力系统最核心的潮流计算问题&#xff0c;解决从理论建模到编程实现的关键落地难点。文档完整阐述牛顿-拉夫逊法&#xff08;直角坐标形式&#xff09;的原理、节点导…

作者头像 李华
网站建设 2026/10/18 14:34:26

汽车空调制冷和暖风是怎么来的?

夏天上车先开冷风&#xff0c;冬天上车先开热风——这事儿你肯定干过大夏天钻进暴晒了一中午的车里&#xff0c;座椅烫得能煎蛋&#xff0c;第一反应肯定是&#xff1a;拧空调&#xff01;冷风开到最大&#xff0c;温度调到最低&#xff0c;巴不得瞬间给车厢降十度。到了冬天&a…

作者头像 李华
网站建设 2026/10/18 14:33:36

代码规范性与不同场景的要求

代码规范性概述 对于程序员而言&#xff0c;写代码在多个场合都会遇到。写代码的时候&#xff0c;代码规范性是一个值得关注的问题。 代码规范性可以列举出很多点&#xff0c;部分常见的规范标准如下。命名规范。无论是变量、函数/方法还是类/模块&#xff0c;都应遵循各自的大…

作者头像 李华
网站建设 2026/10/18 14:32:19

C# Winform通用开发框架:分层设计与避坑指南

简介&#xff1a;这是一套面向C#桌面应用开发者的Winform通用开发框架&#xff0c;适合需要快速搭建CS端系统的中高级程序员与团队使用。框架支持多语言切换与自定义语言扩展&#xff0c;内置多种数据库并封装实体对象&#xff0c;可随意更换数据库&#xff1b;界面风格简洁&am…

作者头像 李华
网站建设 2026/10/18 14:32:13

SVG图标对象化:动态修改颜色与尺寸的前端方案

将SVG对象化&#xff0c;并动态修改SVG图标的颜色、尺寸等做前端时间久了&#xff0c;几乎所有人都会遇到同一个问题&#xff1a;UI设计稿里的SVG图标漂亮&#xff0c;但需求一改&#xff0c;你要么求设计师重新导出&#xff0c;要么打开SVG源码手动改fill和宽高。刚开始我还能…

作者头像 李华