☰
解析Qt Creator的HostOsInfo:跨平台开发的基石
2026/10/13 4:14:08 网站建设 项目流程

看 Qt Creator 这种体量的源码,大部分人的第一反应是从编辑器主窗口、构建系统、插件机制这些大模块下手。我的习惯正好相反:先把 Utils 目录里的小工具类翻一遍。这次要写的主角就是 HostOsInfo 类——它只有十几个静态函数,却是理解 Qt Creator 跨平台工程思路的绝佳样本。

简单说,HostOsInfo 负责回答一个看起来简单、实际容易翻车的问题:当前程序运行在什么操作系统上,以及这个“操作系统”是不是有点特殊。Qt Creator 要同时跑在 Windows、macOS、各种 Linux 发行版上,不同平台的路径分隔符、环境变量、进程启动方式、权限模型完全不一样。如果每个功能模块都自己用#ifdef判断平台,代码会很快变成一张由预处理指令拼起来的地毯。HostOsInfo 做的就是把这层判断收拢到一处:所有想知道“我在哪个系统上”的代码,只需要调用几个静态函数,不需要关心底层是宏还是运行时探测。

更重要的是,它不仅要判断“是不是 Windows”,还要判断“Windows 的哪种运行时”。很多人会在 CI 或者自动化测试环境里用 Wine 跑 Windows 程序,而 Qt 标准库是不会帮你判断这种环境的,只能靠项目自己的工具类收口。这篇文章会把它的头文件、实现、设计边界全部拆开讲一遍,最后再给你一个可以直接抄走的简化版本。

1. 先说清楚:HostOsInfo 在 Qt Creator 里到底干嘛的

在 Qt Creator 的源码里,HostOsInfo 位于src/libs/utils目录下,归属于Utils库。这个库不依赖任何具体插件,是 Qt Creator 自身的“轮子库”:文件路径处理、进程类、算法辅助、系统信息,全在这里。它不解决某个编辑器功能,而是给整个 IDE 提供一个稳定的基础层。

1.1 为什么不能直接全部用 Q_OS_WIN 宏

先泼一盆冷水:如果你在项目里到处写#ifdef Q_OS_WIN,你的代码短期内不会出问题,但长期维护一定很难受。第一,Q_OS_WIN是编译期宏,它描述的是“当前源码正在为哪个目标平台编译”,而不是“程序运行那一刻的平台”。对绝大多数桌面程序,这两者是一回事;但对需要在 Linux 上交叉编译 Windows 版本、或者程序最终跑在 Wine 里的场景,过度依赖宏会导致判断失真。第二,宏判断散落各处之后,你没有办法在测试环境里模拟一个非当前平台,因为预处理器在编译之前就把分支裁掉了。第三,宏判断的语义是分散的,每个调用点都可能写出不一样的平台分支,排查“为什么 Windows 上行为不同”的时候,你需要在整个代码库里搜#ifdef。

HostOsInfo 的思路是:把宏从业务代码里拿掉,收拢到.cpp文件内部,对外只暴露一组函数接口。上层逻辑不再关心底层是宏还是运行时探测,它只问“我是不是在 Windows 上”,然后得到一个可靠的 bool。这看起来是一个很小的工程决策,实际上是跨平台代码可维护性的分水岭。

1.2 和 QSysInfo 的分工:为什么项目要自己再造轮子

很多人会问,Qt 明明提供了QSysInfo,里面有productType()、kernelType()、currentCpuArchitecture(),为什么还要自己封装一遍?问题在于QSysInfo的设计目标是通用性。

productType()在 Linux 上可能返回ubuntu、fedora、debian这样发行版级别的字符串,对 IDE 内部的路径处理、菜单行为判断来说,这个粒度太细了,反而引入了不确定性;kernelType()返回的是winnt、darwin、linux,粒度倒是接近,但它回答的是“内核类型”,不是产品需要的“宿主平台分类”。更关键的是,QSysInfo不会告诉你当前是否跑在 Wine 下。所以 Qt Creator 选择在QSysInfo之下再垫一层自己的抽象,平台分类的语义由项目自己定义,稳定性和可预期性都更好。这不是重复造轮子,而是定制自己的平台词典。

2. 头文件里的全部家当:逐项看它对外提供了什么

