乡镇外卖跑腿小程序开发实战:基于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 技术挑战与解决方案
针对乡镇O2O的特点,系统开发面临以下技术挑战:
- 网络环境相对不稳定
- 用户数字化操作能力有限
- 商家信息化程度低
- 配送路径规划复杂
本文提出的解决方案采用轻量级技术架构,注重系统稳定性、易用性和可维护性,特别针对低带宽环境进行优化。
2. 需求分析
2.1 用户角色分析
系统主要涉及四类用户角色:
- 消费者端:乡镇居民,通过小程序下单购买商品或服务
- 商家端:本地商家,管理商品、订单和店铺
- 骑手端:配送人员,接单、取货、配送
- 管理端:平台管理员,负责系统管理、数据监控
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流程
- 容器化部署方案
- 详细的日志记录与分析
- 数据库备份与恢复机制
乡镇特色体现
本系统特别针对乡镇地区的特点进行了优化:
- 网络适应性强:支持弱网环境,具备断点续传和数据同步能力
- 操作简化:针对乡镇用户习惯,简化了操作流程
- 成本控制:通过技术优化降低服务器和带宽成本
- 本地化支持:支持方言、本地支付方式等乡镇特色功能
- 扩展性强:便于对接本地商家和物流资源
技术价值
本项目展示了PHP在现代化Web开发中的强大能力:
- 高性能表现:通过Swoole、OPcache等技术,PHP在处理高并发场景时表现出色
- 开发效率:ThinkPHP框架提供了丰富的功能模块,加速开发进程
- 生态系统:PHP拥有成熟的Composer包管理体系和丰富的开源组件
- 维护成本:PHP开发人员资源丰富,长期维护成本可控
- 兼容性:支持在多种环境下部署,包括云服务器和本地服务器
展望未来
随着乡村振兴战略的深入推进,乡镇数字化服务市场潜力巨大。未来可进一步:
- AI技术应用:引入智能推荐、需求预测、智能定价等AI能力
- 物联网集成:对接智能配送设备、温控设备等
- 大数据分析:深入挖掘用户行为数据,提供精准运营支持
- 生态扩展:从外卖跑腿扩展到社区团购、本地生活服务等多元化业务
- 技术升级:考虑向PHP 8.x版本迁移,引入Go语言微服务等新技术栈
结语
乡镇外卖跑腿小程序的开发不仅是一个技术项目,更是推动城乡数字鸿沟弥合的重要实践。通过本文的详细技术分享,希望能够为PHP开发者在乡镇O2O领域的技术实践提供有价值的参考。在数字化转型的大潮中,技术开发者应充分发挥技术优势,为乡村振兴贡献专业力量,让科技的发展惠及更广泛的人群。
未来,我们将继续关注乡镇数字化发展需求,不断优化技术方案,为推动数字乡村建设做出更大贡献。