news 2026/10/17 16:55:30

Qt操作Windows服务实战:封装SCM API实现服务启停与看门狗

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt操作Windows服务实战:封装SCM API实现服务启停与看门狗

简介:面向系统服务管理场景的Qt项目代码包,适合需要在桌面工具中集成服务控制能力的开发者。代码演示了利用Qt框架对系统服务进行创建、删除、打开、关闭、启动、停止等操作,并覆盖服务启动类型设置与信息查询,同时强调管理员权限的必要性,可作为开发系统管理工具的参考。压缩包共9个文件,包含2个C++源文件、2个头文件、1个界面文件、1个Qt工程文件及说明文档,另有少量工程配置文件;整体仅10KB,结构紧凑,便于快速阅读和复用。已有50人学习,适合正在学习Qt与系统服务交互的初中级开发者。利用该代码可快速理解Qt调用系统接口控制服务的常见写法,以及服务状态、启动类型等信息的获取途径;说明文档中还梳理了权限检查和操作流程,能够帮助开发者避开权限不足或服务句柄使用错误。

1. 为什么我会把一套“QT操作Windows服务”的代码拆出来给你

有一天,运维同事丢给我一个需求:内部工具链里有三个Windows服务,启动、停止、看状态,原来一直是手工在“服务”窗口里点,既慢又容易点错。他问我能不能做个带界面的小工具,一键拉起整套服务。我翻了翻手头的项目,发现网上讲“Qt操作Windows服务”的资料七零八落,要么是调sc命令的黑匣子,要么是几十行C#示例,跟Qt完全不搭边。后来我干脆用Qt重写了一套操作Windows服务控制管理器(SCM)的封装,从枚举服务、查状态到启动、停止、重启,全部走Windows API,再把结果回传到Qt界面。这套东西不算大,但够稳,适合要写桌面运维工具、服务管理面板,或者想把服务控制嵌入公司内部系统的朋友。下面我把实现思路、关键代码和踩过的坑一起摊开讲,保证你拿走后能照着重现。

2. 选型问题:调sc.exe和直接调Windows API到底差在哪

2.1 为什么QProcess调用sc命令在Qt里看着简单,实际是个黑匣子

很多刚上手的同行会直接QProcess::start("sc", {"query", "MyService"}),然后去parse输出。第一版跑通很快,但接着就会撞上三堵墙:第一,sc.exe在不同Windows版本上输出格式不固定,Win10和Server 2012的本地化语言还会让“STATE”关键字变成“状态”,你的正则表达式得维护两个版本;第二,sc只能做单向查询,你想一次性枚举全系统一百多个服务,就得反复拉起进程,慢到怀疑人生;第三,sc拿不到详细的错误码和依赖关系,服务启动失败时你只能看到一个“service-specific error 1053”,后面什么都没了。

我一般不会把sc命令当主力,但偶尔会在命令行调试时用它验证结果。真正要嵌入Qt程序的逻辑,还是得走Windows的SCM API——OpenSCManager、OpenService、QueryServiceStatus、StartService、ControlService这套。它们是服务控制管理器直接暴露的接口,稳定、快,错误码是HRESULT,可以翻译成可读信息。选API的代价是你要自己处理类型转换和缓冲区长度,但这套数据结构的坑是有限的,半小时就能摸清。

2.2 用WinAPI封装服务控制器:核心函数与权限模型

Windows服务控制管理器本质上是一个系统级单例,程序通过服务控制管理器句柄来获得所有权限操作。第一步是调用OpenSCManager,这一步要指定权限,常见的是SC_MANAGER_CONNECT(连上查列表)和SC_MANAGER_ALL_ACCESS(建服务和改配置),但一般我们不用最高权限,免得被安全软件盯上。第二步是打开具体服务句柄SERVICE_START、SERVICE_STOP、SERVICE_QUERY_STATUS这些权限位要按需要组合。第三步才是执行操作。

这个权限模型值得多说一句:如果你只想启动一个服务,需要的是SERVICE_START;想改服务配置,要SERVICE_CHANGE_CONFIG。把这些权限位组合传进OpenService,就能避免每次都以管理员身份跑。实际上,要操作除自己进程之外的服务,程序本身基本都要提权到管理员,否则OpenSCManager会返回ERROR_ACCESS_DENIED(错误码5)。这里有个常见的误用是:虽然你的程序里写对了权限位,但UAC挡住了进程,代码再对也没辙,后面我会讲manifest提权。