我读过的 Qt Creator 主线版本里,hostosinfo.h文件非常短,也就几十行。头文件是这个类的“对外契约”,看头文件就能知道它能干什么、不能干什么。下面这段的结构和真实源码基本一致,为了便于讲解省略了少量辅助声明:

#pragma once #include "utils_global.h" #include <QString> namespace Utils { class QTCREATOR_UTILS_EXPORT HostOsInfo { public: enum HostOs { HostOsWindows, HostOsMac, HostOsLinux, HostOsOtherUnix, HostOsOther }; static HostOs hostOs(); static bool isWindowsHost(); static bool isMacHost(); static bool isLinuxHost(); static bool isAnyUnixHost(); static bool isRunningUnderWine(); static bool isWindowsNt(); static QString hostOsString(); private: HostOsInfo() = delete; }; } // namespace Utils

不同版本会增加或删除个别方法,但核心骨架基本稳定。从上到下看,能读出三条关键信息:这是一个只有静态方法的工具类,不打算让人实例化;它用枚举而不是字符串作为平台类型的主键;所有方法都是同步、无状态、无异常的。把这些静态方法列成一张表,你就能看出每个方法的消费场景。

方法返回值说明典型场景
hostOs()HostOs返回宿主平台枚举作为其他判断的基础实现
isWindowsHost()bool是否 Windows路径、注册表、进程逻辑
isMacHost()bool是否 macOS菜单、快捷键、框架行为
isLinuxHost()bool是否 Linux平台安装包、系统命令
isAnyUnixHost()bool是否类 Unix 平台POSIX 环境假设
isWindowsNt()bool是否 Windows NT 内核历史兼容开关
isRunningUnderWine()bool是否运行在 Wine兼容层诊断
hostOsString()QString稳定平台 token日志、报告、自动化测试

2.1 HostOs 枚举:五种分类背后的取舍

HostOs枚举把操作系统分成五类:Windows、macOS、Linux、其他类 Unix、其他。你可能会问,为什么没有 FreeBSD?没有 Android?没有 iOS?因为 Qt Creator 是桌面开发工具,宿主平台只需要覆盖主流桌面系统。“其他类 Unix”这个兜底分类,保证了在 FreeBSD、Solaris 这类系统上,代码至少有个可用的分支,而不是直接撞进HostOsOther变成未知状态。

这种分类方式也体现了“分类粒度必须与业务需求匹配”。如果分得过细,把每个 Linux 发行版都枚举出来,未来每次遇到新发行版都要改枚举,而且大部分模块根本不在乎你是 Fedora 还是 Arch;如果分得过粗,只分 Windows 和 Unix,那 macOS 的行为差异就没法表达。Qt Creator 的选择很现实:把确定要特殊处理的平台单独列出来,剩下的一律归入“其他 Unix”,再剩下归入“其他”。这是枚举设计里很实用的思路,自己做工具类时可以直接照抄。

2.2 静态方法群:从命名看使用场景

isWindowsHost()、isMacHost()、isLinuxHost()三个函数直接回答“我是不是在这个平台”,绝大多数上层代码只用到这三个。isAnyUnixHost()解决的问题更宏观:代码不关心具体是 Linux 还是 macOS,只要确认自己跑在类 Unix 系统上,就可以放心使用 POSIX 风格的路径和环境变量语义。isRunningUnderWine()用于特殊运行时环境,后面会展开。isWindowsNt()是历史继承下来的兼容性判断。hostOsString()不是给人看的,是给日志、错误报告、自动化测试用的。整套方法统一使用 bool 或固定枚举作为返回类型,调用方写起来非常舒服。

这里要特别提一下命名风格:isXXXHost()而不是isHostXXX()。前者更像自然语言:“是不是 Windows 宿主”,读起来不需要在脑子里做语法转换。Qt Creator 内部大量采用这种谓词式命名,源码阅读体验因此好了很多。

3. 源码逐行拆解:宏判断、环境变量与 DLL 探测

头文件只是承诺,真正的趣味在实现文件里。把hostosinfo.cpp打开,你会发现它把“编译期宏判断”“运行时环境变量判断”“动态库导出符号探测”三种手段交织在一起,而且每种手段都出现在它最合适的场景里。

3.1 hostOs() 和系列快捷函数:预处理分支的正确打开方式

