简介:这是一套围绕射频识别技术构建的仓库管理系统源码与配套文档,面向物流管理、物联网课程设计及毕业设计学习者,也适合需要理解RFID数据采集如何落地的开发者。系统将RFID引入到货检验、入库、库位分配、库存变动记录、查询与出库等环节,通过自动化采集提升数据录入速度与准确性,帮助企业及时掌握库存状态并合理控制库存。压缩包共42个文件,约1.46MB,包含C++头文件与实现、PHP后端脚本、Qt工程与界面文件,以及说明文档和若干库文件,便于从界面、业务逻辑到服务端接口整体梳理。目录中可见卡片初始化、终端查询、分类入库、市场模块与TraceBack服务端等线索,适合拆解为模块化学习。已有931人学习,可作为课程实践、项目复现与二次开发的参考底稿,帮助读者理解RFID仓库管理的功能划分、数据流转和前后端协作方式。
1. 从一次六人盘点到一套系统:基于射频识别技术的仓库管理系统要解决什么
有个做仓储实施的同行跟我算过一笔账:一个三千平方米的平库,八千个 SKU,季度盘点要六个人扫一天半,扫完还得花半天对差异,最后账实一致率勉强到 97%。问题不在人不够勤快,而在“逐件拿起来扫码”这个动作本身就规定了上限——人的手速就是系统的吞吐量。基于射频识别技术的仓库管理系统要拆掉的就是这个上限:把“找标签、对准、扫码”换成“走过去”,让识别在搬运过程中顺带完成。
这套方案适合两类人:一类是要交毕设或课程设计,需要一个结构完整、指标可量化、能现场演示的题目;另一类是要给中小型仓库出一份能落地的改造方案,预算有限、不能大动土建。核心目标是三件事:批量收货时整托一次过、库内找货能定位到货位、周期盘点把人工工时压下来并且账实一致率抬到 99.5% 以上。下文按需求、选型、架构、代码、避坑、验收的顺序铺开,每一段都可以直接拿去改成你自己的章节。
2. 需求边界与硬件选型:UHF RFID 凭什么替掉逐件扫码
2.1 三类业务场景与可量化的验收指标
做这个题目最容易翻车的地方,是一上来就想“仓库里所有事都管”。八个模块全画在图上,结果每个都只做了个增删改查,答辩时老师问一句“你这个读取率多少”就卡住了。我的建议是先锁三条主链路,把指标钉死,剩下的功能围绕它们做支撑。
第一条是整托收货。供应商把货码在托盘上,托盘四角贴扎带式标签,箱体贴单品标签,叉车过通道门时一次性读完。这条链路考验的是多天线覆盖和抗串读能力,验收看的是“一托 30 箱,单次通过读出多少”。第二条是拣货复核,拣货员用手持终端在货架前扫一遍,屏幕立刻列出“应拣 12 件、实读 12 件、差异 0”,考的是近距识别的稳定性和响应速度。第三条是周期盘点,一个人拿着手持机走一圈,回传后自动生成差异单,考的是效率提升和差异定位精度。
| 业务场景 | 识别方式 | 典型距离 | 标签形态 | 可验收指标 |
|---|---|---|---|---|
| 整托收货 | 固定式四通道 + 门型天线 | 2~4 m | 托盘扎带标签 + 箱体不干胶 | 单托 30 箱一次通过率 ≥ 99%,耗时 ≤ 8 s |
| 拣货复核 | 手持终端内置天线 | 0.3~1 m | 单品柔性标签 | 单次识别 ≤ 1 s,漏读率 ≤ 0.5% |
| 周期盘点 | 手持终端 + 外接天线 | 0.5~2 m | 货位标签 + 单品标签 | 单人日盘 ≥ 8000 件,差异定位到货位 |
| 出库复核 | 固定式通道门 | 2~4 m | 箱体标签 | 错发拦截率 100%,误拦率 ≤ 1% |
指标不要拍脑袋写。读取率的测法通常是:挑 30 个已知 EPC 的标签,按实际业务姿态摆好,连续读 10 轮,每轮记录读到的集合,取十轮的平均命中率。只在实验室桌面上摆一排标签测出来的数字,到了有金属货架和叉车的现场会掉一大截,这一点后面避坑章节还会说。
2.2 与条码、二维码的取舍:不是所有仓库都该上 RFID
很多人做这个题目是“因为题目叫 RFID 所以用 RFID”,真到答辩被问“为什么不用二维码”,答不上来就很被动。合理的回答应该是一笔成本账。
条码和二维码的本质是“光学识别”,必须视线可达、必须对准、一次一个,优点是标签成本低到几分钱、打印即用。RFID 是“无线电识别”,不需要可见、可以批量、可以穿透纸箱和塑料读,代价是标签几毛到一两块、要配读写器和天线、现场要做电磁环境调试。
| 维度 | 条码/二维码 | UHF RFID |
|---|---|---|
| 单次识别数量 | 1 个 | 每秒数十到数百个 |
| 是否要求视线可达 | 是 | 否,可穿透纸箱、塑料 |
| 标签单价 | 几分钱 | 几毛到一两块 |
| 可否重复写入 | 一般不可 | 可,EPC 区可重写 |
| 金属/液体环境 | 不受影响(贴得上就行) | 需抗金属标签、需避开液体遮挡 |
| 人工依赖 | 高,逐件对准 | 低,走过即可 |
判断阈值可以这么算:年盘点与找货的人工工时乘以工时成本,如果大于“标签年消耗量 + 读写器与天线一次性投入按三年摊销”,就值得上。反过来说,SKU 只有几百个、单价极低、出入库频次不高的仓库,硬上 RFID 是浪费,这一点在方案里主动写出来,反而是加分项——说明你做过取舍,不是照着题目抄。
2.3 硬件选型与标签贴附规则
硬件清单不用堆品牌,把参数和选型逻辑写清楚就够了。固定式读写器一般选四通道、输出功率可调至 30 dBm、支持密集读取模式(Dense Reader Mode),用在收货台和出库通道;手持终端选带 UHF 模块的工业手持机,输出 1 W 左右,用在盘点和找货;天线分两种,圆极化天线增益 8~9 dBi、波束宽、容错高,适合门型通道,线极化天线增益 10~12 dBi、方向性强,适合传送带这种标签姿态固定的场合。
# reader_config.yaml —— 单台固定式读写器的现场配置样例 reader: id: R-01 name: 收货通道门 protocol: LLRP # 多数固定式读写器支持 LLRP,也有厂商私有 TCP 协议 host: 192.168.10.21 port: 5084 mode: dense_reader # 多台读写器邻近时必须开密集模式,否则互相干扰 antennas: - port: 1 power_dbm: 26 # 从 20 起调,每次 +2,找到刚好覆盖的档位就停 polarization: circular - port: 2 power_dbm: 26 polarization: circular filter: rssi_threshold: -65 # 低于该信号强度的读数视为边缘误读,直接丢弃 dedup_window_ms: 2000 # 同一 EPC 两秒内只上报一次这份配置里有两个参数值得单独说。power_dbm不是越大越好,功率开满会让天线旁瓣变宽、读到隔壁通道的货,也会因为驻波反射导致近处标签读不到,现场调试的经验是从 20 dBm 起步,用固定位置的基准标签测,每次加 2 dBm,读到稳定为止就停手。rssi_threshold是用来做空间隔离的软手段,配合物理屏蔽效果更好。
标签贴附有几条不成文的规矩,写进方案里会显得你下过现场:金属货架和设备上必须用抗金属标签,并且垫高 5~10 mm,不能直接贴死在金属面上;液体包装要贴在侧面偏上位置,避开液面正对天线;线极化天线下,标签天线方向要跟天线极化方向平行,差 90 度会直接读不到;托盘标签贴在四角而不是正中间,因为叉车货叉会挡住中心区域。
提示:方案里给硬件参数时,写“可调范围”比写死一个数字更稳妥,现场环境差异很大,写死反而容易被追问为什么是这个值。
3. 总体架构设计:四层结构与六个模块的职责划分
3.1 分层结构与数据流向
整个系统按四层切:感知层、采集层、业务层、展示层,数据层横切在业务层下面。感知层就是标签、天线、读写器、手持终端这些硬件;采集层是跑在现场工控机或边缘服务器上的服务,负责跟读写器通信、收原始读数、做去重和过滤、往上送事件;业务层是核心,处理入库、出库、移库、盘点这些有事务语义的动作;展示层是 Web 看板和手持端 App。
数据流是单向的:读写器读到 EPC → 采集层加时间戳和设备号 → 通过消息队列或 HTTP 推给业务层 → 业务层查标签绑定关系、匹配当前任务、写库存流水 → 展示层轮询或订阅变化。反向只有一条控制流:业务层下发“开始盘某区域”的指令,采集层转成读写器的启停命令。
把这条链路画成文字版的层次说明,比画流程图更省篇幅也更清楚:在收货台上,读写器把原始读数交给采集服务,采集服务过滤后变成{epc, reader_id, antenna_id, rssi, ts}这样的事件;业务层拿到事件后,第一步查rfid_tag表确认这个 EPC 绑的是哪个物料,第二步查当前有没有处于“收货中”状态的任务单,第三步决定这笔读数是入账还是丢弃。这三步每一步都可能出错,所以每一步都要能单独打日志。
3.2 六个模块的职责与接口边界
模块划分的原则是“一个模块只对一种状态变化负责”。我一般会切成六个:
设备接入模块负责读写器连接管理、心跳保活、断线重连、指令下发,对外只暴露“订阅读数事件”和“下发盘存指令”两个接口。基础资料模块管物料、货位、标签绑定关系,核心是epc与sku的对应,注意这是一对多还是多对一要先定清楚——按箱管理是一箱一码,按件管理是一件一码。出入库模块负责单据驱动的库存变动,是唯一能改stock表的地方。盘点模块负责生成盘点任务、接收实盘数据、算差异、出差异单。库存与流水模块提供库存查询和全量流水追溯,任何一次数量变化都必须落一条流水。看板与报表模块只读,不写业务表。
| 模块 | 主要输入 | 主要输出 | 依赖 |
|---|---|---|---|
| 设备接入 | 读写器原始帧、控制指令 | 标准化读数事件 | 无 |
| 基础资料 | 物料/货位/标签录入 | 绑定关系、校验结果 | 无 |
| 出入库 | 单据 + 读数事件 | 库存变更、流水 | 基础资料、设备接入 |
| 盘点 | 盘点任务 + 实盘事件 | 差异单 | 基础资料、库存 |
| 库存与流水 | 出入库/盘点的结果 | 库存快照、追溯链 | 出入库、盘点 |
| 看板报表 | 各表只读查询 | 图表、导出 | 全部 |
边界定清楚的好处是答辩时能回答“你这个系统耦合度高不高”。比如盘点模块不允许直接改库存,只能生成差异单,差异单审核通过后才由出入库模块执行调整——这条规则写出来,说明你懂事务边界。
3.3 数据模型:八张表撑起全部业务
表不用多,八张够用。核心思路是“标签身份”和“物料身份”分开存,因为一个物料可能有一批标签,一个标签也可能因为损坏被换掉重新绑定。
-- 标签主表:一个物理标签一行,EPC 全局唯一 CREATE TABLE rfid_tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, epc VARCHAR(64) NOT NULL COMMENT '标签 EPC,全局唯一', tid VARCHAR(64) DEFAULT NULL COMMENT '标签 TID,用于防伪与防克隆', sku_id BIGINT DEFAULT NULL COMMENT '绑定的物料,未绑定为 NULL', pack_level TINYINT NOT NULL DEFAULT 1 COMMENT '1 件 2 箱 3 托', status TINYINT NOT NULL DEFAULT 1 COMMENT '1 在库 2 出库 3 报废', bind_time DATETIME DEFAULT NULL, UNIQUE KEY uk_epc (epc), KEY idx_sku (sku_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存表:按 物料 + 货位 维度存当前数量 CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, location_id BIGINT NOT NULL, qty INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY uk_sku_loc (sku_id, location_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存流水:只增不改,任何数量变动都要落一条 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, location_id BIGINT NOT NULL, change_qty INT NOT NULL COMMENT '正数入 负数出', biz_type VARCHAR(16) NOT NULL COMMENT 'INBOUND/OUTBOUND/MOVE/CHECK_ADJUST', biz_no VARCHAR(32) NOT NULL COMMENT '关联单据号,用于幂等与追溯', operator VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_biz (biz_type, biz_no, sku_id, location_id), KEY idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stock_flow上的那个唯一索引是整个幂等设计的支点。同一张入库单重复提交时,第二次插入会因为唯一键冲突失败,业务层捕获后直接返回成功,不会把库存加两遍。这比在代码里查一遍再插入要可靠,因为查和插之间有空窗期。
另外几张表是sku(物料)、location(货位)、biz_order(单据主表)、check_task(盘点任务)和check_diff(差异明细)。check_task里要存一个snapshot_time,记录任务开始时刻的账面快照,否则盘点期间有出入库,差异会算错——这个问题在避坑章里还会展开。
4. 核心链路实现:从标签事件到账实一致的三段代码
4.1 采集层:读写器适配与去重窗口
采集层最容易写错的地方是去重。新手常见的写法是把读到的 EPC 塞进一个set,读过的就不再处理。这个做法在纯演示里没问题,真实业务里会出事:货物出库后又退货回来,同一个 EPC 应该被重新记录一次,永久去重会把它吃掉。正确做法是时间窗去重。
# collector/dedup.py import threading class DedupWindow: """按 EPC 做滑动时间窗去重:同一标签在 window 秒内只放行一次""" def __init__(self, window_sec=2.0): self.window = window_sec self._last = {} self._lock = threading.Lock() def accept(self, epc: str, ts: float) -> bool: with self._lock: prev = self._last.get(epc) # 窗口内重复读数直接丢弃,窗口外视为一次新的出现 if prev is not None and (ts - prev) < self.window: return False self._last[epc] = ts # 简易清理:超过窗口 10 倍未出现的记录移除,防止字典无限增长 if len(self._last) > 50000: expire = ts - self.window * 10 self._last = {k: v for k, v in self._last.items() if v >= expire} return Truewindow_sec的取值跟业务节奏有关。收货台一次托盘通过大约 5~8 秒,同一标签会被读到几十次,窗口设 2 秒足够把它压成一条;手持盘点时人移动慢,窗口设 1 秒就够。这里的取舍是:窗口太长会漏掉“同一标签短时间内真的动了两次”的情况,太短则去重不彻底、流水表被灌爆。2 秒是我在多数收货场景下用的默认值。
配合去重的还有 RSSI 过滤。读写器读到的信号强度低于阈值时,多半是从隔壁通道飘过来的,或者是从货架背面绕过来的,这类读数必须在上报前就砍掉,否则后面算库存全是脏数据。
# collector/reader_client.py import socket, json, time class ReaderClient: """固定式读写器 TCP 接入的最小实现,协议细节按厂商文档替换""" def __init__(self, host, port, rssi_threshold=-65, dedup_window=2.0): self.host, self.port = host, port self.rssi_threshold = rssi_threshold self.dedup = DedupWindow(dedup_window) # 前面定义的类 self.sock = None def connect(self): self.sock = socket.create_connection((self.host, self.port), timeout=5) self.sock.settimeout(30) def run(self, on_event): """阻塞读取,解析出读数后回调 on_event(epc, reader_id, antenna, rssi, ts)""" buf = b"" while True: chunk = self.sock.recv(4096) if not chunk: break buf += chunk while b"\n" in buf: line, buf = buf.split(b"\n", 1) tag = json.loads(line.decode("utf-8")) ts = time.time() # 两级过滤:先看信号强度,再看时间窗去重 if tag["rssi"] < self.rssi_threshold: continue if not self.dedup.accept(tag["epc"], ts): continue on_event(tag["epc"], tag["reader_id"], tag["antenna"], tag["rssi"], ts)这段代码里,rssi_threshold和dedup_window两个参数是现场调出来的,不是拍出来的。调试方法是在通道门两侧各放一个已知标签,把隔壁那侧的标签功率调到刚好被阈值挡住,同时确认本侧标签在任意姿态下都不被误挡——通常要来回调三四轮。
4.2 业务层:入库事务与幂等控制
业务层最关键的一条是“读数不等于入账”。读到标签只说明东西在附近,是否入账要由单据状态决定。所以入库的流程是:单据先进入“收货中”状态,采集到的读数暂存在会话里,操作员点确认时一次性提交,事务里做三件事——校验单据状态、写流水、更新库存。
# service/inbound.py from decimal import Decimal class InboundService: def confirm(self, biz_no: str, epc_list: list, operator: str): """收货确认:幂等提交,重复调用不会重复加库存""" with db.transaction() as tx: # 1) 锁定单据并校验状态,防止重复提交和并发提交 order = tx.query_one( "SELECT id, status FROM biz_order WHERE biz_no=%s FOR UPDATE", (biz_no,)) if order is None: raise BizError("单据不存在") if order["status"] == "DONE": return {"code": 0, "msg": "已处理,忽略重复提交"} if order["status"] != "RECEIVING": raise BizError(f"单据状态不允许收货: {order['status']}") # 2) EPC 反查物料,未绑定的整批拒绝,避免脏数据入账 tags = tx.query_all( "SELECT epc, sku_id FROM rfid_tag WHERE epc IN %s AND status=1", (tuple(epc_list),)) if len(tags) != len(set(epc_list)): missing = set(epc_list) - {t["epc"] for t in tags} raise BizError(f"存在未绑定或已报废标签: {list(missing)[:5]}") # 3) 按物料聚合数量,逐条写流水并更新库存 agg = {} for t in tags: agg.setdefault(t["sku_id"], []).append(t["epc"]) for sku_id, epcs in agg.items(): qty = len(epcs) tx.execute( "INSERT INTO stock_flow(sku_id, location_id, change_qty, biz_type, biz_no, operator)" " VALUES(%s,%s,%s,'INBOUND',%s,%s)", (sku_id, order["location_id"], qty, biz_no, operator)) # 库存用 插入或累加 一条语句完成,避免先查后改的并发问题 tx.execute( "INSERT INTO stock(sku_id, location_id, qty) VALUES(%s,%s,%s)" " ON DUPLICATE KEY UPDATE qty = qty + VALUES(qty)", (sku_id, order["location_id"], qty)) tx.execute("UPDATE biz_order SET status='DONE' WHERE id=%s", (order["id"],)) return {"code": 0, "msg": "ok", "count": len(tags)}这段代码有两处值得在方案里说明。SELECT ... FOR UPDATE锁住单据行,是为了挡住“操作员手抖点了两次确认”和“两个终端同时提交同一张单”这两种情况,前者在演示时不会出现,后者在真实仓库里天天发生。第二个是ON DUPLICATE KEY UPDATE,把“查库存—算新值—写回”三步压成一条 SQL,规避了并发下的丢失更新。如果你用别的数据库,MERGE或者带WHERE version=?的乐观锁更新是等价做法。
4.3 盘点差异计算与差异单生成
盘点的核心不是“读到了什么”,而是“读到和账面差在哪”。差异计算必须基于任务开始时的快照,不能拿当前库存去比,否则盘点过程中发生的正常出入库会被误判成盘亏。
-- 生成差异明细:账面来自快照表,实盘来自本次任务的读数汇总 INSERT INTO check_diff (task_id, sku_id, location_id, book_qty, real_qty, diff_qty) SELECT t.task_id, s.sku_id, s.location_id, s.qty AS book_qty, IFNULL(r.cnt, 0) AS real_qty, IFNULL(r.cnt, 0) - s.qty AS diff_qty FROM check_snapshot s LEFT JOIN ( -- 同一 EPC 在任务期内只计一次,已由采集层去重保证 SELECT sku_id, location_id, COUNT(DISTINCT epc) AS cnt FROM check_read WHERE task_id = #{taskId} GROUP BY sku_id, location_id ) r ON r.sku_id = s.sku_id AND r.location_id = s.location_id WHERE s.task_id = #{taskId} AND IFNULL(r.cnt, 0) <> s.qty; -- 只落有差异的行check_snapshot是任务创建时把stock表按区域复制的一份快照,用task_id隔离。差异明细只写有差异的行,盘对的库存不占空间,几万个 SKU 的仓库这一条能省下大半的存储和查询时间。差异单生成后不直接改库存,要走审核:审核通过才由出入库模块以CHECK_ADJUST类型写流水,把账面调成实盘。这一步是答辩时最能体现“懂业务”的设计点,因为盘亏盘盈在财务口径上是有责任归属的,不能系统一算就自动调账。
4.4 手持端离线补传:断网时也不能丢数据
仓库里 WiFi 覆盖不好的角落是常态,手持机走到货架深处断网,如果读数只存在内存里,退出 App 就全没了。通常的做法是本地落库加补传队列:每条读数带一个客户端生成的uuid作为幂等键,联网后按顺序上报,服务端对同一个uuid只处理一次。
# handheld/uploader.py import sqlite3, uuid, json, requests def record_scan(epc, task_id, rssi): """本地落库,uuid 作为后续补传的幂等键""" conn = sqlite3.connect("scan.db") conn.execute( "INSERT INTO pending(uuid, epc, task_id, rssi, ts, synced) VALUES(?,?,?,?,?,0)", (str(uuid.uuid4()), epc, task_id, rssi, time.time())) conn.commit() def flush(server_url): """按时间顺序补传,服务端按 uuid 去重,重传安全""" conn = sqlite3.connect("scan.db") rows = conn.execute( "SELECT uuid, epc, task_id, rssi, ts FROM pending WHERE synced=0 ORDER BY ts LIMIT 200" ).fetchall() for r in rows: payload = {"uuid": r[0], "epc": r[1], "task_id": r[2], "rssi": r[3], "ts": r[4]} resp = requests.post(f"{server_url}/api/scan", json=payload, timeout=5) if resp.status_code == 200: conn.execute("UPDATE pending SET synced=1 WHERE uuid=?", (r[0],)) conn.commit()uuid在主键上的唯一约束,配合服务端的“重复即返回成功”,让整个补传可以放心重试。批量大小 200 是权衡:太大单次请求体过重、弱网下容易超时,太小则请求数过多、握手开销占比高。
5. 避坑与排查:五个让 RFID 项目翻车的现场问题
5.1 读取层的三个坑
现象一:功率开到最大,反而读不全。调试时把输出功率拉到 30 dBm,结果近处托盘上的标签漏读,隔壁通道的货倒是读进来一堆。原因有两个:功率过大导致天线近场出现盲区,标签离得太近反而不响应;同时旁瓣变宽,读取范围溢出到相邻通道。解决方法是回到 20 dBm 起步,用固定位置的基准标签做阶梯测试,每加 2 dBm 测一轮,找到“本通道全覆盖、隔壁通道零读数”的那个点就停,别贪大。多台读写器相邻部署时,还要开密集读取模式并错开跳频,否则互相干扰是玄学级别的难查。
现象二:金属货架上的货死活读不到。现象很典型:同一个标签贴在纸箱上能读三米,贴到金属托盘上只剩半米,甚至完全读不到。原因是 UHF 电磁波打到金属表面会产生反射和涡流,标签天线被“短路”了。这种做法必须换抗金属标签,靠内置的介质层把标签天线和金属面隔开,同时安装时垫高 5~10 mm,别让标签背胶直接贴死在金属上。液体同理,水会吸收 UHF 能量,液体包装要贴在侧面偏上、避开液面正对天线的位置。
现象三:同一批标签,有的读十次有的读一次。这是极化方向不匹配。线极化天线的电磁波只在一个方向上振动,标签天线跟它平行时读取距离最远,垂直时几乎读不到。如果标签是工人随手贴的,方向五花八门,就会出现这种“看运气”的读取率。解决方式有两种:通道门这种标签姿态不可控的场合,直接换成圆极化天线,代价是增益低 2~3 dBi;传送带这种姿态固定的场合保留线极化,但在标签上印方向标识,规定贴附方向。
5.2 业务层的两个坑
现象四:库存莫名其妙翻倍。盘完账发现某个 SKU 数量正好是应有数量的两倍,查流水发现同一批 EPC 被记了两遍。原因通常是三层:采集层没做去重、业务层没做幂等、单据状态没有锁。RFID 的特点是同一秒内会读到同一个标签几十次,只要中间任何一层没挡住,就会重复入账。解决方法是三层各设一道闸——采集层时间窗去重、流水表唯一索引、单据行级锁,三者缺一不可。只做其中一层,换个并发场景就能绕过。
现象五:盘点差异永远对不上,差得还很随机。排查半天发现是时间问题:盘点任务晚上 11 点开始,跨了零点,快照取的是任务创建时刻的库存,但盘点期间有夜班在做紧急出库,实盘读到的是已经出库的货,差异表里就凭空多了几十条盘亏。还有一种情况是手持机本地时钟没校准,读数时间戳比服务端慢几分钟,落在快照时间之前,被过滤掉了。解决办法是快照严格按任务创建时刻固化、任务期内锁定对应库区的出入库操作(或者把期间的流水单独标记出来做二次核对),同时所有终端强制 NTP 对时,服务端收到读数后以服务端时间重新打戳,客户端时间只作为参考。
注意:这几条不用全写进论文正文,但答辩 PPT 里放一页“现场问题与对策”,被问到“你遇到过什么困难”时直接翻出来讲,效果比背概念好得多。
6. 验收与答辩:用三张表把系统「证」出来
验收这件事,很多人的做法是“打开系统点一遍功能”,这在课程设计里能过,在工作交付里会被打回来。合理的做法是把验收拆成可测量的三张表:功能验收表、性能验收表、异常验收表。
功能验收表按业务链路列,每行一个动作,写明前置条件和预期结果。比如“整托收货”这一行:前置是托盘已贴标、单据处于收货中,操作是叉车过通道门并在终端确认,预期是库存增加、流水生成、单据状态置为完成。这张表的用处是让验收方按行打勾,不靠印象。
性能验收表只放三个指标,多了测不完:单托读取通过率、单次盘点作业时长、并发提交下的库存准确性。前两个现场测,第三个用脚本模拟两个终端同时提交同一张单据,看最终库存是不是只加了一次——这条最能证明你的幂等设计是真做了而不是写在文档里。
| 验收项 | 测试方法 | 合格标准 |
|---|---|---|
| 整托读取通过率 | 30 箱托盘过通道门,重复 10 次 | 平均 ≥ 99%,无隔壁通道串读 |
| 盘点效率 | 单人手持盘点指定区域 8000 件 | ≤ 6 小时,差异自动生成 |
| 并发幂等 | 两终端同时提交同一入库单 | 库存只增加一次,流水唯一 |
| 断网补传 | 断网扫码 100 条后恢复网络 | 100 条全部入库,无重复 |
| 账实一致率 | 全仓盘点后比对 | ≥ 99.5% |
异常验收表是最容易被忽略、也最能加分的一张。把“标签未绑定”“读写器断线”“重复提交”“盘点跨零点”这些异常场景列出来,逐条演示系统怎么处理和怎么提示。答辩时老师最爱问的就是“如果断网了怎么办”“如果标签坏了怎么办”,有这张表,回答就是翻到某一页指着说,而不是临场编。
演示环节还有个技巧:不要按菜单顺序点,按一条完整的业务故事线走。从供应商送货、贴标、过通道门收货、上架、拣货、出库拦截,一条线走完,中间故意插一个错发的场景让系统拦住。评审看的不是你功能多少,是这条链路闭不闭得住。
提示:演示前一定做一次完整的彩排,特别是无线设备,现场 WiFi 信道拥挤、读写器 IP 冲突这类问题,彩排能提前暴露八成。
我刚做这类项目的时候,总觉得把功能做完就算交付了,验收表是走形式。后来有一次现场验收,对方拿着自己的检查清单一条条问,问到“盘点期间有出库怎么办”的时候我卡了壳,那次之后我每次做方案,都强制自己先把验收表写出来,再倒推去设计功能和数据结构。表写不出来的地方,就是设计没想清楚的地方。希望这套结构能帮你少走一段弯路。
本文还有配套的精品资源,点击获取