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

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

摘要

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

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


1. 引言

1.1 乡镇O2O市场现状

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

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

1.2 技术挑战与解决方案

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

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

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

2. 需求分析

2.1 用户角色分析

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

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

2.2 功能需求

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

2.3 非功能需求

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

3. 系统架构设计

3.1 总体架构

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

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

3.2 技术选型

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

3.3 数据库设计

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

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

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

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

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

4. 核心功能实现

4.1 用户认证与授权

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

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

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

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

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

4.2 附近商家推荐算法

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

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

use think\facade\Db;

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

4.3 订单系统设计

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

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

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

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

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

4.4 骑手派单系统

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

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

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

4.5 实时消息推送

基于Workerman实现WebSocket实时通信:

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

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

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

5. 性能优化策略

5.1 数据库优化

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

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

5.2 缓存优化

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

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

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

use think\facade\Cache;

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

5.3 异步处理

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

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

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

6. 安全防护

6.1 输入验证与过滤

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

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

6.2 数据加密

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

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

7. 部署与监控

7.1 Docker部署配置

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

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

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

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

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

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

# 复制项目文件
COPY . .

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

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

# 暴露端口
EXPOSE 9000

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

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

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

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

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

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

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

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

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

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

networks:
  app-network:
    driver: bridge

7.# .2 系统监控配置

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

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

rule_files: []

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

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

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

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

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

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

7.2 性能监控实现

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

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

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

8. 小程序前端实现

8.1 小程序架构设计

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

8.2 首页实现

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.shop-info {
  padding: 20rpx;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8.3 商家详情页

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

9. 骑手端实现

9.1 骑手接单页面

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

结语

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

技术要点回顾

1. 架构设计方面

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

2. 核心功能实现

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

3. 性能优化策略

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

4. 安全防护体系

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

5. 监控与运维

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

乡镇特色体现

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

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

技术价值

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

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

展望未来

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

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

结语

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

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

相关推荐
霸王大陆3 小时前
《零基础学 PHP:从入门到实战》模块十:从应用到精通——掌握PHP进阶技术与现代化开发实战-1
android·开发语言·php
老华带你飞3 小时前
旅游|基于Java旅游信息推荐系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·后端·旅游
Jane-6667773 小时前
C语言——表达式、语句、函数
c语言·开发语言·算法
arron88993 小时前
C# 项目源码进行全面的技术架构和调用逻辑分析。以下是系统性的技术方案
开发语言·架构·c#
88号技师4 小时前
【2025年1区SCI】最新信号分解方法-JMD的参数优化:15种适应度函数-matlab代码
开发语言·matlab·故障诊断·信号分解
zmzb01034 小时前
C++课后习题训练记录Day44
开发语言·c++
smile_Iris4 小时前
Day 30 函数定义与参数
开发语言·python
老华带你飞4 小时前
医院挂号|基于Java医院挂号管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
豐儀麟阁贵4 小时前
9.6使用正则表达式
java·开发语言·数据库·mysql