云计算百科
云计算领域专业知识百科平台

零售门店智能巡检与盘点系统:物联网与图像识别技术实践

问题背景

零售门店日常运营面临两大高频痛点:巡检效率低与盘点耗时长。传统依赖人工纸质记录的模式存在明显缺陷:

痛点类型

具体表现

运营损失

巡检低效

店员每日花1-2小时巡检货架、检查价签、记录缺货,数据滞后且易遗漏

问题发现延迟4-6小时,影响销售机会

盘点耗时

月度全盘需闭店4-8小时,抽盘需2-3小时,人工计数错误率10%+

闭店损失销售额,抽盘数据不准影响库存决策

数据孤岛

巡检照片、缺货记录、价签异常分散在不同人员手机中,难追溯

管理层无法实时掌握门店状态,决策缺乏依据

行业实践表明,通过物联网传感器+图像识别+移动端闭环的技术方案,可将巡检耗时降低80%,盘点效率提升5倍以上。部分零售SaaS方案(如嘚嘚象)已采用该模式实现分钟级门店状态感知,本文聚焦可复用的技术实现。

一、智能巡检系统架构

1.1 整体技术架构

1.2 巡检任务模型设计

// 巡检任务模型
@Data
@Builder
public class InspectionTask {

// 基础信息
private String taskId;
private String storeId;
private String taskType; // DAILY(日常巡检)/ WEEKLY(周检)/ SPECIAL(专项)
private String taskName;
private String assignee; // 指派人
private String executor; // 执行人
private LocalDateTime scheduledTime; // 计划执行时间
private LocalDateTime startTime; // 实际开始时间
private LocalDateTime endTime; // 实际结束时间

// 任务状态
private String status; // PENDING / IN_PROGRESS / COMPLETED / EXPIRED
private String completionRate; // 完成率:75%

// 巡检项列表
private List<InspectionItem> items;

@Data
@Builder
public static class InspectionItem {
private String itemId;
private String itemType; // SHELF(货架)/ PRICE_TAG(价签)/ PROMOTION(促销)/ CLEANLINESS(清洁)
private String location; // 货位编号:A01-02-03
private String question; // 检查项:货架是否缺货?价签是否清晰?
private String answerType; // YES_NO / PHOTO / TEXT / COUNT
private String answer; // 答案
private String photoUrl; // 照片URL(如有)
private Boolean abnormal; // 是否异常
private String remark; // 备注
private LocalDateTime checkTime; // 检查时间
}
}

// 巡检模板(可复用)
@Data
public class InspectionTemplate {
private String templateId;
private String templateName;
private String businessType; // 业态:convenience / adult / fresh
private List<InspectionItemTemplate> itemTemplates;

@Data
public static class InspectionItemTemplate {
private String itemType;
private String question;
private String answerType;
private Boolean required; // 是否必填
private String defaultValue; // 默认值
}
}

1.3 移动端巡检APP核心功能

采用Flutter跨平台开发,支持离线操作与自动同步:

// 巡检任务执行页面
class InspectionTaskPage extends StatefulWidget {
final String taskId;

@override
_InspectionTaskPageState createState() => _InspectionTaskPageState();
}

class _InspectionTaskPageState extends State<InspectionTaskPage> {
InspectionTask? task;
int currentIndex = 0;
bool isSubmitting = false;

@override
void initState() {
super.initState();
loadTask();
}

// 加载任务(支持离线)
Future<void> loadTask() async {
// 1. 优先从本地缓存加载
task = await LocalCache.getTask(widget.taskId);

if (task == null) {
// 2. 缓存无数据,从云端拉取
try {
task = await InspectionApi.getTask(widget.taskId);
// 3. 保存到本地缓存
await LocalCache.saveTask(task!);
} catch (e) {
if (mounted) {
showToast('网络异常,将使用离线模式');
}
}
}

if (mounted) setState(() {});
}

// 拍照并上传
Future<void> takePhoto() async {
final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
);

if (photo != null) {
// 1. 保存到本地
String localPath = await saveToLocal(photo.path);

// 2. 压缩图片(减少上传流量)
String compressedPath = await compressImage(localPath);

// 3. 更新答案
setState(() {
task!.items[currentIndex].photoUrl = compressedPath;
task!.items[currentIndex].answer = 'PHOTO_TAKEN';
});

// 4. 本地缓存(支持离线)
await LocalCache.saveTask(task!);
}
}

