JienDa聊PHP:乡镇外卖跑腿小程序开发实战:基于PHP的乡镇同城O2O系统开发

乡镇外卖跑腿小程序开发实战:基于PHP的乡镇同城O2O系统开发

摘要

随着移动互联网在乡镇地区的普及,乡镇同城O2O(Online to Offline)服务需求日益增长。本文基于PHP技术栈,详细阐述了一套针对乡镇地区的外卖跑腿小程序的开发实战方案。系统采用前后端分离架构,前端使用小程序原生开发框架,后端采用PHP 7.4+ThinkPHP 6.0框架,数据库选用MySQL 8.0,并集成Redis缓存、消息队列等技术。本文从需求分析、系统设计、技术选型、核心功能实现、性能优化、部署运维等方面进行全面论述,为乡镇同城O2O系统的开发提供了一套完整的解决方案。

关键词:乡镇O2O;PHP开发;外卖跑腿;小程序;ThinkPHP;微服务架构


1. 引言

1.1 乡镇O2O市场现状

近年来,随着"互联网+"战略向农村地区纵深发展,乡镇地区移动互联网普及率显著提升。根据中国互联网络信息中心(CNNIC)第51次《中国互联网络发展状况统计报告》,我国农村地区互联网普及率达61.9%,乡镇居民对本地生活服务的数字化需求日益旺盛。然而,与城市相比,乡镇地区的O2O服务仍存在以下特点:

  1. 地理分散性:乡镇地区用户分布相对分散,配送范围更广
  2. 需求差异化:消费习惯、品类需求与城市存在差异
  3. 技术基础薄弱:本地商家数字化水平较低
  4. 人力成本优势:劳动力成本相对较低,但专业技术人员缺乏

1.2 技术挑战与解决方案

针对乡镇O2O的特点,系统开发面临以下技术挑战:

  • 网络环境相对不稳定
  • 用户数字化操作能力有限
  • 商家信息化程度低
  • 配送路径规划复杂

本文提出的解决方案采用轻量级技术架构,注重系统稳定性、易用性和可维护性,特别针对低带宽环境进行优化。

2. 需求分析

2.1 用户角色分析

系统主要涉及四类用户角色:

  1. 消费者端:乡镇居民,通过小程序下单购买商品或服务
  2. 商家端:本地商家,管理商品、订单和店铺
  3. 骑手端:配送人员,接单、取货、配送
  4. 管理端:平台管理员,负责系统管理、数据监控

2.2 功能需求

2.2.1 消费者端功能
  • 用户注册登录(微信一键登录)
  • 地理位置获取与店铺推荐
  • 商品浏览、搜索、分类查看
  • 购物车管理
  • 在线支付(微信支付、余额支付)
  • 订单管理(下单、取消、评价)
  • 实时订单追踪
  • 优惠券、积分系统
  • 售后与客服
2.2.2 商家端功能
  • 店铺管理(信息、公告、营业时间)
  • 商品管理(分类、上架、库存)
  • 订单处理(接单、拒单、出餐)
  • 数据统计(销量、收入、热销商品)
  • 营销活动(满减、折扣)
2.2.3 骑手端功能
  • 骑手注册与审核
  • 接单与抢单模式
  • 订单路线规划
  • 配送状态更新
  • 收入统计与提现
2.2.4 管理端功能
  • 用户管理(消费者、商家、骑手)
  • 订单监控与干预
  • 数据统计与分析
  • 系统配置
  • 财务管理

2.3 非功能需求

  • 性能要求:首页加载时间<2s,下单响应<3s
  • 并发要求:支持500+并发用户
  • 可用性:99.5%可用性
  • 安全性:数据加密、防SQL注入、XSS攻击
  • 可扩展性:模块化设计,便于功能扩展

3. 系统架构设计

3.1 总体架构

系统采用前后端分离的微服务化架构:

复制代码
┌─────────────────────────────────────────────┐
│                  客户端层                    │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐       │
│  │消费者端 │ │ 商家端  │ │ 骑手端  │       │
│  │ 小程序  │ │ 小程序  │ │ 小程序  │       │
│  └─────────┘ └─────────┘ └─────────┘       │
└───────────────────┬─────────────────────────┘
                    │ HTTP/HTTPS + WebSocket
┌───────────────────┴─────────────────────────┐
│              API网关层                        │
│        负载均衡 + 路由分发 + 鉴权            │
└───────────────────┬─────────────────────────┘
                    │
┌───────────────────┴─────────────────────────┐
│              业务微服务层                     │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐        │
│  │用户服务││订单服务││商品服务││支付服务│        │
│  └──────┘ └──────┘ └──────┘ └──────┘        │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐        │
│  │配送服务││消息服务││营销服务││文件服务│        │
│  └──────┘ └──────┘ └──────┘ └──────┘        │
└───────────────────┬─────────────────────────┘
                    │
┌───────────────────┴─────────────────────────┐
│              基础服务层                       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐     │
│  │  MySQL   │ │  Redis   │ │   RabbitMQ│     │
│  │ 数据库   │ │ 缓存/会话 │ │ 消息队列  │     │
│  └──────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

3.2 技术选型

3.2.1 后端技术栈
  • 开发语言:PHP 7.4+(JIT编译,性能提升显著)
  • 开发框架:ThinkPHP 6.0(轻量、高效、文档完善)
  • API规范:RESTful API + JSON
  • 实时通信:Workerman + WebSocket
  • 消息队列:RabbitMQ(订单异步处理、消息推送)
  • 缓存系统:Redis 6.0+(缓存、会话、消息队列)
  • 搜索引擎:Elasticsearch 7.x(商品搜索、订单检索)
3.2.2 前端技术栈
  • 小程序框架:微信小程序原生 + Vant Weapp UI组件库
  • 地图服务:腾讯位置服务(乡镇地图覆盖更好)
  • 支付接入:微信支付、余额支付
3.2.3 运维部署
  • 服务器:CentOS 7.9
  • Web服务器:Nginx 1.20+
  • PHP运行环境:PHP-FPM
  • 容器化:Docker + Docker Compose
  • 持续集成:Jenkins
  • 监控:Prometheus + Grafana

3.3 数据库设计

