一、引子
「帮我做个充电宝平台。」
朋友找到我的时候,市面上共享充电宝已经铺天盖地了。但他的痛点很具体:想在一个区域市场里自己做运营,不想被大平台的抽成卡脖子。
真正做起来才发现,这玩意儿看着简单,拆开全是细节。设备怎么跟服务器保持长连接?扫码租借那一瞬间到底发生了什么?订单计费怎么处理超时和封顶?微信支付分免押金的流程怎么打通?
网上能查到的资料,大多是商业模式分析,关于设备与服务端的 TCP 长连接通信、订单实时计费逻辑、支付分免押金对接、软硬件全链路调度这些核心技术,几乎没有。于是从零开始,后端 API、运营后台、商户端、微信小程序、设备 TCP 通信,全套自己搞了一遍,全部独立开发,全覆盖自研。
今天这篇,是系列复盘的第一篇,先聊聊技术选型和整体架构设计。
二、一对多架构:一个后端,三个前端
整个平台的服务对象有三类人:
| 角色 | 使用端 | 覆盖功能 |
|---|---|---|
| 平台运营 | OPS 后台(Vue 3) | 设备管理、商户管理、订单监控、系统配置 |
| 合作商户 | Web 后台(Vue 3) | 店铺管理、订单查看、收益提现 |
| C 端用户 | 微信小程序 | 扫码租借、地图找店、订单支付 |
为什么三端共用一个后端?
简单来说,业务逻辑是同一套——设备管理、订单流转、支付回调,只是不同角色看到的页面和数据范围不一样。拆分多个后端服务反而会增加通信成本和数据一致性的维护难度。
所以后端只做了一套,通过 RBAC 权限体系 来控制数据和功能的可见范围。两套 Vue 3 前端打包后部署在不同的域名下,各自独立构建、独立发布。
技术栈选型:
- 后端: .NET 6(后升级到 .NET 8 兼容)+ EF Core 6 + SQL Server + Autofac + SignalR
- 前端(管理后台): Vue 3 + Element Plus + Pinia + Axios
- 前端(小程序): 微信原生开发 + TDesign MiniProgram
- 设备通信: 自研 TCP Socket 长连接,自定义协议
这几个选型不是拍脑袋定的。本人对 .NET 技术栈最熟悉,生态成熟,性能也不差。Vue 3 + Element Plus 是当前中后台开发的主流方案,组件丰富、文档齐全。微信小程序端用原生开发,是因为对接微信支付、手机号授权等原生能力最直接,绕开框架的中间层反而省心。
三、后端架构:四层分离,关注点隔离
项目结构不是那种教科书式的整洁,但四层职责分得很清楚:
COM.HS.Power.Server/
├── COM.HS.HttpApi/ # 表现层 - Controller、Middleware、SignalR Hub
├── COM.HS.Application/ # 业务逻辑层 - Service、Socket、微信支付
├── COM.HS.Domain/ # 领域层 - Entity 定义(数据库表映射)
├── COM.HS.EntityFrameworkCore/ # 数据访问层 - DbContext、Repository、Migration
├── COM.HS.Application.Contracts/ # DTO + 接口定义
├── COM.HS.Shared/ # 共享层 - 常量、缓存、帮助类、枚举
├── COM.HS.Utils/ # 工具层 - Json、加密、表达式树扩展
├── COM.HS.IRepository/ # 仓储接口层
└── COM.HS.License/ # 授权校验几个关键设计点:
1. 无 Repository 模式
很多人可能注意到了,虽然有 IBaseRepository 接口,但实际业务 Service 是直接注入 IBaseRepository<TEntity> ,而不是为每个实体创建一个 Repository 类。原因是业务逻辑主要在 Service 层,Repository 只是做最基本的查询封装,不需要为每个表写一个。
public class OrderService : BaseService<ZnOrderEntity>, IOrderService
{
private readonly IBaseRepository<ZnOrderEntity> currentRepository;
private readonly IBaseRepository<ZnDeviceEntity> deviceRepository;
private readonly IBaseRepository<ZnBatteryEntity> batteryRepository;
// ...
public OrderService(
IUnitOfWork unitOfWork,
IBaseRepository<ZnOrderEntity> currentRepository,
IBaseRepository<ZnDeviceEntity> deviceRepository,
// ...
) : base(unitOfWork, currentRepository)
{
this.currentRepository = currentRepository;
this.deviceRepository = deviceRepository;
// ...
}
}这是一种务实的选择——泛型仓储足够用了,就不多做一层抽象。加上 IUnitOfWork 保证事务一致性。
2. AOP 拦截器替代业务代码中的横切关注点
用 Castle DynamicProxy + Autofac 实现了三个 AOP 拦截器:
- CacheAop:方法级缓存,通过 [Caching] 注解自动缓存和过期
- AuditAop:增删改操作自动清除相关缓存
- LoggerAop :操作日志记录
比如缓存拦截器的核心逻辑只有几十行:
public override void Intercept(IInvocation invocation)
{
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var cacheAttr = method.GetCustomAttributes(true)
.FirstOrDefault(x => x.GetType() == typeof(CachingAttribute));
if (cacheAttr is CachingAttribute cachingAttribute)
{
var cacheKey = CustomCacheKey(invocation);
var cacheValue = cacheService.Get(cacheKey);
if (cacheValue != null)
{
invocation.ReturnValue = cacheValue;
return;
}
invocation.Proceed();
cacheService.Add(cacheKey, invocation.ReturnValue,
TimeSpan.FromMinutes(cachingAttribute.AbsoluteExpiration),
TimeSpan.FromMinutes(cachingAttribute.AbsoluteExpiration));
}
else
{
invocation.Proceed();
}
}3. Program.cs 是整站的总装车间
Program.cs 是最有看点的文件——它把依赖注入、中间件管道、第三方服务全部组织在一起。
服务注册阶段:
DbContext → Autofac → AutoMapper → JWT → SignalR → Quartz → SocketServer
中间件阶段:
静态文件 → 路由 → CORS → License校验 → 异常处理 → 认证 → 授权 → Endpoints
启动阶段:
种子数据 → Quartz调度 → SocketServer.Start() → Swagger核心启动代码像这样:
/* 授权校验 */
app.UseLicenseMiddleware();
/* 异常处理 */
app.UseExceptionMiddleware();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MessageHub>("/messageHub");
endpoints.MapControllers();
});
/* SocketServer */
var socketServer = app.Services.GetService<SocketServer>();
socketServer?.Start();看到这里你可能会问: SocketServer 和 Web API 为什么跑在同一个进程里?
是的,我故意这么做的。TCP 通信服务和 HTTP API 融合在一个进程中,设备上报的数据可以直接操作数据库,不需要再走一层网关转发。对于几百台设备的规模,这种做法足够经济,运维也简单。
四、自研 TCP 通信:不妥协的选择
这是整个项目里最"硬"的部分。
充电宝机柜通过 4G 模块连上服务器,维持一条 TCP 长连接。机柜主板上跑的是 C 固件,服务器端用 C# Socket 异步编程来对接。
为什么不用 MQTT 或 CoAP?
当时考虑过主流的 IoT 协议方案。MQTT 生态成熟、有 Broker 做消息转发,看起来省事。但问题在于:
- 1. 机柜固件端对自定义 TCP 协议的支持最成熟,改协议栈意味着改固件,厂家不支持
- 2. MQTT Broker 是一个额外节点,多一层转发就多一层延迟和故障点
- 3. 协议自定义程度高——心跳、租借、归还、强制弹出、FOTA 升级这些指令,MQTT 的 pub/sub 模型反而绕弯子
所以写了一个 SocketServer 类,单例注入,随应用启动。
public class SocketServer
{
public ConcurrentDictionary<Socket, SocketClient> OnlineSocketClients
= new ConcurrentDictionary<Socket, SocketClient>();
public async Task<ResultModel> Start()
{
serverSocket.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
serverSocket.Listen(maxListen);
ThreadPool.QueueUserWorkItem(async state => await AcceptAsync());
await Task.Run(async () => await CheckHeartbeats(cts.Token));
return new ResultModel(true);
}
}设备上报的报文以 #* 开头、 *# 结尾,统一格式:
{
"msg": 1718691245,
"aims": 0,
"sn": "DS24060001",
"cmd": "heart",
"data": {
"int": 45,
"csq": 18
}
}目前支持 13 种指令,从登录到心跳,从租借到 FOTA 升级,全部基于这个统一的报文结构。
五、项目结构一览
整个仓库的代码模块用一句话概括就是: 一个后端处理所有逻辑,两套 Vue 前端覆盖管理和商户场景,一个小程序连接 C 端用户。
后端项目概览:
COM.HS.HttpApi/ → 50+ Controller,6 个 Swagger 分组
COM.HS.Application/ → 业务 + 系统 + CMS + 微信 + Socket + Quartz 任务
COM.HS.Domain/ → 40+ 实体类
COM.HS.EntityFrameworkCore/ → 数据库迁移
COM.HS.Shared/ → 常量定义、缓存服务、GeoHelper、二维码生成
COM.HS.License/ → AES 加密 License + 硬件绑定前端页面覆盖:
- OPS 运营后台 :CMS管理、数据字典、组织架构、角色权限、定时任务、审计日志
- Web 商户后台 :设备管理、电池管理、订单管理、店铺管理、收益提现、还款管理、心跳监控、IoT指令箱
- 微信小程序 :地图找店、扫码租借、归还、订单列表、收益明细、设备信息
六、小结
这篇是系列的开篇,主要把整体的技术选型和架构脉络交代清楚。下一篇会深入讲讲 自研 TCP 通信协议的设计 ——设备怎么登录、心跳怎么保活、租借和归还的指令是怎么在服务器和机柜之间流转的。
如果你也在做 IoT 相关的项目,或者正在犹豫要不要自研通信协议,欢迎关注这个系列。
有疑问或者想讨论的点,留言区见。
欢迎关注 天草设计 公众号,领取 源代码 福利礼包!
