by tcdos 2026-08-03

复盘与展望 - 做了哪些取舍,接下来往哪走

本文结合实际项目设计进行拆解,建议先看看设计成果《共享充电宝云平台》,再阅读本文。


一、引言

这是系列的最后一篇。前面七篇把整个充电宝平台的核心模块和技术细节都过了一遍:从架构选型到 TCP 通信,从租借归还到微信支付,从权限体系到设备管理,从 AOP 缓存到 License 授权。

这一篇跳出代码,聊聊过程中踩过的坑、面临的取舍、以及未来的方向。希望能给正在做类似项目的朋友一点参考。


二、有哪些坑踩过

坑 1:Socket 粘包/半包处理

TCP 是流式协议,不像 HTTP 那样天然有「消息边界」。如果设备连续发送多条指令,服务器可能一次读取到多条粘在一起(粘包),或者一条指令被拆成两次读(半包)。

我们的协议用 #**# 作为消息边界,但在高并发场景下,ReceiveAsync 的回调中可能收到不完整的数据。

解决方式: 在接收缓冲区中累积数据,每次收到数据后先拼接到 StringBuilder,然后按 #*...*# 的完整格式提取报文,剩余部分留在缓冲区等待下次接收。

var sb = new StringBuilder();
var buffer = new byte[1024 * 5];
var header = "#*";
var footer = "*#";

while (!ct.IsCancellationRequested)
{
    int bytesRead = await clientSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), SocketFlags.None);
    if (bytesRead == 0) break;

    sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
    string content = sb.ToString();

    int headerIndex = content.IndexOf(header);
    int footerIndex = content.IndexOf(footer, headerIndex >= 0 ? headerIndex + 2 : 0);

    if (headerIndex >= 0 && footerIndex > headerIndex)
    {
        string message = content.Substring(
            headerIndex + header.Length, footerIndex - headerIndex - header.Length);

        // 移除已处理的部分(包括当前报文之前可能残留的字节)
        sb.Remove(0, footerIndex + footer.Length);

        // 消息解析 + 业务处理
        var parseResult = await ParseMessage(clientSocket, message);
        await ProcessMessage(clientSocket, parseResult.Item2, message);
    }
    // 未收到完整报文时,数据留在 sb 中,等下次接收继续拼接
}

这个处理逻辑看着简单,但前提是协议定义足够清晰——包头包尾不能出现在数据中。我们在协议设计时就规定了 #**# 为保留字符,确保不会出现误匹配。

坑 2:微信支付分回调幂等性

支付分回调通知后端时,如果网络抖动,微信可能会重发同一个回调多次。如果不做幂等处理,就可能重复扣款或重复完结。

解决方式:WechatPayController 的回调处理中,先查 transaction_id 是否已经处理过:

switch (event_type)
{
    case "TRANSACTION.SUCCESS":
        if (order.PayResult != 1) // 检查是否已支付
        {
            order.PayResult = 1;
            order.PayTime = DateTime.Now;
        }
        break;
    // ...
}

所有回调处理都遵循「先查状态,再更新」的原则。同一个回调通知被多次分发,只有第一次会实际更新数据库,后续的都不会产生副作用。

坑 3:设备离线状态与订单状态的一致性

用户归还后,如果设备恰好在此时断网,return 指令没有发到服务器。服务器以为订单还在进行中,用户以为已经还好了。结果就是计费一直继续,用户产生投诉。

解决方式: 两方面处理:

  1. 设备端重连后补报——机柜重新连上服务器后,会发送 detailup 指令上报所有槽位的当前状态,服务器据此更新已归还的订单
  2. 运营后台手动处理——运营人员可以在后台强制终止订单、调整金额、发起退款

这个场景告诉我们,物联网应用永远要假设设备随时可能掉线,所有的业务流程都必须考虑「设备离线时的降级方案」。


三、做了哪些取舍

取舍 1:自研 TCP 通信 vs 用 IoT 云平台

当时如果选阿里云 IoT 或者 EMQ X,可以省去 Socket 编程、连接管理、消息路由这些工作。但最终选择了自研:

  • 选自研的理由: 设备量不大(几百台),协议定制程度高,不想多维护一个中间件
  • 付出的代价: 粘包/半包、心跳检测、连接池管理这些细节都要自己处理

对于百万级设备的场景,自研肯定不划算。但对于我们的体量,自研的灵活性和可控性更有价值。

取舍 2:单体应用 vs 微服务

很多人看到「设备通信 + Web API + 定时任务 + SignalR 全部在同一个进程」可能会皱眉。为什么不拆成微服务?

  • 不拆的理由: 项目初期人数有限,单体应用开发效率最高。设备上报数据后直接写数据库、直接更新订单,不需要经过网关转发
  • 单体化带来的好处: 部署简单(一个站点搞定)、调试方便(VS 开一个项目就行)、事务一致性好
  • 未来怎么拆: 如果设备量增长到几千台,SocketServer 可能会拆分出去作为一个独立服务

