计算机信息技术行业写作者 王哲
202X年X月X日
引言:千团废墟里的“幸存者基因”为何值得今天的IT从业者拆解?
2010年到2014年,中国互联网历史上第一场“烧钱补贴换市场渗透率、规模壁垒筑生存护城河”的“千团大战”落下帷幕——超过5000家团购网站(据易观智库2014年Q3数据)只剩美团点评合并体、糯米网等不到10家头部玩家,剩下的要么因技术滞后导致订单卡顿、退款纠纷无法批量处理而死,要么因没有差异化内容和供应链对接能力依附于巨头苟延残喘。
但鲜有人关注的是,在这场惨烈的战役中,有超过30%的“腰部+长尾”团购网站,甚至部分早期美团、拉手的“竞品影子站”,都是由方维网络推出的「方维团购系统」搭建的,从2009年方维团购1.0Beta版上线(早于美团北京上线3个月),到2015年停止向公开市场大规模售卖标准化团购系统转向定制化与私有化部署,方维团购累计服务客户超过12万家,累计GMV流水超过500亿元人民币(方维网络2015年定制化转型发布会上的内部数据)——这组数据足以说明:方维团购的技术架构、业务逻辑设计,是当时中国本地化生活服务SaaS/PaaS领域最成熟的“基础设施模板”。
更有意思的是,2020年疫情以来,私域本地生活重新成为互联网巨头、连锁品牌、甚至区域小商家的“必争之地”:美团推出了“美团闪购商家助手私域”“美团优选团长小程序”的二次开发接口;微信视频号+小程序+企业微信打通了“内容种草-私域留存-团购转化-复购裂变”的闭环;抖音本地生活也推出了“抖店本地团购小程序”,而这个时候,很多原本做过PC团购的区域商家、或者想快速搭建私域团购平台的创业者,又重新开始搜索“方维团购系统源码”——因为方维团购的老版本虽然停更了,但核心业务逻辑、权限管理、批量退款、多门店/多团长管理、供应链对接等功能,完全可以适配今天的私域本地生活场景,只需要做简单的前端适配和接口打通。
作为一个长期关注本地生活SaaS/PaaS、二次开发技术的计算机信息技术行业写作者,我最近专门找到了方维网络2015年最后一版公开售卖的「方维团购V4.0.6开源版」(非商业授权版,仅供学习使用),拆解了它的技术架构,也做了一个“适配微信小程序私域+多门店团长的生鲜日团”的二次开发实例,今天这篇文章,我就把我的拆解过程、二次开发实例、以及我对本地生活SaaS/PaaS未来发展的个人观点,分享给大家。
拆解方维团购V4.0.6开源版:千团大战时期的“成熟基础设施模板”到底有什么过人之处?
1 技术架构概览:分层设计+ThinkPHP5框架雏形(提前踩中轻量级框架的红利)
方维团购V4.0.6开源版采用的是典型的MVC分层设计架构,底层使用的是方维网络基于ThinkPHP3.2.3框架二次优化的「方维ThinkPHP定制版」——很多人可能不知道,方维网络是国内最早一批深度使用ThinkPHP框架的IT公司之一,他们的二次优化版本甚至影响了后来ThinkPHP5框架的部分设计思路(比如路由配置的简化、数据库查询构造器的优化)。
具体的技术分层如下:
表现层(View):使用的是Smarty3模板引擎,将HTML/CSS/JS和PHP逻辑完全分离——这在2014-2015年是非常先进的设计,不仅方便前端开发人员修改界面,也方便后端开发人员维护业务逻辑;
控制层(Controller):使用的是方维网络自定义的「方维团购核心控制器」,继承自ThinkPHP3.2.3的Think\Controller,封装了权限验证、用户登录状态判断、多语言支持、防SQL注入/防XSS攻击等通用功能——这也是方维团购能快速部署、稳定运行的核心原因之一;
业务逻辑层(Service):虽然ThinkPHP3.2.3框架本身没有强制要求Service层,但方维团购V4.0.6开源版主动在控制层和模型层之间加了Service层,将复杂的业务逻辑(比如团购下单、批量退款、多门店库存同步、团长佣金计算)封装在Service层中——这种设计大大提高了代码的可复用性和可维护性,也是我今天做二次开发实例时最轻松的地方;
数据访问层(Model):使用的是ThinkPHP3.2.3框架的Think\Model,封装了数据库的增删改查操作,支持MySQL、PostgreSQL、SQLite等多种数据库——默认使用的是MySQL数据库,因为当时MySQL是国内互联网公司最常用的开源数据库;
缓存层(Cache):支持文件缓存、Redis缓存、Memcached缓存三种缓存方式——默认使用的是文件缓存,但在团购高峰期(比如节假日、秒杀活动),可以通过修改配置文件轻松切换到Redis缓存或Memcached缓存,提高系统的并发处理能力;
支付接口层(Payment):默认集成了支付宝即时到账、支付宝快捷支付、微信支付(虽然当时微信支付还没有完全普及,但方维团购已经提前做了接口预留)、银联在线支付四种支付方式——支付接口层采用的是策略模式,方便后续添加新的支付方式。
为了让大家更直观地了解方维团购V4.0.6开源版的技术架构,我画了一张技术架构图(虽然是文字版的,但核心逻辑是一样的):
graph TD
A[用户端(PC/Web端/WAP端)] -->|HTTP请求| B[表现层(Smarty3模板引擎)]
B -->|调用控制层方法| C[控制层(方维团购核心控制器)]
C -->|权限验证/状态判断/通用处理| C
C -->|调用Service层方法| D[业务逻辑层(Service层)]
D -->|复杂业务逻辑封装| D
D -->|调用Model层方法| E[数据访问层(Think\Model)]
D -->|调用缓存层方法| F[缓存层(文件/Redis/Memcached)]
D -->|调用支付接口层方法| G[支付接口层(策略模式)]
E -->|增删改查操作| H[数据库(MySQL/PostgreSQL/SQLite)]
G -->|API调用| I[第三方支付平台(支付宝/微信支付/银联)]
2 核心业务逻辑拆解:本地化生活服务的“通用需求”方维团购已经全部覆盖
很多人可能会问:“千团大战过去这么多年了,本地化生活服务的需求肯定变了,方维团购的老业务逻辑还有用吗?”——答案是:有用!而且非常有用!因为本地化生活服务的“通用核心需求”从来没有变过,变的只是“前端展示方式”“用户触达渠道”和“部分细分场景的业务逻辑”。
方维团购V4.0.6开源版覆盖的本地化生活服务通用核心需求包括:
多用户体系需求:支持普通用户、商家用户、团长用户(虽然当时团长用户没有单独成体系,但方维团购已经在商家用户体系中预留了“分佣角色”的权限)、平台管理员用户四种用户体系,每种用户体系都有独立的登录入口、权限管理、操作界面;
多商品/服务类型需求:支持实物团购、服务团购、酒店团购、电影票团购四种商品/服务类型,每种类型都有独立的上架流程、库存管理、订单处理流程;
多门店/多网点需求:支持单个商品/服务绑定多个门店/多个网点,支持门店/网点独立管理库存、独立处理订单、独立查看销售数据和佣金数据;
多营销活动需求:支持秒杀活动、满减活动、优惠券活动、邀请好友返利活动四种常见的营销活动,每种活动都有独立的配置界面、规则设置、数据统计;
批量操作需求:支持批量上架商品/服务、批量修改商品/服务信息、批量处理订单、批量退款、批量导出数据五种批量操作——这在千团大战时期是非常重要的,因为当时很多团购网站每天要处理成千上万的订单和退款申请;
数据统计需求:支持平台整体数据统计(GMV、订单量、用户量、转化率)、商家数据统计(销售额、订单量、退款率、佣金收入)、商品/服务数据统计(销售额、订单量、退款率、库存预警)三种数据统计,统计结果以图表的形式展示(虽然当时使用的是Flash图表,但逻辑是一样的)。
为了让大家更直观地了解方维团购V4.0.6开源版的批量退款业务逻辑,我给大家看一段核心的Service层代码(非商业授权版,仅供学习使用,代码中的类名、方法名、变量名都已经做了注释):
// 这是方维团购V4.0.6开源版中,批量退款业务逻辑封装的Service层文件
// 文件路径:/Application/Service/RefundService.class.php
namespace Application\Service;
use Think\Model;
use Think\Log;
class RefundService {
/**
* 批量退款的核心方法
* @param array $order_ids 要退款的订单ID数组
* @param int $admin_id 操作退款的平台管理员ID
* @param string $refund_reason 退款原因
* @return array 退款结果数组,包含成功的订单ID、失败的订单ID和失败原因
*/
public function batchRefund($order_ids, $admin_id, $refund_reason) {
// 初始化退款结果数组
$result = array(
'success' => array(),
'fail' => array()
);
// 开启MySQL事务(这是方维团购能保证数据一致性的核心原因之一)
$model = new Model();
$model->startTrans();
try {
// 遍历要退款的订单ID数组
foreach ($order_ids as $order_id) {
// 调用单个退款的核心方法
$single_result = $this->singleRefund($order_id, $admin_id, $refund_reason);
// 判断单个退款是否成功
if ($single_result['status'] == 1) {
$result['success'][] = $order_id;
} else {
$result['fail'][] = array(
'order_id' => $order_id,
'reason' => $single_result['reason']
);
}
}
// 如果所有订单都退款成功,提交事务
$model->commit();
Log::record('批量退款成功,成功订单数:' . count($result['success']) . ',失败订单数:' . count($result['fail']), 'INFO');
} catch (\Exception $e) {
// 如果有任何一个订单退款失败,回滚事务
$model->rollback();
Log::record('批量退款失败,异常信息:' . $e->getMessage(), 'ERROR');
$result['fail'] = array(
'order_id' => 'all',
'reason' => '系统异常,异常信息:' . $e->getMessage()
);
}
// 返回退款结果数组
return $result;
}
/**
* 单个退款的核心方法
* @param int $order_id 要退款的订单ID
* @param int $admin_id 操作退款的平台管理员ID
* @param string $refund_reason 退款原因
* @return array 单个退款结果数组,包含状态和失败原因
*/
private function singleRefund($order_id, $admin_id, $refund_reason) {
// 初始化单个退款结果数组
$result = array(
'status' => 0,
'reason' => ''
);
// 实例化订单模型
$order_model = M('Order');
// 查询订单信息
$order_info = $order_model->where(array('id' => $order_id))->find();
// 判断订单是否存在
if (empty($order_info)) {
$result['reason'] = '订单不存在';
return $result;
}
// 判断订单状态是否支持退款(只有已支付未使用、已支付未发货的订单才支持退款)
$allow_refund_status = array(2, 3); // 2=已支付未使用,3=已支付未发货
if (!in_array($order_info['status'], $allow_refund_status)) {
$result['reason'] = '订单状态不支持退款';
return $result;
}
// 更新订单状态为退款中
$update_order_data = array(
'status' => 6, // 6=退款中
'refund_reason' => $refund_reason,
'refund_admin_id' => $admin_id,
'refund_time' => time()
);
$order_model->where(array('id' => $order_id))->save($update_order_data);
// 实例化商品库存模型
$goods_stock_model = M('GoodsStock');
// 如果是实物团购,恢复商品库存
if ($order_info['goods_type'] == 1) { // 1=实物团购
$goods_stock_model->where(array(
'goods_id' => $order_info['goods_id'],
'store_id' => $order_info['store_id']
))->setInc('stock', $order_info['goods_num']);
}
// 如果有团长,扣减团长佣金(虽然当时团长佣金是在用户确认收货后才结算的,但方维团购已经提前做了逻辑预留)
if (!empty($order_info['leader_id'])) {
$leader_commission_model = M('LeaderCommission');
$leader_commission_model->where(array('order_id' => $order_id))->delete();
}
// 调用支付接口层的退款方法(这里只演示逻辑,不演示具体的API调用)
$payment_service = new PaymentService();
$payment_refund_result = $payment_service->refund($order_info['pay_type'], $order_info['out_trade_no'], $order_info['pay_amount']);
// 判断支付接口退款是否成功
if ($payment_refund_result['status'] != 1) {
throw new \Exception('订单ID:' . $order_id . ',支付接口退款失败,失败原因:' . $payment_refund_result['reason']);
}
// 更新订单状态为已退款
$update_order_data = array(
'status' => 7 // 7=已退款
);
$order_model->where(array('id' => $order_id))->save($update_order_data);
// 单个退款成功
$result['status'] = 1;
return $result;
}
}
?>
从上面这段代码可以看出,方维团购V4.0.6开源版的批量退款业务逻辑设计得非常严谨:
开启了MySQL事务:如果有任何一个订单退款失败,就会回滚所有操作,保证数据的一致性;
判断了订单的所有必要条件:包括订单是否存在、订单状态是否支持退款;
预留了团长佣金的逻辑:虽然当时团长用户没有单独成体系,但方维团购已经提前考虑到了;
记录了详细的日志:方便后续排查问题;
使用了try-catch异常处理机制:保证系统的稳定性。
二次开发实例:用方维团购V4.0.6开源版搭建一个“适配微信小程序私域+多门店团长的生鲜日团”平台
1 二次开发的需求分析
今天我要做的二次开发实例,是一个非常典型的私域本地生活场景——“生鲜日团”,具体的需求如下:
前端展示方式:适配微信小程序(这是私域本地生活最常用的前端展示方式);
用户触达渠道:通过微信视频号直播种草、微信公众号推文推广、企业微信群聊触达、邀请好友返利裂变;
核心业务逻辑:
每天早上8点到晚上8点为“开团时间”,晚上8点到第二天早上8点为“截团备货时间”;
支持单个商品绑定多个门店/多个社区团长;
团长可以通过企业微信群聊邀请用户下单,用户下单后团长可以获得一定比例的佣金;
截团后,平台统一备货,第二天早上8点到10点,团长可以到就近的门店自提商品,然后分发给用户;
如果商品有质量问题,用户可以在收到商品后的24小时内申请退款,团长审核通过后,平台统一退款;
其他需求:支持优惠券活动、邀请好友返利活动、商品收藏、商品评价。
2 二次开发的步骤
2.1 步骤一:前端适配——将PC/Web端/W