ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SolidWorks二次开发模板:从零搭建高效工程化插件框架

SolidWorks二次开发模板:从零搭建高效工程化插件框架 简介面向SolidWorks机械设计人员与C#开发者的SolidWorks二次开发模板是一套帮助快速上手SDK开发的可运行示例工程。资源围绕使用C#扩展SolidWorks功能展开覆盖COM引用、ISldWorks接口调用、Add-in插件注册、Windows Forms/WPF界面定制、命令与事件处理、数据交换及异常处理等关键环节适合需要实现参数化建模、批量处理或定制工具栏的工程师参考。包体共99个文件以34个dll动态库、18个cs源代码文件为主辅以exe可执行程序、bmp界面图标、resx/resources界面资源及xml配置文件等整体10.24MB目录结构完整便于对照学习和二次修改。已有724人学习下载示例中包含了插件主体SwCSharpAddin1、事件处理类、属性页实现等模块可直接在Visual Studio中打开解决方案进行编译调试能有效缩短从零开发SolidWorks插件的学习曲线。1. 什么是SolidWorks二次开发模板解决谁的痛点很久没在社区写长文了最近团队内部正好在整理二次开发的代码规范借这个机会把一套我用了很久的SolidWorks二次开发模板拆开讲讲。先说结论这套模板不是某个具体的插件功能而是一整套围绕SolidWorks API开发的工程化骨架包含项目结构、通用类库、错误处理、事件订阅、界面框架这几大部分拿来之后直接往里面填业务代码就能跑。先说说我为什么会认真做一套模板。早些年接的二次开发需求很杂有的是给非标设备做个选型工具有的是把出图流程自动化还有的是做批量改名、批量转格式的小插件。刚开始图省事每个项目都从零写结果代码越来越散今天新建一个窗体明天直接在宏里堆逻辑后天又需要复制一份之前项目里写过的功能。到后来出现一个特别尴尬的场景——客户说“上次那个改名工具能加个前缀过滤吗”我得翻半天代码才能找到那个功能在哪。这其实就是典型的没有模板化、没有沉淀通用能力导致的。后来我下了决心把过去几年写过的二开项目做了一次大扫除把重复用的内容抽出来整理成了一套标准的二次开发模板。现在不管是自己用还是带新人都会从这套模板起步。它的价值可以概括成三件事第一项目结构清晰代码放到哪里一目了然第二通用功能开箱即用比如获取当前模型、读取自定义属性、批量遍历特征这些不用反复写第三框架统一了错误处理和事件管理不同人写出来的代码风格不会差太多后面维护成本低很多。这套模板适合谁如果你只是偶尔录个宏那其实用不上这么重的框架但如果你要正经开发给同事或客户用的SolidWorks插件或者你已经在维护几个二开项目、觉得代码开始乱到不想动那我强烈建议你看看这套思路。接下来我会把模板的每个部分拆开讲包括我是怎么设计的、里面有哪些坑以及一些在官方文档里翻不到的经验。2. 模板的整体设计与思路拆解2.1 为什么需要一个标准结构而不是直接写代码很多刚开始做二次开发的朋友会有一个疑问SolidWorks的API只是个接口我写功能就行了为什么要花时间搭模板我用一个比喻来解释。你装修房子可以直接在地上堆材料开始干活但如果没有脚手架和固定的水电布局等你做到一半想加个插座、改个水路就得把墙凿开重新来。代码也是一样尤其SolidWorks二次开发和普通业务开发不太一样它涉及API连接、COM对象生命周期、模型事件、UI线程交互这些复杂环节不把地基打好后面寸步难行。我见过最多的反面案例是这样的一个宏文件几百行打开模型、遍历特征、改属性全写在一起还有几十个全局变量。看起来能运行但一旦要加界面、要支持多文档、要做异常恢复整个代码就变成了一团浆糊。最要命的是这种代码调试起来极痛苦因为SolidWorks的API很多调用是会弹错误框的你根本不知道是哪里出的问题。模板的核心思路就是把代码按职责拆开。一个典型的模板至少包含四层入口层负责插件加载和卸载界面层负责人机交互业务层放具体功能通用层放SolidWorks API的封装和辅助工具。这样拆完之后任何一个人拿到模板都能按照固定的套路往里面填代码而不是每次从零开始琢磨结构。2.2 模板里我到底放了哪些模块拿我这套模板来说主目录是分模块放置的。根目录下有一个解决方案文件然后按AddIn、Common、UI、Resources这几个文件夹归类。AddIn文件夹放插件的主入口类负责实现ISwAddin接口这里是SolidWorks加载插件时最先执行的地方Common文件夹是重头戏里面放着封装好的SolidWorks连接对象、模型操作类、属性读写类、特征遍历工具、错误日志模块UI文件夹放WinForm或WPF的窗体以及用户控件Resources放图标、配置文件、模板文件等资源。在Common里最核心的是一个叫SwContext的静态类它负责管理当前SolidWorks应用程序对象SldWorks、当前活动文档ModelDoc2、以及文档类型和路径等信息。这个类做了线程安全的初始化避免从事件回调或后台线程访问API时出现对象未初始化的问题。还有一个叫ErrorHandler的静态类统一处理异常把错误信息写入日志文件同时在Debug模式下弹出提示Release模式下只记录日志不让用户看到一堆莫名其妙的英文弹窗。事件处理这块模板里单独封装了一个SwEventRouter类把SolidWorks的各种通知事件如文档打开、选择变更、重建完成、保存等统一注册和管理。这样做的好处是除了AddIn的ConnectToSW里有一段集中注册的代码其他任何地方想订阅事件都只需要调用SwEventRouter的一个方法即可不会出现事件重复订阅导致回调执行两次的问题。2.3 模板能帮你规避哪些典型的二开问题二开做得久的人都知道最头疼的问题往往不是功能逻辑而是那些和SolidWorks自身的交互问题。比如插件加载失败最常见的两个原因一是.NET程序集没被正确强签名二是注册表信息不完整。模板里在PostBuild事件中写好了自动注册和注销的批处理命令生成一次就能自动完成注册不需要每次手动开命令行执行regasm。再比如COM对象释放的问题。SolidWorks API是基于COM的很多接口返回的对象其实是COM包装对象如果创建了不加释放轻则内存占用越来越高重则导致SolidWorks进程退出时报错。模板里统一封装了ComRelease方法并且在所有可能返回COM对象的地方都做了释放处理实测下来长时间运行内存增长明显放缓。还有文档切换、文档关闭时的事件反注册问题模板里都做了对应的防御处理在断开连接和文档销毁时清理掉事件委托避免访问已释放的对象。正因为这些坑都提前用框架解决掉了所以用这套模板写功能的时候我可以专注在业务逻辑上而不是每次都要处理基础问题。接下来我挑几个模板里的核心模块详细讲讲它们的使用方法和背后的原理。3. 核心模块解析与实操要点3.1 从零搭建模板项目结构与命名规则这里我以C#为例因为目前大部分二开都是基于.NET Framework C#。虽然SolidWorks官方推荐用VB.NET写宏但正规的插件项目我几乎没见过用VB写的C#在生态、语法、可维护性上都明显好于VB。项目的框架建议选.NET Framework 4.6.2或者4.7.2不要用.NET Core或.NET 5/6因为SolidWorks的Interop DLL依赖的是.NET Framework运行时。新建一个类库项目后首先要引用两个关键的Interop DLLSolidWorks.Interop.sldworks和SolidWorks.Interop.swconst这两个文件通常在SolidWorks安装目录下。如果你在GAC里找不到可以去安装目录的Public Assemblies文件夹里手动添加引用。命名空间我按模块划分比如SwAddinBase、SwCommon、SwUI。类名用帕斯卡命名法私有字段用下划线加驼峰这个不用多说。要特别强调的是AddIn类所在的程序集必须做强名称签名否则SolidWorks不会加载它。在项目属性里的“签名”选项卡中勾选“为程序集签名”然后新建一个snk文件。然后还要实现ComVisible(true)并在类上标注Guid特性。这套签名和Guid的机制是SolidWorks识别插件的老规矩每个AddIn必须有一个独立的Guid。3.2 定义了哪些通用API封装怎么用这部分是整个模板的精华。我封装了一批高频使用的API把原来要写十几行的代码压缩到一行调用。举例来说获取当前活动文档原来要写ISldWorks swApp Application.GetSldWorks(); ModelDoc2 swDoc swApp.ActiveDoc as ModelDoc2;这种代码现在直接用SwContext.GetActiveDoc()就行。而且这些封装都做了空值判断和异常处理不会因为文档没打开就让插件崩溃。内容再展开一点模板里封装的高频API包括文档打开与保存类OpenDoc、SaveDoc、CloseDoc支持按路径加载并等待加载完成属性读写类读/写自定义属性、配置特定属性支持字符串、数字、布尔值自动转换特征遍历类遍历FeatureManager中的所有特征支持按类型过滤、按名称查找把递归逻辑封好选择操作类获取当前选中对象、选中特定面/边/特征在批量操作时很有用单位换算类很多API返回的长度值默认是米封装方法统一转成用户的单位设置避免出现数值相差一千倍的情况。这些封装不是简单地把API函数换个名字而是加入了实际使用中的约束。比如遍历特征时默认会跳过SolidWorks内部生成的坐标轴、基准面这类隐藏特征只返回用户创建的特征这个思路是我在实际项目中反复调整出来的。有时候你只是想把钣金件里所有折弯特征找出来结果遍历到一堆默认基准面反而增加了代码判断的复杂度。有了这些封装业务层代码会非常干净可读性也很强。3.3 界面框架与交互设计WinForm还是WPF很多二开新手会纠结该用WinForm还是WPF做界面。我的判断是这样如果你的界面比较简单功能选项不多用WinForm就够了学习成本低、部署简单如果要做复杂的数据展示、动态布局、或需要现代一点的视觉效果那就用WPF。SolidWorks本身是MFC架构WPF作为子窗口嵌进去没有太大问题但要注意DPI适配。在模板里我把两种方案都预留了入口。WinForm方案放在SwUI.WinForms目录下WPF方案放在SwUI.Wpf目录下。选择哪种其实还有个关键考量因素你需要的是模态对话框还是非模态任务窗格对话框适合一次性设置参数然后执行如果是需要常驻侧边栏、实时响应用户操作比如实时显示特征或自定义属性那必须用任务窗格TaskPane。任务窗格嵌入到SolidWorks右侧面板里用UserControl承载内容挂在ITaskpaneView上。模板里任务窗格的实现也已经写好了。创建TaskPane的代码集中在AddIn的ConnectToSW方法里用CreateTaskpane方法生成一个ITaskpaneView实例然后把用户控件的句柄传进去。这里容易踩的坑是控件句柄的传递要用taskpaneView.AddControl(userControl.Handle, )这种方式而不是直接丢一个控件对象进去。很多人在这一步卡很久其实搞懂句柄机制就通了。3.4 模板中的核心代码示例口说无凭我贴两段模板里比较典型的核心代码。第一段是AddIn入口的骨架第二段是通用连接对象的管理方式。第一段AddIn主入口[ComVisible(true)] [Guid(5C4E07A3-6E10-4F7A-9B00-2A54FE6A1B2F)] [ProgId(MyCompany.SwAddIn)] public class SwAddIn : ISwAddin { private SldWorks swApp; private TaskpaneView taskpaneView; private int addinCookie; private SwEventRouter eventRouter; public bool ConnectToSW(object ThisSW, int Cookie) { swApp (SldWorks)ThisSW; addinCookie Cookie; SwContext.Initialize(swApp, addinCookie); eventRouter new SwEventRouter(swApp); eventRouter.SubscribeAll(); AddCommandManagerUI(); ShowTaskPane(); return true; } public bool DisconnectFromSW() { eventRouter.UnsubscribeAll(); if (taskpaneView ! null !taskpaneView.IsClosed()) { taskpaneView.Delete(); } Marshal.ReleaseComObject(swApp); swApp null; return true; } }ConnectToSW方法里的顺序是有讲究的先初始化SwContext再订阅事件再添加命令按钮和任务窗格。千万不能反过来因为命令按钮的回调事件是在文档打开后才触发的如果SwContext还没初始化好一旦用户马上点击按钮回调函数里一访问上下文就会抛NullReferenceException。第二段SwContext关键实现public static class SwContext { private static SldWorks _swApp; private static int _cookie; public static void Initialize(SldWorks app, int cookie) { _swApp app; _cookie cookie; } public static SldWorks App { get { if (_swApp null) throw new InvalidOperationException(SolidWorks Application object not initialized.); return _swApp; } } public static ModelDoc2 GetActiveDoc() { return App.ActiveDoc as ModelDoc2; } public static string GetActiveDocPath() { ModelDoc2 doc GetActiveDoc(); return doc?.GetPathName() ?? string.Empty; } }这套上下文管理的方式好处是所有模块都能通过SwContext拿到应用实例不需要每个类里都传一遍SldWorks对象。同时加了一道防护——如果插件没正确初始化就调用会直接抛出明确异常而不是在调用API时报一个莫名其妙的COM错误。3.5 代码规范与注释约定为什么模板要管这些模板不仅仅是代码文件还附带了一套代码规范文档。这是我后来和团队几个同事一起补齐的。二开项目的代码不像互联网项目有那么多人来维护经常是一个人写好几个人看如果不统一规范半年后连自己都想不起来以前写了啥。我的规范里有几条比较重要。第一文件头必须写清楚这段代码的作用、作者和日期防止后面人根据Git记录去反推。第二所有公共方法必须有XML注释说明输入输出和设计意图。第三业务方法尽量控制在50行以内超过就拆子方法。第四禁止在类里到处声明静态可变字段全局状态统一放SwContext不然多文档操作时很容易串数据。还有一个容易忽视的问题是目录命名。模板里一开始用AddIn、Utils、Forms这种通用名结果发现不同项目里同样叫Utils的文件夹内容完全不一样。后来我约束命名规则跟SolidWorks功能有关的类一律以Sw开头跟界面有关的以Frm或Ctrl开头跟业务逻辑有关的按模块名命名比如DimXpertUtils、ExportStepUtils。这样光看文件名就能知道这个类是干什么的。4. 实操过程与核心环节实现4.1 修改模板实现一个真实功能获取当前选中零件的所有特征这套模板到底怎么用我拿一个实际场景走一遍全流程。假设现在我需要写一个插件功能用户选中装配体中的一个零件插件读取这个零件下所有可见特征并把特征名称输出到一个文本文件里。拿到这个需求后第一步不是急着写代码而是想清楚这个功能需要用到哪些模块能力。首先用户需要先选中零件这涉及到选择事件的监听模板里SwEventRouter已经订阅了选择变更事件然后需要从选中对象拿到对应的Component2对象再通过Component2.GetModelDoc2获取零部件文档最后遍历文档的所有特征这用Common里的FeatureHelper就行。整个过程不会用到复杂的UI只需要在原有基础上加一个菜单按钮和一个简单的执行方法。这个功能的关键点在于如何从选择的对象里正确获取到零部件模型文档。在装配体里用户选中的可能是面、边、特征或零部件本身。如果是面或边得先通过((Entity)selObj).GetComponent()拿到Component2再从Component2拿模型文档。如果是整个零部件那直接转换类型。这个逻辑模板里的SwSelectionUtil已经封装好了方法名叫GetSelectedComponentDoc传入ISelectionMgr即可。遍历特征的封装模板里长这样public static ListFeature GetAllFeatures(ModelDoc2 doc, bool includeHide false) { ListFeature result new ListFeature(); FeatureManager featMgr doc.FeatureManager; Feature feat featMgr.FirstFeature(); while (feat ! null) { if (includeHide || !IsHiddenFeature(feat)) { result.Add(feat); } Feature subFeat feat.GetFirstSubFeature(); TraverseSubFeatures(subFeat, ref result, includeHide); feat feat.GetNextFeature(); } return result; } private static void TraverseSubFeatures(Feature feat, ref ListFeature result, bool includeHide) { while (feat ! null) { if (includeHide || !IsHiddenFeature(feat)) { result.Add(feat); } Feature sub feat.GetFirstSubFeature(); if (sub ! null) { TraverseSubFeatures(sub, ref result, includeHide); } feat feat.GetNextFeature(); } }这里有一个实战经验feat.GetFirstSubFeature()返回的是第一个子特征但子特征之间是通过GetNextFeature()链式连接的。我见过很多人只遍历了顶层特征导致结果缺失一截子特征。另外IsHiddenFeature方法其实看的是Feature.Visible属性但要注意在某些版本中这个属性不靠谱更好的做法是判断特征名称是否以系统默认对象的前缀开头比如“默认基准面”“原点”这些。模板里两种判断都做了实际使用下来效果好很多。功能完成后只需要在AddIn的ConnectToSW里把这个新功能命令添加到菜单编译生成后程序集因为已经在PostBuild事件里配置了自动注册直接启动SolidWorks在插件菜单里就能看到了。4.2 调试技巧如何用附加进程快速排查问题二开调试和普通程序调试有个很大的差别——你的代码跑在SolidWorks进程里不能直接F5启动。模板里我写了一个启动配置说明用Visual Studio附加到进程的方式来调试。在项目属性里的“调试”选项卡中选择“启动外部程序”路径指向SolidWorks的exe文件这样按F5时会启动SolidWorks并加载你的插件。如果已经开着SolidWorks也可以手动“调试”-“附加到进程”选择sldworks.exe进程。但有个坑附加到进程后如果插件还没加载你要先在SolidWorks里手动激活插件然后断点才会命中。更高效的方法是在ConnectToSW方法的第一行就打一个断点然后F5启动SolidWorks这时系统会直接停在断点上。注意如果ConnectToSW里出现异常很多情况下SolidWorks会弹出“插件加载失败”的提示并且不会进入DisconnectFromSW处理起来比较麻烦所以ConnectToSW里建议包一层try-catch把异常细节写进日志免得只能看到一个冷冰冰的加载失败弹窗。4.3 发布与部署从GAC注册到安装包模板在发布环节也做了专门设计。二开插件发布时干净的做法是做一个安装包安装包把DLL放到固定目录然后写注册表完成COM注册。如果公司环境不复杂也可以直接用批处理方式完成注册。具体来说模板的Tools文件夹里放了两个批处理register.bat和unregister.bat。register.bat的核心命令是%SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\regasm.exe /codebase %~dp0MySwAddIn.dllunregister.bat则用/unregister参数。这里有个重要的注意事项regasm用codebase方式注册时DLL必须放在固定的、不会变动的路径下不能直接放在桌面或者临时文件夹。同时DLL和它引用的所有第三方依赖库必须在同一目录。如果是用安装包方式推荐使用Visual Studio Installer Projects或Inno Setup把文件安装到Program Files下再调regasm完成注册。另一个部署相关的问题是.NET运行时版本。目标机器上必须安装了对应版本的.NET Framework否则插件加载时会静默失败SolidWorks界面上根本看不到任何提示。模板的README里专门列了一条检查清单包括确认Framework版本、确认VSTO或Interop依赖、确认杀毒软件没有误删DLL。4.4 模板扩展不只是普通插件还能给PDM/MBD场景用这套模板虽然初始是为标准插件场景设计的但我在长期使用中发现它同样可以扩展到更高阶的场景。比如PDM二次开发SolidWork PDM Professional的API和普通API不一样但插件的加载方式类似。可以在模板基础上增加一个PdmIntegration模块通过EDM/API来管理PDM的登录、文件检入检出、状态变更等操作。模板的事件框架和日志框架同样适用于PDM事件。再比如MBD基于模型的定义场景需要大量读写PMI标注和3D注释。模板的属性读写类扩展一下就能管理PMI属性的读写。这类需求在航空航天和汽车零部件行业越来越多模板的价值就不只是省时间了而是能保证不同项目之间有统一的实现方式后续换人接手也容易。5. 常见问题与排查技巧实录5.1 模板使用中踩过的坑用这套模板做了这么多项目有些坑是新来的人几乎必踩的我在这里集中总结一下。第一个坑是强签名丢失。团队里有人把项目文件复制一份后原本的snk文件因为路径引用问题没有被正确加载导致编译能过但SolidWorks加载插件时报“插件未签名”或直接不显示。排查方法很简单用.NET Reflector或ILSpy打开生成的DLL查看程序集是否有强名称或者看项目输出中的注册日志如果regasm报错多半就是签名问题。所以模板里的snk文件一定要放在固定路径引用并且提交到版本库中统一管理。第二个坑是事件重复订阅导致内存泄漏。尤其是在文档切换、重新打开插件时如果事件没有在DisconnectFromSW里反注册第二次连接时会同时触发两个回调轻则逻辑重复执行重则抛异常。模板里对事件统一走SwEventRouter管理就是为了解决这个问题。新加入的事件订阅方法必须配套对应的取消订阅方法这是全组都要遵守的硬规矩。第三个坑是Release版本和Debug版本行为不一致。有些代码在Debug模式下跑得好好的一编译成Release就有问题最典型的场景是COM对象释放时机。Debug模式下对象生命周期被调试器拉长了掩盖了释放过早的问题Release模式下对象一被释放后续再访问就会崩。经历了几次事故后我在模板里统一把受控的COM释放都收敛到ComRelease方法并且在Release模式下加了更严格的检查日志。第四个坑和版本兼容有关。SolidWorks从2018到2023甚至2024API的变化虽然不算大但有些接口的默认行为有差异。比如遍历特征时某些版本会额外返回一些系统内部特征如果代码里没做版本判断同一个功能在不同版本机器上得到的结果可能不一致。模板里的FeatureHelper做了统一处理用版本号判断来跳过系统特征实测兼容性好了很多。5.2 常见错误速查表把最常见的错误整理成一张表遇到问题先到这里查一遍大部分都能解决。错误表现可能原因排查与解决方法插件不加载SolidWorks无提示程序集未签名、GAC注册失败、.NET版本不匹配用regasm手动注册看报错ILSpy检查强名称确认目标机器Framework版本加载插件时提示“类未注册”AddIn的Guid或ProgId丢失检查类上的ComVisible、Guid、ProgId特性是否完整按钮点击后抛NullReferenceExceptionSwContext未正确初始化检查ConnectToSW里是否先调用了SwContext.Initialize事件处理执行两次事件重复订阅检查事件订阅是否被调用了两次确保断开时Unsubscribe遍历特征数量与SolidWorks特征树不一致没有处理子特征或隐藏了系统特征用模板的GetAllFeatures方法检查是否漏了GetFirstSubFeature递归修改属性后文档未保存API修改后需调用ForceRebuild或Save确认修改的是自定义属性还是配置特定属性必要时调用doc.ForceRebuild3Release版本运行崩溃COM对象释放时机不对把释放逻辑统一收敛到ComRelease方法避免在事件回调里释放COM对象任务窗格显示空白用户控件句柄传递错误使用taskpaneView.AddControl(control.Handle, )而不是传控件对象SolidWorks关闭时进程不退出插件未正确释放COM对象DisconnectFromSW里逐一释放事件、TaskPane和SldWorks对象表格列出的是排查路径而不是所有问题的最终答案。实际情况中同一个表现可能是多种原因叠加的建议改一处做一次测试不要一次性改多个变量。5.3 模板语言与工程化思维为什么模板能持续生效最后聊聊一个容易被忽略但很重要的点——模板不是写一次就完事的它需要持续迭代。每一次在实践中发现一个新的通用问题就应该把它吸收进模板。比如我之前封装单位换算函数就是因为在一个转STEP的批处理项目里发现不同客户装在不同地区的系统单位设置五花八门有的用英寸有的用毫米直接套API结果差得离谱。后来把单位换算逻辑纳入模板之后所有用到尺寸计算的功能都自动对齐了这个处理再也没出现过客户的数对不上的情况。模板的工程化思维简单说就是要把自己从具体的项目里抽离开来看问题。每一次新功能的开发都有一部分是和当前需求无关的、可以做进基础设施的。这种增量式的沉淀长期下来收益非常大。6. 最后分享几个实操心得写到最后说点比较个人化的东西。这套模板初版写出来其实只用了不到一周但后续完善它花了大半年而且到现在还在持续更新。我觉得二开领域最容易被低估的一点是真正的瓶颈往往不是API本身而是对产品生命周期和用户使用习惯的理解。API文档解决不了“用户其实想要批量执行而不是单选一个文件”这种需求判断问题。另外一个小建议别把模板搞得太重。我做第一版的时候把能想到的东西都塞进去结果新人一看一堆类就劝退了。后来我把模板分成两个版本一个是精简版只包含最核心的框架和两三个高频API适合入门学习另一个是完整版所有模块都带齐全适合直接拿来上项目。初学者先跑通精简版理解框架思路后再看完整版会顺畅很多。如果你正在做SolidWorks二次开发或者准备做一个长期维护的插件工具集真心建议花点时间把基础框架搭好。这不是“过度设计”而是对自己未来几个月的负责。模板本身不产生业务价值但它是让业务价值持续产出不会翻车的那条跑道。本文还有配套的精品资源点击获取
返回列表