news 2026/10/22 4:12:04

半边数据结构与欧拉操作:实现多内环扫掠建模核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
半边数据结构与欧拉操作:实现多内环扫掠建模核心原理

简介:《三维CAD建模》课程作业定义了半边数据结构,并基于该结构实现五个基本欧拉操作,以及支持带多个内环的二维区域扫掠操作,最终将构建的实体模型进行真实感图形显示,完整覆盖了CAD建模的核心流程。压缩包共18个文件,以C++的cpp/hpp源文件为主,辅以配置文件、模型数据、PNG效果图及Markdown说明等,整体仅102KB,小巧但结构清晰,适合直接下载学习。代码均测试运行成功,作者表示答辩平均分达96分,适合计算机相关专业学生用于课程设计、毕业设计或项目初期演示。已有96人学习浏览,下载后可在本地直接编译运行,也可在此基础上修改扩展,实现更多建模功能。需要远程教学或运行指导,可与作者私聊联系。

1. 半边数据结构课程作业:从拓扑骨架到扫掠成体

如果有人问你三维CAD建模里最核心的数据结构是什么,答案大概率是半边结构(Half-Edge)。它把顶点、边、面的拓扑关系组织成一张可以高速遍历的网,几乎所有能叫上名字的商业内核都在用它。这份课程作业资源做的正是这件事:从零定义半边数据结构,实现五个基础欧拉操作,再基于欧拉操作完成带多内环的扫掠(Sweep)建模,最后做真实感显示。适合正在做图形学课设、毕业设计,或者想弄懂CAD内核拓扑原理的人。难点不在写代码,而在理解欧拉操作的先后顺序和拓扑约束——这是整个资源含金量最高的地方。

2. 先立骨架:半边数据结构的定义与五个基础欧拉操作

2.1 半边结构到底在维护什么:从实体到拓扑的映射关系

把任何一个三维实体想成一堆面片拼起来的封闭外壳,每个无向边拆成两条方向相反的有向边,这就是半边。每条半边只归属一个面,记录它的起点、下一条半边、所属面以及配对半边。这样设计的好处是:从任意一条半边出发,能沿着面的边界走一圈,再通过配对边跳到相邻面,整个实体变成一个可遍历的图。

这份资源里的main.hpp和CreateSolid.hpp定义了核心数据结构。我在本地复现时看到的是典型的字段组合:顶点坐标、半边指针、面指针,以及用于建立配对关系的twin指针。还有一个细节值得注意——代码里大量用shared_ptr管理半边和顶点,而不是裸指针。这对后续欧拉操作的异常安全很有帮助,因为欧拉操作中最容易翻车的就是指针悬挂:删除一个面但没同步清理相邻半边的指向,遍历时直接崩掉。