2.3 最小可编译的枚举与状态查询代码

我先给一个下载这套代码里最核心的枚举服务片段,它是最小可编译的,把SCM连接、枚举、断开关联都走通了。现贴枚举服务状态的部分,并说明每段的作用:

#include <windows.h> #include <vector> #include <string> struct ServiceInfo { std::wstring name; // 服务名(注册名) std::wstring displayName; // 显示名 DWORD state; // 1=停止, 4=运行中,7=正在停止 }; bool EnumAllServices(std::vector<ServiceInfo>& outList) { SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE); if (!scm) { // 提权不够时,这里返回ERROR_ACCESS_DENIED return false; } DWORD bytesNeeded = 0; DWORD servicesReturned = 0; DWORD resumeHandle = 0; // 第一次调用用来拿缓冲区大小,这是SCM枚举的标准套路 EnumServicesStatusExW( scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, nullptr, 0, &bytesNeeded, &servicesReturned, &resumeHandle, nullptr); std::vector<BYTE> buffer(bytesNeeded); if (!EnumServicesStatusExW( scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, buffer.data(), bytesNeeded, &bytesNeeded, &servicesReturned, &resumeHandle, nullptr)) { CloseServiceHandle(scm); return false; } auto* services = reinterpret_cast<ENUM_SERVICE_STATUS_PROCESSW*>(buffer.data()); for (DWORD i = 0; i < servicesReturned; ++i) { ServiceInfo info; info.name = services[i].lpServiceName; info.displayName = services[i].lpDisplayName; info.state = services[i].ServiceStatusProcess.dwCurrentState; outList.push_back(std::move(info)); } CloseServiceHandle(scm); return true; }

逻辑说明:先用第一次调用拿到byteNeeded,再按这个大小分配缓冲区,第二次调用才真正拿服务列表。这里参数SERVICE_STATE_ALL表示不过滤状态,所有服务都返回;SERVICE_WIN32过滤出Win32服务,避免把内核驱动设备服务也枚举出来。servicesReturned会在第二次调用后告诉你实际拿到多少个。如果你把缓冲区设得太小,EnumServicesStatusExW会返回ERROR_MORE_DATA(234),所以用两次调用的方式最稳妥。

参数说明:SERVICE_ENUM_PROCESS_INFO表示返回结构里带进程状态(比如PID和状态码),比不带这个参数的版本多拿一个ServiceStatusProcess,方便后面做界面显示。resumeHandle在服务数量超过单次返回上限时会用到,这里一个窗口页能拿完,一般不用分页。

3. 把启动、停止、重启写成稳定函数:阻塞与非阻塞两种模式

3.1 阻塞式控制:StartService与ControlService的参数细节

枚举服务只是开头,真正要操作服务,得写启动和停止。先给阻塞式的版本,同步等待操作完成,适合做命令行工具或者后台任务。启动服务本质上就是把服务路径和参数传进去,然后调用StartService,它告诉SCM“该去拉进程了”,但不等服务真的进入RUNNING才返回。所以你需要自己轮询QueryServiceStatus。

我把代码写成这样,注意启动超时和状态判断的处理:

#include <windows.h> bool StartServiceBlocking(const std::wstring& serviceName, DWORD timeoutMs) { SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm) return false; SC_HANDLE service = OpenServiceW(scm, serviceName.c_str(), SERVICE_START); if (!service) { CloseServiceHandle(scm); return false; } BOOL ok = ::StartServiceW(service, 0, nullptr); // argc/argv,这里不传启动参数 if (!ok) { // 错误码1058是服务已被禁用,1072是标记删除 DWORD err = GetLastError(); CloseServiceHandle(service); CloseServiceHandle(scm); SetLastError(err); return false; } // 轮询状态直到RUNNING或超时 SERVICE_STATUS status{}; DWORD elapsed = 0; while (elapsed < timeoutMs) { if (!QueryServiceStatus(service, &status)) break; if (status.dwCurrentState == SERVICE_RUNNING) { CloseServiceHandle(service); CloseServiceHandle(scm); return true; } Sleep(250); elapsed += 250; } CloseServiceHandle(service); CloseServiceHandle(scm); return false; }