hostOs()的实现是典型的宏链:

HostOs HostOsInfo::hostOs() { #if defined(Q_OS_WIN) return HostOsWindows; #elif defined(Q_OS_MACOS) || defined(Q_OS_MAC) return HostOsMac; #elif defined(Q_OS_LINUX) return HostOsLinux; #elif defined(Q_OS_UNIX) return HostOsOtherUnix; #else return HostOsOther; #endif }

这段代码有几个容易被忽略的细节。

第一,判断顺序不能乱。Q_OS_UNIX是一个范围很大的宏,在 macOS、Linux、FreeBSD、Android 等所有类 Unix 系统上都会被定义。如果把它放在最前面,后面的Q_OS_MACOS、Q_OS_LINUX永远没机会执行。所以必须把更具体的平台放在更前面,最后才落到范围更大的Q_OS_UNIX。

第二,Q_OS_MACOS与Q_OS_MAC在新旧 Qt 版本里都出现过。新版本用Q_OS_MACOS更规范,但为了保证换个 Qt 版本还能编译,很多项目会两个都判断。这也提醒我们,平台相关代码对 Qt 版本跨度尤其敏感,写兼容分支是成本很低、收益很高的习惯。

第三,这条宏链最终只产生一个return,其他分支在预处理阶段就被丢弃了,运行期零开销。这是编译期判断最大的优势:你不需要在运行时比较字符串、查注册表,结果在编译那一刻就已经被写死进二进制了。

isWindowsHost()等函数不需要重复写一份宏链,直接基于hostOs()比较枚举即可:

bool HostOsInfo::isWindowsHost() { return hostOs() == HostOsWindows; }

从设计上讲,isWindowsHost()是hostOs()的语义化包装。调用侧读起来是“如果我是 Windows 主机”,比读“hostOs() == HostOsWindows”更自然,而且如果未来hostOs()的实现方式变化,这个包装函数也不需要改动。

isAnyUnixHost()的实现更有意思,我看到的版本直接判断宏,而不是组合三个枚举值:

bool HostOsInfo::isAnyUnixHost() { #ifdef Q_OS_UNIX return true; #else return false; #endif }

这样写的好处是:未来如果新增了一个定义Q_OS_UNIX的平台,isAnyUnixHost()自动覆盖它,不需要改任何代码。这也是一条值得记住的规律:平台工具类里,能用编译期宏收口的地方尽量用宏收口,让行为与平台体系保持一致,而不是自己手工维护一份平台清单,那份清单早晚会漏。

3.2 isWindowsNt():一个带着历史包袱的兼容性判断

先说一下背景。Windows 9x 系列和 Windows NT 系列,在很长一段时间里是两类完全不同的系统:9x 没有完整的 Unicode 支持,路径语义更接近 DOS,API 行为也有很多差异。2000 年之后,9x 逐步退出历史舞台,NT 内核成为唯一主流,但很多企业级软件仍然保留着“判断是不是 NT”的代码,用来决定能不能启用某些高级 API。Qt Creator 里保留isWindowsNt(),本质上就是这段历史的沉淀。

不同版本的实现方式不太一样。老一点的做法是用GetVersionEx取OSVERSIONINFO里的dwPlatformId,再和VER_PLATFORM_WIN32_NT比较;也有实现直接查环境变量OS是否为Windows_NT。不管哪种,在今天这个时代,Windows 主机上这个函数的返回值几乎恒为 true。

读源码的时候看到这种“死函数”,别急着当作垃圾代码。它是一个活的时间戳,说明这个项目曾经为了兼容 Win9x 付出过设计成本。反过来,这给我一个启发:在基础平台判断上预留这样的兼容性切换点,哪怕今天用不上,也比将来被迫大规模修改调用方要稳得多。有时候代码里多一个看似无用的函数,恰恰是项目长寿的秘密。

3.3 isRunningUnderWine():用 DLL 导出符号识别“仿冒系统”

这是整个类里最值得玩味的一个函数:

bool HostOsInfo::isRunningUnderWine() { #ifdef Q_OS_WIN static const bool underWine = []() { HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll"); if (!ntdll) return false; return ::GetProcAddress(ntdll, "wine_get_version") != nullptr; }(); return underWine; #else return false; #endif }

为什么通过ntdll.dll而不是kernel32.dll?因为 Wine 是一个在非 Windows 系统上模拟 Windows API 的兼容层,它必须提供大量系统 DLL 的实现。kernel32.dll的逻辑可以部分复用宿主系统的能力,但ntdll.dll是最贴近底层的原生 DLL,内核相关的核心机制都在这里,Wine 几乎必然用自己的实现替换它,并且会趁机导出一系列wine开头的符号,比如wine_get_version、wine_get_build_id。所以在ntdll.dll的导出表里找不到wine_get_version,基本就可以断定当前进程没有跑在 Wine 里。

这里还有个细节:GetModuleHandleW不是LoadLibraryW,前者只返回“当前进程已经加载的模块”的句柄,不会主动把 DLL 加载进内存。Windows 上任何一个进程,无论是不是 Wine,启动时都会加载ntdll.dll,所以这里用GetModuleHandleW既安全又干净——不会增加加载开销,也不会改变进程状态。拿到句柄之后,再用GetProcAddress查导出表。整个过程没有创建进程、没有读文件、没有碰注册表,是一个开销极低的探测手段。

另一个值得抄进自己代码的细节是static const bool加 lambda 的写法。C++11 之后,函数级静态变量的初始化是线程安全的,第一次调用时自动执行一次探测,之后所有线程共享缓存结果。系统平台在进程生命周期内不会变化,Wine 环境也不会在运行中途突然变成原生 Windows,所以缓存一次完全合理。这种“惰性初始化 + 线程安全 + 结果永不变化”的组合,是平台探测类代码的标准解法。

3.4 hostOsString():给日志和诊断信息的标准化输出

hostOsString()做的事很简单:把HostOs枚举转成小写字符串,比如windows、macos、linux、otherunix、other。它的意义不在于映射本身多难写,而在于整个项目所有模块输出平台信息时,都使用同一套词汇。你去看 Qt Creator 的日志、崩溃报告、诊断界面,平台字段不会出现Windows 10、Microsoft Windows、Win10这种五花八门的写法,只会有一个稳定的 token。

这对日志检索、自动化比对、版本报告生成特别友好。不少项目用QSysInfo::productType()直接当字符串用,结果 Linux 下日志里有时是ubuntu、有时是debian、有时又是alpine,写日志筛选规则的人会被折磨疯。hostOsString()的取舍是:宁可少给信息,也要保持类型稳定。如果你自己写跨平台程序,建议维护一个这样的平台 token,而不是直接把系统 API 的返回值吐到日志里。

4. 从这个小类里总结出的跨平台设计规律

看完实现,退一步看设计,HostOsInfo 其实浓缩了一套跨平台代码的组织方法论。这比记住某个具体 API 重要得多。

4.1 编译期判断与运行期探测的分工

什么该用#ifdef?什么该用GetProcAddress?我总结的规律是:凡是编译完成后不可能改变的事实,交给编译期宏;凡是运行到某个环境才可能知道的事实,交给运行时探测。操作系统类型在绝大多数情况下属于前者,所以hostOs()用宏;Wine 只有在程序运行起来之后才能确定,所以isRunningUnderWine()用运行时探测;isWindowsNt()介于两者之间,历史演进上更偏向运行时。

如果搞反方向,就会出问题:用宏去判断“当前是不是 Wine”是做不到的,因为宏由编译器决定,和运行时环境无关;用运行时 API 去判断“我是不是 Windows”则纯属浪费,因为 Windows SDK 的头文件在非 Windows 平台根本没有,编译都过不去。

另一个规律是:项目里真正需要写#ifdef的地方,应当尽量控制在平台抽象层内部,上层业务代码全部调用函数。我在不少开源项目里见过相反的情况——业务代码里到处是#ifdef _WIN32,平台抽象层反而成了摆设,最后维护成本全花在“这个#ifdef在哪个模块”的考古上。HostOsInfo 提供了一个很干净的反例:把所有平台差异收口在一个文件里,业务代码只依赖函数名。

4.2 静态缓存、线程安全与可测试性

HostOsInfo 的方法基本都在函数内部缓存结果。C++11 之后,函数级静态变量的初始化是线程安全的,所以不用自己加锁,也不用动用Q_GLOBAL_STATIC,代码可以写得很简洁。

缓存能够成立,前提是结果一旦确定就不变。平台类型和 Wine 探测都满足这个前提。不过缓存也带来了可测试性的代价:你没有办法在单测里临时把“当前系统”改成一个虚构的平台。这也是为什么大型项目会在平台层之上再包一层业务接口,让业务模块依赖接口而不是直接调用 HostOsInfo。真到了要模拟某个平台行为的时候,替换接口实现就行,不用去动底层工具类。

这样思考下来,HostOsInfo 其实是一个典型的“稳定但不可 mock 的地基层”。它本身不测,它之上的逻辑拿接口复用,底层只有一个实现。很多基础组件都是这个地位,你不需要让所有代码都变成可 mock 的,但你要知道哪些部分值得 mock。

4.3 自己封装平台信息时怎么取舍

并不是每个项目都需要一个像 HostOsInfo 这样的类。我的判断标准是三条:第一,代码里#ifdef的数量是否已经超过十个;第二,日志、崩溃报告、自动化测试是否需要统一的平台标识;第三,你是否需要模拟非当前平台的行为。三条里占了两条,就值得封装。

封装的时候不要贪心,先把hostOs()、isWindowsHost()、isMacHost()、isLinuxHost()、isAnyUnixHost()这几个核心函数做出来,再按项目需要补充 Wine 检测或架构判断。千万不要一上来就把发行版、内核版本、API 版本全塞进去,那只会让你的平台类变成另一个需要长期维护的泥潭。平台抽象层的目标不是提供最多信息,而是提供最稳定的信息。

5. 实战:手写一个可复用的 HostInfo 工具类

光读不练很容易看完就忘。下面我把 HostOsInfo 的核心设计抽出来,写了一个在自己的项目里可以直接改用的 HostInfo 类。返回值我用QString,你如果不想依赖 Qt,改成const char*或者std::string也很容易。

5.1 头文件设计:先定契约再写实现

#ifndef MYHOSTINFO_H #define MYHOSTINFO_H #include <QString> namespace MyUtils { enum class HostOs { Windows, MacOS, Linux, OtherUnix, Other }; class HostInfo { public: static HostOs hostOs(); static bool isWindowsHost(); static bool isMacHost(); static bool isLinuxHost(); static bool isAnyUnixHost(); static bool runningUnderWine(); static QString hostOsString(); static QString osDisplayName(); private: HostInfo() = delete; }; } // namespace MyUtils #endif // MYHOSTINFO_H

这里我用enum class代替了普通enum,避免枚举名污染全局命名空间。对调用方来说,语义和 Qt Creator 里的 HostOsInfo 完全一致。额外加了一个osDisplayName(),它会在 Wine 环境下返回windows/wine,方便诊断信息里直接展示异常环境。

5.2 实现与踩坑记录

#include "myhostinfo.h" #ifdef Q_OS_WIN #include <windows.h> #endif namespace MyUtils { HostOs HostInfo::hostOs() { #if defined(Q_OS_WIN) return HostOs::Windows; #elif defined(Q_OS_MACOS) || defined(Q_OS_MAC) return HostOs::MacOS; #elif defined(Q_OS_LINUX) return HostOs::Linux; #elif defined(Q_OS_UNIX) return HostOs::OtherUnix; #else return HostOs::Other; #endif } bool HostInfo::isWindowsHost() { return hostOs() == HostOs::Windows; } bool HostInfo::isMacHost() { return hostOs() == HostOs::MacOS; } bool HostInfo::isLinuxHost() { return hostOs() == HostOs::Linux; } bool HostInfo::isAnyUnixHost() { #if defined(Q_OS_UNIX) return true; #else return false; #endif } bool HostInfo::runningUnderWine() { #if defined(Q_OS_WIN) static const bool result = []() { HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll"); if (!ntdll) return false; return ::GetProcAddress(ntdll, "wine_get_version") != nullptr; }(); return result; #else return false; #endif } QString HostInfo::hostOsString() { switch (hostOs()) { case HostOs::Windows: return QStringLiteral("windows"); case HostOs::MacOS: return QStringLiteral("macos"); case HostOs::Linux: return QStringLiteral("linux"); case HostOs::OtherUnix: return QStringLiteral("otherunix"); case HostOs::Other: return QStringLiteral("other"); } return QStringLiteral("unknown"); } QString HostInfo::osDisplayName() { const QString base = hostOsString(); return runningUnderWine() ? base + QStringLiteral("/wine") : base; } } // namespace MyUtils

这段代码基本是 HostOsInfo 的简化复刻,使用时有几个点要特别注意。

第一,windows.h只在 Windows 分支里 include。如果把windows.h无条件包含,非 Windows 平台立刻编译失败。这是所有跨平台 C++ 代码的第一纪律。

第二,GetModuleHandleW使用了宽字符版本。Qt 环境里一般会定义UNICODE宏,但显式写W后缀更保险,换编译器或换构建环境时不容易踩坑。

第三,lambda 里的ntdll是HMODULE类型,它在布尔表达式里可以被隐式转换为 bool,但我保留if (!ntdll)的显式判断,可读性更好,也方便以后插入日志。需要说明的是,ntdll.dll在普通 Windows 进程中一定存在,但写防御性判断不是坏事,万一以后运行在精简版容器里,也不至于崩溃。

第四,hostOsString()的 switch 覆盖了全部枚举值。有了enum class之后,编译器在开-Wswitch的情况下会提示漏掉的枚举值,这是一个免费的防呆机制。

我在自己的项目里这样用了一段时间,遇到过最大的坑是 Qt 版本差异。新版 Qt 定义了Q_OS_MACOS,老版本用的是Q_OS_MAC,所以头文件里两个宏都做兼容判断最稳妥,否则换了 Qt 版本,isMacHost()可能会静默返回 false,排查起来非常耗时。

另外一个常见的移植问题是:如果哪天你决定脱离 Qt,不要依赖Q_OS_WIN、Q_OS_UNIX这些宏,要改用编译器预定义宏_WIN32、__APPLE__、__linux__。实际上 Qt 的Q_OS_*也是在这些预定义宏的基础上再包了一层,本质上是一个语义化封装。你自己封装的时候直接使用编译器预定义宏,还能少一层依赖。

5.3 从 HostInfo 再往前走一步

如果只是复制这个类,收获有限。真正有价值的下一步,是学着像 Qt Creator 那样,把“平台判断”和“业务逻辑”彻底隔离。我现在做项目的习惯是:平台判断只允许出现在 HostInfo 内部和少数初始化代码里;业务模块需要区分平台行为时,定义业务接口,比如PathResolver、ProcessLauncher,在实现类内部去调用 HostInfo。这样以后如果要在 CI 里模拟 macOS 行为,只需要换一个接口实现,不必去改任何#ifdef或底层工具类。

底层平台类就像是地基,地基不用花哨,但一定要稳。HostInfo 只回答“我在哪里”的问题,至于“在这里之后该怎么做”,那是业务层的任务。保持这个边界清晰,你的代码架构就不会因为平台判断而腐烂。

6. 读这类源码的额外收获与个人体会

最后聊一点阅读体会。很多人读大型 C++ 项目源码,喜欢从插件系统、构建引擎、编辑器控件这种大块头入手,结果一头扎进去两三天出不来。我的习惯是先读utils目录。这里的类短、职责单一、相互依赖少,是理解整个项目编码风格、平台抽象思路、命名习惯的最佳起点。HostOsInfo 就是这样一个样本,它只有几十行代码,却把“跨平台代码怎么写才不烂”这件事讲得明明白白。

另外,HostOsInfo 这个命名也值得品味。Host在这里指的是“宿主操作系统”,也就是当前程序运行所在的本机环境。Qt Creator 还要管理远程设备、模拟器、容器,那些对象的系统信息绝不会用HostOsInfo去描述。这种命名上的小心思,在大型代码库里是一种隐性文档:看到Host开头的类,你就知道它只关心本机,不关心远端。

如果你想在自己项目里培养这种平台抽象意识,我的建议很简单:从今天开始,把所有散落在业务代码里的#ifdef往一个平台类里收。哪怕一开始只有一个isWindowsHost()和一个isLinuxHost(),也比满屏的预处理指令强。等哪天你需要跑 Wine、需要做跨平台测试、需要写诊断日志的时候,你会感谢当初的那次收口。我读 Qt Creator 源码最大的体会是,有价值的东西往往不是某个算法的精妙,而是这些不起眼、却决定了项目长期命运的工程习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询