struct HalfEdge { std::shared_ptr<Vertex> vertex; // 该半边指向的终点顶点 std::shared_ptr<HalfEdge> twin; // 配对半边(方向相反) std::shared_ptr<HalfEdge> next; // 同面下一条半边 std::shared_ptr<Face> face; // 所属面 }; struct Solid { std::vector<std::shared_ptr<Face>> faces; std::vector<std::shared_ptr<HalfEdge>> edges; std::vector<std::shared_ptr<Vertex>> vertices; };

逻辑说明:vertex指向的是这条半边在下一条半边遍历时的顶点,注意它存的是终点而不是起点——这是很多初学者第一次看会懵的地方。取起点的正确方式是twin->vertex。next指针沿着面边界逆时针走,twin跳到相邻面,两者组合起来就是完整的拓扑遍历。参数说明:如果你的工程不需要跨函数频繁复制拓扑,用shared_ptr没问题;但如果追求极致性能,原始指针加引用计数会更快,课程作业场景不需要考虑这个。

这套数据结构直接决定了后面五个欧拉操作的实现难度。操作无非是“建”“删”“改”的组合,但因为半边结构要求每一步操作完成后拓扑必须自洽,所以每一步都要同时维护好vertex、twin、next三套指针。常见做法是把指针更新集中在一处,避免散落在各个操作里。

2.2 五个欧拉操作逐个拆:MVFS、MEV、MEF、KEMR、KFMRH

欧拉操作的核心思想很简单:每一步操作都不改变实体的欧拉示性数,即 V - E + F = 2 - 2G(G 为穿透孔个数)。五个基本操作对应的是“从无到有”和“从有到无”的原子步骤。这份资源里的EulerOperation.cpp实现了以下五个:

MVFS(Make Vertex, Face, Solid):从空场景创建一个含一个孤立顶点、一张空面、一个新体的初始状态。这是所有建模的第一步。代码实现里要同时 new 三个对象,然后用faces、vertices、edges三个向量管理起来。

MEV(Make Edge, Vertex):在已有顶点的基础上加一条边和一个新顶点,构成一条新的半边界。扫掠操作的第一步不是直接拉面,而是先用 MEV 把二维轮廓的底边骨架搭出来。注意,MEV 加边时必须同时创建一对待配对的半边,只建一条的话后续遍历就会断链。

MEF(Make Edge, Face):在两个已有顶点之间加一条边,同时生成一个新面。这一步是扫掠操作的核心——它把封闭的二维轮廓“缝合”成一张完整的面。实现时有个关键点:要判断两个顶点之间是否已有路径连通,否则会生成非流形结构。

KEMR(Kill Edge, Make Ring):删掉一条边,同时在一个面上创建一个内环。这是处理带孔洞二维区域的关键操作。比如你要扫掠一个“回”字形截面,内环就是靠 KEMR 建出来的。实现时容易被忽略的是:删除边后,相邻面的边界指针要重新定向。

KFMRH(Kill Face, Make Ring Hole):删掉一张面,同时把它变成一个孔洞环。这个操作常用于把实体内部挖穿。作业里如果只做简单扫掠可能不会触发它,但完整实现时它是闭环里不可缺少的一环。

// MVFS 的核心逻辑:创建孤立点与空面 Solid* mvfs(const Point3D& p) { Solid* s = new Solid(); Vertex* v = new Vertex(p); Face* f = new Face(); // 初始面没有边,next 指针置空 f->halfEdge = nullptr; // 把三个对象挂到 Solid 的管理向量里 s->vertices.push_back(v); s->faces.push_back(f); return s; }

逻辑说明:MVFS 是整个欧拉操作链的入口。注意Face结构里通常会有一个halfEdge字段,指向该面边界上任意一条半边,作为遍历起点。空面的halfEdge为nullptr,后续 MEF 操作会在这里挂上真正的边界边。参数说明:Point3D是三维坐标结构体,源码里在main.hpp中定义,支持x、y、z三个分量。如果你把Face里的halfEdge设计成std::shared_ptr,那么nullptr判断和赋值都要注意引用计数变化。

五个操作之间有严格的依赖顺序。MVFS 创建骨架,MEV 加边加点,MEF 闭合面,KEMR 和 KFMRH 做挖孔和删面。作业里要求扫掠操作允许二维区域带多个内环,所以 KEMR 是必用操作。实现顺序错了会直接导致拓扑自洽性被破坏,后面 debug 会非常痛苦。

2.3 从代码层面理解欧拉操作为什么要按顺序调用

我在翻阅CreateSolid.cpp时发现了一个值得注意的设计:它没有把五个欧拉操作封装成独立函数后到处乱调,而是全部收敛到一个类里,类内部维护一个当前操作的上下文对象。这种设计的好处是每一步操作都从同一个状态出发,上一个操作的结果直接成为下一个操作的输入,出错时能快速定位是哪一步产生的脏数据。

// 典型的操作顺序:先建体,再逐条建边,最后闭合面 Solid* solid = euler->mvfs(origin); HalfEdge* e1 = euler->mev(solid->vertices[0], p1); HalfEdge* e2 = euler->mev(solid->vertices[1], p2); Face* f = euler->mef(solid->vertices[0], solid->vertices[2]);

逻辑说明:这段代码展示了扫掠第一步的拓扑骨架搭建过程——先用 MVFS 创建一个孤立顶点,再连续两次 MEV 把轮廓的折线边建出来,最后用 MEF 把首尾顶点连起来闭合面。参数说明:mev返回的是新建的半边,不是顶点也不是面;要拿到新顶点,需要通过返回的半边去访问。这个细节很多人踩坑:直接用返回值当顶点用,编译期不报错,运行期逻辑全错。

五个欧拉操作不是可以任意排列的。MVFS 之后必须 MEV,MEV 之后才能 MEF,这是拓扑结构决定的:面要存在,必须先有边;边要存在,必须先有点。KEMR 必须在面存在之后才能调用,因为它是在已有面上挖环。这个依赖顺序就是作业评分里“是否正确理解欧拉操作”的核心判断依据。

3. 把平面推成实体:扫掠操作的分步实现与多内环处理

3.1 扫掠的参数模型:轮廓、路径与内环坐标

扫掠(Sweep)的本质是:给定一个二维轮廓,沿某个方向把它推移一段距离,从而生成三维实体。最朴素的扫掠是线性拉伸——轮廓沿法向平移。但这份作业的要求比拉伸复杂一层:轮廓内部可以带多个内环,也就是截面形状上可以有孔洞。

代码里CreateSolid.cpp中定义了扫掠的输入参数,核心是两大块:轮廓坐标数组和扫掠矢量。轮廓坐标按外环逆时针、内环顺时针的顺序传入,这个方向规定不是可选的——它直接决定生成的面的法向朝向。外环逆时针,面的法向朝外;内环顺时针,对应面的法向朝内。如果方向传反了,生成出来的实体面法向混乱,真实感渲染阶段会出现半边黑半边白的情况。

struct SweepProfile { std::vector<std::vector<Point2D>> loops; // loops[0] 是外环,其余是内环 Point3D sweepDir; // 扫掠方向,长度表示拉伸距离 }; Solid* createSweptSolid(const SweepProfile& profile) { Solid* s = new Solid(); // 第一步:在 z=0 平面构造外环和内环的面 Face* bottomFace = buildProfileFace(s, profile.loops); // 第二步:沿 sweepDir 方向平移轮廓到目标平面 Face* topFace = copyFaceTranslated(bottomFace, profile.sweepDir); // 第三步:连接上下环的对应顶点,生成侧面 buildSideFaces(s, bottomFace, topFace); return s; }

逻辑说明:这段代码展示了扫掠操作的完整流程,三层结构非常清晰。第一步先用欧拉操作在二维平面上构建出带孔洞的底面,第二步把整个轮廓沿sweepDir平移一份,第三步在底和顶的对应顶点之间搭侧面。参数说明:loops是一个二维向量,外层是“环”的集合,内层是每个环上的点序列,sweepDir不要求是单位向量,它的模长就是扫掠高度,直接决定实体的拉伸长度。

3.2 多内环的实现路径:不是简单挖空了事

多内环的困难不在“挖空”本身,而在扫掠生成侧面时,内环和外环的侧面处理逻辑完全不同。外环的侧面是一圈“凸”出来的墙,内环的侧面是“凹”进去的孔壁。代码里buildSideFaces函数对这个区别做了分支处理:遍历轮廓环时,区分外环与内环,给内环生成的侧面做反向法向修正。

void buildSideFaces(Solid* s, Face* bottom, Face* top) { // 遍历底面所有环:外环索引 0,内环索引 1..n for (size_t i = 0; i < bottom->loops.size(); i++) { bool isInner = (i > 0); // 沿环的每一条边生成一个四边形侧面 for (HalfEdge* e : bottom->loopEdges[i]) { Face* side = createQuadFromEdge(s, e); if (isInner) { reverseFaceNormal(side); // 内环侧面法向要反向 } } } }

逻辑说明:外环的侧面法向朝外,内环的侧面法向必须朝内,否则渲染时光照方向是反的,模型会呈现“内壁发黑”的效果。reverseFaceNormal在源码里通过交换半边的遍历方向实现,不是真的去修改法向量数值。参数说明:createQuadFromEdge需要传入底面环上的一条半边,它会自动在顶面对应位置找配对顶点,生成一个四边形面。这个函数内部会调用 MEF 操作,所以这里也验证了欧拉操作是扫掠的“原子积木”。

多内环作业里最常见的翻车点不是代码逻辑,而是欧拉操作调用顺序。我本地运行时发现,如果先建内环的侧面再建外环,某些半边指针会丢失正确的next关系,导致遍历底面环时少一条边。这个问题的根因是内环和外环共享同一个底面面片的边界环链,MEF 操作在环境中插入新边时,如果没有正确更新前驱半边的next指针,链就会断。

3.3 扫掠操作的输入校验:作业没写但你应该加的防御

代码里其实没有完善的输入合法性校验,全靠调用者自觉。我在调试时发现,轮廓坐标里有重复点、环没有闭合、内环和外环有交点,这三类问题会直接让欧拉操作产生非流形结构——具体表现就是遍历到某条半边时,它的next指向了自己。虽然这不是作业要求的必做项,但我建议你在调用扫掠函数之前先加一个简单的检查:所有环的首尾点必须重合,内环必须完全包含于外环内部,任意两条环之间不允许有交点。

bool validateProfile(SweepProfile& profile) { for (auto& loop : profile.loops) { if (loop.front() != loop.back()) return false; // 检查环是否闭合 } // 检查内环是否在外环内(取内环任意点做射线法) for (size_t i = 1; i < profile.loops.size(); i++) { Point2D testP = profile.loops[i][0]; if (!pointInLoop(testP, profile.loops[0])) return false; } return true; }

逻辑说明:射线法判断点在多边形内部的算法在很多入门图形学书里都有,这里不赘述。关键是你的工程必须在扫掠开始前做这个校验,否则欧拉操作的错误很难追踪。参数说明:loop.front() != loop.back()比较的是二维坐标的 x 和 y,如果实现Point2D时用了浮点,记得加一个很小的容差 eps,比如 1e-6,而不是直接等号比较——否则手工输入坐标会在闭合检查上误报失败。

这部分的经验是:欧拉操作本身是低层原语,它不管你的输入物理上是否合理,只保证“给定合法输入,输出合法拓扑”。所以上层的几何合法性校验必须自己做,教材里不会讲这个,但实际作业踩坑时百分之百会碰到。

4. 编译、运行与排查:在本地把工程跑起来的完整记录

4.1 构建配置与目录结构:CMakeLists 的正确打开方式

资源里带了CMakeLists.txt和build目录,这是直观的加分项。说明代码工程不是单文件堆砌,而是能构建成完整可执行程序的组织方式。我在本地跑的时候,用的命令是标准的 CMake 三步走:

mkdir build-local && cd build-local cmake .. -DCMAKE_BUILD_TYPE=Debug cmake --build . --parallel 4

逻辑说明:我另建了一个build-local目录,而不是直接用资源自带的build目录,目的很纯粹:不想污染原始文件,万一某个配置要反复调整,删掉自己的目录重新来就行。参数说明:-DCMAKE_BUILD_TYPE=Debug会关闭编译优化,保留调试符号,方便在 GDB 里打断点跟踪半边指针的赋值过程;如果只是想看运行效果,改成Release模式运行速度更快。--parallel 4是并行编译参数,4 是 CPU 物理核数,可以根据你的机器配置调整。

成功构建后会在build-local目录下生成可执行文件。源码里的main.cpp是程序入口,它读取模型文件、调用扫掠函数、启动显示窗口。userInteraction.cpp负责鼠标键盘交互,display.cpp负责 OpenGL 渲染。从文件分工就能看出,这份作业的完成度不低——它不是一个只有算法的裸代码,而是带交互和显示的完整应用。

4.2 模型文件格式与四个测试模型的结构

资源里附带四个模型文件:0.model_8、1.model_C、2.model_A、3.model_D。乍看后缀名很奇怪,但打开看内容后就会发现,这是一种非常朴素的文本格式。每一行代表一个顶点或一个面,顶点用坐标标记,面用顶点的索引序列标记,首行通常是顶点的总数。

8 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0 1.0 0.0 1.0 1.0 4 0 1 2 3 4 4 5 6 7 ...

逻辑说明:这段示例对应一个六面体的部分内容,第一行的 8 表示有 8 个顶点,接着 8 行是顶点坐标,再往后每行第一个数表示该面由几个顶点构成,后面跟着顶点索引。参数说明:模型文件之所以有两套数字,和扫掠操作直接相关——它记录的是扫掠前生成的“二维区域”的轮廓描述,CreateSolid.cpp读取后先构建面片,再沿路径扫掠成体。

三个模型文件分别对应不同的截面形状来源:有的外环带一个内环,有的带两个,有的内环形状是不规则的。你可以逐个加载起来看代码对不同拓扑的处理差异。这四个文件的编号也有意思,是作者从生成程序里直接输出的测试用例,顺序就是开发过程中从简单到复杂的调试顺序。

4.3 交互与显示模块的使用边界:哪些能改哪些别动

display.cpp和userInteraction.cpp是容易被忽略但很重要的两个文件。前者处理 OpenGL 渲染,后者处理鼠标键盘事件。代码本身不复杂——渲染部分基本是标准的固定管线顶点绘制,交互部分是围绕相机旋转展开的。我在复现时注意到一个典型问题:如果 GPU 驱动不支持某个旧版 OpenGL 特性,显示窗口会直接黑屏。这不是代码逻辑问题,而是环境兼容问题。

// display.cpp 中的典型渲染循环结构 void renderScene() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); setupCamera(); // 根据鼠标输入设置视角 drawSolidFaces(); // 遍历半边结构,画所有面片 glutSwapBuffers(); }

逻辑说明:setupCamera依赖userInteraction.cpp里的全局相机状态变量,鼠标拖动时修改这个变量的值,渲染循环每次执行时读取最新值并更新视角。drawSolidFaces是顶点和法向量的核心绘制函数,它要遍历Solid对象中的所有面,通过半边结构的next链把每个面的所有顶点索引收集起来,再交给 OpenGL。参数说明:GL_DEPTH_BUFFER_BIT必须保留,否则面片的前后遮挡关系会错乱,转视角时后面的面会穿到前面来。如果你看到模型半透明或“透视”了,首要检查就是清除标志里是否少了深度位。

这个模块在作业里承载“真实感图形显示”的功能,因此在评分标准里是加分项。但你要清楚它的定位:它只是辅助验证工具,不要花太多时间优化渲染效率,除非你想进一步做真实感增强(比如加光照模型),那是可选的扩展方向。

4.4 避坑自查清单:拓扑遍历、内环方向与渲染黑屏

这一部分直接照着我踩过的坑逐条检查,比盲调查有效率得多。

现象一:扫掠出来的实体在显示窗口里“缺一块”面,或者某个面在特定视角下消失。原因:半边结构的next指针链断裂,遍历面边界时漏掉了一条边。解决:在drawSolidFaces函数里加一条断言——每个面的半边遍历步数必须等于该面的顶点数量,如果不等,打印出面的索引和遍历步数,定位到具体是哪一步 MEF 操作破坏了链。

现象二:内环方向传反,实体内部的结构完全错乱。原因:外环逆时针、内环顺时针是约定俗成的方向规则,但代码里没有自动归一化,直接信任调用者的输入。解决:在buildProfileFace的入口处加一个多边形方向判定函数(鞋带公式),自动把外环逆时针化、内环顺时针化,再传给欧拉操作。

现象三:OpenGL 窗口黑屏但没有任何报错。原因:可能是系统默认 OpenGL 版本过低,或者代码里用了GLUT的旧接口,在 Wayland 环境下会话兼容有问题。解决:启动程序时在命令行加export LIBGL_ALWAYS_SOFTWARE=1(仅限 Linux),强制走软件渲染验证逻辑是否正确;如果软件渲染能显示而硬件渲染不行,就是驱动配置问题,和工程项目无关。

现象四:CMake 配置时提示找不到某个依赖库。原因:资源里的 CMakeLists 大概率假设你已经安装过基本的图形库开发包。解决:Ubuntu/Debian 系执行sudo apt install freeglut3-dev补依赖,RedHat 系用sudo yum install freeglut-devel;装完重新跑一遍 CMake 配置命令即可。

现象五:读入模型文件后程序直接崩溃,没有任何提示。原因:绝大多数情况是顶点索引越界,模型文件里某个面引用了不存在的顶点编号。解决:在CreateSolid.cpp的文件读取函数里,把每个面的索引值在读入后就立刻做上下界检查,越界就打印错误信息并跳过该面,而不是直接把半开放的数据传给欧拉操作。这一步不是作业要求,但能省掉大量 debug 时间。

5. 进阶验证:拓扑合法性自检与 Beyond 作业的扩展玩法

5.1 用欧拉公式自动验证操作结果

作业评分能看到“运行成功”,但看不到你的拓扑是否自洽。给自己加一道保险:在每次扫掠操作完成后,遍历当前Solid的顶点、半边、面,计算 V - E + F,并比对公式预期值。对无洞实体,欧拉示性数是 2;带一个洞是 0;带两个洞是 -2。你可以设计一个自动断言在控制台输出校验结果,这比肉眼判断靠谱得多。

void validateSolid(Solid* s) { int V = s->vertices.size(); int E = s->edges.size() / 2; // 边数 = 半边数 / 2 int F = s->faces.size(); int chi = V - E + F; // 对扫掠生成的实体:G = 内环数,chi = 2 - 2G int expected = 2 - 2 * (int)(/* 得到当前实体的内环数 */); printf("欧拉示性数: %d (预期 %d)\n", chi, expected); if (chi != expected) printf("警告: 拓扑可能不自洽!\n"); }

逻辑说明:代码里的expected计算高度依赖你正确统计内环数量,典型情况是一个内环对应一个穿透孔,所以 G 的值等于内环总数。如果你在扫掠过程中 KEMR 被多次调用,可以维护一个计数器记录调用次数。参数说明:边的数量之所以要除以 2,是因为半边结构里每条无向边对应两条有向半边,edges容器里存的是半边数量。运行此函数后,如果实数与预期值不匹配,说明之前某个欧拉操作执行有误。

这其实不是作业要求,但我在多个类似工程里都用这个做“拓扑连接性和正确性的最终防线”。你在答辩时能很自信地展示这个验证函数,也会让评审老师留下不错的印象。

5.2 扩展方向:不只是扫掠,还有完整 CAD 建模流水线

在这份作业的基础框架上可以做的扩展空间其实很大。我在完成扫掠后顺手做了三件小事,都在这个工程基础上改的。第一个是绕任意轴旋转生成旋转体,用到的是同一个轮廓更新函数,只是把路径从直线变成圆弧。第二个是布尔运算的入口——先把两个 Solid 实体用欧拉操作合并成一个,再用 KFMRH 删掉相交部分,但这一步没做完,因为纯粹的欧拉操作处理布尔交非常容易产生非流形边,需要额外的分裂操作。第三个是把扫掠路径从直线推广到多段线组合,代码上不复杂,就是在buildSideFaces之前先按路径段分段处理轮廓。

这些扩展有一个共同特征:全部沿用在原工程里已经写好的半边结构和欧拉操作,不需要重写几何内核。这也是这份资源最值得使用的地方——它提供了一个可靠的拓扑操作基座,你只需要在上层添加不同的建模策略,就能派生出各种功能。从学以致用的角度来说,即使是过了课程设计的阶段,这段代码也值得留着。

现在每次我做扫掠相关操作,不管是不是这个工程,都会强制先跑一遍拓扑自检再往下走;这个习惯帮我规避过好几轮不知源头在哪的拓扑损坏,从代码层面讲,它算是这份作业给我的最大长期收获。希望这些调试和排查思路也能帮到你,至少让你少走一点我走过的弯路。

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

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

AI伴侣:从聊天机器人到生产力协作对象的实践

1. 从科幻到现实&#xff1a;AI伴侣这个品类是怎么一步步走过来的前几年跟朋友聊人工智能&#xff0c;大家脑子里蹦出来的基本还是《她》里那个温柔细腻的操作系统&#xff0c;或者《银翼杀手》里记忆错乱的仿生人。电影里的AI伴侣要么是情感慰藉的工具&#xff0c;要么是伦理困…

作者头像 李华
网站建设 2026/10/22 4:10:58

质点弹簧系统:图形学物理仿真的入门实践与工程避坑指南

1. 项目概述&#xff1a;为什么质点弹簧系统是图形学入门的“分水岭”GAMES101作业&#xff08;08&#xff09;——质点弹簧系统&#xff0c;这个名字听起来平平无奇&#xff0c;但凡是完整跑通这一关的同学&#xff0c;基本都经历过从“看懂公式”到“亲手让物体动起来”的认知…

作者头像 李华
网站建设 2026/10/22 4:09:50

基于微信小程序与SSM的学生资助管理系统开发全流程

简介&#xff1a;面向毕业设计场景的学生资助在线管理小程序完整项目包&#xff0c;基于微信小程序、SSM与MySQL设计&#xff0c;包含管理员、班主任、家长三类角色&#xff0c;覆盖公告发布、资助申请、在线评议、名单公示、回执公告、申请复议等业务&#xff0c;适合计算机类…

作者头像 李华
网站建设 2026/10/22 4:09:49

微信小程序+SSM架构下的学生资助审批管理系统设计

简介&#xff1a;一套面向毕业设计的学生资助在线管理小程序完整方案&#xff0c;基于微信小程序SSMMySQL架构&#xff0c;实现管理员、班主任、家长三类角色的协同流程&#xff0c;覆盖资助申请、评议、名单公示、申请复议等环节&#xff0c;适合计算机专业学生作为课程设计与…

作者头像 李华
网站建设 2026/10/22 4:09:29

Kylin V10服务器Xorg进程内存泄漏排查与治理实践

凌晨1点40分&#xff0c;监控系统把电话打到了我这里&#xff0c;一台Kylin V10服务器内存占用超过92%&#xff0c;业务系统已经明显卡顿。我登录进去第一件事就是看进程列表&#xff0c;结果发现罪魁祸首不是业务进程&#xff0c;而是Xorg进程&#xff0c;RSS已经涨到7.3GB&am…

作者头像 李华