取舍 3:内存缓存 vs Redis

目前用的是 .NET 内置的 MemoryCache,没有上 Redis。

  • 选 MemoryCache 的理由: 部署简单,不需要额外维护 Redis 服务,对于单实例部署完全够用
  • 瓶颈在哪: 如果未来需要多实例水平扩展,MemoryCache 的数据不共享,每个实例各缓各的,缓存一致性需要改造

迁移到 Redis 的成本不高——只需要替换 ICacheService 接口的实现。接口已经抽象好了,具体实现是 MemoryCache 还是 Redis,只改一行注入代码。


四、时间防回退机制

这是 License 模块里最值得单独拿出来讲的一个设计。

常规的 License 只校验过期时间,但有个漏洞:如果把系统时间调回过期之前,再重启服务,验证就绕过了。

我们的做法是记录每次校验的 UTC 时间到文件,每次校验时拿这次的时间跟上次比:

private DateTime? GetLastCheckUtc()
{
    // 从文件读取上次校验时间
    var text = LicenseUtils.SafeReadFile(lastCheckFilePath);
    return DateTime.Parse(text, ...);
}

private void SaveLastCheckUtc(DateTime utcNow)
{
    LicenseUtils.SafeWriteFile(lastCheckFilePath, utcNow.ToString("o"));
}
// 验证系统时间
var lastCheck = GetLastCheckUtc();
if (lastCheck.HasValue && DateTime.UtcNow < lastCheck.Value)
{
    // 时间被回退了!
    return false;
}
SaveLastCheckUtc(DateTime.UtcNow);

再加上每 60 秒的 OfflineLicenseCheckerService 持续校验,即使启动时绕过了,运行中也会被检测到。


五、当前成果

到目前为止,平台已跑通全流程:

  • 设备接入: 支持 8 槽 / 16 槽机柜,TCP 长连接 + 4G 模块通信
  • 订单系统: 从创建到支付 7 个状态完整流转,日封顶 / 总封顶规则灵活配置
  • 支付体系: 微信支付分免押金 + JSAPI 支付 + 退款,全链路跑通
  • C 端小程序: 扫码租借、地图找店、订单管理、收益明细
  • 运营后台: 设备管理、店铺管理、角色权限、定时任务、审计日志
  • 商户后台: 订单查看、收益提现、设备监控
  • 安全体系: JWT 认证、API 授权、License 硬件绑定
  • 运维能力: 远程操控、心跳监控、全量指令日志

六、接下来往哪走

短期内关注这几个方向:

1. 数据驱动运营

目前报表功能比较基础,接下来想做两个事:

  • 设备热力图:按地理位置展示设备使用率、周转率
  • 智能调度建议:根据历史数据预测热门点位,辅助运维补宝

2. 智能告警

目前设备离线、指令执行失败等异常没有主动通知机制。接下来考虑接入告警规则引擎:

设备离线超过 30 分钟 → 通知区域运维微信
订单超时未弹宝 → 自动重试 + 通知客服
某设备连续 3 次心跳 csq < 10 → 标记为信号异常

告警渠道可以走微信模板消息 + 站内信。SignalR 已经搭好了,实时推送的底座有了。

3. 多租户能力

部分场景下,不同品牌方需要完全隔离的数据和运营后台。当前的 RBAC 可以一定程度支持,但要实现真正的多租户隔离,需要在数据层增加 TenantId 维度。

4. 大数据看板

运营需要一个实时数据大屏:今日订单数、在线设备数、累计收益、充值转化率等。可以用 SignalR 推送 + ECharts 实现,数据实时刷新。


七、写在最后

八篇文章,从架构设计到业务实现,从通信协议到安全防护,基本把充电宝平台的完整面貌讲清楚了。

这个项目最让我觉得值得分享的地方,不是用了多新潮的技术,而是一个朴素的事实:用成熟的工具,做务实的设计,把每一个细节做到位,就能搭起一个能跑、能赚钱的生产级平台。

没有用微服务、没有用 Redis、没有用 Docker——这个项目只在必要的地方用了必要的技术。每个技术决策都有明确的背景和理由,不是盲目追新,也不是固步自封。

如果你正在做或打算做一个 IoT 相关的项目,希望这个系列能给你一些参考。

有技术问题或者想法,欢迎交流。

(全文完)

天草工坊公众号

欢迎关注 天草工坊 公众号

好的设计被看见,好的开发被落地。

围观:9| 收获点赞:0

评论 0

暂无评论,来说点什么吧

?
内容目录
快捷操作
点赞 评论 返回顶部