乡镇外卖跑腿小程序开发实战:基于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 核心表结构

-- 用户表(多角色共用,通过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
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
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
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
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
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
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
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
// 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
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
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
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
// 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
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
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
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
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"]
# 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 系统监控配置

# 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
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 首页实现

// 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'
    };
  }
});
<!-- 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>
/* 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 商家详情页

// 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 骑手接单页面

// 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: [] });
  }
});

10. 测试与部署

10.1 单元测试

<?php
namespace tests\api;

use think\testing\TestCase;
use app\common\service\OrderService;
use app\common\model\Order;
use app\common\model\User;
use app\common\model\Merchant;
use app\common\model\Product;

class OrderServiceTest extends TestCase
{
    protected $orderService;
    
    protected function setUp(): void
    {
        parent::setUp();
        $this->orderService = new OrderService();
    }
    
    /**
     * 测试创建订单
     */
    public function testCreateOrder()
    {
        // 准备测试数据
        $userId = 1;
        $merchantId = 1;
        
        $user = User::create([
            'username' => 'test_user',
            'phone' => '13800138000',
            'user_type' => 1
        ]);
        
        $merchant = Merchant::create([
            'user_id' => 2,
            'shop_name' => '测试商家',
            'shop_type' => 1,
            'delivery_fee' => 3.00,
            'min_order_amount' => 20.00,
            'delivery_range' => 5000,
            'status' => 1
        ]);
        
        $product = Product::create([
            'merchant_id' => $merchantId,
            'name' => '测试商品',
            'price' => 25.00,
            'stock' => 100,
            'status' => 1
        ]);
        
        $products = [
            [
                'product_id' => $product->id,
                'quantity' => 2
            ]
        ];
        
        $address = [
            'address' => '测试地址',
            'lng' => 104.067,
            'lat' => 30.679,
            'name' => '测试用户',
            'phone' => '13800138000'
        ];
        
        // 执行测试
        $result = $this->orderService->createOrder($userId, $merchantId, $products, $address);
        
        // 断言
        $this->assertArrayHasKey('order_id', $result);
        $this->assertArrayHasKey('order_no', $result);
        $this->assertArrayHasKey('pay_amount', $result);
        
        // 验证订单是否正确创建
        $order = Order::where('id', $result['order_id'])->find();
        $this->assertNotNull($order);
        $this->assertEquals($userId, $order->user_id);
        $this->assertEquals($merchantId, $order->merchant_id);
        $this->assertEquals(50.00, $order->total_amount); // 25 * 2
    }
    
    /**
     * 测试未达到起送价
     */
    public function testCreateOrderBelowMinAmount()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('未达到起送价');
        
        $userId = 1;
        $merchantId = 1;
        
        $merchant = Merchant::create([
            'user_id' => 2,
            'shop_name' => '测试商家',
            'shop_type' => 1,
            'delivery_fee' => 3.00,
            'min_order_amount' => 50.00, // 高起送价
            'delivery_range' => 5000,
            'status' => 1
        ]);
        
        $product = Product::create([
            'merchant_id' => $merchantId,
            'name' => '测试商品',
            'price' => 20.00,
            'stock' => 100,
            'status' => 1
        ]);
        
        $products = [
            [
                'product_id' => $product->id,
                'quantity' => 1 // 只有20元
            ]
        ];
        
        $address = [
            'address' => '测试地址',
            'lng' => 104.067,
            'lat' => 30.679,
            'name' => '测试用户',
            'phone' => '13800138000'
        ];
        
        $this->orderService->createOrder($userId, $merchantId, $products, $address);
    }
    
    /**
     * 测试超出配送范围
     */
    public function testCreateOrderOutOfRange()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('超出配送范围');
        
        $userId = 1;
        $merchantId = 1;
        
        $merchant = Merchant::create([
            'user_id' => 2,
            'shop_name' => '测试商家',
            'shop_type' => 1,
            'delivery_fee' => 3.00,
            'min_order_amount' => 20.00,
            'delivery_range' => 1000, // 小配送范围
            'lng' => 104.067,
            'lat' => 30.679,
            'status' => 1
        ]);
        
        $product = Product::create([
            'merchant_id' => $merchantId,
            'name' => '测试商品',
            'price' => 25.00,
            'stock' => 100,
            'status' => 1
        ]);
        
        $products = [
            [
                'product_id' => $product->id,
                'quantity' => 2
            ]
        ];
        
        $address = [
            'address' => '远处地址',
            'lng' => 104.100, // 远处
            'lat' => 30.700,  // 远处
            'name' => '测试用户',
            'phone' => '13800138000'
        ];
        
        $this->orderService->createOrder($userId, $merchantId, $products, $address);
    }
}

10.2 集成测试

<?php
namespace tests\integration;

use think\testing\TestCase;
use think\facade\Db;

class OrderFlowTest extends TestCase
{
    protected $baseUrl = 'http://localhost';
    
    /**
     * 测试完整订单流程
     */
    public function testCompleteOrderFlow()
    {
        // 1. 用户登录
        $loginResponse = $this->post('/api/auth/login', [
            'phone' => '13800138000',
            'code' => '123456'
        ]);
        
        $this->assertEquals(200, $loginResponse->getStatusCode());
        $loginData = json_decode($loginResponse->getContent(), true);
        $this->assertArrayHasKey('token', $loginData['data']);
        
        $token = $loginData['data']['token'];
        
        // 2. 获取附近商家
        $merchantsResponse = $this->withHeader('Authorization', $token)
            ->get('/api/merchant/nearby?lat=30.679&lng=104.067');
        
        $this->assertEquals(200, $merchantsResponse->getStatusCode());
        $merchantsData = json_decode($merchantsResponse->getContent(), true);
        $this->assertArrayHasKey('list', $merchantsData['data']);
        
        if (!empty($merchantsData['data']['list'])) {
            $merchant = $merchantsData['data']['list'][0];
            
            // 3. 获取商家商品
            $productsResponse = $this->withHeader('Authorization', $token)
                ->get("/api/merchant/{$merchant['id']}/products");
            
            $this->assertEquals(200, $productsResponse->getStatusCode());
            $productsData = json_decode($productsResponse->getContent(), true);
            
            if (!empty($productsData['data'])) {
                $product = $productsData['data'][0];
                
                // 4. 创建订单
                $orderData = [
                    'merchant_id' => $merchant['id'],
                    'products' => [
                        [
                            'product_id' => $product['id'],
                            'quantity' => 1
                        ]
                    ],
                    'address' => [
                        'address' => '测试地址',
                        'lng' => 104.067,
                        'lat' => 30.679,
                        'name' => '测试用户',
                        'phone' => '13800138000'
                    ],
                    'remark' => '测试订单'
                ];
                
                $orderResponse = $this->withHeader('Authorization', $token)
                    ->post('/api/order/create', $orderData);
                
                $this->assertEquals(200, $orderResponse->getStatusCode());
                $orderData = json_decode($orderResponse->getContent(), true);
                $this->assertArrayHasKey('order_id', $orderData['data']);
                
                $orderId = $orderData['data']['order_id'];
                
                // 5. 模拟支付
                $paymentData = [
                    'order_id' => $orderId,
                    'pay_method' => 1 // 微信支付
                ];
                
                $paymentResponse = $this->withHeader('Authorization', $token)
                    ->post('/api/payment/pay', $paymentData);
                
                $this->assertEquals(200, $paymentResponse->getStatusCode());
                
                // 6. 模拟商家接单
                $merchantToken = $this->getMerchantToken();
                $acceptResponse = $this->withHeader('Authorization', $merchantToken)
                    ->post("/api/merchant/order/{$orderId}/accept");
                
                $this->assertEquals(200, $acceptResponse->getStatusCode());
                
                // 7. 模拟骑手接单
                $riderToken = $this->getRiderToken();
                $riderAcceptResponse = $this->withHeader('Authorization', $riderToken)
                    ->post("/api/rider/order/{$orderId}/accept");
                
                $this->assertEquals(200, $riderAcceptResponse->getStatusCode());
                
                // 8. 模拟配送完成
                $completeResponse = $this->withHeader('Authorization', $riderToken)
                    ->post("/api/rider/order/{$orderId}/complete");
                
                $this->assertEquals(200, $completeResponse->getStatusCode());
            }
        }
    }
    
    private function getMerchantToken()
    {
        $response = $this->post('/api/auth/login', [
            'phone' => '13800138001',
            'code' => '123456'
        ]);
        
        $data = json_decode($response->getContent(), true);
        return $data['data']['token'];
    }
    
    private function getRiderToken()
    {
        $response = $this->post('/api/auth/login', [
            'phone' => '13800138002',
            'code' => '123456'
        ]);
        
        $data = json_decode($response->getContent(), true);
        return $data['data']['token'];
    }
}

10.3 压力测试

# stress_test.py
import asyncio
import aiohttp
import time
import statistics
from datetime import datetime

