在工业4.0时代背景下,设备管理系统(EMS)已成为制造业数字化转型的核心枢纽。我们基于Vue2.6和.NetCore3.1构建的这套系统,采用前后端分离架构,实现了跨平台、多租户、多组织的复杂业务场景支持。前端采用Vue CLI 4.x脚手架搭建,配合Vuex进行状态管理,Element UI作为基础组件库;后端基于.NetCore3.1的WebAPI构建,使用Entity Framework Core 3.1作为ORM框架,整体架构如下图所示:
code复制[移动端/PDA] ←→ [Nginx反向代理] ←→ [WebAPI集群] ←→ [RabbitMQ] ←→ [SQL Server/MongoDB]
↑
[Vue前端]
这套架构在长三角某汽车零部件集团的实际部署中,成功支撑了12家工厂、3000+设备的实时管理。特别值得注意的是多租户实现方案,我们采用共享数据库、独立Schema的模式,在保证数据隔离性的同时,将硬件成本降低了60%。
车间设备标签需要动态调整字段和排版是常见需求。我们通过JSON Schema定义模板结构,配合Vue的动态组件加载机制,实现了可视化模板设计器。关键技术点包括:
json复制{
"fields": [
{
"name": "deviceNo",
"label": "设备编号",
"componentType": "el-input",
"config": {
"fontSize": 16,
"bold": true,
"position": {"x": 10, "y": 20}
}
},
{
"name": "qrCode",
"componentType": "barcode",
"config": {
"format": "CODE128",
"width": 2,
"height": 40
}
}
]
}
javascript复制// 动态组件加载器
const componentMap = {
'el-input': () => import('element-ui/lib/input'),
'barcode': () => import('@/components/BarcodeGenerator')
}
export default {
components: {
DynamicField: {
functional: true,
render: (h, { props }) => {
const component = componentMap[props.field.componentType]
return h(component, {
props: {
value: props.value,
config: props.field.config
}
})
}
}
}
}
实际踩坑经验:早期版本直接使用Vue的异步组件会导致标签编辑器卡顿,后来改为Webpack的预编译(prefetch)策略,将动态组件打包到独立chunk,加载性能提升70%。
在仓储层实现自动化的租户过滤是关键设计。我们通过EF Core的全局查询过滤器(Global Query Filter)和自定义仓储模式实现:
csharp复制// 租户实体接口
public interface ITenantEntity {
string TenantId { get; set; }
}
// DbContext配置
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Device>()
.HasQueryFilter(d => d.TenantId == _tenantProvider.CurrentTenantId);
}
// 仓储基类
public class TenantRepository<T> where T : class, ITenantEntity {
private readonly string _tenantId;
public IQueryable<T> Query => _context.Set<T>()
.IgnoreQueryFilters() // 需要跨租户查询时使用
.Where(e => e.TenantId == _tenantId);
}
在浙江某纺织企业集群部署时,这套方案成功实现了:
针对车间网络不稳定的痛点,我们设计了三级数据持久化策略:
同步恢复流程如下:
mermaid复制graph TD
A[离线操作] --> B[写入SQLite]
C[网络恢复] --> D[检查未同步记录]
D --> E{记录状态}
E -->|未同步| F[尝试提交主库]
F -->|成功| G[标记为已同步]
F -->|失败| H[加入重试队列]
对应的.NET核心代码实现:
csharp复制public class OfflineSyncService {
public async Task SyncPendingOperations() {
var pendingRecords = _localDb.GetPendingSyncRecords();
foreach (var record in pendingRecords) {
try {
var entity = JsonConvert.DeserializeObject(record.JsonData, record.EntityType);
_mainDb.Add(entity);
await _mainDb.SaveChangesAsync();
record.Status = SyncStatus.Completed;
_localDb.Update(record);
}
catch (Exception ex) {
record.RetryCount++;
record.LastError = ex.Message;
if (record.RetryCount > 3) {
record.Status = SyncStatus.Failed;
}
}
}
}
}
当多个终端离线修改同一条数据时,我们采用"最后写入优先"策略,但会保留冲突记录供管理员仲裁。具体实现:
csharp复制public class ConflictResolver {
public MergeResult<T> Merge<T>(T local, T remote) where T : class, IVersionedEntity {
var localVersion = local.Version;
var remoteVersion = remote.Version;
if (localVersion > remoteVersion) {
return new MergeResult<T> {
Final = local,
Resolution = ResolutionStrategy.LocalWins
};
}
else {
var conflict = new DataConflict {
EntityType = typeof(T).Name,
EntityId = local.Id,
LocalData = JsonConvert.SerializeObject(local),
RemoteData = JsonConvert.SerializeObject(remote)
};
_conflictDb.Add(conflict);
return new MergeResult<T> {
Final = remote,
Resolution = ResolutionStrategy.RemoteWins,
HasConflict = true
};
}
}
}
为了让非技术人员也能配置复杂的保养规则,我们设计了一套类SQL的DSL:
code复制// 示例规则
WHEN (RuntimeHours > 500 OR LastMaintenanceDate < NOW() - INTERVAL 30 DAY)
AND Status = 'Running'
THEN TriggerMaintenance('更换滤芯')
核心解析逻辑采用Roslyn动态编译:
csharp复制public class RuleEngine {
private readonly CSharpScriptEngine _engine;
public RuleEngine() {
_engine = new CSharpScriptEngine();
}
public Func<Device, bool> CompileRule(string ruleText) {
var script = $@"
using System;
public static bool Evaluate(Device d) {{
return {ConvertToCSharp(ruleText)};
}}
";
return _engine.CreateDelegate<Func<Device, bool>>(script);
}
private string ConvertToCSharp(string ruleText) {
// 将自定义语法转换为C#表达式
return ruleText.Replace("NOW()", "DateTime.Now")
.Replace("INTERVAL", "TimeSpan.FromDays");
}
}
实际应用中发现的问题及解决方案:
针对车间环境特点,我们做了多项适配:
javascript复制// Vue自定义指令
Vue.directive('hotkey', {
bind(el, binding) {
const handler = (e) => {
if (e.key === 'F2' && e.altKey) {
binding.value.openCamera();
}
};
el._keyHandler = handler;
el.addEventListener('keydown', handler);
},
unbind(el) {
el.removeEventListener('keydown', el._keyHandler);
}
});
css复制/* 强光下可读的界面样式 */
.industrial-mode {
filter: contrast(150%) brightness(120%);
color: #000 !important;
background: #fff !important;
}
.industrial-button {
min-width: 80px;
min-height: 40px;
border: 2px solid #000 !important;
}
与工业硬件的集成往往需要特殊处理:
csharp复制public class PlcService {
private readonly SerialPort _port;
public PlcService(string comPort) {
_port = new SerialPort(comPort, 9600, Parity.Even, 8, StopBits.One);
_port.ReadTimeout = 500;
}
public string ReadData() {
var retry = 0;
while (retry++ < 3) {
try {
_port.Open();
var data = _port.ReadLine();
return ValidateChecksum(data);
}
catch (TimeoutException) {
Thread.Sleep(100);
}
finally {
if (_port.IsOpen) _port.Close();
}
}
throw new PlcCommException("读取PLC数据失败");
}
}
javascript复制async function printLabel(template, data, retry = 0) {
try {
const pdf = generatePDF(template, data);
const printer = await connectToPrinter();
await printer.print(pdf);
} catch (err) {
if (retry < 2) {
await delay(500);
return printLabel(template, data, retry + 1);
}
throw new Error(`打印失败: ${err.message}`);
}
}
针对低配PDA设备的优化措施:
vue复制<template>
<virtual-list :size="50" :remain="8">
<div v-for="item in items" :key="item.id" class="item">
{{ item.name }}
</div>
</virtual-list>
</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
export default {
components: { VirtualList },
data() {
return {
items: [] // 上万条设备数据
}
}
}
</script>
javascript复制// worker.js
self.onmessage = function(e) {
const result = heavyCalculation(e.data);
self.postMessage(result);
};
// 主线程
const worker = new Worker('./worker.js');
worker.postMessage(inputData);
worker.onmessage = (e) => {
this.result = e.data;
};
csharp复制// 错误示例 - 产生N+1查询
var devices = _context.Devices.ToList();
foreach (var d in devices) {
var maintenance = _context.MaintenanceRecords
.FirstOrDefault(m => m.DeviceId == d.Id);
}
// 正确做法 - 预先加载
var devices = _context.Devices
.Include(d => d.MaintenanceRecords)
.AsNoTracking()
.ToList();
csharp复制services.AddStackExchangeRedisCache(options => {
options.Configuration = Configuration.GetConnectionString("Redis");
});
public class CachedDeviceRepository {
private readonly IDistributedCache _cache;
public async Task<Device> GetByIdAsync(int id) {
var cacheKey = $"device_{id}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null) {
return JsonConvert.DeserializeObject<Device>(cached);
}
var device = await _context.Devices.FindAsync(id);
await _cache.SetStringAsync(cacheKey,
JsonConvert.SerializeObject(device),
new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return device;
}
}
采用Docker Compose的生产环境部署配置:
yaml复制version: '3.8'
services:
webapi:
image: ${DOCKER_REGISTRY}/ems-api:${TAG:-latest}
environment:
- ConnectionStrings__MainDb=Server=sqlserver;Database=ems;User=sa;Password=${SA_PASSWORD}
- ASPNETCORE_ENVIRONMENT=Production
deploy:
resources:
limits:
cpus: '2'
memory: 2G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
sqlserver:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=${SA_PASSWORD}
volumes:
- sql_data:/var/opt/mssql
redis:
image: redis:6-alpine
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis_data:/data
volumes:
sql_data:
redis_data:
关键运维经验:
为保障工业环境系统稳定性,我们实施分阶段发布:
对应的CI/CD流水线配置:
groovy复制pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'dotnet publish -c Release -o ./out'
}
}
stage('Deploy Canary') {
when {
branch 'release/*'
}
steps {
sh 'kubectl rollout status deployment/ems-canary'
sh 'kubectl set image deployment/ems-canary webapi=${IMAGE_URL}:${GIT_COMMIT}'
}
}
stage('Run Tests') {
steps {
sh 'npm run e2e -- --env=canary'
}
}
stage('Deploy Production') {
when {
branch 'release/*'
expression { return env.CANARY_SUCCESS == 'true' }
}
steps {
sh 'kubectl rollout restart deployment/ems-production'
}
}
}
}
当前系统在以下方面仍需持续改进:
技术选型评估:
在江苏某智能制造示范工厂的试点中,我们正在验证基于TensorFlow.js的设备异常检测模型:
javascript复制// 浏览器端运行的振动分析模型
async function analyzeVibration(sensorData) {
const model = await tf.loadLayersModel('/models/vibration/v1.json');
const input = tf.tensor(sensorData, [1, sensorData.length]);
const prediction = model.predict(input);
const result = await prediction.data();
return result[0] > 0.5 ? 'abnormal' : 'normal';
}
工业软件开发的真谛在于:用最合适的技术解决最实际的问题。在车间粉尘中运行一年的代码,比实验室里的完美架构更有价值。