3.3.1 核心表结构
sql 复制代码
-- 用户表(多角色共用,通过user_type区分)
CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
  `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
  `openid` varchar(100) DEFAULT '' COMMENT '微信openid',
  `user_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:消费者 2:商家 3:骑手',
  `avatar` varchar(255) DEFAULT '' COMMENT '头像',
  `balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1正常 0禁用',
  `last_login_time` datetime DEFAULT NULL,
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_phone` (`phone`),
  UNIQUE KEY `uniq_openid` (`openid`),
  KEY `idx_user_type` (`user_type`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 商家表
CREATE TABLE `merchants` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '关联用户ID',
  `shop_name` varchar(100) NOT NULL DEFAULT '' COMMENT '店铺名称',
  `shop_logo` varchar(255) DEFAULT '' COMMENT '店铺logo',
  `shop_type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '店铺类型 1餐饮 2超市 3水果 4医药',
  `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话',
  `province` varchar(20) DEFAULT '' COMMENT '省',
  `city` varchar(20) DEFAULT '' COMMENT '市',
  `district` varchar(20) DEFAULT '' COMMENT '区县',
  `town` varchar(50) DEFAULT '' COMMENT '乡镇',
  `address` varchar(200) DEFAULT '' COMMENT '详细地址',
  `lng` decimal(10,6) DEFAULT NULL COMMENT '经度',
  `lat` decimal(10,6) DEFAULT NULL COMMENT '纬度',
  `business_hours` varchar(100) DEFAULT '' COMMENT '营业时间',
  `delivery_range` int(11) NOT NULL DEFAULT '3000' COMMENT '配送范围(米)',
  `delivery_fee` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '配送费',
  `min_order_amount` decimal(8,2) NOT NULL DEFAULT '0.00' COMMENT '起送价',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0待审核 1正常 2关闭',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_user_id` (`user_id`),
  KEY `idx_location` (`lng`,`lat`),
  KEY `idx_shop_type` (`shop_type`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表';

-- 商品表
CREATE TABLE `products` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `merchant_id` int(11) NOT NULL COMMENT '商家ID',
  `category_id` int(11) NOT NULL COMMENT '分类ID',
  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名称',
  `image` varchar(255) DEFAULT '' COMMENT '商品图片',
  `description` text COMMENT '商品描述',
  `price` decimal(8,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
  `original_price` decimal(8,2) DEFAULT NULL COMMENT '原价',
  `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
  `month_sales` int(11) NOT NULL DEFAULT '0' COMMENT '月销量',
  `total_sales` int(11) NOT NULL DEFAULT '0' COMMENT '总销量',
  `is_recommend` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐',
  `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1上架 0下架',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_merchant_id` (`merchant_id`),
  KEY `idx_category_id` (`category_id`),
  KEY `idx_status` (`status`),
  KEY `idx_sort` (`sort`),
  KEY `idx_sales` (`month_sales`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

-- 订单表
CREATE TABLE `orders` (
  `id` varchar(32) NOT NULL COMMENT '订单号',
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `merchant_id` int(11) NOT NULL COMMENT '商家ID',
  `rider_id` int(11) DEFAULT NULL COMMENT '骑手ID',
  `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总额',
  `delivery_fee` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '配送费',
  `discount_amount` decimal(8,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
  `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
  `pay_method` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支付方式 1微信 2余额',
  `pay_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支付状态 0待支付 1已支付 2已退款',
  `order_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态 0待接单 1已接单 2制作中 3待取货 4配送中 5已完成 6已取消',
  `delivery_address` varchar(255) NOT NULL DEFAULT '' COMMENT '配送地址',
  `delivery_lng` decimal(10,6) DEFAULT NULL COMMENT '配送地址经度',
  `delivery_lat` decimal(10,6) DEFAULT NULL COMMENT '配送地址纬度',
  `contact_name` varchar(50) NOT NULL DEFAULT '' COMMENT '联系人',
  `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话',
  `remark` varchar(255) DEFAULT '' COMMENT '备注',
  `expected_time` datetime DEFAULT NULL COMMENT '期望送达时间',
  `accept_time` datetime DEFAULT NULL COMMENT '接单时间',
  `delivery_time` datetime DEFAULT NULL COMMENT '发货时间',
  `complete_time` datetime DEFAULT NULL COMMENT '完成时间',
  `cancel_time` datetime DEFAULT NULL COMMENT '取消时间',
  `cancel_reason` varchar(100) DEFAULT '' COMMENT '取消原因',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_merchant_id` (`merchant_id`),
  KEY `idx_rider_id` (`rider_id`),
  KEY `idx_order_status` (`order_status`),
  KEY `idx_pay_status` (`pay_status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 配送员表
CREATE TABLE `riders` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '关联用户ID',
  `real_name` varchar(50) NOT NULL DEFAULT '' COMMENT '真实姓名',
  `id_card` varchar(20) NOT NULL DEFAULT '' COMMENT '身份证号',
  `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话',
  `current_lng` decimal(10,6) DEFAULT NULL COMMENT '当前位置经度',
  `current_lat` decimal(10,6) DEFAULT NULL COMMENT '当前位置纬度',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0待审核 1休息 2可接单 3配送中',
  `is_online` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否在线',
  `total_orders` int(11) NOT NULL DEFAULT '0' COMMENT '总接单数',
  `today_orders` int(11) NOT NULL DEFAULT '0' COMMENT '今日接单数',
  `rating` decimal(3,2) NOT NULL DEFAULT '5.00' COMMENT '评分',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_user_id` (`user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_location` (`current_lng`,`current_lat`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送员表';
3.3.2 数据表关系设计
  • 用户表为核心,通过user_type区分角色
  • 商家、骑手与用户表一对一关联
  • 订单表关联用户、商家、骑手
  • 商品表与订单项表一对多关联
  • 采用分库分表策略应对数据增长

4. 核心功能实现

4.1 用户认证与授权

4.1.1 微信登录实现
php 复制代码
<?php
namespace app\api\controller;

use app\BaseController;
use app\common\lib\Wechat;
use app\common\model\User;
use think\facade\Cache;

class Auth extends BaseController
{
    /**
     * 微信登录
     */
    public function wechatLogin()
    {
        $code = $this->request->param('code');
        $encryptedData = $this->request->param('encryptedData');
        $iv = $this->request->param('iv');
        
        if (empty($code)) {
            return json(['code' => 400, 'msg' => '参数错误']);
        }
        
        // 获取微信openid和session_key
        $wechat = new Wechat();
        $result = $wechat->getSessionKey($code);
        
        if ($result['code'] != 200) {
            return json(['code' => 500, 'msg' => '微信登录失败']);
        }
        
        $openid = $result['data']['openid'];
        $sessionKey = $result['data']['session_key'];
        
        // 解密用户信息
        $userInfo = $wechat->decryptData($encryptedData, $iv, $sessionKey);
        
        // 查找或创建用户
        $user = User::where('openid', $openid)->find();
        
        if (!$user) {
            $user = new User();
            $user->openid = $openid;
            $user->username = 'wx_' . substr(md5($openid), 0, 8);
            $user->avatar = $userInfo['avatarUrl'] ?? '';
            $user->user_type = 1; // 消费者
            $user->create_time = date('Y-m-d H:i:s');
        }
        
        $user->last_login_time = date('Y-m-d H:i:s');
        $user->update_time = date('Y-m-d H:i:s');
        $user->save();
        
        // 生成token
        $token = $this->generateToken($user->id);
        
        // 缓存用户信息
        Cache::set('user_token:' . $token, [
            'user_id' => $user->id,
            'user_type' => $user->user_type
        ], 7200); // 2小时
        
        return json([
            'code' => 200,
            'msg' => '登录成功',
            'data' => [
                'token' => $token,
                'user_info' => [
                    'id' => $user->id,
                    'username' => $user->username,
                    'avatar' => $user->avatar,
                    'user_type' => $user->user_type
                ]
            ]
        ]);
    }
    
    /**
     * 生成token
     */
    private function generateToken($userId)
    {
        $str = md5(uniqid(md5(microtime(true)), true));
        $token = sha1($str . $userId . time());
        return $token;
    }
}
4.1.2 JWT令牌验证中间件
php 复制代码
<?php
namespace app\api\middleware;

use think\facade\Cache;
use think\Response;

class Auth
{
    public function handle($request, \Closure $next)
    {
        $token = $request->header('Authorization');
        
        if (empty($token)) {
            return json(['code' => 401, 'msg' => 'Token不能为空']);
        }
        
        // 验证token
        $userInfo = Cache::get('user_token:' . $token);
        
        if (!$userInfo) {
            return json(['code' => 401, 'msg' => 'Token无效或已过期']);
        }
        
        // 将用户信息存入请求对象
        $request->user_id = $userInfo['user_id'];
        $request->user_type = $userInfo['user_type'];
        $request->user_token = $token;
        
        // 刷新token有效期
        Cache::set('user_token:' . $token, $userInfo, 7200);
        
        return $next($request);
    }
}

4.2 附近商家推荐算法

针对乡镇地区地理位置特点,实现基于地理位置的商家推荐:

php 复制代码
<?php
namespace app\common\service;

use think\facade\Db;

class MerchantService
{
    // 地球半径(米)
    const EARTH_RADIUS = 6378137;
    
    /**
     * 获取附近商家
     * @param float $lng 经度
     * @param float $lat 纬度
     * @param int $distance 距离(米)
     * @param int $page 页码
     * @param int $limit 每页数量
     * @return array
     */
    public function getNearbyMerchants($lng, $lat, $distance = 5000, $page = 1, $limit = 20)
    {
        // 计算经纬度范围
        $range = $distance / self::EARTH_RADIUS;
        $latRange = rad2deg($range);
        $lngRange = rad2deg($range / cos(deg2rad($lat)));
        
        $minLat = $lat - $latRange;
        $maxLat = $lat + $latRange;
        $minLng = $lng - $lngRange;
        $maxLng = $lng + $lngRange;
        
        // 使用空间索引查询
        $query = Db::name('merchant')
            ->where('status', 1) // 正常营业
            ->where('lat', 'between', [$minLat, $maxLat])
            ->where('lng', 'between', [$minLng, $maxLng])
            ->whereRaw("ST_Distance_Sphere(point(lng, lat), point(?, ?)) <= ?", [
                $lng, $lat, $distance
            ]);
        
        // 分页查询
        $total = $query->count();
        $list = $query->page($page, $limit)
            ->field('*, 
                ST_Distance_Sphere(point(lng, lat), point(' . $lng . ', ' . $lat . ')) as distance')
            ->order('distance ASC')
            ->select()
            ->toArray();
        
        // 格式化距离
        foreach ($list as &$item) {
            $item['distance'] = $this->formatDistance($item['distance']);
            $item['delivery_time'] = $this->calculateDeliveryTime($item['distance']);
        }
        
        return [
            'list' => $list,
            'total' => $total,
            'page' => $page,
            'limit' => $limit
        ];
    }
    
    /**
     * 计算配送时间(分钟)
     */
    private function calculateDeliveryTime($distance)
    {
        // 假设步行速度1.2m/s,商家准备时间10分钟
        $walkSpeed = 1.2; // 米/秒
        $prepareTime = 10; // 分钟
        
        $walkTime = $distance / $walkSpeed / 60; // 分钟
        $totalTime = ceil($prepareTime + $walkTime);
        
        return min($totalTime, 120); // 最长120分钟
    }
    
    /**
     * 格式化距离
     */
    private function formatDistance($distance)
    {
        if ($distance < 1000) {
            return round($distance) . '米';
        } else {
            return round($distance / 1000, 1) . '公里';
        }
    }
    
    /**
     * 智能推荐商家(基于距离、评分、销量)
     */
    public function getRecommendMerchants($lng, $lat, $userId = 0, $limit = 10)
    {
        // 基础查询
        $query = Db::name('merchant m')
            ->leftJoin('merchant_stat ms', 'm.id = ms.merchant_id')
            ->where('m.status', 1);
        
        // 如果有用户ID,考虑用户偏好
        if ($userId) {
            $userPreferences = $this->getUserPreferences($userId);
            if ($userPreferences) {
                $query->whereIn('m.shop_type', $userPreferences['preferred_types']);
            }
        }
        
        // 计算推荐分数
        $list = $query->field("m.*, 
                ST_Distance_Sphere(point(m.lng, m.lat), point(?, ?)) as distance,
                ms.rating as rating,
                ms.month_order_count as order_count,
                (CASE 
                    WHEN ms.rating >= 4.5 THEN 3
                    WHEN ms.rating >= 4.0 THEN 2
                    WHEN ms.rating >= 3.5 THEN 1
                    ELSE 0
                END) * 0.3 +
                (CASE 
                    WHEN ms.month_order_count >= 1000 THEN 3
                    WHEN ms.month_order_count >= 500 THEN 2
                    WHEN ms.month_order_count >= 100 THEN 1
                    ELSE 0
                END) * 0.4 +
                (CASE 
                    WHEN ST_Distance_Sphere(point(m.lng, m.lat), point(?, ?)) <= 1000 THEN 3
                    WHEN ST_Distance_Sphere(point(m.lng, m.lat), point(?, ?)) <= 3000 THEN 2
                    WHEN ST_Distance_Sphere(point(m.lng, m.lat), point(?, ?)) <= 5000 THEN 1
                    ELSE 0
                END) * 0.3 as recommend_score",
                [$lng, $lat, $lng, $lat, $lng, $lat, $lng, $lat])
            ->order('recommend_score DESC, distance ASC')
            ->limit($limit)
            ->select()
            ->toArray();
        
        return $list;
    }
}

4.3 订单系统设计

4.3.1 下单流程
php 复制代码
<?php
namespace app\common\service;

use think\facade\Db;
use think\facade\Queue;
use app\common\model\Order;
use app\common\model\Product;
use app\common\model\User;
use app\common\lib\Redis;
use app\common\job\OrderTimeout;

class OrderService
{
    /**
     * 创建订单
     */
    public function createOrder($userId, $merchantId, $products, $address, $remark = '')
    {
        Db::startTrans();
        try {
            // 1. 验证商家
            $merchant = Db::name('merchant')
                ->where('id', $merchantId)
                ->where('status', 1)
                ->find();
                
            if (!$merchant) {
                throw new \Exception('商家不存在或已歇业');
            }
            
            // 2. 验证商品
            $totalAmount = 0;
            $productList = [];
            
            foreach ($products as $item) {
                $product = Product::where('id', $item['product_id'])
                    ->where('merchant_id', $merchantId)
                    ->where('status', 1)
                    ->lock(true)
                    ->find();
                    
                if (!$product) {
                    throw new \Exception('商品不存在或已下架');
                }
                
                if ($product->stock < $item['quantity']) {
                    throw new \Exception('商品库存不足');
                }
                
                // 减少库存
                $product->stock -= $item['quantity'];
                $product->save();
                
                $productList[] = [
                    'product_id' => $product->id,
                    'product_name' => $product->name,
                    'price' => $product->price,
                    'quantity' => $item['quantity'],
                    'total_price' => bcmul($product->price, $item['quantity'], 2)
                ];
                
                $totalAmount = bcadd($totalAmount, $productList[count($productList)-1]['total_price'], 2);
            }
            
            // 3. 验证起送价
            if ($totalAmount < $merchant['min_order_amount']) {
                throw new \Exception('未达到起送价');
            }
            
            // 4. 生成订单号
            $orderNo = date('YmdHis') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
            
            // 5. 计算配送费
            $deliveryFee = $this->calculateDeliveryFee($merchant, $address);
            
            // 6. 计算优惠
            $discount = $this->calculateDiscount($userId, $merchantId, $totalAmount);
            
            // 7. 计算实付金额
            $payAmount = bcadd($totalAmount, $deliveryFee, 2);
            $payAmount = bcsub($payAmount, $discount, 2);
            
            // 8. 创建订单
            $order = new Order();
            $order->order_no = $orderNo;
            $order->user_id = $userId;
            $order->merchant_id = $merchantId;
            $order->total_amount = $totalAmount;
            $order->delivery_fee = $deliveryFee;
            $order->discount_amount = $discount;
            $order->pay_amount = $payAmount;
            $order->delivery_address = $address['address'];
            $order->delivery_lng = $address['lng'];
            $order->delivery_lat = $address['lat'];
            $order->contact_name = $address['name'];
            $order->contact_phone = $address['phone'];
            $order->remark = $remark;
            $order->create_time = date('Y-m-d H:i:s');
            $order->update_time = date('Y-m-d H:i:s');
            $order->save();
            
            // 9. 保存订单商品
            foreach ($productList as &$product) {
                $product['order_id'] = $order->id;
                $product['create_time'] = date('Y-m-d H:i:s');
            }
            
            Db::name('order_item')->insertAll($productList);
            
            // 10. 添加订单超时任务(15分钟未支付自动取消)
            Queue::push(OrderTimeout::class, [
                'order_id' => $order->id
            ], 900); // 15分钟延迟
            
            Db::commit();
            
            // 11. 返回订单信息
            return [
                'order_id' => $order->id,
                'order_no' => $orderNo,
                'pay_amount' => $payAmount,
                'expire_time' => date('Y-m-d H:i:s', time() + 900)
            ];
            
        } catch (\Exception $e) {
            Db::rollback();
            throw $e;
        }
    }
    
    /**
     * 计算配送费
     */
    private function calculateDeliveryFee($merchant, $address)
    {
        // 计算距离
        $distance = $this->getDistance(
            $merchant['lng'], $merchant['lat'],
            $address['lng'], $address['lat']
        );
        
        // 超出配送范围
        if ($distance > $merchant['delivery_range']) {
            throw new \Exception('超出配送范围');
        }
        
        // 基础配送费
        $fee = $merchant['delivery_fee'];
        
        // 距离附加费(每公里0.5元)
        if ($distance > 1000) {
            $extraDistance = $distance - 1000;
            $extraFee = ceil($extraDistance / 1000) * 0.5;
            $fee = bcadd($fee, $extraFee, 2);
        }
        
        return $fee;
    }
    
    /**
     * 计算两点距离(米)
     */
    private function getDistance($lng1, $lat1, $lng2, $lat2)
    {
        $radLat1 = deg2rad($lat1);
        $radLat2 = deg2rad($lat2);
        $a = $radLat1 - $radLat2;
        $b = deg2rad($lng1) - deg2rad($lng2);
        
        $s = 2 * asin(sqrt(pow(sin($a/2),2) + cos($radLat1)*cos($radLat2)*pow(sin($b/2),2)));
        $s = $s * 6378137;
        $s = round($s * 10000) / 10000;
        
        return $s;
    }
    
    /**
     * 计算优惠
     */
    private function calculateDiscount($userId, $merchantId, $amount)
    {
        $discount = 0;
        
        // 1. 商户优惠(满减)
        $merchantDiscount = Db::name('merchant_discount')
            ->where('merchant_id', $merchantId)
            ->where('status', 1)
            ->where('start_time', '<=', date('Y-m-d H:i:s'))
            ->where('end_time', '>=', date('Y-m-d H:i:s'))
            ->where('min_amount', '<=', $amount)
            ->order('discount_amount', 'desc')
            ->find();
            
        if ($merchantDiscount) {
            $discount = bcadd($discount, $merchantDiscount['discount_amount'], 2);
        }
        
        // 2. 用户优惠券
        $userCoupon = Db::name('user_coupon uc')
            ->leftJoin('coupon c', 'uc.coupon_id = c.id')
            ->where('uc.user_id', $userId)
            ->where('uc.status', 0) // 未使用
            ->where('c.merchant_id', 'in', [0, $merchantId]) // 0表示通用券
            ->where('c.min_amount', '<=', $amount)
            ->where('c.start_time', '<=', date('Y-m-d H:i:s'))
            ->where('c.end_time', '>=', date('Y-m-d H:i:s'))
            ->order('c.discount_amount', 'desc')
            ->find();
            
        if ($userCoupon) {
            $discount = bcadd($discount, $userCoupon['discount_amount'], 2);
        }
        
        return $discount;
    }
}
4.3.2 订单状态机
php 复制代码
<?php
namespace app\common\service;

use app\common\model\Order;
use think\facade\Db;
use think\facade\Queue;
use app\common\job\OrderRemind;

class OrderStateMachine
{
    // 订单状态
    const STATUS_WAITING = 0; // 待接单
    const STATUS_ACCEPTED = 1; // 已接单
    const STATUS_PREPARING = 2; // 制作中
    const STATUS_READY = 3; // 待取货
    const STATUS_DELIVERING = 4; // 配送中
    const STATUS_COMPLETED = 5; // 已完成
    const STATUS_CANCELLED = 6; // 已取消
    
    // 状态流转
    private static $transitions = [
        self::STATUS_WAITING => [self::STATUS_ACCEPTED, self::STATUS_CANCELLED],
        self::STATUS_ACCEPTED => [self::STATUS_PREPARING, self::STATUS_CANCELLED],
        self::STATUS_PREPARING => [self::STATUS_READY, self::STATUS_CANCELLED],
        self::STATUS_READY => [self::STATUS_DELIVERING, self::STATUS_CANCELLED],
        self::STATUS_DELIVERING => [self::STATUS_COMPLETED, self::STATUS_CANCELLED],
    ];
    
    /**
     * 改变订单状态
     */
    public function changeStatus($orderId, $newStatus, $operatorId, $operatorType, $remark = '')
    {
        Db::startTrans();
        try {
            $order = Order::where('id', $orderId)->lock(true)->find();
            
            if (!$order) {
                throw new \Exception('订单不存在');
            }
            
            // 验证状态流转
            if (!$this->isValidTransition($order->order_status, $newStatus)) {
                throw new \Exception('订单状态不允许此操作');
            }
            
            $oldStatus = $order->order_status;
            $order->order_status = $newStatus;
            $order->update_time = date('Y-m-d H:i:s');
            
            // 记录状态变更时间
            switch ($newStatus) {
                case self::STATUS_ACCEPTED:
                    $order->accept_time = date('Y-m-d H:i:s');
                    // 商家接单,通知用户
                    $this->sendNotification($order->user_id, 'order_accepted', [
                        'order_no' => $order->order_no
                    ]);
                    break;
                case self::STATUS_PREPARING:
                    $order->preparing_time = date('Y-m-d H:i:s');
                    break;
                case self::STATUS_READY:
                    $order->ready_time = date('Y-m-d H:i:s');
                    // 商品已备好,通知骑手
                    $this->notifyRiders($order);
                    break;
                case self::STATUS_DELIVERING:
                    $order->delivery_time = date('Y-m-d H:i:s');
                    // 开始配送,通知用户
                    $this->sendNotification($order->user_id, 'order_delivering', [
                        'order_no' => $order->order_no
                    ]);
                    break;
                case self::STATUS_COMPLETED:
                    $order->complete_time = date('Y-m-d H:i:s');
                    // 订单完成,结算
                    $this->settleOrder($order);
                    // 发送评价提醒
                    Queue::push(OrderRemind::class, [
                        'order_id' => $order->id
                    ], 1800); // 30分钟后提醒评价
                    break;
                case self::STATUS_CANCELLED:
                    $order->cancel_time = date('Y-m-d H:i:s');
                    $order->cancel_reason = $remark;
                    // 取消订单,退款
                    $this->cancelOrder($order);
                    break;
            }
            
            $order->save();
            
            // 记录状态变更日志
            Db::name('order_status_log')->insert([
                'order_id' => $orderId,
                'old_status' => $oldStatus,
                'new_status' => $newStatus,
                'operator_id' => $operatorId,
                'operator_type' => $operatorType,
                'remark' => $remark,
                'create_time' => date('Y-m-d H:i:s')
            ]);
            
            Db::commit();
            
            return true;
            
        } catch (\Exception $e) {
            Db::rollback();
            throw $e;
        }
    }
    
    /**
     * 验证状态流转是否合法
     */
    private function isValidTransition($fromStatus, $toStatus)
    {
        if ($fromStatus == $toStatus) {
            return false;
        }
        
        if ($fromStatus == self::STATUS_COMPLETED || $fromStatus == self::STATUS_CANCELLED) {
            return false; // 已完成或已取消的订单不能改变状态
        }
        
        return in_array($toStatus, self::$transitions[$fromStatus] ?? []);
    }
}

4.4 骑手派单系统

4.4.1 智能派单算法
php 复制代码
<?php
namespace app\common\service;

use think\facade\Db;
use Swoole\Coroutine;
use app\common\lib\Redis;

class DispatchService
{
    // 派单模式
    const MODE_AUTO = 1; // 自动派单
    const MODE_MANUAL = 2; // 手动抢单
    
    /**
     * 智能派单
     */
    public function dispatchOrder($orderId, $mode = self::MODE_AUTO)
    {
        $order = Db::name('order')->where('id', $orderId)->find();
        if (!$order) {
            throw new \Exception('订单不存在');
        }
        
        $merchant = Db::name('merchant')->where('id', $order['merchant_id'])->find();
        
        if ($mode == self::MODE_AUTO) {
            // 自动派单
            $riderId = $this->autoDispatch($order, $merchant);
            if ($riderId) {
                $this->assignOrderToRider($orderId, $riderId);
                return $riderId;
            }
        }
        
        // 转为抢单模式
        $this->publishGrabOrder($order, $merchant);
        return 0;
    }
    
    /**
     * 自动派单算法
     */
    private function autoDispatch($order, $merchant)
    {
        // 1. 查找附近的骑手
        $riders = $this->findNearbyRiders(
            $merchant['lng'], 
            $merchant['lat'], 
            5000, // 5公里范围内
            10    // 最多10个骑手
        );
        
        if (empty($riders)) {
            return 0;
        }
        
        // 2. 计算每个骑手的得分
        $scoredRiders = [];
        foreach ($riders as $rider) {
            $score = $this->calculateRiderScore($rider, $order, $merchant);
            $scoredRiders[] = [
                'rider' => $rider,
                'score' => $score
            ];
        }
        
        // 3. 按得分排序
        usort($scoredRiders, function($a, $b) {
            return $b['score'] <=> $a['score'];
        });
        
        // 4. 选择最佳骑手
        $bestRider = $scoredRiders[0]['rider'];
        
        // 5. 检查骑手是否接受
        if ($this->checkRiderAvailability($bestRider['id'])) {
            return $bestRider['id'];
        }
        
        return 0;
    }
    
    /**
     * 计算骑手得分
     */
    private function calculateRiderScore($rider, $order, $merchant)
    {
        $score = 0;
        
        // 1. 距离分(40%)
        $distanceToMerchant = $this->getDistance(
            $rider['current_lng'], $rider['current_lat'],
            $merchant['lng'], $merchant['lat']
        );
        
        $distanceToUser = $this->getDistance(
            $merchant['lng'], $merchant['lat'],
            $order['delivery_lng'], $order['delivery_lat']
        );
        
        $totalDistance = $distanceToMerchant + $distanceToUser;
        
        if ($totalDistance <= 1000) {
            $score += 40;
        } elseif ($totalDistance <= 3000) {
            $score += 30;
        } elseif ($totalDistance <= 5000) {
            $score += 20;
        } else {
            $score += 10;
        }
        
        // 2. 评分分(30%)
        $ratingScore = ($rider['rating'] / 5.0) * 30;
        $score += $ratingScore;
        
        // 3. 今日接单数分(20%)
        // 鼓励接单少的骑手,避免负载不均衡
        if ($rider['today_orders'] == 0) {
            $score += 20;
        } elseif ($rider['today_orders'] <= 10) {
            $score += 15;
        } elseif ($rider['today_orders'] <= 20) {
            $score += 10;
        } else {
            $score += 5;
        }
        
        // 4. 准时率分(10%)
        $onTimeRate = $this->getRiderOnTimeRate($rider['id']);
        $score += $onTimeRate * 10;
        
        return $score;
    }
    
    /**
     * 发布抢单任务
     */
    private function publishGrabOrder($order, $merchant)
    {
        $redis = Redis::getInstance();
        $key = 'grab_order:' . $order['id'];
        
        $orderData = [
            'order_id' => $order['id'],
            'order_no' => $order['order_no'],
            'merchant_name' => $merchant['shop_name'],
            'merchant_address' => $merchant['address'],
            'delivery_address' => $order['delivery_address'],
            'total_amount' => $order['total_amount'],
            'delivery_fee' => $order['delivery_fee'],
            'create_time' => date('Y-m-d H:i:s'),
            'expire_time' => date('Y-m-d H:i:s', time() + 60) // 60秒内有效
        ];
        
        // 存储到Redis,60秒过期
        $redis->setex($key, 60, json_encode($orderData));
        
        // 推送给附近骑手
        $this->pushGrabOrderToRiders($orderData);
    }
}

4.5 实时消息推送

基于Workerman实现WebSocket实时通信:

php 复制代码
<?php
namespace app\common\lib;

use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use think\facade\Db;
use think\facade\Cache;

class WebSocketService
{
    public static $worker;
    
    // 连接用户映射
    public static $userConnections = [];
    
    /**
     * 启动WebSocket服务
     */
    public static function start()
    {
        self::$worker = new Worker('websocket://0.0.0.0:2346');
        
        // 设置进程数
        self::$worker->count = 4;
        
        // 连接建立时回调
        self::$worker->onConnect = function($connection) {
            echo "New connection\n";
        };
        
        // 收到消息时回调
        self::$worker->onMessage = function($connection, $data) {
            $data = json_decode($data, true);
            
            if (!isset($data['type'])) {
                $connection->send(json_encode([
                    'code' => 400,
                    'msg' => '消息格式错误'
                ]));
                return;
            }
            
            switch ($data['type']) {
                case 'auth':
                    self::handleAuth($connection, $data);
                    break;
                case 'heartbeat':
                    self::handleHeartbeat($connection, $data);
                    break;
                case 'location':
                    self::handleLocation($connection, $data);
                    break;
                case 'message':
                    self::handleMessage($connection, $data);
                    break;
                default:
                    $connection->send(json_encode([
                        'code' => 400,
                        'msg' => '未知消息类型'
                    ]));
            }
        };
        
        // 连接关闭时回调
        self::$worker->onClose = function($connection) {
            self::handleDisconnect($connection);
        };
        
        Worker::runAll();
    }
    
    /**
     * 处理认证
     */
    private static function handleAuth($connection, $data)
    {
        if (!isset($data['token'])) {
            $connection->send(json_encode([
                'code' => 401,
                'msg' => '认证失败'
            ]));
            return;
        }
        
        // 验证token
        $userInfo = Cache::get('user_token:' . $data['token']);
        
        if (!$userInfo) {
            $connection->send(json_encode([
                'code' => 401,
                'msg' => 'Token无效'
            ]));
            return;
        }
        
        // 保存连接
        $userId = $userInfo['user_id'];
        $connection->userId = $userId;
        $connection->userType = $userInfo['user_type'];
        
        self::$userConnections[$userId] = $connection;
        
        // 更新用户在线状态
        if ($userInfo['user_type'] == 3) { // 骑手
            Db::name('rider')
                ->where('user_id', $userId)
                ->update([
                    'is_online' => 1,
                    'update_time' => date('Y-m-d H:i:s')
                ]);
        }
        
        $connection->send(json_encode([
            'code' => 200,
            'msg' => '认证成功',
            'data' => [
                'user_id' => $userId,
                'user_type' => $userInfo['user_type']
            ]
        ]));
    }
    
    /**
     * 处理心跳
     */
    private static function handleHeartbeat($connection, $data)
    {
        $connection->send(json_encode([
            'code' => 200,
            'type' => 'pong',
            'time' => time()
        ]));
    }
    
    /**
     * 处理位置更新
     */
    private static function handleLocation($connection, $data)
    {
        if (!isset($data['lng']) || !isset($data['lat'])) {
            return;
        }
        
        $userId = $connection->userId;
        $userType = $connection->userType;
        
        if ($userType == 3) { // 骑手更新位置
            Db::name('rider')
                ->where('user_id', $userId)
                ->update([
                    'current_lng' => $data['lng'],
                    'current_lat' => $data['lat'],
                    'update_time' => date('Y-m-d H:i:s')
                ]);
            
            // 广播给需要的位置订阅者
            self::broadcastRiderLocation($userId, $data['lng'], $data['lat']);
        }
    }
    
    /**
     * 发送消息给指定用户
     */
    public static function sendToUser($userId, $message)
    {
        if (isset(self::$userConnections[$userId])) {
            $connection = self::$userConnections[$userId];
            $connection->send(json_encode($message));
            return true;
        }
        
        // 用户不在线,存储离线消息
        self::storeOfflineMessage($userId, $message);
        return false;
    }
    
    /**
     * 广播骑手位置
     */
    private static function broadcastRiderLocation($riderId, $lng, $lat)
    {
        $message = [
            'type' => 'rider_location',
            'data' => [
                'rider_id' => $riderId,
                'lng' => $lng,
                'lat' => $lat,
                'time' => time()
            ]
        ];
        
        // 这里可以根据业务需要广播给相关用户
        // 例如:订单的消费者可以收到骑手位置更新
    }
}

5. 性能优化策略

5.1 数据库优化

5.1.1 读写分离
php 复制代码
<?php
// database.php 配置
return [
    // 默认数据连接配置
    'default' => env('database.driver', 'mysql'),
    
    // 数据库连接配置
    'connections' => [
        'mysql' => [
            'type' => 'mysql',
            'hostname' => env('database.hostname', '127.0.0.1'),
            'database' => env('database.database', ''),
            'username' => env('database.username', 'root'),
            'password' => env('database.password', ''),
            'hostport' => env('database.hostport', '3306'),
            'charset' => 'utf8mb4',
            'deploy' => 1, // 部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
            'rw_separate' => true, // 是否读写分离
            'master_num' => 1, // 读写分离后 主服务器数量
            'slave_no' => '', // 指定从服务器序号
            'fields_strict' => true,
            'break_reconnect' => true,
            
            // 主服务器
            'master' => [
                ['hostname' => '192.168.1.101', 'hostport' => 3306],
            ],
            
            // 从服务器
            'slave' => [
                ['hostname' => '192.168.1.102', 'hostport' => 3306],
                ['hostname' => '192.168.1.103', 'hostport' => 3306],
            ],
        ],
    ],
];
5.1.2 分库分表策略
php 复制代码
<?php
namespace app\common\lib;

class Sharding
{
    // 按用户ID分表
    public static function getTableByUserId($userId, $baseTable)
    {
        $suffix = $userId % 16; // 16张表
        return $baseTable . '_' . str_pad($suffix, 2, '0', STR_PAD_LEFT);
    }
    
    // 按时间分表
    public static function getTableByMonth($baseTable)
    {
        $month = date('Ym');
        return $baseTable . '_' . $month;
    }
    
    // 获取分表查询条件
    public static function getShardingQuery($model, $shardField, $shardValue)
    {
        if ($shardField == 'user_id') {
            $table = self::getTableByUserId($shardValue, $model->getTable());
        } elseif ($shardField == 'create_time') {
            $table = self::getTableByMonth($model->getTable());
        } else {
            $table = $model->getTable();
        }
        
        $model->table($table);
        return $model;
    }
}

5.2 缓存优化

5.2.1 多级缓存策略
php 复制代码
<?php
namespace app\common\lib;

use think\facade\Cache;
use think\facade\Config;

class MultiLevelCache
{
    // 本地缓存(APCu)
    private static $localCache = [];
    
    // Redis缓存
    private static $redis = null;
    
    /**
     * 获取缓存
     */
    public static function get($key, $default = null, $expire = 3600)
    {
        // 1. 尝试本地缓存
        if (extension_loaded('apcu') && apcu_enabled()) {
            $value = apcu_fetch($key);
            if ($value !== false) {
                return $value;
            }
        }
        
        // 2. 尝试Redis缓存
        $value = Cache::get($key);
        if ($value !== null) {
            // 回写到本地缓存
            if (extension_loaded('apcu') && apcu_enabled()) {
                apcu_store($key, $value, min(300, $expire)); // 本地缓存5分钟
            }
            return $value;
        }
        
        // 3. 从数据库获取
        if (is_callable($default)) {
            $value = call_user_func($default);
            if ($value !== null) {
                self::set($key, $value, $expire);
            }
            return $value;
        }
        
        return $default;
    }
    
    /**
     * 设置缓存
     */
    public static function set($key, $value, $expire = 3600)
    {
        // 设置Redis缓存
        Cache::set($key, $value, $expire);
        
        // 设置本地缓存(较短的过期时间)
        if (extension_loaded('apcu') && apcu_enabled()) {
            apcu_store($key, $value, min(300, $expire));
        }
        
        return true;
    }
    
    /**
     * 删除缓存
     */
    public static function delete($key)
    {
        // 删除本地缓存
        if (extension_loaded('apcu') && apcu_enabled()) {
            apcu_delete($key);
        }
        
        // 删除Redis缓存
        return Cache::delete($key);
    }
}
5.2.2 热点数据缓存
php 复制代码
<?php
namespace app\common\service;

use think\facade\Cache;

class HotDataService
{
    // 热点数据配置
    private static $hotDataConfig = [
        'merchant_info' => [
            'prefix' => 'merchant:',
            'expire' => 3600, // 1小时
            'version' => 'v1'
        ],
        'product_info' => [
            'prefix' => 'product:',
            'expire' => 1800, // 30分钟
            'version' => 'v1'
        ],
        'user_info' => [
            'prefix' => 'user:',
            'expire' => 7200, // 2小时
            'version' => 'v1'
        ]
    ];
    
    /**
     * 获取商家信息(带缓存)
     */
    public static function getMerchantInfo($merchantId)
    {
        $config = self::$hotDataConfig['merchant_info'];
        $cacheKey = $config['prefix'] . $merchantId . ':' . $config['version'];
        
        $merchant = Cache::get($cacheKey);
        
        if (!$merchant) {
            // 从数据库获取
            $merchant = \think\facade\Db::name('merchant')
                ->where('id', $merchantId)
                ->find();
                
            if ($merchant) {
                // 缓存到Redis
                Cache::set($cacheKey, $merchant, $config['expire']);
                
                // 异步更新相关缓存
                \think\facade\Queue::push(\app\common\job\UpdateMerchantCache::class, [
                    'merchant_id' => $merchantId
                ]);
            }
        }
        
        return $merchant;
    }
    
    /**
     * 批量获取商品信息
     */
    public static function getProductsBatch($productIds)
    {
        if (empty($productIds)) {
            return [];
        }
        
        $config = self::$hotDataConfig['product_info'];
        $products = [];
        $missingIds = [];
        
        // 先从缓存获取
        foreach ($productIds as $id) {
            $cacheKey = $config['prefix'] . $id . ':' . $config['version'];
            $product = Cache::get($cacheKey);
            
            if ($product) {
                $products[$id] = $product;
            } else {
                $missingIds[] = $id;
            }
        }
        
        // 批量查询缺失的数据
        if (!empty($missingIds)) {
            $missingProducts = \think\facade\Db::name('product')
                ->whereIn('id', $missingIds)
                ->select()
                ->toArray();
                
            foreach ($missingProducts as $product) {
                $cacheKey = $config['prefix'] . $product['id'] . ':' . $config['version'];
                Cache::set($cacheKey, $product, $config['expire']);
                $products[$product['id']] = $product;
            }
        }
        
        return $products;
    }
}

5.3 异步处理

5.3.1 消息队列配置
php 复制代码
<?php
// queue.php 配置
return [
    'default' => 'redis',
    
    'connections' => [
        'sync' => [
            'type' => 'sync',
        ],
        
        'redis' => [
            'type' => 'redis',
            'queue' => 'default',
            'host' => '127.0.0.1',
            'port' => 6379,
            'password' => '',
            'select' => 0,
            'timeout' => 0,
            'persistent' => false,
            'expire' => 60, // 任务执行超时时间
            'retry_seconds' => 5, // 失败后重试间隔
        ],
        
        'rabbitmq' => [
            'type' => 'amqp',
            'host' => '127.0.0.1',
            'port' => 5672,
            'user' => 'guest',
            'password' => 'guest',
            'vhost' => '/',
            'exchange' => 'order_exchange',
            'exchange_type' => 'direct',
            'queue' => 'order_queue',
            'timeout' => 0,
        ],
    ],
    
    'failed' => [
        'type' => 'database',
        'table' => 'failed_jobs',
    ],
];
5.3.2 异步任务示例
php 复制代码
<?php
namespace app\common\job;

use think\queue\Job;
use think\facade\Db;
use app\common\service\MessageService;

class OrderTimeout
{
    /**
     * 订单超时处理
     */
    public function fire(Job $job, $data)
    {
        $orderId = $data['order_id'] ?? 0;
        
        if (!$orderId) {
            $job->delete();
            return;
        }
        
        // 查询订单状态
        $order = Db::name('order')
            ->where('id', $orderId)
            ->find();
            
        if (!$order) {
            $job->delete();
            return;
        }
        
        // 如果订单还是待支付,则自动取消
        if ($order['pay_status'] == 0 && $order['order_status'] == 0) {
            Db::startTrans();
            try {
                // 更新订单状态
                Db::name('order')
                    ->where('id', $orderId)
                    ->update([
                        'order_status' => 6, // 已取消
                        'cancel_reason' => '超时未支付,系统自动取消',
                        'cancel_time' => date('Y-m-d H:i:s'),
                        'update_time' => date('Y-m-d H:i:s')
                    ]);
                    
                // 恢复库存
                $orderItems = Db::name('order_item')
                    ->where('order_id', $orderId)
                    ->select();
                    
                foreach ($orderItems as $item) {
                    Db::name('product')
                        ->where('id', $item['product_id'])
                        ->inc('stock', $item['quantity'])
                        ->update();
                }
                
                // 发送通知
                MessageService::sendToUser($order['user_id'], [
                    'type' => 'order_cancelled',
                    'data' => [
                        'order_no' => $order['order_no'],
                        'reason' => '超时未支付'
                    ]
                ]);
                
                Db::commit();
                
            } catch (\Exception $e) {
                Db::rollback();
                // 记录日志
                \think\facade\Log::error('订单超时处理失败:' . $e->getMessage());
                $job->release(300); // 5分钟后重试
                return;
            }
        }
        
        $job->delete();
    }
    
    /**
     * 任务失败处理
     */
    public function failed($data)
    {
        // 记录失败日志
        \think\facade\Log::error('订单超时任务失败:' . json_encode($data));
    }
}

6. 安全防护

6.1 输入验证与过滤

php 复制代码
<?php
namespace app\common\lib;

class Security
{
    /**
     * XSS过滤
     */
    public static function xssClean($input)
    {
        if (is_array($input)) {
            foreach ($input as $key => $value) {
                $input[$key] = self::xssClean($value);
            }
        } else {
            $input = htmlspecialchars($input, ENT_QUOTES | ENT_HTML401, 'UTF-8');
            $input = self::removeXss($input);
        }
        
        return $input;
    }
    
    /**
     * SQL注入防护
     */
    public static function sqlInjectionCheck($input)
    {
        $dangerousPatterns = [
            '/(union\s+select)/i',
            '/(select.*from)/i',
            '/(insert\s+into)/i',
            '/(update.*set)/i',
            '/(delete\s+from)/i',
            '/(drop\s+table)/i',
            '/(truncate\s+table)/i',
            '/\'\s*or\s*\'/i',
            '/\'\s*and\s*\'/i',
            '/\/\*.*\*\//',
            '/--/',
            '/;/',
        ];
        
        foreach ($dangerousPatterns as $pattern) {
            if (preg_match($pattern, $input)) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 请求频率限制
     */
    public static function rateLimit($key, $limit = 60, $period = 60)
    {
        $redis = \think\facade\Cache::store('redis')->handler();
        $now = time();
        
        $redisKey = 'rate_limit:' . $key;
        
        // 使用Redis的zset实现滑动窗口
        $redis->zremrangebyscore($redisKey, 0, $now - $period);
        $current = $redis->zcard($redisKey);
        
        if ($current >= $limit) {
            return false;
        }
        
        $redis->zadd($redisKey, $now, $now . ':' . uniqid());
        $redis->expire($redisKey, $period);
        
        return true;
    }
}

6.2 数据加密

php 复制代码
<?php
namespace app\common\lib;

class Encryption
{
    private static $method = 'AES-256-CBC';
    
    /**
     * 加密数据
     */
    public static function encrypt($data, $key = null)
    {
        if ($key === null) {
            $key = config('app.app_key');
        }
        
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$method));
        $encrypted = openssl_encrypt(
            json_encode($data),
            self::$method,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );
        
        return base64_encode($iv . $encrypted);
    }
    
    /**
     * 解密数据
     */
    public static function decrypt($data, $key = null)
    {
        if ($key === null) {
            $key = config('app.app_key');
        }
        
        $data = base64_decode($data);
        $ivLength = openssl_cipher_iv_length(self::$method);
        $iv = substr($data, 0, $ivLength);
        $encrypted = substr($data, $ivLength);
        
        $decrypted = openssl_decrypt(
            $encrypted,
            self::$method,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );
        
        return json_decode($decrypted, true);
    }
    
    /**
     * 密码哈希
     */
    public static function hashPassword($password)
    {
        return password_hash($password, PASSWORD_BCRYPT, [
            'cost' => 12
        ]);
    }
    
    /**
     * 验证密码
     */
    public static function verifyPassword($password, $hash)
    {
        return password_verify($password, $hash);
    }
}

7. 部署与监控

7.1 Docker部署配置

dockerfile 复制代码
# Dockerfile
FROM php:7.4-fpm

# 安装系统依赖
RUN apt-get update && apt-get install -y \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libpng-dev \
    libzip-dev \
    libssl-dev \
    librabbitmq-dev \
    git \
    curl \
    wget \
    vim \
    && rm -rf /var/lib/apt/lists/*

# 安装PHP扩展
RUN docker-php-ext-install -j$(nproc) \
    bcmath \
    gd \
    mysqli \
    pdo_mysql \
    sockets \
    zip \
    pcntl

# 安装Redis扩展
RUN pecl install redis && docker-php-ext-enable redis

# 安装Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# 设置工作目录
WORKDIR /var/www/html

# 复制项目文件
COPY . .

# 安装PHP依赖
RUN composer install --no-dev --optimize-autoloader

# 设置权限
RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html/storage \
    && chmod -R 755 /var/www/html/runtime

# 暴露端口
EXPOSE 9000

CMD ["php-fpm"]
yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  nginx:
    image: nginx:1.20
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
      - ./logs/nginx:/var/log/nginx
      - ./public:/var/www/html/public
    depends_on:
      - php
      - php-worker
    networks:
      - app-network

  php:
    build: .
    volumes:
      - .:/var/www/html
      - ./php.ini:/usr/local/etc/php/php.ini
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
    networks:
      - app-network

  php-worker:
    build: .
    command: php think queue:work --queue
    volumes:
      - .:/var/www/html
    depends_on:
      - redis
      - rabbitmq
    networks:
      - app-network

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "3306:3306"
    networks:
      - app-network

  redis:
    image: redis:6.2
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    networks:
      - app-network

  rabbitmq:
    image: rabbitmq:3.9-management
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    networks:
      - app-network

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - app-network

  grafana:
    image: grafana/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - app-network

volumes:
  mysql-data:
  redis-data:
  rabbitmq-data:
  prometheus-data:
  grafana-data:

networks:
  app-network:
    driver: bridge

7.# .2 系统监控配置

yaml 复制代码
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets: []

rule_files: []

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'nginx'
    static_configs:
      - targets: ['nginx:9113']

  - job_name: 'mysql'
    static_configs:
      - targets: ['mysql:9104']
    metrics_path: /metrics
    params:
      collect[]:
        - global_status
        - innodb_metrics
        - performance_schema.tableiowaits
        - performance_schema.indexiowaits
        - info_schema.innodb_metrics
        - standard

  - job_name: 'redis'
    static_configs:
      - targets: ['redis:9121']

  - job_name: 'php-fpm'
    static_configs:
      - targets: ['php:9253']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['php:9100']
    metrics_path: /metrics

7.2 性能监控实现

php 复制代码
<?php
namespace app\common\lib;

use think\facade\Db;
use think\facade\Log;

class Monitor
{
    // 慢查询监控
    public static function monitorSlowQuery($sql, $time)
    {
        $slowQueryThreshold = config('app.slow_query_threshold', 1.0);
        
        if ($time > $slowQueryThreshold) {
            $logData = [
                'sql' => $sql,
                'execution_time' => $time,
                'timestamp' => date('Y-m-d H:i:s'),
                'server' => gethostname(),
                'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5)
            ];
            
            Log::warning('慢查询警告: ' . json_encode($logData));
            
            // 记录到数据库
            Db::name('slow_query_log')->insert([
                'sql' => $sql,
                'execution_time' => $time,
                'create_time' => date('Y-m-d H:i:s')
            ]);
        }
    }
    
    // API性能监控
    public static function monitorApiPerformance($route, $startTime)
    {
        $endTime = microtime(true);
        $executionTime = round(($endTime - $startTime) * 1000, 2); // 毫秒
        
        $threshold = config('app.api_performance_threshold', 500); // 500ms
        
        if ($executionTime > $threshold) {
            $logData = [
                'route' => $route,
                'execution_time' => $executionTime,
                'timestamp' => date('Y-m-d H:i:s'),
                'memory_usage' => memory_get_usage(true) / 1024 / 1024, // MB
                'peak_memory' => memory_get_peak_usage(true) / 1024 / 1024
            ];
            
            Log::warning('API性能警告: ' . json_encode($logData));
            
            // 发送到监控系统
            self::sendToPrometheus('api_performance', [
                'route' => $route,
                'duration' => $executionTime
            ]);
        }
    }
    
    // 业务指标监控
    public static function recordBusinessMetrics()
    {
        static $lastRecordTime = 0;
        $currentTime = time();
        
        // 每分钟记录一次
        if ($currentTime - $lastRecordTime < 60) {
            return;
        }
        
        $lastRecordTime = $currentTime;
        
        try {
            // 1. 订单相关指标
            $orderMetrics = self::getOrderMetrics();
            
            // 2. 用户相关指标
            $userMetrics = self::getUserMetrics();
            
            // 3. 商家相关指标
            $merchantMetrics = self::getMerchantMetrics();
            
            // 发送到Prometheus
            self::sendMetricsToPrometheus(array_merge(
                $orderMetrics,
                $userMetrics,
                $merchantMetrics
            ));
            
        } catch (\Exception $e) {
            Log::error('记录业务指标失败: ' . $e->getMessage());
        }
    }
    
    private static function getOrderMetrics()
    {
        $now = date('Y-m-d H:i:s');
        $oneHourAgo = date('Y-m-d H:i:s', time() - 3600);
        $todayStart = date('Y-m-d 00:00:00');
        
        return [
            'orders_total' => Db::name('order')->count(),
            'orders_today' => Db::name('order')
                ->where('create_time', '>=', $todayStart)
                ->count(),
            'orders_last_hour' => Db::name('order')
                ->where('create_time', '>=', $oneHourAgo)
                ->count(),
            'orders_pending' => Db::name('order')
                ->where('order_status', 0)
                ->count(),
            'orders_delivering' => Db::name('order')
                ->where('order_status', 4)
                ->count(),
            'orders_completed_today' => Db::name('order')
                ->where('order_status', 5)
                ->where('complete_time', '>=', $todayStart)
                ->count(),
            'orders_cancelled_today' => Db::name('order')
                ->where('order_status', 6)
                ->where('cancel_time', '>=', $todayStart)
                ->count(),
        ];
    }
}

8. 小程序前端实现

8.1 小程序架构设计

复制代码
小程序项目结构:
├── app.js              # 小程序入口文件
├── app.json            # 小程序配置
├── app.wxss            # 全局样式
├── config/             # 配置文件
│   ├── api.js         # API接口配置
│   └── env.js         # 环境配置
├── lib/               # 第三方库
│   ├── wxParse/       # 富文本解析
│   └── qqmap-wx-jssdk.js # 腾讯地图SDK
├── components/        # 公共组件
│   ├── loading/      # 加载组件
│   ├── empty/        # 空状态组件
│   └── order-item/   # 订单项组件
├── pages/            # 页面目录
│   ├── index/        # 首页
│   ├── merchant/     # 商家页
│   ├── order/        # 订单页
│   ├── user/         # 用户中心
│   └── rider/        # 骑手端
└── services/         # 服务层
    ├── api.js        # API请求封装
    ├── auth.js       # 认证服务
    └── storage.js    # 存储服务

8.2 首页实现

javascript 复制代码
// pages/index/index.js
const app = getApp();
const API = require('../../services/api');
const MAP_KEY = '您的腾讯地图KEY';

Page({
  data: {
    // 用户位置
    userLocation: null,
    // 附近商家
    nearbyMerchants: [],
    // 推荐商家
    recommendMerchants: [],
    // 搜索关键字
    searchKeyword: '',
    // 加载状态
    isLoading: true,
    // 分页参数
    page: 1,
    limit: 20,
    hasMore: true,
    // 分类列表
    categories: [
      { id: 1, name: '全部', icon: 'all' },
      { id: 2, name: '美食', icon: 'food' },
      { id: 3, name: '超市', icon: 'market' },
      { id: 4, name: '水果', icon: 'fruit' },
      { id: 5, name: '医药', icon: 'medicine' }
    ],
    activeCategory: 1
  },

  onLoad() {
    this.initLocation();
    this.getRecommendMerchants();
  },

  onShow() {
    // 检查登录状态
    if (!app.globalData.isLogin) {
      wx.redirectTo({
        url: '/pages/auth/login'
      });
    }
  },

  // 初始化定位
  initLocation() {
    const that = this;
    
    // 获取当前位置
    wx.getLocation({
      type: 'gcj02',
      success(res) {
        const { latitude, longitude } = res;
        that.setData({
          userLocation: { lat: latitude, lng: longitude }
        });
        
        // 获取附近商家
        that.getNearbyMerchants(latitude, longitude);
        
        // 逆地址解析获取详细地址
        that.reverseGeocoder(latitude, longitude);
      },
      fail(err) {
        console.error('获取位置失败:', err);
        that.setData({ isLoading: false });
        
        // 使用默认位置
        that.getNearbyMerchants(30.67, 104.07);
      }
    });
  },

  // 获取附近商家
  getNearbyMerchants(lat, lng) {
    const { page, limit, activeCategory } = this.data;
    
    API.getNearbyMerchants({
      lat,
      lng,
      distance: 5000,
      page,
      limit,
      category_id: activeCategory === 1 ? 0 : activeCategory
    }).then(res => {
      if (res.code === 200) {
        const merchants = res.data.list || [];
        const hasMore = res.data.total > page * limit;
        
        this.setData({
          nearbyMerchants: page === 1 ? merchants : [...this.data.nearbyMerchants, ...merchants],
          isLoading: false,
          hasMore
        });
      }
    }).catch(err => {
      console.error('获取附近商家失败:', err);
      this.setData({ isLoading: false });
    });
  },

  // 获取推荐商家
  getRecommendMerchants() {
    const { userLocation } = this.data;
    
    if (!userLocation) return;
    
    API.getRecommendMerchants({
      lat: userLocation.lat,
      lng: userLocation.lng
    }).then(res => {
      if (res.code === 200) {
        this.setData({
          recommendMerchants: res.data || []
        });
      }
    });
  },

  // 逆地址解析
  reverseGeocoder(lat, lng) {
    const qqmapsdk = app.globalData.qqmapsdk;
    
    if (!qqmapsdk) return;
    
    qqmapsdk.reverseGeocoder({
      location: { latitude: lat, longitude: lng },
      success: (res) => {
        const address = res.result.address_component;
        const currentAddress = `${address.city}${address.district}${address.street}`;
        
        // 保存到全局
        app.globalData.currentAddress = currentAddress;
        app.globalData.currentCity = address.city;
        
        this.setData({ currentAddress });
      }
    });
  },

  // 分类切换
  onCategoryChange(e) {
    const categoryId = e.currentTarget.dataset.id;
    
    if (categoryId === this.data.activeCategory) return;
    
    this.setData({
      activeCategory: categoryId,
      page: 1,
      isLoading: true
    });
    
    const { userLocation } = this.data;
    if (userLocation) {
      this.getNearbyMerchants(userLocation.lat, userLocation.lng);
    }
  },

  // 搜索商家
  onSearch(e) {
    const keyword = e.detail.value.trim();
    
    if (!keyword) {
      this.setData({ searchKeyword: '' });
      return;
    }
    
    wx.navigateTo({
      url: `/pages/search/result?keyword=${keyword}`
    });
  },

  // 加载更多
  onLoadMore() {
    if (!this.data.hasMore || this.data.isLoading) return;
    
    this.setData({ page: this.data.page + 1 });
    
    const { userLocation } = this.data;
    if (userLocation) {
      this.getNearbyMerchants(userLocation.lat, userLocation.lng);
    }
  },

  // 跳转到商家详情
  goToMerchant(e) {
    const merchantId = e.currentTarget.dataset.id;
    
    wx.navigateTo({
      url: `/pages/merchant/detail?id=${merchantId}`
    });
  },

  // 下拉刷新
  onPullDownRefresh() {
    this.setData({
      page: 1,
      isLoading: true
    });
    
    const { userLocation } = this.data;
    if (userLocation) {
      Promise.all([
        this.getNearbyMerchants(userLocation.lat, userLocation.lng),
        this.getRecommendMerchants()
      ]).then(() => {
        wx.stopPullDownRefresh();
      });
    } else {
      this.initLocation();
      wx.stopPullDownRefresh();
    }
  },

  // 上拉加载
  onReachBottom() {
    this.onLoadMore();
  },

  // 分享
  onShareAppMessage() {
    return {
      title: '乡镇外卖跑腿,便捷生活送到家',
      path: '/pages/index/index',
      imageUrl: '/images/share.jpg'
    };
  }
});
xml 复制代码
<!-- pages/index/index.wxml -->
<view class="container">
  <!-- 顶部搜索栏 -->
  <view class="search-bar">
    <view class="location" bindtap="goToLocation">
      <text class="location-icon">📍</text>
      <text class="location-text">{{currentAddress || '正在定位...'}}</text>
      <text class="location-arrow">▼</text>
    </view>
    <view class="search-input">
      <input 
        placeholder="搜索商家、商品" 
        confirm-type="search"
        bindconfirm="onSearch"
        value="{{searchKeyword}}"
      />
      <text class="search-icon">🔍</text>
    </view>
  </view>

  <!-- 分类导航 -->
  <scroll-view class="category-scroll" scroll-x>
    <view class="category-list">
      <block wx:for="{{categories}}" wx:key="id">
        <view 
          class="category-item {{activeCategory === item.id ? 'active' : ''}}"
          data-id="{{item.id}}"
          bindtap="onCategoryChange"
        >
          <view class="category-icon">{{item.icon}}</view>
          <text class="category-name">{{item.name}}</text>
        </view>
      </block>
    </view>
  </scroll-view>

  <!-- 推荐商家 -->
  <view class="section recommend-section" wx:if="{{recommendMerchants.length > 0}}">
    <view class="section-header">
      <text class="section-title">推荐商家</text>
      <text class="section-more">查看更多</text>
    </view>
    <scroll-view class="recommend-scroll" scroll-x>
      <view class="recommend-list">
        <block wx:for="{{recommendMerchants}}" wx:key="id">
          <view 
            class="recommend-item" 
            data-id="{{item.id}}"
            bindtap="goToMerchant"
          >
            <image class="shop-image" src="{{item.shop_logo || '/images/default-shop.png'}}" mode="aspectFill" />
            <view class="shop-info">
              <text class="shop-name">{{item.shop_name}}</text>
              <view class="shop-tags">
                <text class="tag">{{item.distance}}</text>
                <text class="tag">{{item.delivery_time}}分钟</text>
              </view>
              <view class="shop-rating">
                <text class="rating">⭐ {{item.rating || 5.0}}</text>
                <text class="sales">月售{{item.month_sales || 0}}</text>
              </view>
            </view>
          </view>
        </block>
      </view>
    </scroll-view>
  </view>

  <!-- 附近商家 -->
  <view class="section nearby-section">
    <view class="section-header">
      <text class="section-title">附近商家</text>
    </view>
    
    <block wx:if="{{!isLoading && nearbyMerchants.length === 0}}">
      <view class="empty">
        <image src="/images/empty.png" class="empty-image" />
        <text class="empty-text">附近暂无商家</text>
      </view>
    </block>
    
    <block wx:else>
      <view class="merchant-list">
        <block wx:for="{{nearbyMerchants}}" wx:key="id">
          <view 
            class="merchant-item" 
            data-id="{{item.id}}"
            bindtap="goToMerchant"
          >
            <image class="merchant-image" src="{{item.shop_logo || '/images/default-shop.png'}}" />
            <view class="merchant-info">
              <view class="merchant-header">
                <text class="merchant-name">{{item.shop_name}}</text>
                <view class="merchant-status">
                  <text class="status-dot {{item.status === 1 ? 'open' : 'close'}}"></text>
                  <text class="status-text">{{item.status === 1 ? '营业中' : '已打烊'}}</text>
                </view>
              </view>
              
              <view class="merchant-meta">
                <text class="meta-item">⭐ {{item.rating || 5.0}}</text>
                <text class="meta-item">月售{{item.month_sales || 0}}</text>
                <text class="meta-item">{{item.distance}}</text>
                <text class="meta-item">{{item.delivery_time}}分钟</text>
              </view>
              
              <view class="merchant-extra">
                <text class="delivery-fee">配送费¥{{item.delivery_fee}}</text>
                <text class="min-amount">起送¥{{item.min_order_amount}}</text>
                <text class="business-hours">{{item.business_hours}}</text>
              </view>
              
              <view class="merchant-tags" wx:if="{{item.tags && item.tags.length > 0}}">
                <block wx:for="{{item.tags.slice(0, 3)}}" wx:key="index">
                  <text class="tag">{{item}}</text>
                </block>
              </view>
            </view>
          </view>
        </block>
      </view>
      
      <!-- 加载更多 -->
      <block wx:if="{{hasMore}}">
        <view class="load-more" bindtap="onLoadMore">
          <text>{{isLoading ? '加载中...' : '点击加载更多'}}</text>
        </view>
      </block>
      
      <block wx:else>
        <view class="no-more">
          <text>没有更多商家了</text>
        </view>
      </block>
    </block>
  </view>

  <!-- 加载中 -->
  <block wx:if="{{isLoading}}">
    <view class="loading">
      <view class="loading-spinner"></view>
      <text>加载中...</text>
    </view>
  </block>
</view>
css 复制代码
/* pages/index/index.wxss */
.container {
  background-color: #f5f5f5;
  min-height: 100vh;
}

/* 搜索栏 */
.search-bar {
  background: linear-gradient(135deg, #ff6b6b, #ee5a52);
  padding: 20rpx 30rpx;
  display: flex;
  align-items: center;
  gap: 20rpx;
}

.location {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  color: white;
  font-size: 28rpx;
  max-width: 200rpx;
}

.location-icon {
  margin-right: 10rpx;
  font-size: 32rpx;
}

.location-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.location-arrow {
  margin-left: 5rpx;
  font-size: 24rpx;
}

.search-input {
  flex: 1;
  background: white;
  border-radius: 50rpx;
  padding: 15rpx 30rpx;
  display: flex;
  align-items: center;
}

.search-input input {
  flex: 1;
  font-size: 28rpx;
}

.search-icon {
  color: #999;
  font-size: 32rpx;
}

/* 分类导航 */
.category-scroll {
  white-space: nowrap;
  background: white;
  padding: 20rpx 0;
}

.category-list {
  display: inline-flex;
  padding: 0 30rpx;
}

.category-item {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  margin-right: 40rpx;
  padding: 20rpx 0;
  min-width: 100rpx;
}

.category-item.active .category-icon {
  color: #ee5a52;
  background: rgba(238, 90, 82, 0.1);
}

.category-item.active .category-name {
  color: #ee5a52;
  font-weight: bold;
}

.category-icon {
  width: 80rpx;
  height: 80rpx;
  border-radius: 50%;
  background: #f5f5f5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40rpx;
  margin-bottom: 10rpx;
  color: #666;
}

.category-name {
  font-size: 24rpx;
  color: #666;
}

/* 区域样式 */
.section {
  background: white;
  margin-top: 20rpx;
  padding: 0 30rpx;
}

.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 30rpx 0 20rpx;
  border-bottom: 2rpx solid #f5f5f5;
}

.section-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
}

.section-more {
  font-size: 28rpx;
  color: #999;
}

/* 推荐商家 */
.recommend-scroll {
  white-space: nowrap;
  padding: 30rpx 0;
}

.recommend-list {
  display: inline-flex;
}

.recommend-item {
  display: inline-flex;
  flex-direction: column;
  width: 300rpx;
  margin-right: 20rpx;
  background: #f9f9f9;
  border-radius: 20rpx;
  overflow: hidden;
}

.shop-image {
  width: 100%;
  height: 200rpx;
}

.shop-info {
  padding: 20rpx;
}

.shop-name {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  display: block;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shop-tags {
  display: flex;
  gap: 10rpx;
  margin-bottom: 10rpx;
}

.tag {
  font-size: 22rpx;
  color: #666;
  background: #f0f0f0;
  padding: 4rpx 10rpx;
  border-radius: 6rpx;
}

.shop-rating {
  display: flex;
  justify-content: space-between;
  font-size: 24rpx;
  color: #999;
}

/* 商家列表 */
.merchant-list {
  padding: 30rpx 0;
}

.merchant-item {
  display: flex;
  padding: 30rpx 0;
  border-bottom: 2rpx solid #f5f5f5;
}

.merchant-item:last-child {
  border-bottom: none;
}

.merchant-image {
  width: 180rpx;
  height: 180rpx;
  border-radius: 10rpx;
  margin-right: 30rpx;
  flex-shrink: 0;
}

.merchant-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.merchant-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15rpx;
}

.merchant-name {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  max-width: 400rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.merchant-status {
  display: flex;
  align-items: center;
}

.status-dot {
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  margin-right: 8rpx;
}

.status-dot.open {
  background-color: #52c41a;
}

.status-dot.close {
  background-color: #999;
}

.status-text {
  font-size: 24rpx;
  color: #666;
}

.merchant-meta {
  display: flex;
  gap: 20rpx;
  margin-bottom: 15rpx;
}

.meta-item {
  font-size: 24rpx;
  color: #666;
}

.merchant-extra {
  display: flex;
  gap: 20rpx;
  margin-bottom: 15rpx;
  font-size: 24rpx;
  color: #666;
}

.merchant-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10rpx;
}

/* 加载更多 */
.load-more, .no-more {
  text-align: center;
  padding: 40rpx 0;
  color: #999;
  font-size: 28rpx;
}

/* 加载中 */
.loading {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(255, 255, 255, 0.9);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.loading-spinner {
  width: 60rpx;
  height: 60rpx;
  border: 6rpx solid #f3f3f3;
  border-top: 6rpx solid #ee5a52;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 20rpx;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* 空状态 */
.empty {
  padding: 100rpx 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.empty-image {
  width: 200rpx;
  height: 200rpx;
  margin-bottom: 30rpx;
}

.empty-text {
  font-size: 28rpx;
  color: #999;
}

8.3 商家详情页

javascript 复制代码
// pages/merchant/detail.js
const app = getApp();
const API = require('../../services/api');

Page({
  data: {
    // 商家ID
    merchantId: null,
    // 商家信息
    merchant: null,
    // 商品分类
    categories: [],
    // 商品列表
    products: [],
    // 购物车
    cart: [],
    // 购物车总价
    cartTotal: 0,
    // 购物车商品数量
    cartCount: 0,
    // 活动展开状态
    showAllDiscounts: false,
    // 当前分类
    currentCategory: 0,
    // 加载状态
    isLoading: true
  },

  onLoad(options) {
    const merchantId = options.id;
    
    if (!merchantId) {
      wx.navigateBack();
      return;
    }
    
    this.setData({ merchantId });
    this.loadMerchantDetail(merchantId);
  },

  onShow() {
    // 从缓存恢复购物车
    this.restoreCart();
  },

  // 加载商家详情
  loadMerchantDetail(merchantId) {
    this.setData({ isLoading: true });
    
    Promise.all([
      this.getMerchantInfo(merchantId),
      this.getMerchantProducts(merchantId),
      this.getMerchantDiscounts(merchantId)
    ]).then(() => {
      this.setData({ isLoading: false });
    }).catch(err => {
      console.error('加载商家详情失败:', err);
      this.setData({ isLoading: false });
      
      wx.showToast({
        title: '加载失败',
        icon: 'none'
      });
    });
  },

  // 获取商家信息
  getMerchantInfo(merchantId) {
    return new Promise((resolve, reject) => {
      API.getMerchantDetail(merchantId).then(res => {
        if (res.code === 200) {
          this.setData({ merchant: res.data });
          resolve();
        } else {
          reject();
        }
      }).catch(reject);
    });
  },

  // 获取商家商品
  getMerchantProducts(merchantId) {
    return new Promise((resolve, reject) => {
      API.getMerchantProducts(merchantId).then(res => {
        if (res.code === 200) {
          const products = res.data.products || [];
          const categories = this.processCategories(products);
          
          this.setData({
            products,
            categories
          });
          resolve();
        } else {
          reject();
        }
      }).catch(reject);
    });
  },

  // 处理商品分类
  processCategories(products) {
    const categoryMap = {};
    const categories = [];
    
    // 添加"全部"分类
    categories.push({
      id: 0,
      name: '全部',
      count: products.length
    });
    
    // 统计各分类商品数量
    products.forEach(product => {
      if (!categoryMap[product.category_id]) {
        categoryMap[product.category_id] = {
          id: product.category_id,
          name: product.category_name || '未分类',
          count: 0
        };
      }
      categoryMap[product.category_id].count++;
    });
    
    // 添加到分类列表
    Object.values(categoryMap).forEach(category => {
      categories.push(category);
    });
    
    return categories;
  },

  // 获取商家优惠
  getMerchantDiscounts(merchantId) {
    return API.getMerchantDiscounts(merchantId).then(res => {
      if (res.code === 200) {
        this.setData({ discounts: res.data || [] });
      }
    });
  },

  // 切换商品分类
  onCategoryChange(e) {
    const categoryId = e.currentTarget.dataset.id;
    
    if (categoryId === this.data.currentCategory) return;
    
    this.setData({ currentCategory: categoryId });
    
    // 滚动到对应分类的商品位置
    if (categoryId > 0) {
      this.scrollToCategory(categoryId);
    }
  },

  // 滚动到指定分类
  scrollToCategory(categoryId) {
    const query = wx.createSelectorQuery();
    query.select(`#category-${categoryId}`).boundingClientRect();
    query.selectViewport().scrollOffset();
    query.exec(res => {
      if (res[0]) {
        wx.pageScrollTo({
          scrollTop: res[0].top - 100,
          duration: 300
        });
      }
    });
  },

  // 添加到购物车
  addToCart(e) {
    const product = e.currentTarget.dataset.product;
    
    if (!product || product.stock <= 0) {
      return;
    }
    
    let cart = this.data.cart;
    let cartItem = cart.find(item => item.id === product.id);
    
    if (cartItem) {
      // 增加数量
      cartItem.quantity++;
    } else {
      // 添加新商品
      cartItem = {
        id: product.id,
        name: product.name,
        price: product.price,
        image: product.image,
        quantity: 1
      };
      cart.push(cartItem);
    }
    
    // 更新购物车
    this.updateCart(cart);
    
    // 显示添加成功动画
    this.showAddAnimation(e);
  },

  // 减少购物车商品
  removeFromCart(e) {
    const productId = e.currentTarget.dataset.id;
    
    let cart = this.data.cart;
    const cartItem = cart.find(item => item.id === productId);
    
    if (!cartItem) return;
    
    if (cartItem.quantity > 1) {
      // 减少数量
      cartItem.quantity--;
    } else {
      // 移除商品
      cart = cart.filter(item => item.id !== productId);
    }
    
    this.updateCart(cart);
  },

  // 更新购物车
  updateCart(cart) {
    // 计算总价和数量
    let total = 0;
    let count = 0;
    
    cart.forEach(item => {
      total += item.price * item.quantity;
      count += item.quantity;
    });
    
    this.setData({
      cart,
      cartTotal: total.toFixed(2),
      cartCount: count
    });
    
    // 保存到缓存
    this.saveCart(cart);
  },

  // 保存购物车到缓存
  saveCart(cart) {
    const { merchantId } = this.data;
    const cartKey = `cart_${merchantId}`;
    
    wx.setStorageSync(cartKey, {
      merchantId,
      items: cart,
      timestamp: Date.now()
    });
  },

  // 从缓存恢复购物车
  restoreCart() {
    const { merchantId } = this.data;
    const cartKey = `cart_${merchantId}`;
    const cartData = wx.getStorageSync(cartKey);
    
    if (cartData && cartData.merchantId === merchantId) {
      // 检查缓存是否过期(2小时)
      if (Date.now() - cartData.timestamp < 2 * 60 * 60 * 1000) {
        this.updateCart(cartData.items);
      } else {
        wx.removeStorageSync(cartKey);
      }
    }
  },

  // 显示添加成功动画
  showAddAnimation(e) {
    const { x, y } = e.detail;
    
    // 创建动画节点
    const animation = wx.createAnimation({
      duration: 800,
      timingFunction: 'ease-out'
    });
    
    // 执行动画
    animation.translate(0, -100).opacity(0).step();
    
    this.setData({
      addAnimation: animation.export()
    });
    
    // 重置动画
    setTimeout(() => {
      this.setData({ addAnimation: null });
    }, 1000);
  },

  // 去结算
  goToCheckout() {
    const { merchant, cart, cartTotal, cartCount } = this.data;
    
    if (cartCount === 0) {
      wx.showToast({
        title: '购物车为空',
        icon: 'none'
      });
      return;
    }
    
    // 验证是否达到起送价
    if (cartTotal < merchant.min_order_amount) {
      wx.showToast({
        title: `未达到起送价¥${merchant.min_order_amount}`,
        icon: 'none'
      });
      return;
    }
    
    // 跳转到结算页
    wx.navigateTo({
      url: `/pages/order/checkout?merchant_id=${merchant.id}`
    });
  },

  // 收藏商家
  onCollect() {
    const { merchant, merchant: { is_collected } } = this.data;
    
    API.toggleCollectMerchant(merchant.id, !is_collected).then(res => {
      if (res.code === 200) {
        this.setData({
          merchant: {
            ...merchant,
            is_collected: !is_collected
          }
        });
        
        wx.showToast({
          title: !is_collected ? '收藏成功' : '取消收藏',
          icon: 'success'
        });
      }
    });
  },

  // 联系商家
  onContact() {
    const { merchant } = this.data;
    
    wx.makePhoneCall({
      phoneNumber: merchant.contact_phone
    });
  },

  // 分享商家
  onShare() {
    const { merchant } = this.data;
    
    return {
      title: merchant.shop_name,
      path: `/pages/merchant/detail?id=${merchant.id}`,
      imageUrl: merchant.shop_logo || '/images/default-shop.png'
    };
  },

  // 返回首页
  goBackHome() {
    wx.switchTab({
      url: '/pages/index/index'
    });
  }
});

9. 骑手端实现

9.1 骑手接单页面

javascript 复制代码
// pages/rider/home.js
const app = getApp();
const API = require('../../services/api');
const WS = require('../../services/websocket');

Page({
  data: {
    // 骑手状态
    riderStatus: 1, // 1休息 2可接单
    // 当前位置
    currentLocation: null,
    // 当前订单
    currentOrder: null,
    // 待接订单列表
    pendingOrders: [],
    // 今日数据
    todayData: {
      orders: 0,
      income: 0,
      distance: 0
    },
    // 连接状态
    wsConnected: false,
    // 是否显示订单详情
    showOrderDetail: false,
    // 选中的订单
    selectedOrder: null
  },

  onLoad() {
    this.initData();
  },

  onShow() {
    // 检查骑手登录状态
    if (!app.globalData.riderInfo) {
      wx.redirectTo({
        url: '/pages/rider/auth'
      });
      return;
    }
    
    this.connectWebSocket();
    this.startLocationUpdate();
  },

  onHide() {
    this.disconnectWebSocket();
    this.stopLocationUpdate();
  },

  onUnload() {
    this.disconnectWebSocket();
    this.stopLocationUpdate();
  },

  // 初始化数据
  initData() {
    this.getTodayData();
    this.getCurrentOrder();
  },

  // 连接WebSocket
  connectWebSocket() {
    const token = wx.getStorageSync('rider_token');
    
    WS.connect({
      token,
      onMessage: this.handleWsMessage.bind(this),
      onOpen: () => {
        this.setData({ wsConnected: true });
        console.log('WebSocket连接成功');
      },
      onClose: () => {
        this.setData({ wsConnected: false });
        console.log('WebSocket连接关闭');
      },
      onError: (err) => {
        console.error('WebSocket连接错误:', err);
        this.setData({ wsConnected: false });
        
        // 5秒后重连
        setTimeout(() => {
          this.connectWebSocket();
        }, 5000);
      }
    });
  },

  // 断开WebSocket
  disconnectWebSocket() {
    WS.disconnect();
  },

  // 处理WebSocket消息
  handleWsMessage(message) {
    const { type, data } = message;
    
    switch (type) {
      case 'new_order':
        this.handleNewOrder(data);
        break;
      case 'order_update':
        this.handleOrderUpdate(data);
        break;
      case 'system_message':
        this.handleSystemMessage(data);
        break;
      case 'heartbeat':
        this.sendHeartbeat();
        break;
    }
  },

  // 处理新订单
  handleNewOrder(order) {
    // 添加到待接订单列表
    const pendingOrders = [...this.data.pendingOrders, order];
    this.setData({ pendingOrders });
    
    // 显示新订单提示
    if (this.data.riderStatus === 2) { // 可接单状态
      this.showNewOrderNotification(order);
    }
  },

  // 处理订单更新
  handleOrderUpdate(update) {
    const { order_id, status } = update;
    
    // 更新当前订单状态
    if (this.data.currentOrder && this.data.currentOrder.id === order_id) {
      this.setData({
        currentOrder: {
          ...this.data.currentOrder,
          order_status: status
        }
      });
    }
    
    // 从待接订单列表中移除
    if (status !== 0) { // 非待接单状态
      const pendingOrders = this.data.pendingOrders.filter(
        order => order.id !== order_id
      );
      this.setData({ pendingOrders });
    }
  },

  // 显示新订单通知
  showNewOrderNotification(order) {
    wx.showModal({
      title: '新订单提醒',
      content: `您有新的配送订单,配送费¥${order.delivery_fee},距离${order.distance}`,
      confirmText: '查看详情',
      cancelText: '忽略',
      success: (res) => {
        if (res.confirm) {
          this.showOrderDetail(order);
        }
      }
    });
  },

  // 开始位置更新
  startLocationUpdate() {
    this.locationTimer = setInterval(() => {
      this.updateLocation();
    }, 10000); // 10秒更新一次
    
    // 立即更新一次
    this.updateLocation();
  },

  // 停止位置更新
  stopLocationUpdate() {
    if (this.locationTimer) {
      clearInterval(this.locationTimer);
      this.locationTimer = null;
    }
  },

  // 更新位置
  updateLocation() {
    const that = this;
    
    wx.getLocation({
      type: 'gcj02',
      success(res) {
        const { latitude, longitude } = res;
        
        that.setData({
          currentLocation: {
            lat: latitude,
            lng: longitude
          }
        });
        
        // 发送位置到服务器
        that.sendLocationToServer(latitude, longitude);
      }
    });
  },

  // 发送位置到服务器
  sendLocationToServer(lat, lng) {
    WS.send({
      type: 'location',
      data: { lat, lng }
    });
  },

  // 发送心跳
  sendHeartbeat() {
    WS.send({
      type: 'heartbeat',
      data: { timestamp: Date.now() }
    });
  },

  // 获取今日数据
  getTodayData() {
    API.getRiderTodayData().then(res => {
      if (res.code === 200) {
        this.setData({ todayData: res.data });
      }
    });
  },

  // 获取当前订单
  getCurrentOrder() {
    API.getRiderCurrentOrder().then(res => {
      if (res.code === 200 && res.data) {
        this.setData({ currentOrder: res.data });
      }
    });
  },

  // 切换接单状态
  toggleRiderStatus() {
    const newStatus = this.data.riderStatus === 1 ? 2 : 1;
    
    API.updateRiderStatus(newStatus).then(res => {
      if (res.code === 200) {
        this.setData({ riderStatus: newStatus });
        
        wx.showToast({
          title: newStatus === 2 ? '开始接单' : '休息中',
          icon: 'success'
        });
      }
    });
  },

  // 显示订单详情
  showOrderDetail(order) {
    this.setData({
      selectedOrder: order,
      showOrderDetail: true
    });
  },

  // 隐藏订单详情
  hideOrderDetail() {
    this.setData({
      showOrderDetail: false,
      selectedOrder: null
    });
  },

  // 接单
  acceptOrder(order) {
    wx.showLoading({ title: '接单中...' });
    
    API.acceptOrder(order.id).then(res => {
      wx.hideLoading();
      
      if (res.code === 200) {
        // 从待接订单中移除
        const pendingOrders = this.data.pendingOrders.filter(
          item => item.id !== order.id
        );
        
        this.setData({
          pendingOrders,
          currentOrder: order,
          showOrderDetail: false
        });
        
        wx.showToast({
          title: '接单成功',
          icon: 'success'
        });
        
        // 跳转到配送页面
        wx.navigateTo({
          url: `/pages/rider/delivery?order_id=${order.id}`
        });
      } else {
        wx.showToast({
          title: res.msg || '接单失败',
          icon: 'none'
        });
      }
    }).catch(err => {
      wx.hideLoading();
      wx.showToast({
        title: '接单失败',
        icon: 'none'
      });
    });
  },

  // 抢单
  grabOrder(order) {
    this.acceptOrder(order);
  },

  // 查看订单详情
  viewOrderDetail(e) {
    const order = e.currentTarget.dataset.order;
    this.showOrderDetail(order);
  },

  // 开始配送
  startDelivery() {
    const { currentOrder } = this.data;
    
    if (!currentOrder) return;
    
    wx.navigateTo({
      url: `/pages/rider/delivery?order_id=${currentOrder.id}`
    });
  },

  // 完成配送
  completeDelivery() {
    const { currentOrder } = this.data;
    
    if (!currentOrder) return;
    
    wx.showModal({
      title: '确认完成',
      content: '确认订单已送达?',
      success: (res) => {
        if (res.confirm) {
          API.completeOrder(currentOrder.id).then(res => {
            if (res.code === 200) {
              this.setData({ currentOrder: null });
              
              wx.showToast({
                title: '配送完成',
                icon: 'success'
              });
              
              // 刷新今日数据
              this.getTodayData();
            }
          });
        }
      }
    });
  },

  // 上报异常
  reportException() {
    const { currentOrder } = this.data;
    
    if (!currentOrder) return;
    
    wx.showActionSheet({
      itemList: ['无法联系顾客', '地址错误', '商品异常', '其他问题'],
      success: (res) => {
        const reasons = ['无法联系顾客', '地址错误', '商品异常', '其他问题'];
        const reason = reasons[res.tapIndex];
        
        API.reportOrderException(currentOrder.id, reason).then(res => {
          if (res.code === 200) {
            wx.showToast({
              title: '已上报',
              icon: 'success'
            });
          }
        });
      }
    });
  },

  // 联系顾客
  contactCustomer() {
    const { currentOrder } = this.data;
    
    if (!currentOrder) return;
    
    wx.makePhoneCall({
      phoneNumber: currentOrder.contact_phone
    });
  },

  // 联系商家
  contactMerchant() {
    const { currentOrder } = this.data;
    
    if (!currentOrder || !currentOrder.merchant) return;
    
    wx.makePhoneCall({
      phoneNumber: currentOrder.merchant.contact_phone
    });
  },

  // 查看收入
  viewIncome() {
    wx.navigateTo({
      url: '/pages/rider/income'
    });
  },

  // 查看历史订单
  viewHistory() {
    wx.navigateTo({
      url: '/pages/rider/history'
    });
  },

  // 刷新订单列表
  refreshOrders() {
    this.setData({ pendingOrders: [] });
  }
});

结语

本文详细阐述了一套针对乡镇地区的外卖跑腿小程序系统的PHP开发实战方案。通过对系统的需求分析、架构设计、技术选型、核心功能实现、性能优化、安全防护、部署监控等全方位的论述,为PHP开发者提供了一个完整的乡镇同城O2O系统开发参考。

技术要点回顾

1. 架构设计方面

  • 采用前后端分离的微服务化架构,提升了系统可扩展性和可维护性
  • 引入缓存、队列、数据库读写分离等机制,保障高并发场景下的性能表现
  • 针对乡镇网络环境进行优化,实现了网络不稳定的容错处理

2. 核心功能实现

  • 基于ThinkPHP框架的RESTful API设计
  • 智能商家推荐算法,结合距离、评分、销量等多维度因素
  • 灵活的状态机驱动的订单系统
  • 基于地理位置的骑手智能派单系统
  • 实时消息推送与WebSocket通信

3. 性能优化策略

  • 多级缓存架构(Redis + APCu + 数据库缓存)
  • 数据库查询优化与索引策略
  • 异步处理与消息队列应用
  • 数据库连接池管理
  • OPcache预加载机制

4. 安全防护体系

  • 全方位的输入验证与XSS防护
  • SQL注入防护与参数绑定
  • CSRF令牌验证
  • 数据加密存储与传输
  • 请求频率限制与DDoS防护

5. 监控与运维

  • 完整的性能监控与告警体系
  • 自动化部署与CI/CD流程
  • 容器化部署方案
  • 详细的日志记录与分析
  • 数据库备份与恢复机制

乡镇特色体现

本系统特别针对乡镇地区的特点进行了优化:

  1. 网络适应性强:支持弱网环境,具备断点续传和数据同步能力
  2. 操作简化:针对乡镇用户习惯,简化了操作流程
  3. 成本控制:通过技术优化降低服务器和带宽成本
  4. 本地化支持:支持方言、本地支付方式等乡镇特色功能
  5. 扩展性强:便于对接本地商家和物流资源

技术价值

本项目展示了PHP在现代化Web开发中的强大能力:

  1. 高性能表现:通过Swoole、OPcache等技术,PHP在处理高并发场景时表现出色
  2. 开发效率:ThinkPHP框架提供了丰富的功能模块,加速开发进程
  3. 生态系统:PHP拥有成熟的Composer包管理体系和丰富的开源组件
  4. 维护成本:PHP开发人员资源丰富,长期维护成本可控
  5. 兼容性:支持在多种环境下部署,包括云服务器和本地服务器

展望未来

随着乡村振兴战略的深入推进,乡镇数字化服务市场潜力巨大。未来可进一步:

  1. AI技术应用:引入智能推荐、需求预测、智能定价等AI能力
  2. 物联网集成:对接智能配送设备、温控设备等
  3. 大数据分析:深入挖掘用户行为数据,提供精准运营支持
  4. 生态扩展:从外卖跑腿扩展到社区团购、本地生活服务等多元化业务
  5. 技术升级:考虑向PHP 8.x版本迁移,引入Go语言微服务等新技术栈

结语

乡镇外卖跑腿小程序的开发不仅是一个技术项目,更是推动城乡数字鸿沟弥合的重要实践。通过本文的详细技术分享,希望能够为PHP开发者在乡镇O2O领域的技术实践提供有价值的参考。在数字化转型的大潮中,技术开发者应充分发挥技术优势,为乡村振兴贡献专业力量,让科技的发展惠及更广泛的人群。

未来,我们将继续关注乡镇数字化发展需求,不断优化技术方案,为推动数字乡村建设做出更大贡献。

相关推荐
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1235 天前
matlab画图工具
开发语言·matlab
dustcell.5 天前
haproxy七层代理
java·开发语言·前端