class StressTest:
    def __init__(self, base_url, concurrency=100, requests_per_session=100):
        self.base_url = base_url
        self.concurrency = concurrency
        self.requests_per_session = requests_per_session
        self.results = []
        
    async def test_create_order(self, session, user_index):
        """测试创建订单接口"""
        start_time = time.time()
        
        try:
            # 1. 登录
            login_data = {
                'phone': f'138001380{user_index:02d}',
                'code': '123456'
            }
            
            async with session.post(f'{self.base_url}/api/auth/login', 
                                   json=login_data) as response:
                if response.status != 200:
                    return {'error': '登录失败', 'status': response.status}
                
                login_result = await response.json()
                token = login_result['data']['token']
                
                headers = {'Authorization': token}
                
                # 2. 获取商家列表
                async with session.get(
                    f'{self.base_url}/api/merchant/nearby?lat=30.679&lng=104.067',
                    headers=headers
                ) as response:
                    if response.status != 200:
                        return {'error': '获取商家失败', 'status': response.status}
                    
                    merchants = await response.json()
                    
                    if not merchants['data']['list']:
                        return {'error': '无商家数据'}
                    
                    merchant = merchants['data']['list'][0]
                    
                    # 3. 获取商品
                    async with session.get(
                        f"{self.base_url}/api/merchant/{merchant['id']}/products",
                        headers=headers
                    ) as response:
                        if response.status != 200:
                            return {'error': '获取商品失败', 'status': response.status}
                        
                        products = await response.json()
                        
                        if not products['data']:
                            return {'error': '无商品数据'}
                        
                        product = products['data'][0]
                        
                        # 4. 创建订单
                        order_data = {
                            'merchant_id': merchant['id'],
                            'products': [{
                                'product_id': product['id'],
                                'quantity': 1
                            }],
                            'address': {
                                'address': f'测试地址{user_index}',
                                'lng': 104.067 + (user_index * 0.001),
                                'lat': 30.679 + (user_index * 0.001),
                                'name': f'测试用户{user_index}',
                                'phone': f'138001380{user_index:02d}'
                            }
                        }
                        
                        order_start = time.time()
                        async with session.post(
                            f'{self.base_url}/api/order/create',
                            json=order_data,
                            headers=headers
                        ) as response:
                            order_end = time.time()
                            
                            result = {
                                'user_index': user_index,
                                'status': response.status,
                                'order_time': order_end - order_start,
                                'total_time': time.time() - start_time
                            }
                            
                            if response.status == 200:
                                order_result = await response.json()
                                result['order_id'] = order_result['data']['order_id']
                            else:
                                result['error'] = await response.text()
                            
                            return result
                        
        except Exception as e:
            return {
                'user_index': user_index,
                'error': str(e),
                'total_time': time.time() - start_time
            }
    
    async def worker(self, session, user_start_index):
        """工作协程"""
        for i in range(self.requests_per_session):
            user_index = user_start_index + i
            result = await self.test_create_order(session, user_index)
            self.results.append(result)
            
            # 控制请求频率
            await asyncio.sleep(0.1)
    
    async def run(self):
        """运行压力测试"""
        print(f"开始压力测试: 并发数={self.concurrency}, 每会话请求数={self.requests_per_session}")
        print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        
        start_time = time.time()
        
        connector = aiohttp.TCPConnector(limit=0)  # 不限制连接数
        async with aiohttp.ClientSession(connector=connector) as session:
            tasks = []
            for i in range(self.concurrency):
                user_start_index = i * self.requests_per_session
                task = self.worker(session, user_start_index)
                tasks.append(task)
            
            await asyncio.gather(*tasks)
        
        end_time = time.time()
        total_time = end_time - start_time
        
        # 分析结果
        self.analyze_results(total_time)
    
    def analyze_results(self, total_time):
        """分析测试结果"""
        total_requests = len(self.results)
        successful_requests = len([r for r in self.results if r.get('status') == 200])
        failed_requests = total_requests - successful_requests
        
        order_times = [r['order_time'] for r in self.results 
                      if r.get('order_time') is not None]
        total_times = [r['total_time'] for r in self.results 
                      if r.get('total_time') is not None]
        
        print(f"\n测试完成!")
        print(f"总时间: {total_time:.2f}秒")
        print(f"总请求数: {total_requests}")
        print(f"成功请求: {successful_requests}")
        print(f"失败请求: {failed_requests}")
        print(f"成功率: {(successful_requests/total_requests)*100:.2f}%")
        print(f"吞吐量: {total_requests/total_time:.2f} 请求/秒")
        
        if order_times:
            print(f"\n订单创建时间统计:")
            print(f"  平均: {statistics.mean(order_times)*1000:.2f}ms")
            print(f"  中位数: {statistics.median(order_times)*1000:.2f}ms")
            print(f"  最小: {min(order_times)*1000:.2f}ms")
            print(f"  最大: {max(order_times)*1000:.2f}ms")
            print(f"  95百分位: {self.percentile(order_times, 95)*1000:.2f}ms")
        
        if total_times:
            print(f"\n总时间统计:")
            print(f"  平均: {statistics.mean(total_times):.2f}秒")
            print(f"  最小: {min(total_times):.2f}秒")
            print(f"  最大: {max(total_times):.2f}秒")
        
        # 打印错误统计
        errors = {}
        for result in self.results:
            if 'error' in result:
                error = result['error'][:50]  # 截取前50个字符
                errors[error] = errors.get(error, 0) + 1
        
        if errors:
            print(f"\n错误统计:")
            for error, count in errors.items():
                print(f"  {error}: {count}次")
    
    def percentile(self, data, percentile):
        """计算百分位数"""
        if not data:
            return 0
        data.sort()
        index = (len(data) - 1) * percentile / 100
        lower = int(index)
        upper = lower + 1
        weight = index - lower
        
        if upper >= len(data):
            return data[lower]
        
        return data[lower] * (1 - weight) + data[upper] * weight

# 运行压力测试
if __name__ == '__main__':
    test = StressTest(
        base_url='http://localhost:8000',
        concurrency=50,  # 并发数
        requests_per_session=20  # 每个会话的请求数
    )
    
    asyncio.run(test.run())

11. 总结与展望

11.1 项目总结

本系统基于PHP技术栈,成功实现了一套适用于乡镇地区的外卖跑腿小程序系统。主要成果包括:

  1. 架构设计:采用前后端分离的微服务架构,支持高并发和高可用
  2. 核心功能:实现了用户端、商家端、骑手端和管理端的完整业务流程
  3. 性能优化:通过缓存、异步处理、数据库优化等手段提升系统性能
  4. 安全防护:实现了多层次的安全防护机制
  5. 乡镇适配:针对乡镇特点进行了多项优化

11.2 技术亮点

  1. 智能派单算法:结合距离、评分、负载均衡等因素的智能派单
  2. 实时通信:基于WebSocket的实时订单状态更新和消息推送
  3. 多级缓存:Redis + 本地缓存的多级缓存策略
  4. 异步处理:消息队列实现异步订单处理
  5. 监控告警:完善的系统监控和性能监控

11.3 未来展望

  1. AI技术应用
    • 智能推荐系统
    • 需求预测
    • 智能定价
  2. 功能扩展
    • 社区团购
    • 本地生活服务
    • 农产品直销
  3. 技术升级
    • 迁移到PHP 8.x
    • 引入Go语言微服务
    • 容器化部署优化
  4. 乡镇特色功能
    • 方言语音交互
    • 农忙季特殊服务
    • 乡镇节庆活动

11.4 部署建议

  1. 硬件要求
    • 服务器:4核8G起步
    • 带宽:10Mbps独享
    • 存储:SSD硬盘
  2. 软件环境
    • 操作系统:CentOS 7.9/8.0
    • 数据库:MySQL 8.0集群
    • 缓存:Redis 6.0集群
    • 队列:RabbitMQ 3.9
  3. 部署步骤# 1. 环境准备 sudo yum install -y docker docker-compose # 2. 克隆代码 git clone https://github.com/yourproject/town-delivery.git cd town-delivery # 3. 配置环境变量 cp .env.example .env vi .env # 4. 启动服务 docker-compose up -d # 5. 初始化数据库 docker-compose exec php php think migrate:run # 6. 导入初始数据 docker-compose exec php php think seed:run
  4. 监控部署# 部署Prometheus和Grafana cd monitoring docker-compose up -d # 访问监控面板 # Grafana: http://localhost:3000 # Prometheus: http://localhost:9090

11.5 运维建议

  1. 日常维护
    • 每日备份数据库
    • 监控系统资源使用情况
    • 定期清理日志文件
  2. 性能调优
    • 定期分析慢查询
    • 优化Redis内存使用
    • 调整PHP-FPM配置
  3. 安全加固
    • 定期更新安全补丁
    • 配置防火墙规则
    • 开启操作日志审计
  4. 应急响应
    • 建立故障处理流程
    • 准备灾难恢复方案
    • 定期进行应急演练

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

12. 系统性能优化(续)

12.1 数据库深度优化

12.1.1 索引优化策略

<?php
namespace app\common\service;

use think\facade\Db;