逻辑说明:StartServiceW拉到的是SERVICE_START权限,虽然启动后要轮询QueryServiceStatus,但你此时已经打开了服务句柄,QueryServiceStatus不需要单独再开一次。这里Sleep(250)是为了避免空转耗CPU,250毫秒在人工点界面时感觉不到延迟,又不会把CPU打满。timeoutMs建议给5~10秒,因为某些服务启动阶段要做网络等待,短了会误判失败。

参数说明:StartServiceW的第二个参数和第三个参数是启动参数数组,普通服务传0和nullptr表示用服务配置里的ImagePath启动,不带额外参数。如果你的工具要支持传参,就要把命令行拆成argv数组传进去,注意Windows服务默认不允许交互式传参,传了也会被忽略。

3.2 异步化改造:把服务操作推给线程,避免卡死UI

直接在Qt主线程里调上面的阻塞函数,界面会僵住,尤其是服务启动慢的时候。血泪经验是:服务启动不是瞬间完成的,有些服务要跑30多秒才算起来,界面卡30秒等于被判死刑。所以我在Qt项目里做了异步封装,用QtConcurrent::run丢给线程池。

#include <QtConcurrent> #include <QFutureWatcher> #include <QObject> QFutureWatcher<bool>* SetupAsyncStart(const std::wstring& serviceName, QObject* receiver, const std::function<void(bool)>& callback) { auto* watcher = new QFutureWatcher<bool>(receiver); QObject::connect(watcher, &QFutureWatcher<bool>::finished, receiver, [watcher, callback]() { callback(watcher->result()); watcher->deleteLater(); }); watcher->setFuture(QtConcurrent::run([serviceName]() { return StartServiceBlocking(serviceName, 15000); })); return watcher; }

逻辑说明:这段把StartServiceBlocking放到全局线程池里跑,跑完通过QFutureWatcher回到调用线程执行callback。不要在lambda里直接操作Qt界面部件,只能返回结果,再由回调去更新界面,避免线程竞争。代码里用了std::function做泛型回调,这样可以在绑定按钮时直接把UI更新逻辑通过lambda捕获进来。

参数说明:receiver可以是按钮或主窗口的this指针,这样watcher会在receiver销毁时自动取消连接,省掉手动断开的麻烦。15000毫秒是启动超时,适合大多数应用服务,如果系统负载高,可以再拉长。

3.3 供Qt界面直接使用的C++封装类

为了让这套代码能被界面直接调用,我把所有操作收敛到一个ServiceController类里。现在给你一个摘掉无关部分的最小类结构:

#include <QObject> #include <QMutex> #include <QPendingFuture> // 仅作示意,实际用QFuture class ServiceController : public QObject { Q_OBJECT public: explicit ServiceController(QObject* parent = nullptr); Q_INVOKABLE bool enumAll(); // 调用后发出serviceListChanged Q_INVOKABLE void startAsync(const QString& name); Q_INVOKABLE void stopAsync(const QString& name); Q_INVOKABLE void restartAsync(const QString& name); signals: void serviceListChanged(); void opFinished(const QString& serviceName, bool ok, int errorCode); private: MsvQObjectForward // 示意占位,真正实现要写私有工具函数 };

这里我没有把全部实现贴出来,重点说设计套路:类内部维护一个服务名到当前状态的缓存,界面通过listView显示时可以直接读缓存。startAsync内部调用3.2节的异步启动,stopAsync同理但要调用ControlService。restartAsync则先停后启,注意不能同时发两个异步操作,否则会互相干扰,所以我内部用QMutex串行化。

为什么要单独建一个类而不是把所有代码放在窗口里?因为服务控制涉及权限和状态变化,分开后方便做单元测试,也方便多个窗口共用同一套服务连接。参数上所有公开方法都建议显式注明明线程安全问题,比如startAsync和stopAsync不能同时对一个服务调用,否则SCM会返回错误码1051(服务控制请求有冲突)。

4. 在Qt界面里落地:信号槽、日志与UAC提权

4.1 界面侧怎么接:用QFutureWatcher还是自建QThread