// 提交任务
Future<void> submitTask() async {
if (isSubmitting) return;

setState(() { isSubmitting = true; });

try {
// 1. 校验必填项
if (!validateRequiredItems()) {
showToast('请完成所有必填项');
setState(() { isSubmitting = false; });
return;
}

// 2. 标记任务完成
task!.status = 'COMPLETED';
task!.endTime = DateTime.now();

// 3. 上传到云端(支持断点续传)
await uploadWithRetry(task!);

// 4. 清除本地缓存
await LocalCache.deleteTask(widget.taskId);

if (mounted) {
showToast('提交成功');
Navigator.pop(context, true);
}

} catch (e) {
if (mounted) {
showToast('提交失败,请检查网络');
}
} finally {
if (mounted) setState(() { isSubmitting = false; });
}
}

// 离线数据同步(网络恢复后自动触发)
void syncOfflineData() {
LocalCache.getPendingTasks().then((tasks) {
for (var task in tasks) {
uploadWithRetry(task).then((_) {
LocalCache.deleteTask(task.taskId);
}).catchError((e) {
// 上传失败保留,下次再试
});
}
});
}
}

二、图像识别技术应用

2.1 货架缺货检测

采用轻量级YOLO模型在移动端实时检测货架商品缺失:

# TensorFlow Lite 货架缺货检测模型
class ShelfStockDetector:
def __init__(self, model_path='shelf_detector.tflite'):
# 加载TFLite模型
self.interpreter = tf.lite.Interpreter(model_path=model_path)
self.interpreter.allocate_tensors()

self.input_details = self.interpreter.get_input_details()
self.output_details = self.interpreter.get_output_details()

def detect(self, image):
"""
检测货架商品缺失
:param image: RGB图像 (H, W, 3)
:return: 缺货区域列表 [(x1, y1, x2, y2, confidence), …]
"""
# 1. 预处理:resize + normalize
input_shape = self.input_details[0]['shape']
resized = cv2.resize(image, (input_shape[2], input_shape[1]))
normalized = resized.astype(np.float32) / 255.0
input_data = np.expand_dims(normalized, axis=0)

# 2. 推理
self.interpreter.set_tensor(self.input_details[0]['index'], input_data)
self.interpreter.invoke()

# 3. 后处理:获取检测结果
boxes = self.interpreter.get_tensor(self.output_details[0]['index'])[0]
scores = self.interpreter.get_tensor(self.output_details[1]['index'])[0]
classes = self.interpreter.get_tensor(self.output_details[2]['index'])[0]

# 4. 过滤低置信度检测
threshold = 0.5
stockout_regions = []

for i in range(len(scores)):
if scores[i] > threshold and classes[i] == 0: # 0=缺货区域
y1, x1, y2, x2 = boxes[i]
h, w = image.shape[:2]
stockout_regions.append((
int(x1 * w), int(y1 * h),
int(x2 * w), int(y2 * h),
float(scores[i])
))

return stockout_regions

def draw_result(self, image, stockout_regions):
"""在图像上绘制缺货区域"""
result = image.copy()

