外卖点餐连锁店餐饮生鲜奶茶外卖店内扫码点餐源码同城外卖校园外卖源码的扫码逻辑
扫码点餐系统 - 完整扫码逻辑 源码示例外卖点餐 | 连锁店 | 餐饮生鲜 | 奶茶 | 店内扫码点餐 | 同城外卖 | 校园外卖 扫码业务场景总览场景扫码后行为核心逻辑️ 店内扫码点餐进入店铺菜单页识别店铺ID → 加载菜单 外卖下单进入外卖店铺页识别店铺位置 → 计算配送费 营销活动领券/红包页识别活动ID → 发放优惠券 员工登录员工管理后台识别员工ID → 权限验证 自取码取餐码页面识别订单ID → 显示取餐码 一、后端扫码逻辑 (Java Spring Boot)1️⃣ 扫码码生成规则java/** * 二维码内容格式: * 店内点餐: CAMPUS://SHOP?id1001tableA05 * 外卖下单: CAMPUS://DELIVERY?shopId1001address宿舍3号楼 * 营销活动: CAMPUS://COUPON?id2001type1 * 取餐码: CAMPUS://PICKUP?orderNoTX20240101001 */ Service public class QRCodeService { Autowired private ShopMapper shopMapper; /** * 生成店内点餐二维码 */ public String generateShopQRCode(Long shopId, String tableNo) { // 格式: CAMPUS://SHOP?id1001tableA05 String content String.format(CAMPUS://SHOP?id%dtable%s, shopId, tableNo); // 使用ZXing生成二维码图片(Base64) return QRCodeUtil.generateBase64(content, 300, 300); } /** * 生成外卖店铺二维码 */ public String generateDeliveryQRCode(Long shopId, String address) { String content String.format(CAMPUS://DELIVERY?shopId%daddress%s, shopId, URLEncoder.encode(address, StandardCharsets.UTF_8)); return QRCodeUtil.generateBase64(content, 300, 300); } /** * 生成营销活动二维码 */ public String generateCouponQRCode(Long activityId, Integer type) { String content String.format(CAMPUS://COUPON?id%dtype%d, activityId, type); return QRCodeUtil.generateBase64(content, 300, 300); } /** * 生成取餐码二维码 */ public String generatePickupQRCode(String orderNo) { String content String.format(CAMPUS://PICKUP?orderNo%s, orderNo); return QRCodeUtil.generateBase64(content, 300, 300); } }2️⃣ 扫码解析Controller ⭐⭐⭐javaRestController RequestMapping(/api/scan) CrossOrigin public class ScanController { Autowired private ShopService shopService; Autowired private CouponService couponService; Autowired private OrderService orderService; /** * 核心扫码后统一入口 * 前端传入: ?codeCAMPUS://SHOP?id1001tableA05 */ GetMapping(/parse) public Result? parseQRCode(RequestParam String code) { log.info(扫码内容: {}, code); // 1. 解析协议头 if (!code.startsWith(CAMPUS://)) { return Result.error(无效的二维码); } String path code.substring(11); // 去掉 CAMPUS:// String[] parts path.split(\\?); String type parts[0]; // SHOP / DELIVERY / COUPON / PICKUP switch (type) { case SHOP: return parseShopQRCode(parts[1]); case DELIVERY: return parseDeliveryQRCode(parts[1]); case COUPON: return parseCouponQRCode(parts[1]); case PICKUP: return parsePickupQRCode(parts[1]); default: return Result.error(未知的二维码类型); } } /** * ️ 店内点餐二维码解析 */ private Result? parseShopQRCode(String params) { MapString, String paramMap parseParams(params); Long shopId Long.parseLong(paramMap.get(id)); String tableNo paramMap.get(table); // 1. 查询店铺信息 Shop shop shopService.getById(shopId); if (shop null) { return Result.error(店铺不存在); } // 2. 查询店铺菜单 ListMenuItem menuList shopService.getMenuByShopId(shopId); // 3. 查询桌台信息(可选) TableInfo table null; if (StringUtils.isNotBlank(tableNo)) { table tableService.getByTableNo(shopId, tableNo); } // 4. 返回前端所需数据 MapString, Object data new HashMap(); data.put(shop, shop); data.put(menu, menuList); data.put(table, table); data.put(scanTime, LocalDateTime.now()); // 记录扫码时间(用于统计) // 5. 记录扫码日志(用于数据分析) scanLogService.save(shopId, tableNo, SHOP); return Result.success(data); } /** * 外卖下单二维码解析 */ private Result? parseDeliveryQRCode(String params) { MapString, String paramMap parseParams(params); Long shopId Long.parseLong(paramMap.get(shopId)); String address paramMap.get(address); Shop shop shopService.getById(shopId); if (shop null) { return Result.error(店铺不存在); } // 计算配送费 BigDecimal deliveryFee calculateDeliveryFee(shopId, address); MapString, Object data new HashMap(); data.put(shop, shop); data.put(deliveryFee, deliveryFee); data.put(address, address); data.put(isDelivery, true); // 标记为外卖模式 return Result.success(data); } /** * 营销活动二维码解析 */ private Result? parseCouponQRCode(String params) { MapString, String paramMap parseParams(params); Long activityId Long.parseLong(paramMap.get(id)); Integer type Integer.parseInt(paramMap.get(type)); CouponActivity activity couponService.getById(activityId); if (activity null || activity.getStatus() ! 1) { return Result.error(活动不存在或已结束); } // 检查是否已领取 boolean alreadyReceived couponService.hasReceived(activityId); MapString, Object data new HashMap(); data.put(activity, activity); data.put(alreadyReceived, alreadyReceived); return Result.success(data); } /** * 取餐码二维码解析 */ private Result? parsePickupQRCode(String params) { MapString, String paramMap parseParams(params); String orderNo paramMap.get(orderNo); Order order orderService.getByOrderNo(orderNo); if (order null) { return Result.error(订单不存在); } // 生成6位取餐码 String pickupCode generatePickupCode(); order.setPickupCode(pickupCode); order.setStatus(3); // 已完成待取餐 orderMapper.updateById(order); MapString, Object data new HashMap(); data.put(order, order); data.put(pickupCode, pickupCode); return Result.success(data); } /** * 工具解析URL参数 */ private MapString, String parseParams(String params) { MapString, String map new HashMap(); String[] pairs params.split(); for (String pair : pairs) { String[] kv pair.split(); if (kv.length 2) { map.put(kv[0], kv[1]); } } return map; } /** * 计算配送费(根据距离) */ private BigDecimal calculateDeliveryFee(Long shopId, String address) { // 简化逻辑3km内5元每超1km加1元 Double distance 2.5; // 实际应调用地图API计算 if (distance 3) { return new BigDecimal(5.00); } else { int extraKm (int) Math.ceil(distance - 3); return new BigDecimal(5.00).add(new BigDecimal(extraKm)); } } /** * 生成6位取餐码 */ private String generatePickupCode() { return String.format(%06d, new Random().nextInt(1000000)); } }3️⃣ 店铺菜单查询ShopService.javajavaService public class ShopServiceImpl implements ShopService { Autowired private ShopMapper shopMapper; Autowired private MenuMapper menuMapper; /** * 根据店铺ID查询菜单(店内点餐用) */ Override public ListMenuItem getMenuByShopId(Long shopId) { LambdaQueryWrapperMenuItem wrapper new LambdaQueryWrapper(); wrapper.eq(MenuItem::getShopId, shopId) .eq(MenuItem::getStatus, 1) // 上架 .orderByAsc(MenuItem::getSort); ListMenuItem list menuMapper.selectList(wrapper); // 按分类分组 MapLong, ListMenuItem grouped list.stream() .collect(Collectors.groupingBy(MenuItem::getCategoryId)); // 转换为前端需要的格式 ListCategoryVO categories grouped.entrySet().stream() .map(entry - { CategoryVO vo new CategoryVO(); vo.setCategoryId(entry.getKey()); vo.setItems(entry.getValue()); return vo; }) .collect(Collectors.toList()); return categories; } } 二、UniApp前端扫码逻辑 ⭐⭐⭐1️⃣pages/scan/scan.vue- 扫码入口页vuetemplate view classscan-page !-- 顶部提示 -- view classheader text classtitle扫码点餐/text text classsubtitle扫描桌上二维码开始点餐/text /view !-- 区域 -- view classscan-area clickstartScan camera v-if!scanResult classcamera device-positionback flashoff erroronCameraError view classscan-frame view classcorner top-left/view view classcorner top-right/view view classcorner bottom-left/view view classcorner bottom-right/view text classscan-tip将二维码放入框内/text /view /camera !-- 扫码成功 -- view v-ifscanResult classresult-card uni-icons typecheckbox-filled size60 color#07c160/uni-icons text classresult-text扫码成功/text button classbtn-confirm clickgoToPage进入点餐/button /view /view !-- 备选方案手动输入 -- view classmanual-input text classdivider或者/text input v-modelmanualCode placeholder输入桌号如A05 classinput confirmmanualSearch / button classbtn-search clickmanualSearch查询/button /view /view /template script export default { data() { return { scanResult: null, manualCode: , shopData: null } }, methods: { // 核心调用微信扫码API startScan() { uni.scanCode({ onlyFromCamera: true, // 只从相机扫码 success: (res) { console.log(扫码结果:, res.result) this.scanResult res.result this.parseQRCode(res.result) }, fail: (err) { console.error(扫码失败:, err) uni.showToast({ title: 扫码失败, icon: none }) } }) }, // 解析二维码内容请求后端 async parseQRCode(code) { uni.showLoading({ title: 解析中... }) try { const res await uni.request({ url: http://localhost:8080/api/scan/parse, method: GET, data: { code } }) if (res.data.code 200) { this.shopData res.data.data // 根据类型跳转不同页面 if (this.shopData.isDelivery) { // 外卖模式 uni.navigateTo({ url: /pages/delivery/delivery?shopId${this.shopData.shop.id} }) } else { // 店内点餐模式 uni.navigateTo({ url: /pages/order/order?shopId${this.shopData.shop.id}table${this.shopData.table?.tableNo || } }) } } else { uni.showToast({ title: res.data.message, icon: none }) } } catch (e) { console.error(e) } finally { uni.hideLoading() } }, // 手动输入桌号查询 async manualSearch() { if (!this.manualCode) { return uni.showToast({ title: 请输入桌号, icon: none }) } uni.showLoading({ title: 查询中... }) try { const res await uni.request({ url: http://localhost:8080/api/shop/getByTable, method: GET, data: { tableNo: this.manualCode } }) if (res.data.code 200) { this.shopData res.data.data uni.navigateTo({ url: /pages/order/order?shopId${this.shopData.shop.id}table${this.manualCode} }) } } catch (e) { console.error(e) } finally { uni.hideLoading() } }, goToPage() { if (this.shopData.isDelivery) { uni.navigateTo({ url: /pages/delivery/delivery?shopId${this.shopData.shop.id} }) } else { uni.navigateTo({ url: /pages/order/order?shopId${this.shopData.shop.id} }) } }, onCameraError(e) { console.error(相机错误:, e) uni.showModal({ title: 提示, content: 无法打开相机请检查权限设置, showCancel: false }) } } } /script style scoped .scan-page { min-height: 100vh; background: #f5f5f5; padding: 40rpx; } .header { text-align: center; margin-bottom: 60rpx; } .title { font-size: 48rpx; font-weight: bold; color: #333; display: block; } .subtitle { font-size: 28rpx; color: #999; margin-top: 16rpx; } .scan-area { width: 100%; height: 600rpx; background: #000; border-radius: 20rpx; overflow: hidden; position: relative; } .camera { width: 100%; height: 100%; } .scan-frame { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400rpx; height: 400rpx; border: 4rpx solid rgba(255,255,255,0.5); border-radius: 20rpx; } .corner { position: absolute; width: 60rpx; height: 60rpx; border-color: #07c160; border-style: solid; } .top-left { top: 0; left: 0; border-width: 6rpx 0 0 6rpx; border-radius: 10rpx 0 0 0; } .top-right { top: 0; right: 0; border-width: 6rpx 6rpx 0 0; border-radius: 0 10rpx 0 0; } .bottom-left { bottom: 0; left: 0; border-width: 0 0 6rpx 6rpx; border-radius: 0 0 0 10rpx; } .bottom-right { bottom: 0; right: 0; border-width: 0 6rpx 6rpx 0; border-radius: 0 0 10rpx 0; } .scan-tip { position: absolute; bottom: -60rpx; left: 50%; transform: translateX(-50%); color: #fff; font-size: 28rpx; white-space: nowrap; } .result-card { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255,255,255,0.95); padding: 60rpx; border-radius: 20rpx; text-align: center; } .result-text { display: block; font-size: 32rpx; color: #333; margin: 20rpx 0 40rpx; } .btn-confirm { background: #07c160; color: #fff; border: none; border-radius: 50rpx; padding: 20rpx 80rpx; font-size: 32rpx; } .manual-input { margin-top: 60rpx; text-align: center; } .divider { color: #999; font-size: 28rpx; margin: 0 20rpx; } .input { display: inline-block; width: 300rpx; background: #fff; border-radius: 40rpx; padding: 16rpx 30rpx; font-size: 28rpx; } .btn-search { display: inline-block; margin-left: 20rpx; background: #07c160; color: #fff; border: none; border-radius: 40rpx; padding: 16rpx 40rpx; font-size: 28rpx; } /style2️⃣pages/order/order.vue- 扫码后的点餐页 ⭐vuetemplate view classorder-page !-- 店铺信息 -- view classshop-header image :srcshop.coverImg classshop-cover modeaspectFill/image view classshop-info text classshop-name{{ shop.name }}/text text classtable-info v-iftable桌号: {{ table }}/text text classshop-desc{{ shop.description }}/text /view /view !-- 菜单列表 -- view classmenu-container view v-forcategory in menuList :keycategory.categoryId classcategory-section :idcat- category.categoryId text classcategory-title{{ category.categoryName }}/text view v-foritem in category.items :keyitem.id classmenu-item clickshowItemDetail(item) image :srcitem.image classitem-img modeaspectFill/image view classitem-info text classitem-name{{ item.name }}/text text classitem-desc{{ item.description }}/text view classitem-bottom text classitem-price¥{{ item.price }}/text view classcart-control click.stopaddToCart(item) uni-icons v-ifgetCartCount(item.id) 0 typecheckbox-filled size20 color#07c160/uni-icons text v-ifgetCartCount(item.id) 0 classcount {{ getCartCount(item.id) }} /text uni-icons v-else typeplus-filled size20 color#999/uni-icons /view /view /view /view /view /view !-- 底部购物车栏 -- view classcart-bar v-ifcartList.length 0 view classcart-info clickshowCartPopup true view classcart-icon-wrap uni-icons typecart-filled size32 color#fff/uni-icons text classcart-badge{{ totalCount }}/text /view view classcart-price text classtotal-label合计:/text text classtotal-amount¥{{ totalPrice }}/text /view /view button classbtn-submit clicksubmitOrder去结算/button /view !-- 购物车弹窗 -- uni-popup refcartPopup typebottom view classcart-popup view classpopup-header text classpopup-title购物车/text text classclear-btn clickclearCart清空/text /view scroll-view scroll-y classcart-list view v-foritem in cartList :keyitem.id classcart-item text classcart-item-name{{ item.name }}/text view classcart-item-control view classminus-btn clickremoveFromCart(item.id)-/view text classcart-item-count{{ item.count }}/text view classplus-btn clickaddToCart(item)/view /view text classcart-item-price¥{{ (item.price * item.count).toFixed(2) }}/text /view /scroll-view /view /uni-popup /view /template script export default { data() { return { shopId: null, table: , shop: {}, menuList: [], cartList: [], // { id, name, price, count, image } showCartPopup: false, totalPrice: 0.00, totalCount: 0 } }, onLoad(options) { this.shopId options.shopId this.table options.table || this.loadShopData() }, methods: { async loadShopData() { uni.showLoading({ title: 加载中... }) try { const res await uni.request({ url: http://localhost:8080/api/scan/parse, method: GET, data: { code: CAMPUS://SHOP?id${this.shopId}table${this.table} } }) if (res.data.code 200) { this.shop res.data.data.shop this.menuList res.data.data.menu } } catch (e) { console.error(e) } finally { uni.hideLoading() } }, addToCart(item) { const exist this.cartList.find(c c.id item.id) if (exist) { exist.count } else { this.cartList.push({ id: item.id, name: item.name, price: item.price, count: 1, image: item.image }) } this.calcTotal() }, removeFromCart(id) { const item this.cartList.find(c c.id id) if (item) { item.count-- if (item.count 0) { this.cartList this.cartList.filter(c c.id ! id) } } this.calcTotal() }, getCartCount(id) { const item this.cartList.find(c c.id id) return item ? item.count : 0 }, calcTotal() { this.totalCount this.cartList.reduce((sum, item) sum item.count, 0) this.totalPrice this.cartList.reduce( (sum, item) sum item.price * item.count, 0 ).toFixed(2) }, async submitOrder() { if (this.cartList.length 0) return uni.showLoading({ title: 提交中... }) try { const res await uni.request({ url: http://localhost:8080/api/errand/publish, method: POST, header: { Authorization: Bearer uni.getStorageSync(token) }, data: { shopId: this.shopId, tableNo: this.table, items: this.cartList.map(item ({ menuId: item.id, count: item.count, price: item.price })), totalPrice: this.totalPrice } }) if (res.data.code 200) { uni.showModal({ title: 下单成功, content: 订单号: ${res.data.data.orderNo}, showCancel: false, success: () { uni.redirectTo({ url: /pages/orderDetail/orderDetail?orderNo${res.data.data.orderNo} }) } }) } } catch (e) { console.error(e) } finally { uni.hideLoading() } }, clearCart() { this.cartList [] this.calcTotal() this.showCartPopup false } } } /script style scoped .order-page { padding-bottom: 140rpx; } .shop-header { background: #fff; padding: 30rpx; display: flex; gap: 20rpx; } .shop-cover { width: 160rpx; height: 160rpx; border-radius: 16rpx; } .shop-info { flex: 1; display: flex; flex-direction: column; justify-content: center; } .shop-name { font-size: 34rpx; font-weight: bold; color: #333; } .table-info { font-size: 24rpx; color: #07c160; margin-top: 8rpx; } .shop-desc { font-size: 24rpx; color: #999; margin-top: 8rpx; } .menu-container { background: #fff; margin-top: 20rpx; padding: 0 30rpx; } .category-title { font-size: 30rpx; font-weight: bold; color: #333; padding: 30rpx 0 20rpx; display: block; border-bottom: 1rpx solid #f0f0f0; } .menu-item { display: flex; padding: 24rpx 0; border-bottom: 1rpx solid #f5f5f5; } .item-img { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin-right: 20rpx; } .item-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; } .item-name { font-size: 28rpx; font-weight: bold; color: #333; } .item-desc { font-size: 24rpx; color: #999; margin-top: 8rpx; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .item-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 16rpx; } .item-price { font-size: 32rpx; color: #ff4d4f; font-weight: bold; } .cart-control { display: flex; align-items: center; gap: 16rpx; } .count { font-size: 28rpx; color: #07c160; font-weight: bold; min-width: 40rpx; text-align: center; } .cart-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 120rpx; background: #2d2d2d; display: flex; align-items: center; justify-content: space-between; padding: 0 30rpx; box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.1); } .cart-info { display: flex; align-items: center; gap: 20rpx; } .cart-icon-wrap { position: relative; width: 80rpx; height: 80rpx; background: #07c160; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-top: -40rpx; } .cart-badge { position: absolute; top: -10rpx; right: -10rpx; background: #ff4d4f; color: #fff; font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 20rpx; } .total-label { font-size: 28rpx; color: #999; } .total-amount { font-size: 36rpx; color: #ff4d4f; font-weight: bold; margin-left: 10rpx; } .btn-submit { background: linear-gradient(135deg, #07c160, #06ad56); color: #fff; border: none; border-radius: 50rpx; padding: 24rpx 60rpx; font-size: 32rpx; font-weight: bold; } .cart-popup { background: #fff; border-radius: 30rpx 30rpx 0 0; padding: 30rpx; max-height: 60vh; } .popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; } .popup-title { font-size: 34rpx; font-weight: bold; color: #333; } .clear-btn { font-size: 28rpx; color: #999; } .cart-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; } .cart-item-name { flex: 1; font-size: 28rpx; color: #333; } .cart-item-control { display: flex; align-items: center; gap: 24rpx; margin: 0 20rpx; } .minus-btn, .plus-btn { width: 48rpx; height: 48rpx; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #333; } .cart-item-count { font-size: 28rpx; font-weight: bold; min-width: 40rpx; text-align: center; } .cart-item-price { font-size: 28rpx; color: #ff4d4f; font-weight: bold; min-width: 120rpx; text-align: right; } /style️ 三、数据库表设计sql-- 店铺表 CREATE TABLE shop ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 店铺名, cover_img VARCHAR(500) COMMENT 封面图, description TEXT COMMENT 描述, address VARCHAR(200) COMMENT 地址, phone VARCHAR(20) COMMENT 电话, business_hours VARCHAR(50) COMMENT 营业时间, status TINYINT DEFAULT 1 COMMENT 1-营业 0-休业, qrcode_content TEXT COMMENT 二维码内容(JSON), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 店铺表; -- 桌台表 CREATE TABLE table_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_id BIGINT NOT NULL COMMENT 店铺ID, table_no VARCHAR(20) NOT NULL COMMENT 桌号 A01, qrcode_content VARCHAR(500) COMMENT 桌台二维码内容, status TINYINT DEFAULT 1 COMMENT 1-空闲 0-占用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_shop_table (shop_id, table_no) ) COMMENT 桌台表; -- 扫码记录表(用于数据分析) CREATE TABLE scan_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_id BIGINT NOT NULL, table_no VARCHAR(20) COMMENT 桌号, scan_type VARCHAR(20) COMMENT SHOP/DELIVERY/COUPON, user_id BIGINT COMMENT 用户ID(可选), openid VARCHAR(100) COMMENT 微信openid, scan_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_shop_time (shop_id, scan_time) ) COMMENT 扫码记录表; 四、扫码流程总结用户扫码 ↓ uni.scanCode() 获取二维码内容 ↓ 请求后端 /api/scan/parse?codexxx ↓ 后端解析协议头 CAMPUS:// ↓ 根据类型(SHOP/DELIVERY/COUPON/PICKUP)分发 ↓ 返回对应数据(店铺菜单/配送费/优惠券/取餐码) ↓ 前端跳转对应页面 关键技术点技术用途✅uni.scanCode()微信扫码API✅ 协议头识别CAMPUS://区分业务类型✅ URL参数解析提取店铺ID、桌号等✅ Redis记录扫码日志数据统计分析✅ 分布式锁防止重复接单✅ JWT认证用户身份验证需要完整的连锁店多店铺管理、奶茶点餐特殊逻辑(规格选择)、同城外卖配送系统代码吗请告诉我
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2611298.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!