3.2节里我用QtConcurrent::run实现了异步,这里再展开说另一种做法:如果你不想引入线程池调度,可以直接在QThread里跑一个循环队列。我两种都试过,最后固定用QtConcurrent——它按CPU核心自适应调度,对界面工具这种短任务更合适。自建QThread的问题是,你只能跑一个任务,但服务启动/停止是离散操作,每次开一个新线程太浪费。

界面的接入很简单,按钮点击时调用startAsync,然后监听opFinished信号:

connect(ctrl, &ServiceController::opFinished, this, [this](const QString& name, bool ok, int errorCode) { if (!ok) { statusBar()->showMessage(name + QStringLiteral(" 操作失败: ") + errorStr(errorCode), 5000); return; } refreshServiceStatus(name); }); statusBar()->showMessage是单行短提示,别在这里写复杂弹窗,用户连续点几个按钮时会烦。

参数说明:errorStr()是一个把GetLastError错误码转成中文提示的工具函数,错误码末尾的1053、1079、1068(依赖未启动)会直接影响用户怎么处理,比直接显示数字友好很多。

4.2 UAC提权的两种做法与manifest配置

操作别人服务必须提权。这里说两条路,二选一。第一条最简单:在程序exe同目录放一个manifest,声明requireAdministrator。用Qt Creator开发时,在.pro里加一行配置:

QMAKE_MANIFEST = $$PWD/app.manifest

manifest文件内容只写最关键的一段:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>

这样exe运行时系统会自动弹UAC提权框。另一条路是程序启动时,用ShellExecuteW以runas方式重新启动自己,把原来的进程退掉。第二条适合你希望低权限时也能浏览基本信息,只在操作服务时再提权。但改代码麻烦,还要处理参数传递。我的习惯是直接用manifest,稳定简单。

4.3 服务状态轮询与变更通知怎么做

服务状态变化,比如服务崩溃后自动退出,界面怎么知道?最准确的方式是调用NotifyServiceStatusChangeW,它可以在服务状态变化时发出事件。但传两个大头:第一,它需要SERVICE_QUERY_STATUS权限,而且回调机制在Qt里很容易和事件循环撞车;第二,它一次只能监听一个服务,监听多个就要多开线程。

所以在实际工具里,我宁可做3秒一次的轻量轮询,把所有服务的state存进缓存,对比前后两次值,再更新界面。轮询成本很低,QueryServiceStatusW一次调用也就几微秒,一百个服务才半毫秒。

界面侧我建议用QTimer,超时后调枚举函数,然后差异化刷新model:

QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [this]() { std::vector<ServiceInfo> currentList; if (!EnumAllServices(currentList)) return; compareAndUpdateModel(currentList); // 只发增量变化信号 }); timer->start(3000);

逻辑说明:compareAndUpdateModel负责把新的currentList和服务列表里旧数据对比,只更新状态变化的那一列。千万别直接清空model再重建,否则界面会闪烁,用户也会觉得工具很“脆”。参数上3秒是个折中选择,太短浪费CPU,太长服务崩溃了要等半天才反应过来。

5. 排查:服务操作最常见的六个坑

下面这六个问题,是我在这套工具从零到能上线的过程中实际翻车过的,每个都按“现象→原因→解决”写。

5.1 现象:Qt打包后,在别人机器上双击运行,报“qt.qpa.plugin: could not find the Qt platform plugin ‘windows’”

原因:Qt程序发布时,没有把plugins/platforms/qwindows.dll目录拷到exe旁边。这不是服务代码的问题,但一旦出现,你再怎么写服务操作都跑不起来。

解决:把Qt安装目录下plugins/platforms整个文件夹复制到exe同级的platforms目录里,保证结构是exe所在目录/platforms/qwindows.dll。另外把qt的bin目录下的所有dll放进exe目录,最简单的办法是用windeployqt工具自动部署。从那以后,我每次打包都会顺手跑一遍windeployqt,不再手抖漏文件。

5.2 现象:调用StartServiceW返回错误码1079,但同样的服务在“服务”窗口里能手动启动

原因:1079是“为这个服务指定的账户与服务于其他进程指定的账户不同”。通常是因为服务登录账户填的是某个域账号或本地账号,但服务控制器无法用该账户启动。你写程序触发启动时,程序自己的权限和服务账户的权限是两回事,前者是“谁能发命令”,后者是“服务进程以谁的身份跑”。

解决:先用服务管理器打开该服务的“登录”选项卡,确认“此账户”里的用户名和密码没被改。程序侧要确保OpenService时带SERVICE_START权限,且程序已提权到管理员。如果服务是“本地系统账户”那没问题,如果是自定义账户,就要检查密码是否过期。另外,命令行里用sc qc 服务名查看服务对应二进制路径是否还在,如果PathName指向的文件被删了,也有类似现象。

5.3 现象:QueryServiceStatus返回的state是SERVICE_RUNNING,但服务实际啥也不干

原因:服务启动分两步:SCM把进程拉起来,然后服务自己报告“已就绪”。如果服务进程启动后在后台初始化失败,没调用SetServiceStatus(SERVICE_RUNNING),那query只会看到START_PENDING(值2)或一直停留在START_PENDING。但有些服务进程自己会超时前强制填成RUNNING,导致界面显示“正在运行”,实际内部线程已经废了。

解决:程序侧不要只信dwCurrentState,再用QueryServiceConfig2W查询服务的启动类型和可执行路径,确认是不是被替换过。另外,用任务管理器看服务进程PID是否活着,如果PID存在但CPU为0,说明卡在死循环。我在工具里给status列加了一个“PID”字段,能直观区分“真运行”和“假运行”。

5.4 现象:EnumServicesStatusExW查不到某些驱动服务

原因:枚举时用了SERVICE_WIN32过滤器,它只返回Win32服务(类型为0x30),驱动服务和文件系统过滤驱动(类型0x10或0x20)被过滤掉了。如果你要管理的是设备驱动类服务,比如打印驱动、网络过滤器,就会漏。

解决:把枚举过滤器改成SERVICE_DRIVER或者直接全部枚举,用SERVICE_STATE_ALL过滤状态,类型用混合位掩码。一般管理工具不用管驱动,但如果你这款工具的目标用户是IT运维,记得加一个“显示驱动服务”开关。

5.5 现象:getlastError返回错误5(拒绝访问),但程序明明已经提权

原因:程序提权的只是主进程,但你的代码里可能用OpenSCManagerW时请求了SC_MANAGER_ALL_ACCESS权限,这个权限要求进程“以管理员令牌运行”,但对某些系统服务组策略,仍会拒绝。另外,32位程序在64位系统上跑会触发WOW64重定向,导致部分注册表和服务配置读取的是32位视图。

解决:不要一上来就请求ALL_ACCESS,按最小权限原则,查询用SC_MANAGER_CONNECT,启停服务用SERVICE_START/SERVICE_STOP。如果确实要改服务配置,再动态提权。还有注意编译架构:建议用64位编译发布,或同时输出两个架构,避免WOW64带来的额外麻烦。

5.6 现象:修改了服务操作的代码,重新编译后启动工具,却还是旧的逻辑在跑

原因:Qt的debug和release执行文件混了,或者exe实际是从别的目录启动了旧版本。还有一个隐蔽原因:服务操作的dll/库被系统服务缓存锁定,重新复制dll时失败。

解决:先用任务管理器确认正在跑的exe完整路径。再把exe旁的旧dll全部删干净,重新部署。如果发现服务控制管理器句柄被某个后台线程占住不释放,也会导致新进程连不上,这时候只能杀掉僵尸进程或重启开发机。别问我怎么知道的,我因为没清干净旧dll,连续排查了一个下午。

6. 进阶:让服务管理工具带上“依赖链一键启动”和“看门狗”

6.1 用QueryServiceConfig2读依赖关系,按序启动

单一服务操作只是起点,生产环境里服务之间经常有依赖链,比如数据库服务没起来,应用服务起不来。我在这套工具里加了一个“依赖启动”功能:先枚举目标服务的依赖服务,递归查询,把依赖树按依赖关系排成序列,然后逐个启动。

核心做法是枚举服务配置里的lpDependencies字段,这个字段的每一项是依赖的服务名。你可以调用QueryServiceConfigW拿到DEPENDENCY_SERVICE_DESCRIPTOR结构,再层层递归。实际代码里要注意递归深度限制,某些服务依赖关系很乱,比如A依赖B,B依赖A,会形成环。我直接加了一个访问次数上限,访问超过15次就放弃并提示“检测到循环依赖”。

6.2 看门狗:服务意外退出后自动拉起

服务进程如果自己崩溃退出,SCM会有崩溃记录,但不会自动恢复。我利用5.3节里说的PID跟踪,写了个看门狗线程:每一轮检查已知服务名对应PID是否还活着,如果死了且该服务启动类型是自动,就自动调用StartServiceBlocking重新拉起。

看门狗工作参数建议每次检测间隔5秒,最多连续重启3次,3次后发送通知,避免服务反复崩溃导致系统资源耗尽。这个功能对内部工具特别贴心,至少能保证你凌晨三点不需要爬起来手动重启。

判断服务是否还活着,光靠QueryServiceStatus不够,万一服务状态卡在RUNNING但进程没了,状态还是RUNNING。所以我在看门狗里直接用OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid)检查进程句柄,如果返回错误码ERROR_INVALID_PARAMETER或ERROR_INVALID_HANDLE,说明进程已经不在了。注意不要打服务状态本身,很多时候状态和真实进程是脱节的。

