1. 面对工业现场的千奇百怪,先聊聊这套CPS系统的由来
工业互联网喊了好几年,真正落到车间里,你会发现绝大多数项目根本不是技术不够花哨,而是“软件形态”压根没跟上现场节奏。有大厂直接从云端给你一个SaaS账号,说你们工厂接上去就行——结果现场网络不稳定、数据不出厂的要求卡在那儿、车间里既有Windows工控机又有Linux网关,老板还想要一套系统所有部门共用。最后项目要么烂尾,要么成了PPT上的摆设。
我这两年做的一套基于Vue2.6和.NetCore3.1的工业互联网CPS系统,就是冲着这些真实痛点来的。CPS这个概念听着玄乎,其实就是信息物理系统——把物理世界的设备、产线、传感器,跟数字世界的业务系统打通,形成“感知-分析-决策-执行”的闭环。我们这套软件定位是工业应用软件底座,核心解决三件事:跨平台部署、多租户共用、多业务模块按需组合。
先说跨平台,这是个被很多人低估的需求。一套系统如果只能装在Windows上,那工业现场一半的场景你就覆盖不了——很多边缘网关是Linux,很多老的工控机又只能是Windows 7,还有一些客户为了安全要求必须部署在内网服务器上。而.NetCore3.1天生跨平台,前端Vue又跑在浏览器里,这套组合天然就能做到“写一次,到处部署”。
再说多租户,这个在纯互联网SaaS里很常见,但放到工业场景里会复杂得多。同一个集团下面可能有多个工厂,每个工厂有自己的用户、设备、工艺数据,但又需要集团层面汇总分析。传统做法是给每个工厂上一套独立系统,结果运维成本翻几倍,数据标准还不统一。多租户就是要解决“一套系统、多组织共用、数据互相隔离、权限独立控制”的问题。
至于多业务需求,工业软件最怕的就是做成“大而全”的怪物。今天要设备管理,明天要工单系统,后天又要质量管理,每加一个模块就改一遍主体代码,最后没人敢动。我们把业务拆成可插拔的模块,像搭积木一样按客户需求组合,这个思路在服务多个行业客户时特别管用。
这篇文章不聊虚的,直接拆解这套系统的架构设计、落地方案和踩过的坑。如果你是做工业软件、IoT平台或者SaaS系统开发的,尤其正在纠结技术选型和多租户方案的,可以认真看看。
2. 整体架构拆解:为什么是Vue2.6和.NetCore3.1,而不是其他组合
2.1 后端选型:.NetCore3.1在工业场景里的优势
先说说为什么选.NetCore3.1。当时团队评估过Java Spring Cloud、Go、甚至Python,最后还是定了.NetCore3.1,原因很实际。
第一,性能上有天然优势。.NetCore的运行时性能在TechEmpower的基准测试里常年排在第一梯队,同样的硬件配置,处理高并发设备数据上报的业务场景,比Java节省不少机器。工业现场的设备数据特点是“高频小包”,一个车间几百台设备,每台设备几秒钟上报一次状态数据,吞吐量要求比普通企业应用高一个量级。
第二,生态对工业协议的支持够用。工业领域最常见的Modbus、OPC UA、S7协议,在.Net生态里都有成熟的类库,像IoT Gateway这种开源项目就是基于.Net写的,直接拿来改改就能对接设备层。如果用Java,有些事情就得自己造轮子。
第三,部署方式灵活。发布成单文件、自包含部署包,扔到Linux服务器上直接跑,不依赖外部运行时。这在工业现场很关键——很多工厂的服务器是不能连外网的,你不能现场装环境、装依赖,自包含部署解压就能跑。
2.2 前端选型:Vue2.6在工业系统里的生命力
前端选了Vue2.6而不是Vue3或者React,这个决策当时也有争议。但Vue2.6在国内工业软件圈的渗透率实在太高了,尤其是老项目维护和招人成本这两个维度,Vue2.6的优势无可比拟。
工业管理系统的前端特点就是表格多、表单多、实时数据刷新频繁,这些场景Vue2.6的响应式模型处理起来很成熟。再加上Element UI这套组件库,工业后台管理页面的开发效率极高。我们现在看Vue3的生态系统已经非常完善了,但如果你接手的是既有项目,或者团队里全是Vue2的老手,Vue2.6在2025年的今天完全够用,关键在于架构设计得当。
代码层面我们用了Vuex做状态管理,vue-router做前端路由。组件库选了Element UI,配合ECharts做工业可视化大屏。前端只做交互和展示,所有业务逻辑全部走后端API,这样前端代码的复杂度可控。
2.3 CPS分层架构:从设备到业务的完整链路
整个系统的架构分层大概是这样的:
设备接入层:负责对接各类工业设备,通过Modbus TCP、OPC UA、MQTT等协议采集数据。这一层我们开发了独立的边缘网关程序,部署在车间现场,负责协议解析和数据清洗,然后将标准化后的数据推送到平台。
平台服务层:这是整个CPS系统的核心,包含设备管理、数据存储、规则引擎、报警中心、权限管理等基础能力。所有的业务模块都构建在这一层之上。
业务应用层:面向不同角色提供的具体功能模块,比如设备运维人员的点检保养模块、生产管理人员的工单排产模块、质量部门的质量追溯模块。
展示层:Vue2.6构建的Web端,包含管理后台、车间看板、领导驾驶舱大屏三种形态。
这套分层的好处是,每一层都可以独立部署、独立扩展。设备接入层可以单独部署到边缘节点,平台服务层可以横向扩展,业务应用层按需装载。客户只需要基础平台,那我们就只交付设备管理和报警中心;客户需要生产和质量模块,再把对应的业务模块部署上去。这种可裁剪性,正是多业务需求落地的关键。
3. 多租户落地方案:一套系统怎么服务多个工厂、多个客户
3.1 三种租户隔离策略,我们应该怎么选
多租户的核心问题是隔离,也就是不同的租户之间数据不能互相串。隔离策略通常有三种,我做了个对比:
| 隔离方案 | 隔离级别 | 成本 | 适用场景 |
|---|---|---|---|
| 独立数据库 | 最高 | 高 | 大型客户、数据敏感行业 |
| 共享数据库,独立Schema | 中 | 中 | 多数SaaS场景 |
| 共享表,租户ID区分 | 低 | 低 | 租户间信任度高的场景 |
我们最终选了“共享数据库 + 独立Schema”的方案,原因很现实。
独立数据库方案的成本太高,每个租户一套数据库实例,光是数据库连接数、备份任务、监控告警就够运维喝一壶的。而共享表方案虽然成本最低,但在工业场景下有隐患——一旦代码里某个查询漏掉了租户条件,A工厂的设备数据就会出现在B工厂的界面上,这种事故在工业项目里是绝对不能发生的。万一出了事故,责任划分也非常麻烦。
独立Schema的方案是把数据库里的数据按Schema隔开,同一个数据库实例,每个租户一个Schema,数据表结构一样,但数据物理隔离。查询的时候只要指定Schema,天然就带上了租户隔离条件,就算代码里忘了加“where tenant_id = xxx”,数据也串不了。这个方案在隔离级别和运维成本之间取得了平衡,是我们权衡之后的选择。
3.2 租户上下文解析:中间件怎么识别“你是谁”
多租户要解决的第一件事,就是用户的每一次请求,系统得知道这个请求属于哪个租户。我们的实现方案是通过JWT令牌携带租户信息,而不是每个请求都传输租户ID。
具体做法是:用户登录的时候,后端根据用户名找到对应的租户,把租户ID写进JWT的Claim里。之后每次请求,网关或API层从JWT里解析出租户ID,然后通过一个自定义中间件,把租户上下文存入当前请求的线程安全容器里。
public class TenantMiddleware { private readonly RequestDelegate _next; public TenantMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var tenantId = context.User?.FindFirst("tenant_id")?.Value; if (!string.IsNullOrEmpty(tenantId)) { var tenantContext = context.RequestServices.GetService<TenantContext>(); tenantContext.TenantId = Guid.Parse(tenantId); } await _next(context); } }这里有个重要的细节:租户上下文不能存到静态变量里,因为多租户系统是并发的,A租户的请求和B租户的请求可能同时在服务器上处理,静态变量存租户ID会造成数据串。我们用的是Scoped生命周期的依赖注入,每个HTTP请求创建独立的TenantContext实例,请求结束自动释放,这样才能保证租户上下文不串。
租户上下文建立之后,数据库访问层会在每次创建数据库连接时,根据租户ID自动切换Schema。我们的DbContext是动态创建的,根据TenantContext里的租户ID拼接连接字符串里的Initial Catalog或Schema参数。
3.3 租户级别的功能开关与个性化配置
光有数据隔离还不够,不同租户对功能的需求也不一样。有的工厂要设备点检,有的要能耗管理,有的要计件工资,多租户系统必须支持“租户级功能开关”。
我们设计了FeatureFlag机制,每个租户在开通时都会分配一套功能清单,清单里包含了这个租户能访问哪些业务模块、每个模块的按钮权限有哪些。前端根据租户的功能清单动态渲染菜单和按钮,后端在API层也做同样的权限校验,双重保障。
这个机制带来的一个好处是:系统内核可以不断扩展新功能,但不会影响到存量租户。比如我们研发了新的“设备预测性维护”模块,测试通过后,先给试点租户开通,验证没问题再推给其他租户。这跟传统软件“一升级所有客户全变”的模式完全不同。
4. 多业务需求模块化:从设备管理到生产执行的不停扩展
4.1 核心业务模块清单:工业互联网到底要管什么
工业互联网CPS系统承载的业务远不止“设备连上网”这么简单。围绕设备全生命周期管理,我们沉淀了一套基础业务模块:
设备管理:设备台账、设备档案、备品备件管理、设备维修记录。这个模块是工业系统的地基,所有设备相关的业务都从这里开始。
生产工单管理:生产任务下达、工单进度跟踪、工序流转记录。把生产计划拆解成可执行的工单,再分配到具体设备和人。
质量追溯:生产过程中的质量数据采集、不良品记录、批次追溯。一旦产品出现质量问题,能快速定位到是哪台设备、哪个批次、哪个操作工。
报警中心:设备异常报警、参数超限报警、报警处理闭环。支持微信、短信、邮件多种通知方式。
点检保养:设备点检计划、保养计划自动生成、点检记录上报。这个模块最琐碎,但客户最需要。
能源管理:水、电、气的分项计量、能耗趋势分析、能耗异常报警。
报表中心:各类管理报表的自动生成,包括设备OEE报表、稼动率报表、质量统计报表等。
4.2 模块化开发的边界划分和接口约定
这么多业务模块放在一起,如果没有清晰的边界,很快就会变成意大利面条式的代码。我们通过几个原则来维持模块独立:
每一个业务模块都做成了独立的后端项目,通过项目引用或NuGet包的方式引用公共类库。模块之间不允许直接引用彼此的业务代码,只允许通过事件或API进行通信。比如设备管理模块触发“设备维修完成”事件后,生产模块监听这个事件,自动恢复对应的工单排程。
数据库层面,每个模块有自己的数据表前缀,比如设备相关表统一前缀“equ_”,工单模块统一前缀“pro_”,互不干扰。这样做的好处是数据库结构一目了然,新接手的开发也能快速定位到某个功能涉及的是哪些表。
前端代码也按模块拆分,每个模块一个目录,路由和菜单配置单独维护。新增一个业务模块,只需要在“模块注册中心”登记一下,前端会自动生成菜单入口,后端挂载对应API路由,就能让租户看到新功能。
4.3 一个完整的业务流:设备报警怎么驱动维修工单
光说模块化有点抽象,我讲一个具体场景:设备报警如何自动驱动维修工单的生成,感受一下各个模块怎么协作。
第一步,设备接入层的网关程序采集到设备数据,发现主轴温度超过85度,触发了预设的报警规则。报警信息通过MQTT推送到平台服务层,报警中心模块存储报警记录,同时通过实时通道推送到前端页面。
第二步,报警中心的规则引擎判断这个设备属于哪个租户,然后查找该设备关联的维修策略,发现该设备启用了“报警自动创建维修工单”的配置。规则引擎调用生产工单模块的API,自动生成一张待处理的维修工单,工单里自动带上了报警详情:设备编号、报警时间、报警参数、位置信息。
第三步,设备管理模块更新该设备的状态为“异常”,同时通过消息通知机制,把维修任务指派给负责该设备的维修工。维修工的手机或电脑上,实时看板弹出提醒。
第四步,维修工到现场处理完后,在系统里填写维修记录和处理结果,工单状态变为“已完成”。报警中心根据处理结果自动关闭报警,设备状态恢复为“正常”。
这个流程涉及了报警中心、生产工单、设备管理三个模块的联动,但各个模块代码之间完全没有直接引用,全靠事件驱动和API调用。这就是模块化设计的价值——业务流程可以跨模块编排,但代码边界始终清晰。
5. Vue2.6前端架构设计:动态路由、权限控制与工业可视化
5.1 动态路由和菜单:不同租户看到不同功能
前端最核心的机制就是动态路由。传统后台管理系统的路由是写死的,所有用户进来看到的菜单都一样。但在多租户系统里,不同租户的功能清单可能完全不同,所以菜单和路由必须根据当前登录用户的权限动态生成。
我在Vue2.6里这样实现的:用户登录后,拿到JWT令牌,前端调用“获取用户信息”接口,返回该用户可见的菜单列表和按钮权限标识。然后通过router.addRoutes方法,把这些菜单对应的路由动态注册到Vue Router实例里。同时用Vuex保存菜单列表,侧边栏组件根据菜单列表递归渲染。
// 登录成功后动态添加路由 const routeMap = { '/device': () => import('@/views/device/index.vue'), '/workorder': () => import('@/views/workorder/index.vue'), '/quality': () => import('@/views/quality/index.vue'), '/energy': () => import('@/views/energy/index.vue') }; function generateRoutes(menus) { const routes = []; menus.forEach(menu => { if (routeMap[menu.path]) { routes.push({ path: menu.path, name: menu.name, component: routeMap[menu.path], meta: { title: menu.title, icon: menu.icon } }); } }); return routes; }按钮级权限用自定义指令实现。我们在Vue里注册了一个“v-permission”指令,按钮元素上加上这个指令并传入权限标识,如果当前用户没有这个权限标识,指令就把这个DOM元素移除。用指令的好处是模板里不需要写太多v-if判断,代码干净很多。
Vue.directive('permission', { inserted(el, binding) { const requiredPermission = binding.value; const hasPermission = store.getters.permissions.includes(requiredPermission); if (!hasPermission) { el.parentNode.removeChild(el); } } });5.2 工业大屏和实时数据:告别浏览器卡死的刷新方式
工业系统跟普通管理系统的最大区别,就是有大量的实时数据展示。车间看板大屏要显示设备状态、产量、报警信息,数据几秒钟就要刷新一次。第一个版本我们用了“定时器每3秒请求一次API”的粗暴方案,结果用户数一多,浏览器窗口一多,服务器压力山大,界面还经常闪烁跳动。
后来我们把所有实时数据通道全部改为WebSocket长连接,通过SignalR(.NetCore自带的实时通信库)服务端主动推送数据。前端只负责订阅和渲染,不再反复发起HTTP请求。设备状态变化、报警产生、产量更新,全部通过WebSocket通道实时推送到前端,前端用Vuex统一管理推送过来的数据,界面组件用computed属性做依赖更新。
这里有个Vue2.6的性能优化细节:大屏上的数据点非常多,如果每个数据变化都立刻触发DOM更新,浏览器根本扛不住。我们给高频变化的数据加了一个缓存层,前端收到推送后先更新到Vuex里,然后用requestAnimationFrame或setTimeout做批量更新,把一秒钟内收到的几十条数据合并成一次DOM渲染。实测下来,200个设备同时在线、每秒变化1000个数据点的大屏,浏览器帧率能稳定在50fps以上。
5.3 大屏可视化布局和图表选型
工业大屏的可视化,组件选型和布局同样重要。我们的方案是ECharts做图表 + DataV的边框装饰效果,再加上CSS Grid做宫格布局。ECharts的图表种类多、社区生态好,在工业场景里无论是折线图、柱状图、环形图还是地图,都能找到现成的案例。
大屏布局我们一般遵循“从上到下、从左到右”的信息流逻辑。顶部是标题和时间,左侧放设备综合状态和报警统计,中间放设备拓扑图或生产事件流,右侧放产量趋势和能耗曲线。16:9的页面用Grid栅格切成6列4行,每个图表组件占一定的跨度和高度,这样在大屏和小屏上都能自适应缩放。
关于图表性能,工业大屏要特别注意ECharts的“细粒度更新”而不是“整图重绘”。我们用setOption方法传入新的data,并且把notMerge参数设为true,让ECharts只更新变化的系列,而不是整个图表全部重绘。如果一个页面同时有10个图表,整图重绘会明显卡顿,但细粒度更新几乎无感。
6. .NetCore3.1跨平台部署实战:Docker、Linux和Windows全兼容
6.1 用Docker抹平环境差异
跨平台说起来简单,真正落地的时候坑特别多。最大的坑就是环境依赖不一致:Windows上的IIS部署、CentOS上的守护进程方式、Ubuntu上的systemd配置各不相同,每次部署都要写一套新的运维文档。
我们的解决办法是全面容器化。发布后的.NetCore3.1应用打成一个Docker镜像,前端Vue项目构建后的静态文件用Nginx镜像承载,数据库用的SQL Server容器版或PostgreSQL容器版。不管底层的宿主机是Windows Server、CentOS还是Ubuntu,部署命令都是一样的“docker-compose up -d”,彻底抹平了平台差异。
version: '3.8' services: api: image: cps-api:latest container_name: cps-api ports: - "8080:8080" environment: - ConnectionStrings__Default=Server=db;Database=CPS_Main;User Id=sa;Password=******** - TZ=Asia/Shanghai depends_on: - db restart: always web: image: nginx:alpine container_name: cps-web ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./dist:/usr/share/nginx/html:ro depends_on: - api restart: alwaysDocker不仅解决了跨平台部署问题,还解决了“多租户快速交付”的问题。客户现场要新装一套系统,以前得派实施人员跑现场安装配置,现在直接把镜像拷贝到客户服务器上,一条命令就能起一套环境。
6.2 Linux部署和Windows部署的差异清单
虽然Docker抹平了大部分差异,但有些底层细节还是要注意,尤其是如果你的客户坚持不用Docker,要在物理机上直接跑。我整理了几个容易踩坑的点:
路径分隔符:Windows用反斜杠“\”,Linux用正斜杠“/”。代码里所有文件路径拼接,统一用Path.Combine而不是手动拼字符串,否则Linux下必炸。
文件权限:Linux对文件权限很敏感,应用需要的写目录(比如日志目录、临时文件目录)要在部署时创建好并设置好权限,否则运行时报“Access denied”很难排查。
数据库连接:如果数据库在另一台服务器上,Windows机器连SQL Server可能默认走Windows身份认证,而Linux下必须用SQL Server身份认证或配置文件中的用户名密码,这个连接字符串配置要提前处理好。
时区问题:工业场景里时间戳极其重要,设备上报的数据里时间错一个小时,报警和统计就全错了。.NetCore跨平台部署默认时区是UTC,我们在所有部署环境里都通过环境变量指定了Asia/Shanghai时区,并且数据库连接字符串里也加了“TrustServerCertificate=True”这样避免证书校验问题。
6.3 自包含部署:没有运行时的服务器也能跑
还有一个工业现场常见的情况:服务器不能连外网,不能安装.NetCore运行时。这种情况用自包含部署模式(self-contained deployment)发布,应用会带一份完整的运行时,解压即可运行,不依赖系统环境。
发布命令很简单:
dotnet publish -c Release -r linux-x64 --self-contained true发布完成后,一个目录里包含了可执行文件和所有依赖,把它拷到Linux服务器上直接执行。
需要注意,自包含部署的包体积会比框架依赖部署大很多,大约80MB到150MB,而且要为每个目标平台单独发布一个版本。比如客户用Linux x64,你就发布linux-x64版本;客户用Windows Server 2019,你就发布win-x64版本。所以我们会提前问清楚客户现场的操作系统版本,再决定发布哪个目标。
7. 常见问题与排查技巧:多租户和跨平台场景的实战避坑
7.1 多租户数据串号的排查方法
多租户系统最怕的就是A租户看到B租户的数据。我们上线初期就出过一次事故:客户A的领导在大屏上看到了客户B的产线数据,场面极其尴尬。后来我们总结了一套排查方案,现在分享出来。
第一,确认租户上下文是否传递正确。在中间件里打日志,输出每次请求的租户ID,对比实际访问的租户数据,看有没有不对齐的。
第二,确认数据库连接字符串是否正确。我们遇到过一个问题:多租户DbContext在创建时从TenantContext读取租户ID,但服务注册时误把TenantContext注册成了Singleton生命周期,导致所有请求复用一个TenantContext实例,租户ID被后登录的用户覆盖,前面的请求全部串号。这个问题的排查方法是在日志里打印TenantContext的实例ID,如果多个请求的实例ID一样,就是生命周期配置错误。
第三,检查有没有绕过租户中间件的API。比如某些内部API或者后台任务,不走HTTP请求管道,就没有租户上下文。这种API里如果直接查询数据库,就会查出所有租户的数据。解决方案是这类API强制要求显式传入租户ID参数,不允许缺省。
7.2 缓存穿透导致的多租户数据不一致
多租户系统里缓存用不好,也会出问题。我们早期把设备信息缓存到Redis,但是缓存Key只用了设备ID,没用租户ID。结果A租户改了设备名称,B租户看到的设备名称也跟着变了。
排查这种问题的技巧是,检查所有缓存Key的生成规则,是否包含租户ID。正确的做法是缓存Key用“租户ID:业务对象类型:业务ID”的格式,比如“tenant_123:device:456”。同时,任何涉及租户隔离的数据,一律不允许做全局缓存,这是多租户系统的铁律。
7.3 .NetCore3.1跨平台部署的常见坑
坑一:Linux下读取文件路径异常。客户现场用Windows部署一切正常,换到Linux就报“找不到配置文件”。原因是代码里用了硬编码的反斜杠路径。排查技巧是在代码里全局搜索“\”,把所有硬编码路径全部替换为Path.Combine。
坑二:中文乱码。Linux下默认编码是UTF-8,Windows下默认可能是GBK。如果对接的老设备系统返回的是GBK编码的数据,在Linux下解析就会乱码。解决方案是在代码里显式指定编码:Encoding.GetEncoding("GBK"),并且确保Linux系统安装了对应的编码支持。
坑三:端口被占用。Linux上默认的HTTP端口是80和443,都是特权端口,普通用户不能直接监听。如果直接用非root用户运行应用并监听80端口,启动就会失败。一般我们会让应用监听8080端口,然后通过Nginx反向代理到80端口,顺带解决了HTTPS证书配置的问题。
7.4 Vue2.6性能问题的定位手段
Vue2.6在数千个DOM节点的大型页面上,性能问题主要集中在两次:一次是首次渲染,一次是数据频繁更新。
定位首次渲染慢的问题,用Vue Devtools的Performance标签,能看到每个组件的渲染时间分布。通常罪魁祸首是某些组件在mounted钩子里同步请求了过多数据,阻塞了渲染进程。优化手段包括:把不重要的组件改为异步组件(通过import()动态加载)、首屏只渲染核心区域、数据请求改为并行发出而不是串行。
定位频繁更新卡的问题,看Data里的数据有没有“过度响应化”。Vue2.6的响应式系统会对data里每个属性做getter/setter代理,如果一个对象有几万个属性,初始化响应式的过程中就会卡顿。我们遇到过一个场景:设备明细数据一次性从后端取了10万条,前端用v-for渲染,浏览器直接崩溃。后来改成服务端分页 + 前端虚拟滚动组件,才解决了问题。
8. 最后分享两个我在实际项目里觉得最值钱的经验
这套系统从0到1、从1到N折腾了快两年,交付给十几个不同类型的工业客户后,我有两个体会特别深。
第一个体会是:多租户架构一定要在项目一开始就定好,后期再改成本极其昂贵。我们最早给第一个客户定制开发时,压根没考虑多租户,代码里全是单租户假设。后来第二个客户进来,光是改造数据隔离就重构了整整一个半月,还差点耽误客户上线。如果你预料到未来可能有多个工厂、多个客户共用系统,第一版就要按多租户架构设计,这个不能省。
第二个体会是:工业现场的技术栈选择,稳定比新潮重要一百倍。我见过太多团队为了炫技,选了最新的框架、最前沿的架构,结果客户现场的硬件环境和团队技术水平根本跟不上。Vue2.6 + .NetCore3.1这个组合在工业互联网圈子里属于“老成持重”的搭配,性能足够、生态成熟、招人容易、踩坑文档多。技术选型不是选最好的,而是选最不容易出错的,这句话在工业软件领域尤其成立。
如果你正在规划类似的工业互联网平台,希望这篇文章能帮你少走点弯路。有具体的架构问题或者多租户实现细节想聊的,欢迎在评论区交流,我看到都会回复。