创建数据库
CREATE TABLE `product` (
`id` int NOT NULL AUTO_INCREMENT,
`barcode` varchar(50) NOT NULL COMMENT '主条码',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`display_name` varchar(150) DEFAULT NULL COMMENT '显示名称(如:100元6包XX)',
`spec` varchar(100) DEFAULT NULL COMMENT '规格',
`origin` varchar(100) DEFAULT NULL COMMENT '产地',
`price` decimal(10,2) NOT NULL COMMENT '单品售价',
`unit` varchar(20) DEFAULT '件' COMMENT '单位',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`stock` int DEFAULT '0' COMMENT '库存',
-- 捆绑销售相关字段
`bundle_enabled` tinyint DEFAULT '0' COMMENT '捆绑销售启用:0-不启用,1-启用',
`bundle_quantity` int DEFAULT '1' COMMENT '捆绑数量',
`bundle_price` decimal(10,2) DEFAULT NULL COMMENT '捆绑价格',
`bundle_barcode` varchar(50) DEFAULT NULL COMMENT '捆绑条码(可选)',
`status` tinyint DEFAULT '1' COMMENT '状态:1-上架,0-下架',
`create_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3),
`update_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `idx_barcode` (`barcode`),
UNIQUE KEY `idx_bundle_barcode` (`bundle_barcode`),
KEY `idx_name` (`name`),
KEY `idx_category` (`category`),
KEY `idx_status_bundle` (`status`, `bundle_enabled`),
KEY `idx_category_status` (`category`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
-- 多条码表:一个商品可以有多个条码
CREATE TABLE `product_barcodes` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL COMMENT '商品ID',
`barcode` varchar(50) NOT NULL COMMENT '附加条码',
`type` varchar(20) DEFAULT 'normal' COMMENT '条码类型:normal-普通, bundle-捆绑',
`status` tinyint DEFAULT '1' COMMENT '状态',
`create_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `idx_barcode` (`barcode`),
KEY `idx_product_type` (`product_id`, `type`),
KEY `idx_product_status` (`product_id`, `status`),
CONSTRAINT `fk_product_barcodes_product`
FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品多条码表';
-- 插入商品数据(包含捆绑销售)
INSERT INTO `product` (`barcode`, `name`, `display_name`, `spec`, `origin`, `price`, `unit`, `category`, `stock`, `bundle_enabled`, `bundle_quantity`, `bundle_price`, `bundle_barcode`) VALUES
('6934024550017', '康师傅冰红茶', '100元6包康师傅冰红茶', '500ml', '上海', 3.50, '瓶', '茶饮料', 100, 1, 6, 100.00, '6934024550018'),
('6902538005148', '王老吉凉茶', NULL, '310ml', '广州', 4.00, '罐', '凉茶', 50, 0, 1, NULL, NULL),
('6901424332025', '可口可乐', '80元24罐可口可乐套装', '330ml', '北京', 2.50, '罐', '碳酸饮料', 200, 1, 24, 80.00, '6901424332026'),
('6940557300096', '百事可乐', NULL, '330ml', '深圳', 2.50, '罐', '碳酸饮料', 150, 0, 1, NULL, NULL);
-- 插入多条码数据
INSERT INTO `product_barcodes` (`product_id`, `barcode`, `type`) VALUES
(1, '6934024550019', 'normal'), -- 康师傅冰红茶附加条码
(1, '6934024550020', 'normal'),
(3, '6901424332027', 'normal'), -- 可口可乐附加条码
(3, '6901424332028', 'normal'),
(3, '6901424332029', 'bundle'); -- 可口可乐捆绑条码
主要优化点
1. 修复整数显示宽度警告
将 tinyint(1) 改为 tinyint
移除所有整数字段的显示宽度
2. 索引优化
product表:
添加 bundle_barcode 的唯一索引,避免重复捆绑条码
合并 status 和 bundle_enabled 为复合索引,适合常见查询场景
添加 category 和 status 的复合索引
移除单独的 bundle_enabled 索引(使用复合索引替代)
product_barcodes表:
移除冗余的 idx_product 和 idx_product_barcode 索引
添加 idx_product_type 用于按类型查询条码
添加 idx_product_status 用于状态查询
3. 数据完整性优化
为 bundle_barcode 添加唯一约束,确保捆绑条码不重复
添加了 stock 字段的默认值
4. 查询性能优化
复合索引设计更适合以下常见查询:
sql
-- 查找某个分类下上架的商品
SELECT * FROM product WHERE category = '茶饮料' AND status = 1;
-- 查找启用捆绑销售的上架商品
SELECT * FROM product WHERE status = 1 AND bundle_enabled = 1;
-- 按类型查询商品的条码
SELECT * FROM product_barcodes WHERE product_id = 1 AND type = 'normal';
5. 建议的额外优化
sql
-- 如果需要更严格的枚举约束,可以考虑使用ENUM
ALTER TABLE `product_barcodes`
MODIFY COLUMN `type` ENUM('normal','bundle') DEFAULT 'normal' COMMENT '条码类型';
-- 为商品名称添加全文索引(如果需要搜索)
ALTER TABLE `product` ADD FULLTEXT KEY `idx_fulltext_name` (`name`, `display_name`);
这样修复后既消除了警告,又提升了数据库的性能和数据完整性。前端WEB代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>超市收银系统 v1.1</title>
<link href="/static/layui/css/layui.css" rel="stylesheet">
<script src="/static/layui/layui.js"></script>
<style>
.cashier-container { padding: 15px; }
.barcode-input { margin-bottom: 15px; }
.product-table { margin-top: 20px; }
.total-area {
margin-top: 20px;
padding: 15px;
background: #f8f8f8;
border-radius: 4px;
font-size: 16px;
font-weight: bold;
}
.product-name { font-weight: bold; }
.spec-info { font-size: 12px; color: #666; }
.bundle-badge { margin-left: 5px; }
.original-price { text-decoration: line-through; color: #999; font-size: 12px; }
.discount-input { width: 80px; display: inline-block; }
.price-input { width: 100px; display: inline-block; }
.quantity-input { width: 80px; }
.row-total { font-weight: bold; color: #ff5722; }
.discount-info { color: #ff5722; font-size: 12px; }
.mode-switch { margin-bottom: 10px; }
</style>
</head>
<body>
<div class="cashier-container">
<!-- 条码输入区域 -->
<div class="barcode-input">
<input type="text" id="barcodeInput" autocomplete="off" lay-affix="clear"
placeholder="请扫描商品条码" class="layui-input">
</div>
<!-- 商品列表 -->
<table class="layui-table product-table" id="productTable">
<thead>
<tr>
<th width="40">模式</th>
<th width="120">条码</th>
<th>商品信息</th>
<th width="120">单价</th>
<th width="150">数量/折扣</th>
<th width="100">小计</th>
<th width="80">操作</th>
</tr>
</thead>
<tbody id="productList">
<!-- 商品行将通过JS动态添加 -->
</tbody>
</table>
<!-- 总计区域 -->
<div class="total-area layui-text-right">
<div>商品数量:<span id="totalQuantity">0</span> 件</div>
<div>合计金额:<span id="totalAmount">0.00</span> 元</div>
<div>应收金额:<span id="receivableAmount">0.00</span> 元</div>
</div>
</div>
<script>
layui.use(['jquery', 'layer'], function(){
var $ = layui.$;
var layer = layui.layer;
// IndexedDB数据库实例
var db = null;
// 购物车商品列表
var cart = [];
/**
* 初始化IndexedDB数据库
*/
function initIndexedDB() {
var request = indexedDB.open("SupermarketDB", 2); // 版本升级到2
request.onerror = function(event) {
console.error("数据库打开失败:", event.target.error);
layer.msg('本地数据库初始化失败,将使用网络查询');
};
request.onsuccess = function(event) {
db = event.target.result;
console.log("数据库打开成功");
checkDataExists();
};
request.onupgradeneeded = function(event) {
db = event.target.result;
// 删除旧的对象存储(如果存在)
if (db.objectStoreNames.contains('products')) {
db.deleteObjectStore('products');
}
// 创建新的对象存储
var objectStore = db.createObjectStore('products', { keyPath: 'id' });
objectStore.createIndex('barcode', 'barcode', { unique: true });
objectStore.createIndex('name', 'name', { unique: false });
};
}
/**
* 检查本地数据是否存在
*/
function checkDataExists() {
if (!db) return;
var transaction = db.transaction(['products'], 'readonly');
var store = transaction.objectStore('products');
var countRequest = store.count();
countRequest.onsuccess = function() {
if (countRequest.result === 0) {
layer.msg('正在下载商品数据...', { icon: 16, time: 0 });
downloadProductData();
} else {
layer.msg('本地数据加载完成,可以开始扫描', { icon: 1 });
}
};
}
/**
* 下载商品数据
*/
function downloadProductData() {
$.get('/api/product/exportProducts', function(res) {
layer.closeAll();
if (res.code === 1 && res.data.length > 0) {
var transaction = db.transaction(['products'], 'readwrite');
var store = transaction.objectStore('products');
res.data.forEach(function(product) {
store.add(product);
});
transaction.oncomplete = function() {
layer.msg('商品数据下载完成,共 ' + res.data.length + ' 条', { icon: 1 });
};
} else {
layer.msg('服务器数据获取失败');
}
}).fail(function() {
layer.closeAll();
layer.msg('网络请求失败,请检查连接');
});
}
/**
* 在IndexedDB中查询商品
*/
function searchProduct(barcode) {
return new Promise(function(resolve, reject) {
if (!db) {
reject('数据库未就绪');
return;
}
var transaction = db.transaction(['products'], 'readonly');
var store = transaction.objectStore('products');
var index = store.index('barcode');
var request = index.get(barcode);
request.onsuccess = function() {
var product = request.result;
if (product) {
resolve(product);
return;
}
// 搜索附加条码
var cursorRequest = store.openCursor();
var foundProduct = null;
cursorRequest.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
var product = cursor.value;
if (product.additional_barcodes) {
var found = product.additional_barcodes.find(function(additional) {
return additional.barcode === barcode;
});
if (found) {
foundProduct = product;
}
}
if (!foundProduct) {
cursor.continue();
} else {
resolve(foundProduct);
}
} else {
resolve(null);
}
};
};
request.onerror = function() {
reject('查询失败');
};
});
}
/**
* 处理条码输入
*/
function handleBarcodeInput(barcode) {
searchProduct(barcode).then(function(product) {
if (product) {
addToCart(product);
} else {
layer.msg('本地未找到商品:' + barcode);
}
}).catch(function(error) {
console.error("查询错误:", error);
layer.msg('查询失败,请重试');
});
}
/**
* 添加商品到购物车
*/
function addToCart(product) {
var cartItem = {
id: product.id,
barcode: product.barcode,
name: product.display_name || product.name,
spec: product.spec,
origin: product.origin,
unit: product.unit,
// 价格相关
original_price: parseFloat(product.price),
current_price: parseFloat(product.price),
quantity: 1,
discount: 1.00,
// 捆绑销售相关
bundle_enabled: product.bundle_enabled || 0,
bundle_quantity: product.bundle_quantity || 1,
bundle_price: product.bundle_price ? parseFloat(product.bundle_price) : null,
// 模式:0-单品模式,1-捆绑模式
sale_mode: 0,
// 计算字段
subtotal: 0,
original_subtotal: 0
};
// 如果商品支持捆绑销售,默认使用捆绑模式
if (cartItem.bundle_enabled === 1 && cartItem.bundle_price) {
cartItem.sale_mode = 1;
cartItem.current_price = cartItem.bundle_price;
cartItem.quantity = 1; // 捆绑模式数量固定为1
}
calculateItemTotal(cartItem);
// 检查是否已存在相同商品相同模式
var existingIndex = cart.findIndex(function(item) {
return item.id === cartItem.id && item.sale_mode === cartItem.sale_mode;
});
if (existingIndex !== -1) {
cart[existingIndex].quantity += cartItem.quantity;
calculateItemTotal(cart[existingIndex]);
} else {
cart.push(cartItem);
}
renderCart();
var message = cartItem.sale_mode === 1 ?
`已添加捆绑装:${cartItem.name}` :
`已添加:${cartItem.name}`;
layer.msg(message, { icon: 1 });
}
/**
* 计算单个商品的总价
*/
function calculateItemTotal(item) {
if (item.sale_mode === 1) {
// 捆绑模式
item.original_subtotal = item.original_price * item.bundle_quantity;
item.subtotal = item.current_price;
} else {
// 单品模式
item.original_subtotal = item.original_price * item.quantity;
item.subtotal = item.current_price * item.quantity * item.discount;
}
}
/**
* 计算购物车总计
*/
function calculateCartTotal() {
var totalQuantity = 0;
var totalAmount = 0;
var originalTotal = 0;
cart.forEach(function(item) {
if (item.sale_mode === 1) {
totalQuantity += item.bundle_quantity * item.quantity;
} else {
totalQuantity += item.quantity;
}
totalAmount += item.subtotal;
originalTotal += item.original_subtotal;
});
return {
totalQuantity: totalQuantity,
totalAmount: totalAmount,
originalTotal: originalTotal
};
}
/**
* 渲染购物车
*/
function renderCart() {
var $tbody = $('#productList');
$tbody.empty();
var totals = calculateCartTotal();
cart.forEach(function(item, index) {
var discountPercent = ((1 - item.discount) * 100).toFixed(1);
var discountText = item.discount < 1 ? `(${discountPercent}% off)` : '';
var $tr = $('<tr></tr>');
$tr.html(`
<td>
${item.bundle_enabled === 1 ? `
<input type="checkbox" name="modeSwitch" lay-skin="switch"
lay-text="单品|捆绑" data-index="${index}"
${item.sale_mode === 1 ? 'checked' : ''}>
` : '<span class="layui-badge">单品</span>'}
</td>
<td>
<div>${item.barcode}</div>
${item.sale_mode === 1 ? '<span class="layui-badge layui-bg-orange bundle-badge">捆绑</span>' : ''}
</td>
<td>
<div class="product-name">${item.name}</div>
<div class="spec-info">
${item.spec ? '规格:' + item.spec : ''}
${item.origin ? '产地:' + item.origin : ''}
</div>
${item.sale_mode === 1 ? `<div class="spec-info">${item.bundle_quantity}${item.unit}/组</div>` : ''}
</td>
<td>
<div>
<input type="number" class="layui-input price-input"
data-index="${index}" value="${item.current_price.toFixed(2)}"
step="0.01" min="0.01">
</div>
<div class="original-price">原价:${item.original_price.toFixed(2)}</div>
</td>
<td>
${item.sale_mode === 1 ? `
<div>数量:<span class="layui-badge">${item.quantity}</span></div>
` : `
<div>数量:
<input type="number" class="layui-input quantity-input"
data-index="${index}" value="${item.quantity}" min="1">
</div>
`}
<div style="margin-top: 5px;">
折扣:<input type="number" class="layui-input discount-input"
data-index="${index}" value="${item.discount.toFixed(2)}"
step="0.01" min="0.01" max="1.00">
</div>
<div class="discount-info">${discountText}</div>
</td>
<td>
<div class="row-total">${item.subtotal.toFixed(2)}</div>
<div class="original-price">原价:${item.original_subtotal.toFixed(2)}</div>
</td>
<td>
<button class="layui-btn layui-btn-sm layui-btn-danger remove-btn"
data-index="${index}">删除</button>
</td>
`);
$tbody.append($tr);
});
// 更新总计
$('#totalQuantity').text(totals.totalQuantity);
$('#totalAmount').text(totals.totalAmount.toFixed(2));
$('#receivableAmount').text(totals.totalAmount.toFixed(2));
// 绑定事件
bindCartEvents();
}
/**
* 绑定购物车事件
*/
function bindCartEvents() {
// 模式切换
$('input[name="modeSwitch"]').on('change', function() {
var index = $(this).data('index');
var isBundleMode = $(this).is(':checked');
cart[index].sale_mode = isBundleMode ? 1 : 0;
if (isBundleMode) {
cart[index].current_price = cart[index].bundle_price || cart[index].original_price;
cart[index].quantity = 1;
} else {
cart[index].current_price = cart[index].original_price;
}
calculateItemTotal(cart[index]);
renderCart();
});
// 价格修改
$('.price-input').on('change', function() {
var index = $(this).data('index');
var newPrice = parseFloat($(this).val()) || cart[index].original_price;
cart[index].current_price = newPrice;
calculateItemTotal(cart[index]);
renderCart();
});
// 数量修改
$('.quantity-input').on('change', function() {
var index = $(this).data('index');
var newQuantity = parseInt($(this).val()) || 1;
cart[index].quantity = newQuantity;
calculateItemTotal(cart[index]);
renderCart();
});
// 折扣修改
$('.discount-input').on('change', function() {
var index = $(this).data('index');
var newDiscount = parseFloat($(this).val()) || 1.00;
if (newDiscount > 1) newDiscount = 1.00;
if (newDiscount < 0.01) newDiscount = 0.01;
cart[index].discount = newDiscount;
calculateItemTotal(cart[index]);
renderCart();
});
// 删除商品
$('.remove-btn').on('click', function() {
var index = $(this).data('index');
cart.splice(index, 1);
renderCart();
layer.msg('商品已删除');
});
}
// 条码输入框事件
$('#barcodeInput').on('keydown', function(e) {
if (e.keyCode === 13) {
var barcode = $(this).val().trim();
if (barcode) {
handleBarcodeInput(barcode);
}
$(this).val('').focus();
return false;
}
});
// 页面加载完成后初始化
$(document).ready(function() {
initIndexedDB();
$('#barcodeInput').focus();
});
});
</script>
</body>
</html>ThinkPHP8后端代码
application/api/controller/Product.php
控制器
<?php
namespace app\api\controller;
use think\facade\Db;
class Product
{
/**
* 导出所有商品数据(包含多条码和捆绑信息)
*/
public function exportProducts()
{
try {
// 获取所有上架的商品基本信息
$products = Db::name('product')
->field('id, barcode, name, display_name, spec, origin, price, unit, category, bundle_enabled, bundle_quantity, bundle_price, bundle_barcode')
->where('status', 1)
->select()
->toArray();
// 获取所有附加条码
$additionalBarcodes = Db::name('product_barcodes')
->field('product_id, barcode, type')
->where('status', 1)
->select();
// 将附加条码合并到商品数据中
$barcodeMap = [];
foreach ($additionalBarcodes as $barcode) {
$barcodeMap[$barcode['product_id']][] = [
'barcode' => $barcode['barcode'],
'type' => $barcode['type']
];
}
foreach ($products as &$product) {
$product['additional_barcodes'] = $barcodeMap[$product['id']] ?? [];
// 确保价格是浮点数
$product['price'] = (float)$product['price'];
if ($product['bundle_price']) {
$product['bundle_price'] = (float)$product['bundle_price'];
}
}
return json([
'code' => 1,
'msg' => 'success',
'data' => $products
]);
} catch (\Exception $e) {
return json([
'code' => 0,
'msg' => '数据导出失败:' . $e->getMessage()
]);
}
}
}前端JavaScript逻辑
/**
* 在IndexedDB中查询商品(支持多条码)
*/
function searchProduct(barcode) {
return new Promise(function(resolve, reject) {
if (!db) {
reject('数据库未就绪');
return;
}
var transaction = db.transaction(['products'], 'readonly');
var store = transaction.objectStore('products');
// 先直接查询主条码
var request = store.get(barcode);
request.onsuccess = function() {
var product = request.result;
if (product) {
resolve(product);
return;
}
// 如果主条码没找到,在additional_barcodes中查找
var index = store.index('barcode');
var cursorRequest = store.openCursor();
cursorRequest.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
var product = cursor.value;
// 检查附加条码
if (product.additional_barcodes && product.additional_barcodes.length > 0) {
var found = product.additional_barcodes.find(function(additional) {
return additional.barcode === barcode;
});
if (found) {
resolve(product);
return;
}
}
cursor.continue();
} else {
resolve(null); // 没找到
}
};
cursorRequest.onerror = function() {
reject('查询附加条码失败');
};
};
request.onerror = function() {
reject('查询失败');
};
});
}
/**
* 添加商品到购物车(支持捆绑销售)
*/
function addToCart(product) {
var cartItem;
// 检查捆绑销售
if (product.bundle_enabled === 1) {
// 使用捆绑销售信息
cartItem = {
barcode: product.barcode,
name: product.display_name || (product.name + ' ' + product.bundle_quantity + product.unit + '装'),
spec: product.spec,
origin: product.origin,
price: parseFloat(product.bundle_price),
unit: product.unit,
quantity: 1,
bundle_quantity: product.bundle_quantity,
is_bundle: true,
original_price: parseFloat(product.price)
};
} else {
// 普通单品
cartItem = {
barcode: product.barcode,
name: product.display_name || product.name,
spec: product.spec,
origin: product.origin,
price: parseFloat(product.price),
unit: product.unit,
quantity: 1,
is_bundle: false
};
}
// 检查是否已在购物车中
var existingItem = cart.find(function(item) {
return item.barcode === cartItem.barcode && item.is_bundle === cartItem.is_bundle;
});
if (existingItem) {
existingItem.quantity += 1;
} else {
cart.push(cartItem);
}
renderCart();
var message = cartItem.is_bundle ?
`已添加捆绑装:${cartItem.name} (${cartItem.bundle_quantity}${cartItem.unit})` :
`已添加:${cartItem.name}`;
layer.msg(message, { icon: 1 });
}
/**
* 渲染购物车(显示规格产地信息)
*/
function renderCart() {
var $tbody = $('#productList');
$tbody.empty();
var totalAmount = 0;
cart.forEach(function(item, index) {
var subtotal = item.price * item.quantity;
totalAmount += subtotal;
// 显示规格和产地信息
var specInfo = '';
if (item.spec) specInfo += `<div class="spec">规格:${item.spec}</div>`;
if (item.origin) specInfo += `<div class="origin">产地:${item.origin}</div>`;
// 如果是捆绑装,显示原价信息
var priceInfo = item.is_bundle ?
`<div>捆绑价:${item.price.toFixed(2)}</div>
<div class="original-price">原价:${item.original_price.toFixed(2)} × ${item.bundle_quantity}</div>` :
`<div>单价:${item.price.toFixed(2)}</div>`;
var $tr = $('<tr></tr>');
$tr.html(`
<td>
<div>${item.barcode}</div>
${item.is_bundle ? '<span class="layui-badge layui-bg-orange">捆绑</span>' : ''}
</td>
<td>
<div class="product-name">${item.name}</div>
${specInfo}
</td>
<td>${priceInfo}</td>
<td>
<div class="layui-input-inline" style="width: 100px;">
<input type="number" class="layui-input quantity-input"
data-index="${index}" value="${item.quantity}" min="1">
</div>
${item.is_bundle ? `<div class="bundle-info">${item.bundle_quantity}${item.unit}/组</div>` : ''}
</td>
<td>${item.unit}</td>
<td>${subtotal.toFixed(2)}</td>
<td>
<button class="layui-btn layui-btn-sm layui-btn-danger remove-btn"
data-index="${index}">删除</button>
</td>
`);
$tbody.append($tr);
});
$('#totalAmount').text(totalAmount.toFixed(2));
// 绑定事件...
$('.quantity-input').on('change', function() {
var index = $(this).data('index');
var newQuantity = parseInt($(this).val()) || 1;
cart[index].quantity = newQuantity;
renderCart();
});
$('.remove-btn').on('click', function() {
var index = $(this).data('index');
cart.splice(index, 1);
renderCart();
});
}
评论区