
简介这是一份基于Java构建的智能电表远程抄表缴费管理平台源码面向物业、房东、写字楼等用电管理场景也适合物联网及Java后端开发者学习二次开发。平台以物联网技术为核心借助GPRS、LoRa、NB-IoT等方式实现远程自动抄表同时集成线上缴费、用户权限管理、异常报警、能耗统计与报表生成等功能可适配正泰、人民、天正、许继等主流品牌电表减少人工抄表负担。资源包共86个文件主体为77个Java源文件另有8个XML配置文件和1个iml工程文件整体仅67KB目录紧凑核心工作组件‘wwby-worker-ammeter’清晰展现了定时采集、任务调度与数据处理流程便于开发者快速定位逻辑并做功能扩展。此外源码预留了API接口可对接物业管理或楼宇自控系统对学习设备接入、数据聚合、支付集成等物联网应用开发技巧有较高参考价值。目前已有2178人学习下载适合需要搭建智能用电管理原型或深入理解Java物联网项目结构的技术人员参考。1. 智能电表远程抄表缴费管理平台这套Java源码到底解决什么问题一栋写字楼的物业经理会发现最头疼的不是抄表而是抄完表之后对不上账。智能电表远程抄表缴费管理平台本质是用一套Java后端把三件原本靠人工的事接起来定时从电表读走读数、按读数算费并完成缴费入账、对异常用电和欠费告警。它能解决人工抄表错漏、电费回收周期长、数据没法追溯这些问题。适合两类人一类是正在找 java 课程设计案例源码的学生答辩时需要一条讲得清、跑得通的技术链路另一类是给园区、公寓做水电计费的小团队需要一个能二次开发的 Java 主站。要强调的是这套源码覆盖的是上位机主站不是电表里的嵌入式固件开局前先把这个边界定住。2. 从电表到平台的数据链路DL/T645报文怎么用Java解析2.1 为什么是DL/T645而不是Modbus选型边界国内智能电表用得最多的协议是 DL/T6451997 版和 2007 版并存新招标表计基本都是 2007 版。很多第一次做这个方向的人会问电表能不能用 Modbus RTU 来读能但要看表计类型。国网、南网招标的居民表和工商业表默认走 645只有工业现场用的第三方电表、或者你自己加的采集模块才更多见 Modbus。做平台前先做一步调研把现场表计的规约、波特率、表号清单拿到手这决定了采集服务的底层实现。这里的关键认知是645 是字节级帧格式跟 Java 语言本身没有关系但 Java 解析时有几个细节特别容易踩坑。帧结构是固定的——起始符 68H、6 字节地址域、1 字节控制码、1 字节数据域长度、数据域、校验和 CS、结束符 16H。读数据的控制码是 0x11正常应答是 0x91异常应答是 0xD1。控制码 bit6 为 1 表示从站异常应答解析时先判断这一位再决定是取数还是打日志。选型上还有一个判断表计是走 RS485 串口还是已经接了集中器。RS485 总线是半双工一主多从主站发一帧、电表回一帧天然一问一答如果表计已经挂到集中器下面集中器会做规约转换主站可能面对的是集中器暴露出来的另一个协议。做平台时不要把「抄表」写死成串口把链路抽象成一层采集适配器后面接串口、接 TCP、接集中器 API 都能换。2.2 最小可用的645解析器读有功总电能的请求与应答先写一个最小可用的工具类能组读请求、能解析应答。这里以读「组合有功总电能」为例数据标识用 00 00 00 00具体值要按表计厂家协议文档确认。完整代码框架如下public class Dl645Utils { /** * 组一个读数据请求帧 * param meterNo 12位表号如 123456789012 * param dataIdent 4字节数据标识默认组合有功总电能 {0x00,0x00,0x00,0x00} */ public static byte[] buildReadRequest(String meterNo, byte[] dataIdent) { byte[] frame new byte[15]; int pos 0; frame[pos] 0x68; // 帧起始符 byte[] bcd toBcd(meterNo); // 表号转BCD for (int i bcd.length - 1; i 0; i--) { frame[pos] bcd[i]; // 地址域低字节在前 } frame[pos] 0x11; // 控制码读数据 frame[pos] 0x04; // 数据域长度只有数据标识 frame[pos] dataIdent[0]; frame[pos] dataIdent[1]; frame[pos] dataIdent[2]; frame[pos] dataIdent[3]; frame[pos] calcCs(frame, 1, pos); // 校验和 frame[pos] 0x16; // 帧结束符 return frame; } /** * 解析应答帧返回电量值kWh * 帧结构68 地址(6) 控制码 长度 数据标识(4) 数据值 08 16 */ public static double parseReadResponse(byte[] frame) throws IOException { if (frame null || frame.length 17) { throw new IOException(帧长不足); } if (frame[0] ! 0x68 || frame[frame.length - 1] ! 0x16) { throw new IOException(帧头帧尾不匹配); } int csPos frame.length - 2; if (frame[csPos] ! calcCs(frame, 1, csPos)) { throw new IOException(校验和错误); } int control frame[7] 0xFF; if ((control 0x40) ! 0) { throw new IOException(电表返回异常应答控制码 String.format(%02X, control)); } int len frame[8] 0xFF; // 数据域长度 int dataStart 9; // 跳过4字节数据标识从 dataStart4 开始读 BCD 电能值 double value bcdToDouble(frame, dataStart 4, len - 4); return value; } private static byte[] toBcd(String meterNo) { if (meterNo null || meterNo.length() ! 12) { throw new IllegalArgumentException(表号必须是12位数字); } byte[] out new byte[6]; for (int i 0; i 6; i) { int hi meterNo.charAt(i * 2) - 0; int lo meterNo.charAt(i * 2 1) - 0; out[i] (byte) ((hi 4) | lo); } return out; } private static double bcdToDouble(byte[] data, int offset, int len) { long raw 0; for (int i 0; i len; i) { int b data[offset i] 0xFF; raw raw * 100 (b 4) * 10 (b 0x0F); } return raw * 0.01; // 默认两位小数按表计实际配置调整 } private static byte calcCs(byte[] data, int from, int to) { int cs 0; for (int i from; i to; i) { cs data[i] 0xFF; } return (byte) (cs 0xFF); } }逻辑说明buildReadRequest 里最关键的是地址域的反序发送。表号是 12 位十进制数字BCD 编码后是 6 字节但 645 规范要求发送时低字节在前所以循环里从 bcd[5] 往前填。控制码 0x11 表示读数据数据域长度 0x04 表示后面只跟 4 字节数据标识。参数说明里有一个容易错的地方bcdToDouble 里的默认两位小数。不同型号电表对电能值的小数位定义不一样有的是整数有的是三位小数。我一般把小数位数做成配置项接入新表型时先读一帧人工核对一次读数再定参数不要写死。解析应答时先做三道校验起始符、结束符、校验和。三道都过再判断控制码异常位。这里特别提醒校验和范围是「从地址域开始到数据域结束」起始符 68 和结束符 16 都不参与具体厂家差异放到第 5 章单独讲。2.3 串口与网络接入实际上电表怎么连到Java服务常见的物理链路有两种。第一种是 RS485 总线直接拉到机房通过串口服务器转 TCPJava 服务连 TCP 端口第二种是现场已经有集中器主站走集中器的 4G 或者以太网口。不管哪种Java 侧看到的都是一个字节流区别只在连接方式。RS485 直连时串口参数通常是 2400 波特率、8 数据位、1 停止位、无校验但部分厂家表计默认偶校验接入前用表计手册核对。用 jSerialComm 做串口读写的代码如下import com.fazecast.jSerialComm.SerialPort; public class SerialChannel { private SerialPort port; public void open(String comName, int baud, int readTimeoutMs) { port SerialPort.getCommPort(comName); port.setComPortParameters(baud, 8, SerialPort.ONESTOPBIT, SerialPort.NO_PARITY); port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, readTimeoutMs, readTimeoutMs); if (!port.openPort()) { throw new IllegalStateException(串口打开失败: comName); } } public byte[] transceive(byte[] request) { port.writeBytes(request, request.length); byte[] buffer new byte[64]; int n port.readBytes(buffer, buffer.length); if (n 0) { throw new RuntimeException(读取超时或未收到数据); } return Arrays.copyOf(buffer, n); } }逻辑说明串口是流式接口没有消息边界readBytes 返回的数据不保证正好是一整帧。这里先把读取超时设为 2000 毫秒保证表计不响应时线程能及时退出不会把任务池打满。粘包和半包的完整处理放到第 5 章。参数说明readTimeoutMs 建议 1500 到 3000 之间。645 应答在表计侧通常不到 200 毫秒留 10 倍余量是为了覆盖集中器转发时间。超时设太短会把慢表误判为故障设太长会导致整个轮询周期拉长。3. 批量抄表的任务调度轮询、并发与补抄3.1 三个必调参数轮询周期、单表超时、并发数抄表平台的核心不是解析协议而是把成千上万只表在有限时间内抄完。这里有三组参数决定成败轮询周期、单表超时、并发数。轮询周期按业务需求定物业月底出账单日抄一次就够要做用电曲线分析就按 15 分钟一个周期。单表超时按链路类型定串口直连 1 到 2 秒能完成一问一答经过集中器转发可能需要 3 到 5 秒。并发数要看链路瓶颈——串口半双工本质上是串行的一个串口上并发没有意义走 TCP 的集中器才能多路并发。我实践下来常用的组合轮询周期 15 分钟、单表超时 5 秒、单通道并发 8。为什么并发取 8因为大多数集中器下行模块同时能处理的表地址有限并发太高会出现电表应答质量下降表现为偶发校验错和超时太低又会拉长整轮抄表时间。这个值没有绝对标准现场接入后先跑一小时看超时率再调。3.2 定时抄表主循环ScheduledExecutorService CompletableFuture用 ScheduledExecutorService 做定时触发用 CompletableFuture 做并发控制是这套源码里最常见的写法。示例如下public class MeterReadScheduler { private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); private final ExecutorService readExecutor Executors.newFixedThreadPool(8); private final MeterCollector collector; private final MeterReadMapper readMapper; public void start() { scheduler.scheduleAtFixedRate(this::round, 0, 15, TimeUnit.MINUTES); } private void round() { ListMeter meters collector.listOnlineMeters(); Semaphore semaphore new Semaphore(8); ListCompletableFutureVoid futures new ArrayList(); for (Meter meter : meters) { CompletableFutureVoid future CompletableFuture.runAsync(() - { semaphore.acquire(); try { double value collector.readMeter(meter); readMapper.insert(new MeterReadRecord(meter.getId(), value, new Date())); } catch (Exception e) { retryQueue.offer(meter); // 抄表失败进补抄队列 } finally { semaphore.release(); } }, readExecutor).orTimeout(5, TimeUnit.SECONDS); futures.add(future); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } }逻辑说明外层 scheduleAtFixedRate 保证每 15 分钟触发一轮内层每个表任务用 Semaphore 限流到 8 个并发。orTimeout(5, TimeUnit.SECONDS) 是单表超时兜底超时后 future 会异常完成但底层任务线程不一定会被中断这是第 5 章要展开的坑。参数说明readExecutor 的线程数要大于等于 Semaphore 许可数否则会让部分任务一直排队。我一般线程池设 16信号量设 8留一倍余量给超时任务和重试任务。这里还要注意一点allOf(...).join() 会让调度线程阻塞等待整轮完成。如果某只表迟迟不返回会拖慢下一轮调度。解决方法是把轮询周期拆成「触发间隔」和「本轮最大等待」两个配置触发时先检查上一轮是否还在跑还在跑就跳过本轮避免任务堆叠。3.3 抄表记录的数据模型存原始读数还是存用量抄表数据落库最忌讳只存「本次用量」。一旦后续发现倍率配错或者补抄了几条数据原始读数丢了就再也对不回来。我建议抄表流水表存原始读数用量通过相邻两条记录计算。建表 SQL 如下CREATE TABLE meter_read_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, meter_id BIGINT NOT NULL, meter_no VARCHAR(20) NOT NULL, read_value DECIMAL(12,4) NOT NULL, read_time DATETIME NOT NULL, biz_date DATE NOT NULL, source TINYINT NOT NULL DEFAULT 1 COMMENT 1定时抄表 2补抄 3手工录入, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1异常, UNIQUE KEY uk_meter_bizdate (meter_id, biz_date), KEY idx_read_time (read_time) ) COMMENT 抄表原始读数流水;字段说明read_value 是电表原始读数不带倍率倍率在电表档案表里维护biz_date 是业务日期配合唯一键防止同一只表同一天被重复插入source 字段用来区分定时抄表、补抄和人工录入后面做对账时能看出数据来源。逻辑说明为什么存原始读数而不是算好的用量因为倍率可能调错、表计可能被更换原始数据是审计的底账。用量计算放到月度结算时做用相邻两条 read_value 做差再乘倍率如果出现负数则说明表计被换过或者读数翻转需要在结算任务里单独标记。这里有个跟 MyBatis 相关的细节如果用 MyBatis-Plus 的 saveBatch 批量插入默认并没有走真正的 JDBC 批量执行每一条还是单条 insert几千条数据会明显拖慢一轮抄表。我一般用自定义 SQL 的 foreach 写批量插入或者开启 rewriteBatchedStatements 参数批量性能能差出一个量级。3.4 补抄机制失败表怎么不丢一轮抄表不可能百分之百成功现场总有停电、表计故障、通信瞬断。补抄机制的核心是「先记录再处理」。每只失败表进入补抄队列后按指数退避重试第一次 1 分钟后补抄第二次 5 分钟第三次 15 分钟超过三次后标记为故障表转入人工工单。补抄队列我用内存队列加数据库标记表双写重启不丢任务。轻量做法如下Component public class RetryQueue { private final DelayQueueDelayItemMeter queue new DelayQueue(); public void offer(Meter meter) { queue.offer(new DelayItem(meter, 60 * 1000L)); } public void runLoop() { while (true) { DelayItemMeter item queue.poll(); if (item null) { Thread.sleep(500); continue; } try { double value collector.readMeter(item.getData()); readMapper.insert(...); } catch (Exception e) { // 重试次数1失败超3次转人工工单 } } } }逻辑说明DelayQueue 是 JDK 自带的延迟队列取不到任务时线程阻塞不会空转。重试次数记录在电表档案上超过 3 次后不再自动重试写一条告警记录让运维人员去现场确认是表计故障还是通信问题。参数说明退避间隔我常用 1 分钟、5 分钟、15 分钟三档。为什么不一直快重试因为现场通信模块在故障恢复初期并不平稳连续快速重试反而会加剧集中器负担拉开间隔让链路自己缓过来。4. 缴费管理怎么落地台账、订单与余额的Java实现4.1 先定账务模型预付费还是后付费缴费管理最怕边做边改账务模型必须在一开始定清楚。两种模式差异很大预付费模式是用户先充值、用电时扣减余额余额不足自动跳闸后付费模式是先用电、出账单后再交钱存在欠费追缴。园区、公寓多采用预付费写字楼多采用后付费。平台源码里两种模式都要支持但核心账务表可以共用一套。我的做法是用户账户表只存余额和冻结金额所有变动走流水表。这样不管是充值、扣费、退款还是冲正账户余额都是一个累积结果出问题能靠流水回溯。这是面试里常被追问的「Java 怎么保证数据一致性」问题——落到这个项目上答案就是「事务 唯一索引 流水对账」三件套而不是某个分布式事务中间件。4.2 缴费入账的幂等与事务别把支付回调包进事务缴费入账有一个非常典型的并发场景用户同时用两个支付渠道交费或者支付网关回调重复推送。如果入账方法没有幂等保护用户会被重复加钱。核心做法是给每笔缴费单一个业务编号数据库唯一索引兜底入账方法如下Transactional(rollbackFor Exception.class) public PayResult recharge(PayOrder order) { // 幂等校验同一业务编号只允许入账一次 int exists payOrderMapper.countByBizNo(order.getBizNo()); if (exists 0) { return PayResult.duplicated(); } payOrderMapper.insert(order); // 原子累加余额避免 read-modify-write 并发丢更新 int rows accountMapper.increaseBalance(order.getAccountId(), order.getAmount()); if (rows ! 1) { throw new IllegalStateException(账户不存在或余额更新失败); } accountFlowMapper.insert(FlowRecord.of(order)); return PayResult.ok(order); }逻辑说明increaseBalance 的 SQL 是 UPDATE account SET balance balance #{amount} WHERE id #{accountId}这一条语句由数据库行锁保证原子性。比起先 SELECT 再 UPDATE它不需要应用层加锁也不会出现两个事务互相覆盖余额的问题。业务编号唯一索引是第二道防线即使两个请求同时进来也只有一个能 insert 成功。特别强调这个事务里不能调用支付网关、发短信、发 websocket 通知。因为这些远程调用的耗时不可控事务会一直攥着数据库连接不放。正确做法是事务里只写本地账务提交成功后再把「入账完成」的消息发到消息队列由消费者去通知用户。如果支付回调到达时订单已存在直接查订单状态返回即可这样天然幂等。4.3 阶梯电价与月度账单用量怎么换算成账单阶梯电价是缴费平台里最容易算错的地方。电表抄回来的是累计读数两个读数做差得到周期用量再用区间单价分段计算。阶梯电价配置表结构如下CREATE TABLE price_step ( id BIGINT PRIMARY KEY, meter_type VARCHAR(20) NOT NULL, step_no INT NOT NULL, min_usage DECIMAL(12,4) NOT NULL, max_usage DECIMAL(12,4), unit_price DECIMAL(10,4) NOT NULL ) COMMENT 阶梯电价区间表;计算逻辑对每个账户拿到周期用量 usage按 step 顺序扣减。比如第一阶梯 0 到 200 度单价 0.5第二阶梯 200 到 400 度单价 0.6第三阶梯 400 度以上单价 0.8。某用户用了 350 度费用是 2000.5 1500.6。核心代码如下public BigDecimal calcFee(ListPriceStep steps, BigDecimal usage) { BigDecimal remain usage; BigDecimal total BigDecimal.ZERO; for (PriceStep step : steps) { if (remain.compareTo(BigDecimal.ZERO) 0) break; BigDecimal span step.getMaxUsage() null ? remain : step.getMaxUsage().subtract(step.getMinUsage()); BigDecimal used remain.min(span); total total.add(used.multiply(step.getUnitPrice())); remain remain.subtract(used); } return total; }逻辑说明BigDecimal 是账务计算的唯一选择任何 double 累加都会在后面的对账里翻车。区间用 min 和 max 表示max 为 null 表示上不封顶。月度结算任务先把每个账户的周期用量算出来再逐账户跑这个分段逻辑最后生成账单记录。踩坑提醒账单和缴费是两个东西。账单是「应该交多少」缴费是「实际交了多少钱」。很多实现把这两个混在一张表里导致一笔缴费又想抵扣历史欠费又想冲抵当月账单时数据变得一塌糊涂。我做平台时把账单表、缴费订单表、账户流水表严格分开账单只负责算费缴费只负责入账两者通过账户余额间接联动。5. JAVA源码落地避坑抄表缴费平台的5个血泪教训5.1 表号字节序反了BCD地址域的发送顺序现象读请求发出去电表就是不回或者返回的地址域不是自己那只表解析器直接当陌生设备丢弃。原因DL/T645 的地址域虽然是 6 字节 BCD但规范要求「低字节在前」发送。表号 123456789012 按顺序 BCD 编码是 12 34 56 78 90 12发送时却要反序成 12 90 78 56 34 12。第一次写的人十有八九按自然顺序填结果就是报文对不上。解决在 buildReadRequest 里对所有地址域做一次反转代码见 2.2 节。接新表时先用厂家调试软件抓一帧正常报文把这帧逐字节跟自己组出来的对比一遍字节序问题当场就能发现。5.2 校验和总对不上不同厂家对CS范围的实现不一致现象同一帧报文用自己封装的解析器校验和报错但用厂家调试软件读同一只表却一切正常。原因645 规范里校验和范围是「从地址域开始到数据域结束」但部分厂家表计实现时把起始符 68 也加了进去或者漏掉了数据域长度字节。这是历史遗留的兼容性问题不是标准不标准的问题现场就是存在。解决CS 计算做一个开关支持「从地址域开始」和「从起始符开始」两种模式。接入新表型时先用厂家协议文档里的样例报文手动校验一次确认 CS 范围再改配置。顺手在日志里把接收到的原始帧打出来排查校验错时能直接看到是哪个字节对不上。5.3 串口粘包与半包读回来的帧断成两截现象readBytes 一次读回来的数据有时比一帧长有时比一帧短。解析器按整帧处理时要么报帧长不足要么把两帧数据当成一帧导致校验和错误。原因串口是流式协议没有消息边界。一次 read 可能只读到一帧的前半段也可能把下一帧的开头一起带回来。这在 RS485 半双工场景下特别常见因为表计应答速度快连续读两只表时相邻帧会挤在一起。解决不做「一次 read 就是一帧」的假设改为帧积累器。先找到 68 起始符读固定帧头从帧头取数据域长度 L再凑够 L2 个字节数据域加校验和加结束符凑不满就继续等。拿到完整帧后再交给解析器。public class FrameAccumulator { private final ByteArrayOutputStream buf new ByteArrayOutputStream(); public Listbyte[] push(byte[] chunk) { buf.write(chunk, 0, chunk.length); Listbyte[] frames new ArrayList(); byte[] data buf.toByteArray(); int pos 0; while (pos data.length) { if (data[pos] ! 0x68) { pos; continue; } // 找起始符 if (pos 10 data.length) break; // 帧头没凑齐 int len data[pos 8] 0xFF; // 数据域长度 int total 1 6 1 1 len 1 1; // 整帧长度 if (pos total data.length) break; // 整帧没到齐 frames.add(Arrays.copyOfRange(data, pos, pos total)); pos total; // 跳到下一帧 } buf.reset(); buf.write(data, pos, data.length - pos); // 剩余数据留到下次 return frames; } }逻辑说明核心是每次 push 只处理完整帧剩余半截留在缓冲区。这个类放在串口读取层和协议解析层中间能解决 90% 的粘包半包问题。参数上要注意 total 计算里的 68 起始符本身占了 1 字节地址域 6 字节控制码 1 字节长度 1 字节数据域 len 字节CS 1 字节16 结束符 1 字节。5.4 并发抄表把线程池打满超时配置的级联效应现象一轮抄表本来应该 5 分钟跑完实际跑了 40 分钟而且越跑越慢最后几轮任务全部卡死。原因这是一个典型的级联故障。单表读取的网络层没设读超时表计不响应时线程一直挂在 read 上CompletableFuture.orTimeout 虽然让调用方超时返回了但底层线程池里的任务并没有被中断线程被占满后续任务全部排队。解决网络层和任务层两层都要兜底。网络层 sockread 必须设置读超时我说过一般 2000 到 3000 毫秒任务层 orTimeout 设置 5 秒比网络层超时略长让底层线程先被释放。线程池任务里用 try-finally 保证信号量释放这样即使线程卡住也不会阻塞整个调度循环。上线前做一次故障演练拔掉一半表计的通信线看整轮抄表时间是否还受控。5.5 事务里调远程接口数据库连接池被一场缴费拖死现象缴费高峰期应用偶尔卡顿数据库连接池耗尽日志里全是获取连接超时。原因入账方法加了 Transactional里面调了支付网关接口。支付网关平均响应 300 毫秒但偶发 10 秒超时这个期间事务一直握着数据库连接。几十个缴费请求同时进来连接池瞬间被打满连普通的抄表入库都进不去了。解决事务里只做本地库操作远程调用一律移到事务外面。入账事务提交成功后通过 Spring 的事件机制或者消息队列触发后续通知。代码审查时定一条死规矩所有 Service 方法里只要加了 Transactional就不准出现 HTTP 调用、MQ 发送、短信发送这是 Java 开发规范里最常见也最容易被突破的一条。6. 不连真表也能验证本地模拟电表跑通全链路6.1 最小模拟从站一个字节数组造出完整应答帧没有真电表时用一段 ServerSocket 代码模拟一只表计。监听一个端口收到读请求后返回写死的应答帧全链路从「采集服务、入库、账单计算到缴费入账」都能验证。核心代码如下public class MeterSimulator { public static void main(String[] args) throws Exception { byte[] response buildResponse(); try (ServerSocket server new ServerSocket(5020)) { while (true) { try (Socket socket server.accept()) { byte[] req socket.getInputStream().readAllBytes(); if (req.length 0 req[0] 0x68) { socket.getOutputStream().write(response); } } } } } private static byte[] buildResponse() { byte[] frame new byte[19]; frame[0] 0x68; // 地址域反序的BCD表号 123456789012 byte[] addr {0x12, (byte) 0x90, 0x78, 0x56, 0x34, 0x12}; System.arraycopy(addr, 0, frame, 1, 6); frame[7] (byte) 0x91; // 正常应答 frame[8] 0x08; // 数据域长度4字节标识 4字节电能 frame[9] 0x00; // 数据标识 DI0 frame[10] 0x00; frame[11] 0x00; frame[12] 0x00; frame[13] 0x00; // 电能 BCD 高位 frame[14] 0x13; frame[15] 0x05; // 读数 1305两位小数 13.05 kWh frame[16] 0x16; // CS 占位 frame[17] frame[17]; // 这里换成调用 CS 计算逻辑 frame[18] 0x16; return frame; } }逻辑说明模拟器只做一件事——收到 68 开头的帧就回一帧固定数据。这样 Java 主站侧的调度、入库、算费流程全部能跑起来。响应的数据标识要和请求里的保持一致否则解析器会取出错值。6.2 从模拟到真表验证平台要盯的三个指标本地模拟跑通后接真表时我习惯盯三个指标。第一个是一轮抄表成功率初次接入应达到 99% 以上低于这个值先查通信参数第二个是日冻结数据完整率跑满 24 小时后比对电表屏幕读数确认小数位和倍率配对了第三个是缴费入账延迟本地转账模拟从下单到余额变更不超过 2 秒不含支付网关时间。三个指标全过平台才算具备交付条件。6.3 进阶技巧冻结曲线与报表导出的量纲统一量纲统一是进阶阶段最常见的坑。抄表原始值是 kWh报表要显示万 kWh同比环比又要百分比一个数值在三套体系里转来转去很容易出 bug。我的习惯是数据库一律存原始值只在最外层展示层做单位换算。导出 Excel 报表时同样用转换后的数据避免在底层沉淀「已换算」的字段。负责做这个功能的人总会问 Apache POI 能不能生成图表能但真正的坑从来不在图表而在单位没统一导致曲线差几个数量级。这行做的越久越觉得把数据和事务的边界管好比引入花哨的框架可靠得多。希望这份拆解帮你在智能电表这条赛道上少踩几个坑。本文还有配套的精品资源点击获取