class DatabaseOptimizer
{
    /**
     * 分析表索引
     */
    public static function analyzeTableIndexes($table)
    {
        $sql = "SHOW INDEX FROM `{$table}`";
        $indexes = Db::query($sql);
        
        $analysis = [
            'table' => $table,
            'indexes' => [],
            'suggestions' => []
        ];
        
        foreach ($indexes as $index) {
            $analysis['indexes'][] = [
                'name' => $index['Key_name'],
                'columns' => $index['Column_name'],
                'unique' => $index['Non_unique'] == 0,
                'cardinality' => $index['Cardinality']
            ];
        }
        
        // 分析查询语句
        $slowQueries = Db::name('slow_query_log')
            ->where('table', $table)
            ->where('create_time', '>', date('Y-m-d H:i:s', time() - 86400))
            ->select();
        
        $suggestions = self::analyzeSlowQueries($slowQueries, $table);
        $analysis['suggestions'] = $suggestions;
        
        return $analysis;
    }
    
    /**
     * 分析慢查询
     */
    private static function analyzeSlowQueries($queries, $table)
    {
        $suggestions = [];
        $patterns = [
            'like' => '/LIKE\s+[\'"]([^%]*%[^%]+%)[\'"]/i',
            'or' => '/WHERE.*\sOR\s/i',
            'in' => '/IN\s*\(([^)]+)\)/i',
            'not_null' => '/IS NOT NULL/i',
            'range' => '/(<|>|<=|>=|BETWEEN)/i'
        ];
        
        foreach ($queries as $query) {
            $sql = $query['sql'];
            
            // 分析查询条件
            if (preg_match('/WHERE\s+(.*?)(?:ORDER|GROUP|LIMIT|$)/i', $sql, $matches)) {
                $whereClause = $matches[1];
                
                // 检查是否使用LIKE前缀匹配
                if (preg_match($patterns['like'], $whereClause)) {
                    $suggestions[] = [
                        'type' => 'warning',
                        'message' => '发现LIKE %...查询,考虑添加全文索引或调整查询逻辑',
                        'sql' => $sql
                    ];
                }
                
                // 检查OR条件
                if (preg_match($patterns['or'], $whereClause)) {
                    $suggestions[] = [
                        'type' => 'warning',
                        'message' => '使用OR条件可能无法使用索引,考虑使用UNION',
                        'sql' => $sql
                    ];
                }
                
                // 检查IN子查询
                if (preg_match($patterns['in'], $whereClause, $inMatch)) {
                    $inValues = trim($inMatch[1]);
                    if (strpos($inValues, 'SELECT') !== false) {
                        $suggestions[] = [
                            'type' => 'warning',
                            'message' => 'IN子查询可能效率低下,考虑使用JOIN或EXISTS',
                            'sql' => $sql
                        ];
                    }
                }
            }
        }
        
        return $suggestions;
    }
    