这套QT操作Windows服务的代码我清理了好几轮,最终稳定版就保留前面讲到的枚举查询、异步启停、UAC提权、看门狗这几个模块。从那以后,我每次做桌面运维工具都强制自己先走一遍“权限最小化+线程不阻塞界面+服务状态用PID验证”这三板斧,踩过的坑明显少了很多。希望帮到你。

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

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

artcraft 创作工具链:从灵感落地到成品导出的工程实践

1. 从“artcraft”这个名字说起&#xff1a;一个被低估的创作工具链思路第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的不是某个具体软件&#xff0c;而是一类东西——把“art”和“craft”拼在一起&#xff0c;本身就暗示了一种定位&#xff1a;既要有艺术性的表…

作者头像 李华
网站建设 2026/10/17 16:53:08

ArtCraft:可编排的原子化创意模块方法论

1. 项目概述&#xff1a;ArtCraft 不是“艺术手作”的简单拼贴&#xff0c;而是一套可落地的创意生产方法论ArtCraft 这个词乍看像英文直译——art&#xff08;艺术&#xff09;和 craft&#xff08;工艺/手艺&#xff09;的组合&#xff0c;很容易被理解成“艺术手工”“创意D…

作者头像 李华
网站建设 2026/10/17 16:52:18

PHP资源库系统源码安装与自定义下载网址配置指南

简介&#xff1a;这是一套基于PHP开发的资源库系统源码&#xff0c;面向需要搭建数字资源下载平台的个人站长、开发者或小型团队&#xff0c;帮助解决资源分类展示、下载链接管理与后台维护等问题。压缩包共20个文件&#xff0c;以13个PHP程序文件为核心&#xff0c;搭配4个PNG…

作者头像 李华
网站建设 2026/10/17 16:51:54

房源列表前端开发实战:性能优化与交互逻辑整合

房源列表这个页面&#xff0c;看起来简单&#xff0c;实际上是个典型的“麻雀虽小五脏俱全”的前端场景。我做过好几个租房、二手房、公寓管理类的项目&#xff0c;几乎每一个都绕不开这个页面。它不像详情页那样可以靠堆视觉撑场面&#xff0c;也不像首页那样有足够的空间做品…

作者头像 李华
网站建设 2026/10/17 16:50:24

虚拟线程在Spring Boot 4中的高并发实战:秒杀与批量处理

1. 先把结论放前面&#xff1a;虚拟线程到底解决了什么虚拟线程是我做后端这些年觉得最值得跟进的一个 JVM 特性&#xff0c;不是因为它性能比线程池高多少&#xff0c;而是它彻底改变了写高并发代码的姿势。以前一个 Tomcat 线程默认处理一个请求&#xff0c;线程数一多&#…

作者头像 李华
网站建设 2026/10/17 16:50:03

基于CNN的昆虫类别识别:从数据准备到训练调参的完整实战指南

每年到了毕业设计季&#xff0c;总能看到不少同学在“图像识别”这个方向里打转。尤其是“基于CNN的昆虫类别识别”这类题目&#xff0c;几乎是深度学习毕设里最经典、也最稳的一种选题。它不像目标检测那样需要处理复杂的边界框回归&#xff0c;也不像语义分割那样涉及逐像素的…

作者头像 李华