for (x1, y1, x2, y2, conf) in stockout_regions:
# 绘制红色矩形框
cv2.rectangle(result, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 绘制置信度标签
label = f'Stockout: {conf:.2f}'
cv2.putText(result, label, (x1, y1 – 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

return result

模型优化实践:

优化项

技术方案

效果

模型量化

FP32 → INT8

模型体积↓75%,推理速度↑1.8x

输入分辨率

640×640 → 320×320

推理速度↑3x,mAP↓0.05

模型剪枝

移除冗余卷积层

参数量↓40%,精度损失<2%

硬件加速

NNAPI(Android)/ Core ML(iOS)

推理速度↑2x

2.2 价签识别与比对

通过OCR识别价签价格,与系统价格自动比对:

# 价签OCR识别
class PriceTagOCR:
def __init__(self):
# 使用PaddleOCR轻量模型
self.ocr = PaddleOCR(
use_angle_cls=True,
lang='ch',
det_model_dir='ch_PP-OCRv4_det_infer',
rec_model_dir='ch_PP-OCRv4_rec_infer',
cls_model_dir='ch_ppocr_mobile_v2.0_cls_infer'
)

def recognize_price(self, image, sku_id):
"""
识别价签价格并比对系统价格
:param image: 价签区域图像
:param sku_id: 商品SKU
:return: 识别结果
"""
# 1. OCR识别
result = self.ocr.ocr(image, cls=True)

# 2. 提取数字(价格)
recognized_text = ''
for line in result:
if line is None: continue
for word_info in line:
text = word_info[1][0]
# 提取数字和小数点
price_match = re.search(r'\\d+\\.?\\d*', text)
if price_match:
recognized_text = price_match.group()
break

if not recognized_text:
return {
'success': False,
'error': '未识别到价格数字'
}

# 3. 转换为数字
try:
recognized_price = float(recognized_text)
except ValueError:
return {
'success': False,
'error': f'价格格式错误: {recognized_text}'
}

# 4. 与系统价格比对
system_price = self.get_system_price(sku_id)

is_match = abs(recognized_price – system_price) < 0.1 # 允许0.1元误差

return {
'success': True,
'recognized_price': recognized_price,
'system_price': system_price,
'is_match': is_match,
'difference': recognized_price – system_price,
'confidence': self.calculate_confidence(result)
}

def get_system_price(self, sku_id):
"""从系统获取商品价格"""
# 实际项目中调用API
# return priceService.getPrice(sku_id)
return 15.9 # 示例

def calculate_confidence(self, ocr_result):
"""计算OCR置信度"""
if not ocr_result or not ocr_result[0]:
return 0.0

scores = [word[1][1] for line in ocr_result if line for word in line]
return sum(scores) / len(scores) if scores else 0.0

三、IoT传感器联动

3.1 传感器数据采集

部署温湿度、门磁、客流计数器等传感器,实时监控门店环境:

// 传感器数据模型
@Data
public class SensorData {
private String sensorId;
private String sensorType; // TEMPERATURE / HUMIDITY / DOOR / FOOTFALL
private String storeId;
private String location; // 安装位置:冷藏柜A / 入口 / 后仓
private Double value; // 传感器值
private String unit; // 单位:℃ / % / COUNT
private LocalDateTime timestamp;
private String status; // NORMAL / WARNING / ALARM
}

// MQTT传感器数据接收器
@Component
public class SensorDataReceiver {

@Autowired
private SensorDataService sensorDataService;

@Autowired
private AlertService alertService;

/**
* 订阅传感器数据主题
* 主题格式:sensor/{storeId}/{sensorType}/{sensorId}
*/
@PostConstruct
public void subscribe() {
mqttClient.subscribe("sensor/+/+/+", (topic, payload) -> {
try {
// 1. 解析MQTT消息
String[] parts = topic.split("/");
String storeId = parts[1];
String sensorType = parts[2];
String sensorId = parts[3];

SensorDataPayload payloadObj = JsonUtils.fromJson(
new String(payload), SensorDataPayload.class
);

// 2. 构建传感器数据
SensorData data = SensorData.builder()
.sensorId(sensorId)
.sensorType(sensorType)
.storeId(storeId)
.value(payloadObj.getValue())
.unit(payloadObj.getUnit())
.timestamp(LocalDateTime.now())
.build();

// 3. 状态判定
data.setStatus(evaluateStatus(sensorType, payloadObj.getValue()));

// 4. 保存到数据库
sensorDataService.save(data);

// 5. 异常告警
if ("WARNING".equals(data.getStatus()) || "ALARM".equals(data.getStatus())) {
alertService.triggerSensorAlert(data);
}

} catch (Exception e) {
log.error("传感器数据处理失败, topic={}", topic, e);
}
});
}

/**
* 评估传感器状态
*/
private String evaluateStatus(String sensorType, Double value) {
switch (sensorType) {
case "TEMPERATURE":
// 冷藏柜温度:0-8℃正常,<-2℃或>10℃告警
if (value < -2 || value > 10) return "ALARM";
if (value < 0 || value > 8) return "WARNING";
return "NORMAL";

case "HUMIDITY":
// 湿度:40-70%正常,<30%或>80%告警
if (value < 30 || value > 80) return "ALARM";
if (value < 40 || value > 70) return "WARNING";
return "NORMAL";

case "DOOR":
// 门磁:0=关闭,1=开启,非营业时间开启告警
if (value > 0 && !isBusinessHours()) return "ALARM";
return "NORMAL";

case "FOOTFALL":
// 客流:异常高/低值预警
Double avg = getAverageFootfallToday();
if (value > avg * 2) return "WARNING"; // 客流激增
if (value < avg * 0.3) return "WARNING"; // 客流骤降
return "NORMAL";

default:
return "NORMAL";
}
}
}

@Data
class SensorDataPayload {
private Double value;
private String unit;
private Long timestamp;
}

3.2 传感器异常告警规则

@Component
public class SensorAlertRules {

/**
* 温度异常告警规则
*/
@Rule(name = "冷藏柜温度异常")
public boolean coldStorageTempAlert(SensorData data) {
return "TEMPERATURE".equals(data.getSensorType()) &&
("ALARM".equals(data.getStatus()) || "WARNING".equals(data.getStatus())) &&
data.getLocation().contains("冷藏");
}

/**
* 非营业时间门磁开启告警
*/
@Rule(name = "非营业时间门磁开启")
public boolean doorOpenAfterHours(SensorData data) {
return "DOOR".equals(data.getSensorType()) &&
data.getValue() > 0 && // 门开启
!isBusinessHours() && // 非营业时间
!"ALARM".equals(data.getStatus()); // 避免重复告警
}

/**
* 客流异常波动告警
*/
@Rule(name = "客流异常波动")
public boolean footfallAbnormal(SensorData data) {
if (!"FOOTFALL".equals(data.getSensorType())) return false;

// 连续3次客流低于平均值30%
List<SensorData> recent = sensorDataService.getRecentData(
data.getSensorId(), 3
);

long lowCount = recent.stream()
.filter(d -> "WARNING".equals(d.getStatus()))
.count();

return lowCount >= 3;
}

private boolean isBusinessHours() {
LocalTime now = LocalTime.now();
LocalTime open = LocalTime.of(7, 0); // 营业时间7:00
LocalTime close = LocalTime.of(22, 0); // 闭店时间22:00
return !now.isBefore(open) && now.isBefore(close);
}
}

四、智能盘点技术实现

4.1 蓝牙盘点枪集成

通过蓝牙连接盘点枪,实现快速扫码盘点:

// Flutter蓝牙盘点枪集成
class BluetoothScannerService {
final FlutterBlue flutterBlue = FlutterBlue.instance;
BluetoothDevice? connectedDevice;
StreamController<String> _scanResultController = StreamController.broadcast();

Stream<String> get scanResultStream => _scanResultController.stream;

// 搜索并连接盘点枪
Future<bool> connectToDevice() async {
try {
// 1. 搜索蓝牙设备
List<ScanResult> results = await flutterBlue.scan(
timeout: Duration(seconds: 5),
).toList();

// 2. 过滤盘点枪设备(根据名称或MAC)
ScanResult? scannerResult = results.firstWhere(
(r) => r.device.name.contains('Scanner') ||
r.device.id.id.startsWith('XX:XX:XX'), // 盘点枪MAC前缀
orElse: () => null,
);

if (scannerResult == null) {
showToast('未找到盘点枪设备');
return false;
}

// 3. 连接设备
connectedDevice = scannerResult.device;
await connectedDevice!.connect();

// 4. 监听扫码数据
_listenToScanData();

return true;

} catch (e) {
log.error('蓝牙连接失败: $e');
return false;
}
}

// 监听扫码数据
void _listenToScanData() {
if (connectedDevice == null) return;

// 订阅通知特征值(盘点枪扫码后会发送通知)
connectedDevice!.discoverServices().then((services) {
for (BluetoothService service in services) {
for (BluetoothCharacteristic characteristic in service.characteristics) {
// 找到扫码数据特征值(根据UUID)
if (characteristic.uuid.toString() == '0000XXXX-0000-1000-8000-00805F9B34FB') {
characteristic.setNotifyValue(true);
characteristic.onValueChanged().listen((value) {
// 解析扫码数据
String barcode = String.fromCharCodes(value);
_scanResultController.add(barcode);
});
}
}
}
});
}

// 断开连接
Future<void> disconnect() async {
if (connectedDevice != null && connectedDevice!.isConnected) {
await connectedDevice!.disconnect();
}
connectedDevice = null;
}
}

// 盘点页面
class InventoryCountingPage extends StatefulWidget {
@override
_InventoryCountingPageState createState() => _InventoryCountingPageState();
}

class _InventoryCountingPageState extends State<InventoryCountingPage> {
BluetoothScannerService scanner = BluetoothScannerService();
Map<String, int> countedItems = {}; // 已盘点商品:barcode -> count
bool isBluetoothConnected = false;

@override
void initState() {
super.initState();
initBluetooth();
}

Future<void> initBluetooth() async {
bool connected = await scanner.connectToDevice();
if (mounted) {
setState(() {
isBluetoothConnected = connected;
});

if (connected) {
showToast('盘点枪连接成功');
}
}
}

@override
void dispose() {
scanner.disconnect();
scanner.scanResultStream.close();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('智能盘点'),
actions: [
IconButton(
icon: Icon(isBluetoothConnected ? Icons.bluetooth : Icons.bluetooth_disabled),
onPressed: isBluetoothConnected ? null : initBluetooth,
),
],
),
body: Column(
children: [
// 扫码结果显示区域
StreamBuilder<String>(
stream: scanner.scanResultStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return Container();

String barcode = snapshot.data!;

// 自动累加计数
setState(() {
countedItems[barcode] = (countedItems[barcode] ?? 0) + 1;
});

// 播放扫码音效
playScanSound();

return Container(
padding: EdgeInsets.all(16),
color: Colors.green[100],
child: Text('扫码成功: $barcode', style: TextStyle(fontSize: 16)),
);
},
),

// 盘点进度
Expanded(
child: ListView.builder(
itemCount: countedItems.length,
itemBuilder: (context, index) {
String barcode = countedItems.keys.elementAt(index);
int count = countedItems[barcode]!;

return ListTile(
title: Text('商品: $barcode'),
subtitle: Text('数量: $count'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
countedItems.remove(barcode);
});
},
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: submitInventory,
child: Icon(Icons.upload),
tooltip: '提交盘点',
),
);
}

Future<void> submitInventory() async {
if (countedItems.isEmpty) {
showToast('请先进行盘点');
return;
}

// 构建盘点数据
List<InventoryItem> items = countedItems.entries.map((entry) {
return InventoryItem(
barcode: entry.key,
countedQty: entry.value,
);
}).toList();

// 提交到云端
bool success = await InventoryApi.submitCounting(items);

if (success) {
showToast('盘点提交成功');
Navigator.pop(context);
} else {
showToast('提交失败,请重试');
}
}
}

4.2 盘点差异自动处理

@Component
public class InventoryDifferenceHandler {

/**
* 处理盘点差异
* @param countedItems 盘点结果
* @param systemItems 系统库存
*/
public DifferenceReport processDifferences(
List<InventoryItem> countedItems,
List<InventoryItem> systemItems
) {
// 1. 构建索引
Map<String, InventoryItem> systemIndex = systemItems.stream()
.collect(Collectors.toMap(InventoryItem::getBarcode, Function.identity()));

// 2. 计算差异
List<DifferenceItem> differences = new ArrayList<>();
BigDecimal totalDifferenceAmount = BigDecimal.ZERO;

for (InventoryItem counted : countedItems) {
InventoryItem system = systemIndex.get(counted.getBarcode());

if (system == null) {
// 差异1:盘盈(系统无记录)
DifferenceItem diff = DifferenceItem.builder()
.barcode(counted.getBarcode())
.differenceType("SURPLUS")
.systemQty(0)
.countedQty(counted.getCountedQty())
.differenceQty(counted.getCountedQty())
.build();
differences.add(diff);

} else {
int diffQty = counted.getCountedQty() – system.getSystemQty();

if (diffQty != 0) {
// 差异2:数量不一致
DifferenceItem diff = DifferenceItem.builder()
.barcode(counted.getBarcode())
.differenceType(diffQty > 0 ? "SURPLUS" : "SHORTAGE")
.systemQty(system.getSystemQty())
.countedQty(counted.getCountedQty())
.differenceQty(diffQty)
.build();
differences.add(diff);

// 计算差异金额
BigDecimal itemAmount = skuService.getPrice(counted.getBarcode())
.multiply(BigDecimal.valueOf(Math.abs(diffQty)));
totalDifferenceAmount = totalDifferenceAmount.add(itemAmount);
}
}
}

// 3. 生成差异报告
return DifferenceReport.builder()
.totalItems(countedItems.size())
.differenceCount(differences.size())
.totalDifferenceAmount(totalDifferenceAmount)
.differences(differences)
.build();
}

/**
* 自动调账(小额差异)
*/
public void autoAdjust(DifferenceReport report) {
for (DifferenceItem diff : report.getDifferences()) {
// 差异金额 < 50元:自动调账
BigDecimal diffAmount = skuService.getPrice(diff.getBarcode())
.multiply(BigDecimal.valueOf(Math.abs(diff.getDifferenceQty())));

if (diffAmount.compareTo(BigDecimal.valueOf(50)) <= 0) {
// 生成调账凭证
AdjustmentVoucher voucher = AdjustmentVoucher.builder()
.barcode(diff.getBarcode())
.adjustmentQty(diff.getDifferenceQty())
.reason("盘点差异自动调账(<50元)")
.operator("SYSTEM")
.build();

inventoryService.adjustStock(voucher);
}
}
}
}

@Data
@Builder
public class DifferenceReport {
private int totalItems; // 盘点商品总数
private int differenceCount; // 差异商品数
private BigDecimal totalDifferenceAmount; // 差异总金额
private List<DifferenceItem> differences; // 差异明细
}

@Data
@Builder
public class DifferenceItem {
private String barcode;
private String differenceType; // SURPLUS(盘盈)/ SHORTAGE(盘亏)
private int systemQty; // 系统数量
private int countedQty; // 盘点数量
private int differenceQty; // 差异数量(正=盘盈,负=盘亏)
}

五、实际部署效果

基于该方案的系统在实际部署中达到:

指标

优化前

优化后

日常巡检耗时

60-90分钟/店/日

15-20分钟/店/日

月度全盘耗时

闭店4-8小时

1-2小时(无需闭店)

巡检问题发现时效

4-6小时

实时(<5分钟)

盘点准确率

90%左右

98%+

人工记录错误率

15%+

<2%

注:以上数据基于典型便利店场景实测,实际效果受门店面积、商品SKU数量、网络环境影响。


总结

智能巡检与盘点系统的技术价值在于将重复劳动转化为自动化流程,核心设计原则:

  • 移动端优先
    采用Flutter等跨平台技术,支持离线操作与自动同步,适配店员高频移动场景。
  • 边缘计算赋能
    图像识别、传感器数据聚合等计算下沉至边缘端,减少云端依赖,提升响应速度。
  • 闭环管理
    从任务下发→执行→异常告警→处理反馈,形成完整闭环,避免"发现问题但无人跟进"的断层。
  • 该方案已在部分零售SaaS系统中实践,技术核心不在于算法创新,而在于精准匹配门店运营的轻量级需求:无需昂贵硬件投入,基于普通手机+低成本传感器即可构建实用巡检能力。门店管理的终极目标不是"完全替代人工",而是"让店员从重复记录中解放,专注于客户服务与异常处理"。

    注:本文仅讨论智能巡检与盘点系统的技术实现方案,所有组件基于开源技术栈。文中提及的行业实践仅为技术存在性佐证,不构成商业产品推荐。实际部署需结合具体门店条件与业务需求调整。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 零售门店智能巡检与盘点系统:物联网与图像识别技术实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!