    /**
     * 优化表结构
     */
    public static function optimizeTableStructure($table)
    {
        $results = [];
        
        // 1. 分析表大小
        $sizeInfo = Db::query("
            SELECT 
                table_name AS `Table`,
                round(((data_length + index_length) / 1024 / 1024), 2) AS `Size_MB`,
                table_rows AS `Rows`
            FROM information_schema.TABLES 
            WHERE table_schema = DATABASE() 
            AND table_name = '{$table}'
        ");
        
        if (!empty($sizeInfo)) {
            $sizeInfo = $sizeInfo[0];
            
            if ($sizeInfo['Rows'] > 1000000) { // 超过100万行
                $results[] = [
                    'type' => 'suggestion',
                    'message' => "表 {$table} 数据量较大({$sizeInfo['Rows']}行),建议考虑分表策略"
                ];
            }
        }
        
        // 2. 检查字段类型
        $columns = Db::query("DESCRIBE `{$table}`");
        
        foreach ($columns as $column) {
            $field = $column['Field'];
            $type = strtoupper($column['Type']);
            
            // 检查VARCHAR长度
            if (preg_match('/VARCHAR\((\d+)\)/', $type, $matches)) {
                $length = (int)$matches[1];
                if ($length > 255) {
                    $results[] = [
                        'type' => 'warning',
                        'message' => "字段 {$field} 使用VARCHAR({$length}),建议使用TEXT类型或调整长度"
                    ];
                }
            }
            
            // 检查DATETIME字段
            if (strpos($type, 'DATETIME') !== false) {
                $results[] = [
                    'type' => 'suggestion',
                    'message' => "字段 {$field} 使用DATETIME,考虑是否需要时区支持"
                ];
            }
            
            // 检查默认值
            if ($column['Null'] === 'NO' && $column['Default'] === null) {
                $results[] = [
                    'type' => 'warning',
                    'message' => "字段 {$field} 不允许NULL但没有默认值"
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * 创建智能索引
     */
    public static function createSmartIndex($table, $queries)
    {
        $indexCandidates = [];
        
        foreach ($queries as $query) {
            $whereClause = $this->extractWhereClause($query['sql']);
            if ($whereClause) {
                $conditions = $this->parseWhereConditions($whereClause);
                $indexCandidates = array_merge($indexCandidates, $conditions);
            }
        }
        
        // 统计条件频率
        $frequency = [];
        foreach ($indexCandidates as $condition) {
            $key = implode(',', $condition['columns']);
            if (!isset($frequency[$key])) {
                $frequency[$key] = [
                    'columns' => $condition['columns'],
                    'count' => 0,
                    'types' => []
                ];
            }
            $frequency[$key]['count']++;
            $frequency[$key]['types'][] = $condition['type'];
        }
        
        // 按频率排序
        uasort($frequency, function($a, $b) {
            return $b['count'] - $a['count'];
        });
        
        $suggestions = [];
        foreach ($frequency as $key => $info) {
            if ($info['count'] >= 3) { // 至少出现3次
                $suggestions[] = [
                    'table' => $table,
                    'columns' => $info['columns'],
                    'frequency' => $info['count'],
                    'query_types' => array_unique($info['types']),
                    'sql' => "CREATE INDEX idx_" . implode('_', $info['columns']) . 
                            " ON {$table} (" . implode(',', $info['columns']) . ")"
                ];
            }
        }
        
        return $suggestions;
    }
    
    /**
     * 分区表管理
     */
    public static function partitionTable($table, $partitionField = 'create_time')
    {
        $sql = "CREATE TABLE {$table}_partitioned LIKE {$table}";
        Db::execute($sql);
        
        // 按月分区
        $partitionSql = "ALTER TABLE {$table}_partitioned 
            PARTITION BY RANGE (YEAR({$partitionField}) * 100 + MONTH({$partitionField}))
            (
                PARTITION p202301 VALUES LESS THAN (202302),
                PARTITION p202302 VALUES LESS THAN (202303),
                PARTITION p202303 VALUES LESS THAN (202304),
                PARTITION p202304 VALUES LESS THAN (202305),
                PARTITION p202305 VALUES LESS THAN (202306),
                PARTITION p_future VALUES LESS THAN MAXVALUE
            )";
        
        Db::execute($partitionSql);
        
        // 数据迁移
        Db::execute("INSERT INTO {$table}_partitioned SELECT * FROM {$table}");
        
        return true;
    }
}

12.2 PHP性能优化

12.2.1 OPcache配置优化

; php.ini

[opcache]

opcache.enable=1 opcache.enable_cli=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.max_wasted_percentage=5 opcache.use_cwd=1 opcache.validate_timestamps=0 opcache.revalidate_freq=2 opcache.revalidate_path=0 opcache.save_comments=1 opcache.load_comments=1 opcache.fast_shutdown=1 opcache.enable_file_override=0 opcache.optimization_level=0x7FFFBFFF opcache.max_file_size=0 opcache.consistency_checks=0 opcache.force_restart_timeout=180 opcache.error_log=”/var/log/php-opcache.log” opcache.log_verbosity_level=1 opcache.preferred_memory_model=”” opcache.protect_memory=0 opcache.mmap_base=null opcache.restrict_api=”” opcache.file_update_protection=2 opcache.huge_code_pages=1 opcache.validate_permission=0 opcache.validate_root=0

12.2.2 预加载配置

<?php
// preload.php
$preload = [];

// 预加载框架核心文件
$frameworkFiles = [
    __DIR__ . '/vendor/topthink/framework/src/App.php',
    __DIR__ . '/vendor/topthink/framework/src/Request.php',
    __DIR__ . '/vendor/topthink/framework/src/Response.php',
    __DIR__ . '/vendor/topthink/framework/src/Container.php',
    __DIR__ . '/vendor/topthink/framework/src/Facade.php',
    __DIR__ . '/vendor/topthink/think-orm/src/Db.php',
    __DIR__ . '/vendor/topthink/think-orm/src/Model.php',
];

// 预加载常用模型
$modelFiles = glob(__DIR__ . '/app/common/model/*.php');

// 预加载常用服务
$serviceFiles = glob(__DIR__ . '/app/common/service/*.php');

// 合并所有文件
$allFiles = array_merge($frameworkFiles, $modelFiles, $serviceFiles);

foreach ($allFiles as $file) {
    if (file_exists($file)) {
        opcache_compile_file($file);
        $preload[] = $file;
    }
}

// 生成预加载统计
file_put_contents(
    '/tmp/preload_stats.json',
    json_encode([
        'timestamp' => date('Y-m-d H:i:s'),
        'count' => count($preload),
        'files' => $preload
    ], JSON_PRETTY_PRINT)
);

12.3 缓存策略优化

<?php
namespace app\common\lib;

use think\facade\Cache;

class AdvancedCache
{
    // 缓存策略配置
    private static $cacheStrategies = [
        'merchant_detail' => [
            'prefix' => 'merchant:detail:',
            'ttl' => 300, // 5分钟
            'version' => 'v1',
            'stale_ttl' => 3600, // 过期后仍可使用1小时
            'lock_timeout' => 5, // 锁超时时间
            'circuit_breaker' => 3, // 熔断阈值
        ],
        'product_list' => [
            'prefix' => 'product:list:',
            'ttl' => 1800, // 30分钟
            'version' => 'v1',
            'stale_ttl' => 7200,
            'lock_timeout' => 3,
            'circuit_breaker' => 5,
        ],
        'user_session' => [
            'prefix' => 'user:session:',
            'ttl' => 7200, // 2小时
            'version' => 'v1',
            'stale_ttl' => 0,
            'lock_timeout' => 1,
            'circuit_breaker' => 0,
        ],
    ];
    
    // 缓存统计
    private static $stats = [
        'hits' => 0,
        'misses' => 0,
        'stale_hits' => 0,
        'fallback_hits' => 0,
        'errors' => 0,
    ];
    
    /**
     * 获取缓存 - 带降级策略
     */
    public static function getWithFallback($strategy, $key, $callback, $params = [], $options = [])
    {
        $config = self::$cacheStrategies[$strategy] ?? [
            'prefix' => 'cache:',
            'ttl' => 3600,
            'version' => 'v1',
        ];
        
        $cacheKey = $config['prefix'] . $key . ':' . $config['version'];
        
        try {
            // 1. 尝试从缓存获取
            $cached = Cache::get($cacheKey);
            
            if ($cached !== null) {
                $data = json_decode($cached, true);
                
                // 检查是否过期但可使用
                if (isset($data['expire_time']) && $data['expire_time'] < time()) {
                    if (isset($data['data']) && $config['stale_ttl'] > 0) {
                        self::$stats['stale_hits']++;
                        
                        // 异步更新缓存
                        if (!self::isLocked($cacheKey)) {
                            self::updateCacheAsync($strategy, $key, $callback, $params);
                        }
                        
                        return $data['data'];
                    }
                } elseif (isset($data['data'])) {
                    self::$stats['hits']++;
                    return $data['data'];
                }
            }
            
            self::$stats['misses']++;
            
            // 2. 获取锁,防止缓存击穿
            $lockKey = 'lock:' . $cacheKey;
            if (!self::acquireLock($lockKey, $config['lock_timeout'])) {
                // 获取锁失败,使用降级策略
                return self::fallback($strategy, $key, $callback, $params, $options);
            }
            
            try {
                // 3. 执行回调获取数据
                $result = call_user_func_array($callback, $params);
                
                // 4. 写入缓存
                $cacheData = [
                    'data' => $result,
                    'create_time' => time(),
                    'expire_time' => time() + $config['ttl'],
                ];
                
                Cache::set($cacheKey, json_encode($cacheData), $config['ttl'] + $config['stale_ttl']);
                
                return $result;
                
            } finally {
                // 释放锁
                self::releaseLock($lockKey);
            }
            
        } catch (\Exception $e) {
            self::$stats['errors']++;
            
            // 记录错误
            Log::error('缓存获取失败: ' . $e->getMessage(), [
                'strategy' => $strategy,
                'key' => $key,
            ]);
            
            // 使用降级策略
            return self::fallback($strategy, $key, $callback, $params, $options);
        }
    }
    
    /**
     * 降级策略
     */
    private static function fallback($strategy, $key, $callback, $params, $options)
    {
        $config = self::$cacheStrategies[$strategy] ?? [];
        
        // 1. 尝试从备用缓存获取
        $fallbackKey = 'fallback:' . $strategy . ':' . $key;
        $fallbackData = Cache::get($fallbackKey);
        
        if ($fallbackData !== null) {
            self::$stats['fallback_hits']++;
            return json_decode($fallbackData, true);
        }
        
        // 2. 尝试从本地文件缓存获取
        $localCache = self::getLocalCache($strategy, $key);
        if ($localCache !== null) {
            self::$stats['fallback_hits']++;
            return $localCache;
        }
        
        // 3. 执行回调(可能有异常)
        try {
            $result = call_user_func_array($callback, $params);
            
            // 写入备用缓存
            Cache::set($fallbackKey, json_encode($result), 60); // 1分钟
            
            // 写入本地缓存
            self::setLocalCache($strategy, $key, $result);
            
            return $result;
            
        } catch (\Exception $e) {
            // 4. 返回默认值
            return $options['default'] ?? null;
        }
    }
    
    /**
     * 异步更新缓存
     */
    private static function updateCacheAsync($strategy, $key, $callback, $params)
    {
        // 使用队列异步更新缓存
        \think\facade\Queue::push(\app\common\job\CacheWarmup::class, [
            'strategy' => $strategy,
            'key' => $key,
            'callback' => serialize($callback),
            'params' => $params,
        ]);
    }
    
    /**
     * 获取锁
     */
    private static function acquireLock($lockKey, $timeout)
    {
        $lockValue = uniqid('lock_', true);
        $result = Cache::set($lockKey, $lockValue, ['nx', 'ex' => $timeout]);
        return $result;
    }
    
    /**
     * 释放锁
     */
    private static function releaseLock($lockKey)
    {
        Cache::delete($lockKey);
    }
    
    /**
     * 检查是否被锁定
     */
    private static function isLocked($cacheKey)
    {
        $lockKey = 'lock:' . $cacheKey;
        return Cache::has($lockKey);
    }
    
    /**
     * 获取本地文件缓存
     */
    private static function getLocalCache($strategy, $key)
    {
        $cacheFile = self::getLocalCacheFile($strategy, $key);
        
        if (file_exists($cacheFile)) {
            $data = file_get_contents($cacheFile);
            $cache = json_decode($data, true);
            
            if ($cache && $cache['expire_time'] > time()) {
                return $cache['data'];
            }
        }
        
        return null;
    }
    
    /**
     * 设置本地文件缓存
     */
    private static function setLocalCache($strategy, $key, $data)
    {
        $cacheFile = self::getLocalCacheFile($strategy, $key);
        $cacheDir = dirname($cacheFile);
        
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
        
        $cache = [
            'data' => $data,
            'expire_time' => time() + 300, // 5分钟
        ];
        
        file_put_contents($cacheFile, json_encode($cache), LOCK_EX);
    }
    
    private static function getLocalCacheFile($strategy, $key)
    {
        $hash = md5($strategy . ':' . $key);
        $dir1 = substr($hash, 0, 2);
        $dir2 = substr($hash, 2, 2);
        
        return runtime_path('cache/' . $strategy . '/' . $dir1 . '/' . $dir2 . '/' . $hash . '.json');
    }
    
    /**
     * 批量获取缓存
     */
    public static function mget($strategy, array $keys, $callback, $options = [])
    {
        $config = self::$cacheStrategies[$strategy] ?? [
            'prefix' => 'cache:',
            'ttl' => 3600,
            'version' => 'v1',
        ];
        
        $results = [];
        $missingKeys = [];
        $cacheKeys = [];
        
        // 1. 批量获取缓存
        foreach ($keys as $key) {
            $cacheKey = $config['prefix'] . $key . ':' . $config['version'];
            $cacheKeys[$key] = $cacheKey;
            
            $cached = Cache::get($cacheKey);
            if ($cached !== null) {
                $data = json_decode($cached, true);
                if (isset($data['data'])) {
                    $results[$key] = $data['data'];
                } else {
                    $missingKeys[] = $key;
                }
            } else {
                $missingKeys[] = $key;
            }
        }
        
        // 2. 批量获取缺失的数据
        if (!empty($missingKeys)) {
            $missingData = call_user_func($callback, $missingKeys);
            
            // 3. 批量设置缓存
            $cacheItems = [];
            foreach ($missingData as $key => $data) {
                $results[$key] = $data;
                
                $cacheKey = $cacheKeys[$key];
                $cacheData = [
                    'data' => $data,
                    'create_time' => time(),
                    'expire_time' => time() + $config['ttl'],
                ];
                
                $cacheItems[$cacheKey] = json_encode($cacheData);
            }
            
            if (!empty($cacheItems)) {
                Cache::setMultiple($cacheItems, $config['ttl'] + $config['stale_ttl']);
            }
        }
        
        // 4. 按原顺序返回
        $orderedResults = [];
        foreach ($keys as $key) {
            if (isset($results[$key])) {
                $orderedResults[$key] = $results[$key];
            }
        }
        
        return $orderedResults;
    }
    
    /**
     * 清理缓存
     */
    public static function clearByPattern($pattern)
    {
        $redis = Cache::store('redis')->handler();
        
        // 使用SCAN迭代删除
        $iterator = null;
        do {
            $keys = $redis->scan($iterator, $pattern, 1000);
            if (!empty($keys)) {
                $redis->del($keys);
            }
        } while ($iterator > 0);
        
        return true;
    }
    
    /**
     * 获取缓存统计
     */
    public static function getStats()
    {
        $total = self::$stats['hits'] + self::$stats['misses'] + 
                self::$stats['stale_hits'] + self::$stats['fallback_hits'];
        
        if ($total > 0) {
            $hitRate = (self::$stats['hits'] + self::$stats['stale_hits']) / $total * 100;
        } else {
            $hitRate = 0;
        }
        
        return array_merge(self::$stats, [
            'total' => $total,
            'hit_rate' => round($hitRate, 2) . '%',
        ]);
    }
}

12.4 数据库连接池

<?php
namespace app\common\pool;

use Swoole\Coroutine\Channel;
use think\facade\Db;

class DatabasePool
{
    private $pool;
    private $config;
    private $min;
    private $max;
    private $timeout;
    private $current = 0;
    private $lastHealthCheck = 0;
    
    public function __construct($config)
    {
        $this->config = $config;
        $this->min = $config['min'] ?? 5;
        $this->max = $config['max'] ?? 20;
        $this->timeout = $config['timeout'] ?? 5.0;
        
        $this->pool = new Channel($this->max);
        
        // 初始化连接池
        $this->initPool();
    }
    
    /**
     * 初始化连接池
     */
    private function initPool()
    {
        for ($i = 0; $i < $this->min; $i++) {
            $connection = $this->createConnection();
            if ($connection) {
                $this->pool->push($connection);
                $this->current++;
            }
        }
    }
    
    /**
     * 创建数据库连接
     */
    private function createConnection()
    {
        try {
            $config = [
                'type' => 'mysql',
                'hostname' => $this->config['hostname'],
                'database' => $this->config['database'],
                'username' => $this->config['username'],
                'password' => $this->config['password'],
                'hostport' => $this->config['hostport'],
                'charset' => 'utf8mb4',
                'debug' => false,
            ];
            
            $connection = Db::connect($config);
            
            // 测试连接
            $connection->query('SELECT 1');
            
            return $connection;
        } catch (\Exception $e) {
            \think\facade\Log::error('数据库连接失败: ' . $e->getMessage());
            return null;
        }
    }
    
    /**
     * 获取连接
     */
    public function getConnection()
    {
        // 健康检查
        $this->healthCheck();
        
        // 从池中获取连接
        if ($this->pool->isEmpty() && $this->current < $this->max) {
            // 创建新连接
            $connection = $this->createConnection();
            if ($connection) {
                $this->current++;
                return $connection;
            }
        }
        
        // 等待可用连接
        $connection = $this->pool->pop($this->timeout);
        if ($connection === false) {
            throw new \RuntimeException('获取数据库连接超时');
        }
        
        // 检查连接是否有效
        if (!$this->checkConnection($connection)) {
            $connection = $this->createConnection();
            if (!$connection) {
                throw new \RuntimeException('创建数据库连接失败');
            }
        }
        
        return $connection;
    }
    
    /**
     * 释放连接
     */
    public function releaseConnection($connection)
    {
        if ($connection && $this->pool->length() < $this->max) {
            $this->pool->push($connection);
        } else {
            $this->closeConnection($connection);
            $this->current--;
        }
    }
    
    /**
     * 检查连接
     */
    private function checkConnection($connection)
    {
        try {
            $connection->query('SELECT 1');
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
    
    /**
     * 关闭连接
     */
    private function closeConnection($connection)
    {
        try {
            if ($connection instanceof \think\Db) {
                $connection->close();
            }
        } catch (\Exception $e) {
            // 忽略关闭错误
        }
    }
    
    /**
     * 健康检查
     */
    private function healthCheck()
    {
        $now = time();
        if ($now - $this->lastHealthCheck < 60) { // 每分钟检查一次
            return;
        }
        
        $this->lastHealthCheck = $now;
        
        // 清理无效连接
        $validConnections = [];
        $invalidCount = 0;
        
        while (!$this->pool->isEmpty()) {
            $connection = $this->pool->pop(0.1);
            if ($connection && $this->checkConnection($connection)) {
                $validConnections[] = $connection;
            } else {
                $invalidCount++;
                $this->current--;
            }
        }
        
        // 放回有效连接
        foreach ($validConnections as $connection) {
            $this->pool->push($connection);
        }
        
        // 补充连接
        $need = $this->min - $this->pool->length();
        for ($i = 0; $i < $need; $i++) {
            $connection = $this->createConnection();
            if ($connection) {
                $this->pool->push($connection);
                $this->current++;
            }
        }
        
        if ($invalidCount > 0) {
            \think\facade\Log::info("数据库连接池清理了{$invalidCount}个无效连接");
        }
    }
    
    /**
     * 获取连接池状态
     */
    public function getStats()
    {
        return [
            'total' => $this->current,
            'idle' => $this->pool->length(),
            'busy' => $this->current - $this->pool->length(),
            'min' => $this->min,
            'max' => $this->max,
        ];
    }
    
    /**
     * 关闭连接池
     */
    public function close()
    {
        while (!$this->pool->isEmpty()) {
            $connection = $this->pool->pop(0.1);
            if ($connection) {
                $this->closeConnection($connection);
            }
        }
        
        $this->current = 0;
    }
}

13. 高可用架构

13.1 负载均衡配置

# nginx负载均衡配置
upstream backend_servers {
    # 加权轮询
    server 192.168.1.101:9501 weight=3;
    server 192.168.1.102:9501 weight=2;
    server 192.168.1.103:9501 weight=2;
    server 192.168.1.104:9501 backup; # 备份服务器
    
    # 健康检查
    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
    
    # 会话保持
    hash $remote_addr consistent;
    
    # 失败重试
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_next_upstream_tries 3;
    proxy_next_upstream_timeout 10s;
}

# WebSocket负载均衡
upstream websocket_servers {
    server 192.168.1.101:9502;
    server 192.168.1.102:9502;
    server 192.168.1.103:9502;
    
    # 长连接优化
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.town-delivery.com;
    
    # SSL配置
    ssl_certificate /etc/nginx/ssl/town-delivery.com.crt;
    ssl_certificate_key /etc/nginx/ssl/town-delivery.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    
    # 安全头
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
    
    # 限流
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req zone=api burst=20 nodelay;
    
    # API接口
    location /api/ {
        proxy_pass http://backend_servers;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时设置
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 30s;
        
        # 缓冲区优化
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        proxy_busy_buffers_size 8k;
        
        # 启用压缩
        gzip on;
        gzip_min_length 1k;
        gzip_comp_level 6;
        gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss;
        
        # 缓存静态资源
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 7d;
            add_header Cache-Control "public, immutable";
        }
    }
    
    # WebSocket
    location /ws {
        proxy_pass http://websocket_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # 超时设置
        proxy_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;
    }
    
    # 健康检查
    location /health {
        access_log off;
        return 200 "healthy\n";
    }
    
    # 状态监控
    location /nginx_status {
        stub_status on;
        access_log off;
        allow 127.0.0.1;
        allow 192.168.1.0/24;
        deny all;
    }
}

13.2 数据库主从复制

-- 主数据库配置 (my.cnf)

[mysqld]

server-id = 1 log_bin = /var/log/mysql/mysql-bin.log binlog_format = ROW expire_logs_days = 7 max_binlog_size = 100M binlog_cache_size = 1M sync_binlog = 1 innodb_flush_log_at_trx_commit = 1 — 创建复制用户 CREATE USER ‘repl’@’%’ IDENTIFIED WITH mysql_native_password BY ‘Repl123456!’; GRANT REPLICATION SLAVE ON *.* TO ‘repl’@’%’; FLUSH PRIVILEGES; — 查看主库状态 SHOW MASTER STATUS; — 从数据库配置 (my.cnf)

[mysqld]

server-id = 2 relay_log = /var/log/mysql/mysql-relay-bin.log read_only = 1 skip_slave_start = 0 log_slave_updates = 1 relay_log_recovery = 1 — 配置从库复制 CHANGE MASTER TO MASTER_HOST=’192.168.1.101′, MASTER_USER=’repl’, MASTER_PASSWORD=’Repl123456!’, MASTER_LOG_FILE=’mysql-bin.000001′, MASTER_LOG_POS=154; — 启动复制 START SLAVE; — 查看从库状态 SHOW SLAVE STATUS\G; — 读写分离中间件配置 (ProxySQL) — 添加后端服务器 INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (10, ‘192.168.1.101’, 3306), — 写组 (20, ‘192.168.1.102’, 3306), — 读组 (20, ‘192.168.1.103’, 3306); — 读组 — 配置用户 INSERT INTO mysql_users(username, password, default_hostgroup) VALUES (‘app_user’, ‘App123456!’, 10); — 配置查询规则 INSERT INTO mysql_query_rules(rule_id, active, match_digest, destination_hostgroup, apply) VALUES (1, 1, ‘^SELECT.*FOR UPDATE$’, 10, 1), — SELECT FOR UPDATE 路由到写库 (2, 1, ‘^SELECT’, 20, 1), — 其他SELECT路由到读库 (3, 1, ‘.*’, 10, 1); — 其他路由到写库 — 保存配置 SAVE MYSQL SERVERS TO DISK; SAVE MYSQL USERS TO DISK; SAVE MYSQL QUERY RULES TO DISK; LOAD MYSQL SERVERS TO RUNTIME; LOAD MYSQL USERS TO RUNTIME; LOAD MYSQL QUERY RULES TO RUNTIME;

13.3 Redis集群配置

# redis-cluster.yaml
version: '3.8'

services:
  redis-node-1:
    image: redis:6.2-alpine
    container_name: redis-node-1
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-1:/data
    ports:
      - "6379:6379"
    networks:
      - redis-cluster
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "--cluster", "check", "127.0.0.1:6379"]
      interval: 10s
      timeout: 5s
      retries: 3

  redis-node-2:
    image: redis:6.2-alpine
    container_name: redis-node-2
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-2:/data
    networks:
      - redis-cluster
    depends_on:
      - redis-node-1

  redis-node-3:
    image: redis:6.2-alpine
    container_name: redis-node-3
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-3:/data
    networks:
      - redis-cluster
    depends_on:
      - redis-node-1

  redis-node-4:
    image: redis:6.2-alpine
    container_name: redis-node-4
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-4:/data
    networks:
      - redis-cluster
    depends_on:
      - redis-node-1

  redis-node-5:
    image: redis:6.2-alpine
    container_name: redis-node-5
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-5:/data
    networks:
      - redis-cluster
    depends_on:
      - redis-node-1

  redis-node-6:
    image: redis:6.2-alpine
    container_name: redis-node-6
    command: >
      redis-server
      --port 6379
      --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
      --masterauth ${REDIS_PASSWORD}
    volumes:
      - redis-data-6:/data
    networks:
      - redis-cluster
    depends_on:
      - redis-node-1

  redis-cluster-init:
    image: redis:6.2-alpine
    container_name: redis-cluster-init
    depends_on:
      - redis-node-1
      - redis-node-2
      - redis-node-3
      - redis-node-4
      - redis-node-5
      - redis-node-6
    networks:
      - redis-cluster
    command: >
      sh -c '
        echo "等待Redis节点启动..."
        sleep 10
        echo "初始化集群..."
        redis-cli -a ${REDIS_PASSWORD} --cluster create
          redis-node-1:6379
          redis-node-2:6379
          redis-node-3:6379
          redis-node-4:6379
          redis-node-5:6379
          redis-node-6:6379
          --cluster-replicas 1
          --cluster-yes
        echo "集群初始化完成"
      '

networks:
  redis-cluster:
    driver: bridge

volumes:
  redis-data-1:
  redis-data-2:
  redis-data-3:
  redis-data-4:
  redis-data-5:
  redis-data-6:
<?php
// Redis集群客户端
namespace app\common\lib;

use think\facade\Log;
use think\facade\Config;

class RedisClusterClient
{
    private static $instance = null;
    private $redis = null;
    private $config = [];
    
    private function __construct()
    {
        $this->config = Config::get('cache.stores.redis_cluster', [
            'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7002'],
            'timeout' => 5.0,
            'read_timeout' => 5.0,
            'persistent' => false,
            'password' => null,
            'prefix' => 'td:',
        ]);
        
        $this->init();
    }
    
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function init()
    {
        try {
            $seeds = [];
            foreach ($this->config['host'] as $host) {
                list($hostname, $port) = explode(':', $host);
                $seeds[] = [$hostname, (int)$port];
            }
            
            $this->redis = new \RedisCluster(
                null,
                $seeds,
                $this->config['timeout'],
                $this->config['read_timeout'],
                $this->config['persistent'],
                $this->config['password'] ?? null
            );
            
            // 设置选项
            $this->redis->setOption(\RedisCluster::OPT_SERIALIZER, \RedisCluster::SERIALIZER_PHP);
            $this->redis->setOption(\RedisCluster::OPT_SCAN, \RedisCluster::SCAN_RETRY);
            
            // 测试连接
            $this->redis->ping('ping');
            
        } catch (\Exception $e) {
            Log::error('Redis集群连接失败: ' . $e->getMessage());
            throw $e;
        }
    }
    
    /**
     * 获取Redis实例
     */
    public function getRedis()
    {
        if (!$this->redis) {
            $this->init();
        }
        return $this->redis;
    }
    
    /**
     * 设置缓存
     */
    public function set($key, $value, $ttl = null)
    {
        $key = $this->config['prefix'] . $key;
        
        try {
            if ($ttl) {
                $result = $this->getRedis()->setex($key, $ttl, $value);
            } else {
                $result = $this->getRedis()->set($key, $value);
            }
            
            if (!$result) {
                Log::error("Redis设置失败: {$key}");
                return false;
            }
            
            return true;
        } catch (\Exception $e) {
            Log::error("Redis设置异常: {$key} - " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * 获取缓存
     */
    public function get($key)
    {
        $key = $this->config['prefix'] . $key;
        
        try {
            $value = $this->getRedis()->get($key);
            return $value !== false ? $value : null;
        } catch (\Exception $e) {
            Log::error("Redis获取异常: {$key} - " . $e->getMessage());
            return null;
        }
    }
    
    /**
     * 删除缓存
     */
    public function delete($key)
    {
        $key = $this->config['prefix'] . $key;
        
        try {
            return $this->getRedis()->del($key) > 0;
        } catch (\Exception $e) {
            Log::error("Redis删除异常: {$key} - " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * 批量获取
     */
    public function mget(array $keys)
    {
        $prefixedKeys = array_map(function($key) {
            return $this->config['prefix'] . $key;
        }, $keys);
        
        try {
            $values = $this->getRedis()->mget($prefixedKeys);
            
            $result = [];
            foreach ($keys as $index => $key) {
                $result[$key] = $values[$index] !== false ? $values[$index] : null;
            }
            
            return $result;
        } catch (\Exception $e) {
            Log::error("Redis批量获取异常: " . $e->getMessage());
            return array_fill_keys($keys, null);
        }
    }
    
    /**
     * 分布式锁
     */
    public function lock($key, $timeout = 10, $expire = 30)
    {
        $lockKey = 'lock:' . $key;
        $identifier = uniqid('', true);
        
        $start = time();
        while (time() - $start < $timeout) {
            $result = $this->getRedis()->set(
                $lockKey,
                $identifier,
                ['nx', 'ex' => $expire]
            );
            
            if ($result) {
                return $identifier;
            }
            
            usleep(100000); // 100ms
        }
        
        return false;
    }
    
    /**
     * 释放锁
     */
    public function unlock($key, $identifier)
    {
        $lockKey = 'lock:' . $key;
        
        $script = '
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            else
                return 0
            end
        ';
        
        try {
            $result = $this->getRedis()->eval($script, [$lockKey, $identifier], 1);
            return $result === 1;
        } catch (\Exception $e) {
            Log::error("Redis解锁异常: {$key} - " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * 获取集群信息
     */
    public function getClusterInfo()
    {
        try {
            $info = $this->getRedis()->info();
            
            $clusterInfo = [
                'nodes' => $this->getRedis()->_masters(),
                'slots' => [],
                'memory' => 0,
                'connected_clients' => 0,
                'ops_per_sec' => 0,
            ];
            
            // 收集各节点信息
            foreach ($clusterInfo['nodes'] as $node) {
                list($host, $port) = $node;
                $nodeInfo = $this->getRedis()->info($host . ':' . $port);
                
                if (isset($nodeInfo['Memory'])) {
                    $clusterInfo['memory'] += $nodeInfo['Memory'];
                }
                if (isset($nodeInfo['connected_clients'])) {
                    $clusterInfo['connected_clients'] += $nodeInfo['connected_clients'];
                }
                if (isset($nodeInfo['instantaneous_ops_per_sec'])) {
                    $clusterInfo['ops_per_sec'] += $nodeInfo['instantaneous_ops_per_sec'];
                }
            }
            
            return $clusterInfo;
        } catch (\Exception $e) {
            Log::error("获取Redis集群信息失败: " . $e->getMessage());
            return null;
        }
    }
}

14. 安全防护

14.1 安全中间件

<?php
namespace app\http\middleware;

use think\Request;
use think\Response;
use think\facade\Log;
use app\common\lib\Security;

class SecurityMiddleware
{
    public function handle(Request $request, \Closure $next)
    {
        // 1. 请求频率限制
        if (!$this->rateLimit($request)) {
            return $this->tooManyRequests();
        }
        
        // 2. SQL注入检测
        if (!$this->sqlInjectionCheck($request)) {
            Log::warning('SQL注入尝试', [
                'ip' => $request->ip(),
                'url' => $request->url(),
                'params' => $request->param()
            ]);
            return $this->badRequest('请求参数非法');
        }
        
        // 3. XSS攻击检测
        if (!$this->xssCheck($request)) {
            Log::warning('XSS攻击尝试', [
                'ip' => $request->ip(),
                'url' => $request->url(),
                'params' => $request->param()
            ]);
            return $this->badRequest('请求参数非法');
        }
        
        // 4. CSRF保护
        if (!$this->csrfCheck($request)) {
            return $this->badRequest('CSRF令牌无效');
        }
        
        // 5. 敏感信息过滤
        $this->filterSensitiveData($request);
        
        // 6. 请求签名验证
        if (!$this->signatureCheck($request)) {
            return $this->badRequest('请求签名无效');
        }
        
        $response = $next($request);
        
        // 7. 响应头安全设置
        $response = $this->setSecurityHeaders($response);
        
        return $response;
    }
    
    /**
     * 请求频率限制
     */
    private function rateLimit(Request $request)
    {
        $ip = $request->ip();
        $path = $request->pathinfo();
        
        $key = 'rate_limit:' . md5($ip . ':' . $path);
        $limit = 100; // 每分钟100次
        $period = 60; // 60秒
        
        return Security::rateLimit($key, $limit, $period);
    }
    
    /**
     * SQL注入检测
     */
    private function sqlInjectionCheck(Request $request)
    {
        $params = array_merge(
            $request->get(),
            $request->post(),
            $request->route(),
            $request->header()
        );
        
        foreach ($params as $value) {
            if (is_string($value) && !Security::sqlInjectionCheck($value)) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * XSS攻击检测
     */
    private function xssCheck(Request $request)
    {
        $dangerousPatterns = [
            '/<script.*?>.*?<\/script>/is',
            '/javascript:/i',
            '/on\w+\s*=/i',
            '/expression\s*\(/i',
            '/vbscript:/i',
            '/<iframe.*?>.*?<\/iframe>/is',
            '/<object.*?>.*?<\/object>/is',
            '/<embed.*?>.*?<\/embed>/is',
            '/<applet.*?>.*?<\/applet>/is',
        ];
        
        $params = array_merge(
            $request->get(),
            $request->post(),
            $request->route()
        );
        
        foreach ($params as $key => $value) {
            if (is_string($value)) {
                foreach ($dangerousPatterns as $pattern) {
                    if (preg_match($pattern, $value)) {
                        return false;
                    }
                }
            }
        }
        
        return true;
    }
    
    /**
     * CSRF保护
     */
    private function csrfCheck(Request $request)
    {
        // API接口不需要CSRF保护
        if ($request->isAjax() || strpos($request->pathinfo(), 'api/') === 0) {
            return true;
        }
        
        $token = $request->param('csrf_token') ?: $request->header('X-CSRF-TOKEN');
        $sessionToken = session('csrf_token');
        
        if (!$token || $token !== $sessionToken) {
            return false;
        }
        
        return true;
    }
    
    /**
     * 敏感信息过滤
     */
    private function filterSensitiveData(Request $request)
    {
        $sensitiveFields = ['password', 'pwd', 'passwd', 'token', 'secret', 'key'];
        
        foreach ($sensitiveFields as $field) {
            if ($request->has($field)) {
                $request->delete($field);
            }
        }
    }
    
    /**
     * 请求签名验证
     */
    private function signatureCheck(Request $request)
    {
        // 公共接口不需要签名
        $publicPaths = ['/api/auth/login', '/api/auth/register', '/api/public/'];
        $path = $request->pathinfo();
        
        foreach ($publicPaths as $publicPath) {
            if (strpos($path, $publicPath) === 0) {
                return true;
            }
        }
        
        $timestamp = $request->header('X-Timestamp');
        $signature = $request->header('X-Signature');
        $accessKey = $request->header('X-Access-Key');
        
        if (!$timestamp || !$signature || !$accessKey) {
            return false;
        }
        
        // 检查时间戳(5分钟内有效)
        if (abs(time() - (int)$timestamp) > 300) {
            return false;
        }
        
        // 获取密钥
        $secretKey = $this->getSecretKey($accessKey);
        if (!$secretKey) {
            return false;
        }
        
        // 生成签名
        $params = $request->param();
        ksort($params);
        $queryString = http_build_query($params);
        $signString = $request->method() . "\n"
            . $request->host() . "\n"
            . $request->pathinfo() . "\n"
            . $queryString . "\n"
            . $timestamp;
        
        $expectedSignature = hash_hmac('sha256', $signString, $secretKey);
        
        return hash_equals($expectedSignature, $signature);
    }
    
    /**
     * 设置安全响应头
     */
    private function setSecurityHeaders(Response $response)
    {
        $headers = [
            'X-Content-Type-Options' => 'nosniff',
            'X-Frame-Options' => 'SAMEORIGIN',
            'X-XSS-Protection' => '1; mode=block',
            'Referrer-Policy' => 'strict-origin-when-cross-origin',
            'Content-Security-Policy' => "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
        ];
        
        foreach ($headers as $key => $value) {
            $response->header($key, $value);
        }
        
        return $response;
    }
    
    /**
     * 获取密钥
     */
    private function getSecretKey($accessKey)
    {
        $keys = [
            'web' => 'web_secret_key_' . env('app.app_key'),
            'app' => 'app_secret_key_' . env('app.app_key'),
            'merchant' => 'merchant_secret_key_' . env('app.app_key'),
            'rider' => 'rider_secret_key_' . env('app.app_key'),
        ];
        
        return $keys[$accessKey] ?? null;
    }
    
    /**
     * 返回429响应
     */
    private function tooManyRequests()
    {
        return Response::create([
            'code' => 429,
            'msg' => '请求过于频繁,请稍后再试',
            'data' => null
        ], 'json', 429);
    }
    
    /**
     * 返回400响应
     */
    private function badRequest($message = '请求参数错误')
    {
        return Response::create([
            'code' => 400,
            'msg' => $message,
            'data' => null
        ], 'json', 400);
    }
}

14.2 数据加密存储

<?php
namespace app\common\lib;

class DataEncryption
{
    // 加密密钥
    private static $key;
    private static $cipher = 'aes-256-gcm';
    
    public static function init()
    {
        self::$key = hash('sha256', env('app.app_key', 'default_key'), true);
    }
    
    /**
     * 加密数据
     */
    public static function encrypt($data, $associatedData = '')
    {
        if (!is_string($data)) {
            $data = json_encode($data, JSON_UNESCAPED_UNICODE);
        }
        
        $iv = random_bytes(openssl_cipher_iv_length(self::$cipher));
        $tag = '';
        
        $encrypted = openssl_encrypt(
            $data,
            self::$cipher,
            self::$key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            $associatedData,
            16
        );
        
        if ($encrypted === false) {
            throw new \RuntimeException('加密失败');
        }
        
        $result = base64_encode($iv . $tag . $encrypted);
        return $result;
    }
    
    /**
     * 解密数据
     */
    public static function decrypt($encryptedData, $associatedData = '')
    {
        $data = base64_decode($encryptedData);
        
        $ivLength = openssl_cipher_iv_length(self::$cipher);
        $tagLength = 16;
        
        if (strlen($data) < $ivLength + $tagLength) {
            throw new \RuntimeException('加密数据格式错误');
        }
        
        $iv = substr($data, 0, $ivLength);
        $tag = substr($data, $ivLength, $tagLength);
        $ciphertext = substr($data, $ivLength + $tagLength);
        
        $decrypted = openssl_decrypt(
            $ciphertext,
            self::$cipher,
            self::$key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            $associatedData
        );
        
        if ($decrypted === false) {
            throw new \RuntimeException('解密失败');
        }
        
        // 尝试解析JSON
        $result = json_decode($decrypted, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            return $result;
        }
        
        return $decrypted;
    }
    
    /**
     * 哈希密码
     */
    public static function hashPassword($password)
    {
        $options = [
            'cost' => 12,
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3
        ];
        
        return password_hash($password, PASSWORD_ARGON2ID, $options);
    }
    
    /**
     * 验证密码
     */
    public static function verifyPassword($password, $hash)
    {
        return password_verify($password, $hash);
    }
    
    /**
     * 生成API密钥
     */
    public static function generateApiKey()
    {
        $random = random_bytes(32);
        $key = 'td_' . bin2hex($random);
        
        // 存储哈希值
        $hash = hash('sha256', $key);
        
        return [
            'key' => $key,
            'hash' => $hash
        ];
    }
    
    /**
     * 验证API密钥
     */
    public static function verifyApiKey($key, $hash)
    {
        return hash_equals(hash('sha256', $key), $hash);
    }
    
    /**
     * 加密存储敏感信息
     */
    public static function encryptSensitiveData($data, $context = '')
    {
        if (is_array($data)) {
            $encrypted = [];
            foreach ($data as $key => $value) {
                if (in_array($key, ['password', 'token', 'secret', 'key', 'id_card', 'bank_card'])) {
                    $encrypted[$key] = self::encrypt($value, $context);
                } else {
                    $encrypted[$key] = $value;
                }
            }
            return $encrypted;
        }
        
        return self::encrypt($data, $context);
    }
    
    /**
     * 解密敏感信息
     */
    public static function decryptSensitiveData($data, $context = '')
    {
        if (is_array($data)) {
            $decrypted = [];
            foreach ($data as $key => $value) {
                if (in_array($key, ['password', 'token', 'secret', 'key', 'id_card', 'bank_card'])) {
                    try {
                        $decrypted[$key] = self::decrypt($value, $context);
                    } catch (\Exception $e) {
                        $decrypted[$key] = null;
                    }
                } else {
                    $decrypted[$key] = $value;
                }
            }
            return $decrypted;
        }
        
        try {
            return self::decrypt($data, $context);
        } catch (\Exception $e) {
            return null;
        }
    }
    
    /**
     * 数据脱敏
     */
    public static function maskSensitiveData($data)
    {
        if (is_array($data)) {
            $masked = [];
            foreach ($data as $key => $value) {
                if (in_array($key, ['phone', 'id_card', 'bank_card', 'email'])) {
                    $masked[$key] = self::maskValue($value, $key);
                } elseif ($key === 'password' || $key === 'token') {
                    $masked[$key] = '***';
                } else {
                    $masked[$key] = $value;
                }
            }
            return $masked;
        }
        
        return self::maskValue($data);
    }
    
    /**
     * 脱敏值
     */
    private static function maskValue($value, $type = '')
    {
        if (!is_string($value)) {
            return $value;
        }
        
        switch ($type) {
            case 'phone':
                return substr_replace($value, '****', 3, 4);
            case 'id_card':
                return substr_replace($value, '********', 6, 8);
            case 'bank_card':
                return substr_replace($value, '**** **** **** ', 4, 10);
            case 'email':
                list($username, $domain) = explode('@', $value, 2);
                if (strlen($username) > 2) {
                    $username = substr($username, 0, 1) . '***' . substr($username, -1);
                }
                return $username . '@' . $domain;
            default:
                if (strlen($value) > 4) {
                    return substr($value, 0, 2) . '***' . substr($value, -2);
                }
                return '***';
        }
    }
}

15. 监控与告警

15.1 应用性能监控

<?php
namespace app\common\lib;

use think\facade\Log;
use think\facade\Request;

class APM
{
// 性能数据存储
private static $metrics = [];

/**
* 开始监控
*/
public static function start($name)
{
if (!isset(self::$metrics[$name])) {
self::$metrics[$name] = [
‘start_time’ => microtime(true),
‘memory_start’ => memory_get_usage(true),
‘peak_memory_start’ => memory_get_peak_usage(true),
‘sql_queries’ => [],
‘cache_operations’ => [],
‘http_requests’ => [],
];
}

return $name;
}

/**
* 结束监控
*/
public static function end($name)
{
if (!isset(self::$metrics[$name])) {
return null;
}

$metric = self::$metrics[$name];
$endTime = microtime(true);

$data = [
‘name’ => $name,
‘duration’ => round(($endTime – $metric[‘start_time’]) * 1000, 2), // 毫秒
‘memory_usage’ => memory_get_usage(true) – $metric[‘memory_start’],
‘peak_memory’ => memory_get_peak_usage(true) – $metric[‘peak_memory_start’],
‘sql_queries’ => $metric[‘sql_queries’],
‘cache_operations’ => $metric[‘cache_operations’],
‘http_requests’ => $metric[‘http_requests’],
‘timestamp’ => date(‘Y-m-d H:i:s’),
‘url’ => Request::url(),
‘ip’ => Request::ip(),
];

// 记录到数据库
self::recordMetric($data);

// 发送到监控系统
self::sendToMonitoring($data);

// 检查是否超过阈值
self::checkThresholds($data);

unset(self::$metrics[$name]);

return $data;
}

/**
* 记录SQL查询
*/
public static function recordSqlQuery($sql, $time)
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
$caller = $trace[2] ?? $trace[1] ?? [];

$query = [
‘sql’ => $sql,
‘time’ => round($time * 1000, 2), // 毫秒
‘file’ => $caller[‘file’] ?? ”,
‘line’ => $caller[‘line’] ?? ”,
‘function’ => $caller[‘function’] ?? ”,
];

foreach (self::$metrics as &$metric) {
$metric[‘sql_queries’][] = $query;
}
}

/**
* 记录缓存操作
*/
public static function recordCacheOperation($operation, $key, $time, $success = true)
{
$op = [
‘operation’ => $operation,
‘key’ => $key,
‘time’ => round($time * 1000, 2),
‘success’ => $success,
‘timestamp’ => microtime(true),
];

foreach (self::$metrics as &$metric) {
$metric[‘cache_operations’][] = $op;
}
}

/**
* 记录HTTP请求
*/
public static function recordHttpRequest($url, $method, $time, $statusCode, $success = true)
{
$request = [
‘url’ => $url,
‘method’ => $method,
‘time’ => round($time * 1000, 2),
‘status_code’ => $statusCode,
‘success’ => $success,
‘timestamp’ => microtime(true),
];

foreach (self::$metrics as &$metric) {
$metric[‘http_requests’][] = $request;
}
}

/**
* 记录性能指标
*/
private static function recordMetric($data)
{
// 记录到数据库
if ($data[‘duration’] > 1000) { // 超过1秒
\think\facade\Db::name(‘performance_log’)->insert([
‘name’ => $data[‘name’],
‘duration’ => $data[‘duration’],
‘memory_usage’ => $data[‘memory_usage’],
‘peak_memory’ => $data[‘peak_memory’],
‘sql_count’ => count($data[‘sql_queries’]),
‘cache_count’ => count($data[‘cache_operations’]),
‘http_count’ => count($data[‘http_requests’]),
‘url’ => $data[‘url’],
‘ip’ => $data[‘ip’],
‘create_time’ => $data[‘timestamp’],
]);
}

// 记录到日志
Log::info(‘性能监控’, $data);
}

/**
* 发送到监控系统
*/
private static function sendToMonitoring($data)
{
$metrics = [
‘apm_duration’ => $data[‘duration’],
‘apm_memory_usage’ => $data[‘memory_usage’],
‘apm_sql_count’ => count($data[‘sql_queries’]),
‘apm_cache_count’ => count($data[‘cache_operations’]),
‘apm_http_count’ => count($data[‘http_requests’]),
];

// 发送到Prometheus
foreach ($metrics as $name => $value) {
self::pushToPrometheus($name, $value, [
‘endpoint’ => $data[‘name’],
‘url’ => $data[‘url’],
]);
}

// 发送到StatsD
self::pushToStatsD($metrics, $data[‘name’]);
}

/**
* 推送到Prometheus
*/
private static function pushToPrometheus($name, $value, $labels = [])
{
$labelsStr = ”;
foreach ($labels as $key => $val) {
$labelsStr .= “{$key}=\”{$val}\”,”;
}
$labelsStr = rtrim($labelsStr, ‘,’);

$metric = “{$name}{{{$labelsStr}}} {$value}”;

// 发送到Pushgateway
$ch = curl_init(‘http://localhost:9091/metrics/job/apm’);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, ‘POST’);
curl_setopt($ch, CURLOPT_POSTFIELDS, $metric);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
‘Content-Type: text/plain’,
]);
curl_exec($ch);
curl_close($ch);
}

/**
* 推送到StatsD
*/
private static function pushToStatsD($metrics, $endpoint)
{
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

foreach ($metrics as $name => $value) {
$stat = “apm.{$endpoint}.{$name}:{$value}|ms”;
@socket_sendto($socket, $stat, strlen($stat), 0, ‘localhost’, 8125);
}

socket_close($socket);
}

/**
* 检查阈值
*/
private static function checkThresholds($data)
{
$thresholds = [
‘duration’ => 1000, // 1秒
‘memory’ => 50 * 1024 * 1024, // 50MB
‘sql_queries’ => 50,
‘sql_slow’ => 100, // 慢查询阈值(毫秒)
];

$alerts = [];

}

结语

本文详细阐述了一套针对乡镇地区的外卖跑腿小程序系统的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领域的技术实践提供有价值的参考。在数字化转型的大潮中,技术开发者应充分发挥技术优势,为乡村振兴贡献专业力量,让科技的发展惠及更广泛的人群。

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

版权声明:本文为JienDa博主的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
若内容若侵犯到您的权益,请发送邮件至:platform_service@jienda.com我们将第一时间处理!
所有资源仅限于参考和学习,版权归JienDa作者所有,更多请访问JienDa首页。

给TA赞助
共{{data.count}}人
人已赞助
后端

【PHP 8.2特性深度挖掘】只读类的继承与扩展艺术:从语法限制到设计模式突破

2025-12-5 7:15:53

后端

当AI化身用户画像预言家:PHP初级开发者的创意突围战——老码农的幽默生存手册

2025-12-10 8:45:04

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索