什么是PHP面向对象的接口
在PHP中,面向对象的接口是用来定义类的行为规范的一种机制,接口可以定义一个类应该实现哪些方法,但不需要具体实现这些方法。
使用场景(以单据导出功能为例)
UML类图
实现原理
在日常业务功能开发过程中,单据导出是一个很常用的功能,为了能够顺利的导出数据,主要考虑数据量太大导致内存溢出和响应超时的问题, 因此一般采用异步的方式,虽然每个单据导出的字段和内容不一样,但是可以抽象出共有的地方,异步导出功能共有的地方一般包括开始导出任务、执行导出任务、任务成功回调以及任务失败回调这些方法,这些共有的方法可以用接口来定义,每个单据类可以实现这个接口来补充具体的导出逻辑,然后异步任务根据这个接口依次执行这些方法,如上图所示,定义ExportTaskInterface
接口,接口包含startExportTask
、doExportTask
、exportTaskFailedHandle
、taskFinishHandle
方法,定义Order
类并实现ExportTaskInterface
接口,定义ExportExcelJob
类对Order
实例导出数据相关方法进行调用。
实现步骤
定义ExportTaskInterface
接口
<?php
namespace app\contract\excel;
use think\queue\Job;
interface ExportTaskInterface
{
/**
* 开始导出任务
* @param array $params 创建任务的参数
* @return mixed
*/
public function startExportTask(array $params) : array;
/**
* 导出Excel
* @param array $params
* @return mixed
*/
public function doExportTask(array $params) : array;
/**
* 任务失败时的处理方法
* @param \Exception $e
* @param array $params
* @param Job $job
* @return mixed
*/
public function exportTaskFailedHandle(\Exception $e, array $params, Job $job) : array;
/**
* 任务完成时的处理方法
* @param array $exportResult
* @param Job $job
* @return mixed
*/
public function taskFinishHandle(array $exportResult, Job $job) : array;
}
定义Order
类,并实现ExportTaskInterface
接口
<?php
namespace app\admin\service;
use app\admin\model\Region;
use app\common\MyLog;
use think\Db;
use think\facade\Env;
use think\facade\Config;
use app\admin\model\GoodsBrand as GoodsBrandModel;
use app\admin\model\Order;
use app\admin\model\OrderDelivery as OrderDeliveryModel;
use app\admin\model\OrderGoods;
use app\admin\model\OrderPackGoods;
use app\admin\model\Products;
use app\contract\excel\ExportTaskInterface;
use app\facade\model\OrderChange;
use app\library\enum\AsyncTaskEnum;
use app\library\enum\ErpEnum;
use app\models\AsyncTask;
use app\models\OrderChangeGoods;
use app\models\OrderChangePackGoods;
use app\service\common\XlsWriterService;
use app\service\QueueService;
use app\admin\logic\Erp as ErpLogic;
use app\library\exception\{ServiceLogicException};
use app\admin\model\OrderPackGoods as OrderPackGoodsModel;
use app\facade\model\OrderGoods as OrderGoodsModelFacade;
use app\facade\model\Order as OrderModelFacade;
use app\facade\model\Goods as GoodsModelFacade;
use app\facade\model\OrderChange as OrderChangeModelFacade;
use app\facade\model\UserAccountLog as UserAccountLogModelFacade;
use app\facade\model\OrderDelivery as OrderDeliveryModelFacade;
use app\facade\model\UserBill as UserBillModelFacade;
use app\facade\model\DiyPack as DiyPackModel;
use app\facade\model\DiySolution as DiySolutionModel;
use app\admin\model\Order as AdminOrderModel;
use app\api\model\Order as ApiOrderModel;
use app\facade\service\client\ActivityService;
use app\plogic\OrderLogic;
use excel\Excel;
use think\queue\Job;
use app\common\BaseResult;
use app\common\basic\Base;
use app\library\enum\ErrorCodeEnum;
class Order implements ExportTaskInterface
{
/**
* 开始导出任务
* @param array $params
* @return array
*/
public function startExportTask(array $params): array
{
$name = '订货单数据_' . date("YmdHis");
$data = [
'user_id' => $params['admin_id'],
'name' => $name,
'task_type' => AsyncTaskEnum::TASK_TYPE_EXCEL_EXPORT,
'identification' => AsyncTaskEnum::IDENTIFICATION_EXCEL_EXPORT,
'task_params' => json_encode($params),
'out_file_name' => $name . '.xlsx',
];
$taskId = AsyncTask::createTask($data);
$params['task_id'] = $taskId;
$params['handler'] = self::class;
QueueService::push('excel_export', $params);
return ['msg' => '已创建导出任务,请在下载中心查看', 'task_id' => $taskId];
}
/**
* 进行导出任务
* @param array $params
* @return array
* @throws \Pxlswrite\DataFormatException
*/
public function doExportTask(array $params): array
{
$orderModel=new Order();
$data= $orderModel->get_order($params['where']);
if (empty($data)){
throw new ServiceLogicException('没有可导出的数据');
}
$now = time();
$filename = "订货单数据{$params['admin_id']}_{$now}.xlsx";
$field = [
'order_sn' => ['name' =>'订单编号'],
'erp_sn' => ['name' =>'ERP订单编号'],
'user_name' => ['name' =>'客户名称'],
'brand_name' => ['name' =>'品牌'],
'order_status' => ['name' =>'订单状态'],
'pay_status' => ['name' =>'付款状态'],
'pay_name' => ['name' =>'支付方式'],
'is_special_approval' => ['name' =>'是否特殊审批单'],
'prepare' => ['name' =>'是否备货单'],
'goods_amount' => ['name' =>'商品总金额'],
'order_amount' => ['name' =>'订单金额'],
'money_paid' => ['name' =>'已付款金额'],
'cash_money' => ['name' =>'抵扣券抵扣金额'],
'add_time' => ['name' =>'下单时间'],
'pay_time' => ['name' =>'付款时间'],
'address' => ['name' =>'详细地址'],
'tel' => ['name' =>'电话号码'],
'mobile' => ['name' =>'手机号码'],
'consignee' => ['name' =>'收货人姓名'],
'close_reason' => ['name' =>'订单关闭原因'],
'remark' => ['name' =>'订单备注'],
'action_user' => ['name' =>'最后操作人'],
'email' => ['name' =>'邮箱'],
'service_name' => ['name' =>'销售内勤'],
'pay_type' => ['name' =>'结款方式'],
];
$fileObj = XlsWriterService::getInstance()
->constMemory($filename, null, false)
->field($field);
$orderStatusFields = ['0'=>'未确认','1'=>'已确认','2'=>'已取消','3'=>'无效','4'=>'退货','5'=>'已分单','6'=>'部分分单','7'=>'已关闭'];
$payStatusFields = ['0'=>'未付款','1'=>'付款中','2'=>'已付款','3'=>'部分付款'];
$page = 1;
while (true){
$fields = ['order_id','is_valet','is_from_quotation','order_total_amount','od.user_id','order_sn','pay_status','money_paid','cash_money','surplus_money','company_name','od.add_time','od.pay_time','order_amount','goods_amount','order_status','shipping_status','action_user','od.remark','od.pay_type','od.consignee','od.tel','od.mobile','pm.pay_name','ui.user_rank','brand_id','order_type','order_from','od.erp_sn','is_special_approval','prepare','od.address','od.close_reason','od.email','ui.service_name','od.integral','od.extension_code','od.is_unite_promotion', 'od.province', 'od.city', 'od.district'];
$data= $orderModel->get_order($params['where'],$fields,[],'list',true,$page);
$data=$data->toArray();
if (empty($data['data'])){
break;
}
$orderListData = $data['data'];
$order_ids = array_column($orderListData, 'order_id');
$brand_ids = array_column($orderListData, 'brand_id');
$order_goods = OrderService::getInstance()->getOrderGoodsList($order_ids);
// 订单商品品牌
$brands = GoodsBrandModel::where('brand_id', 'in', array_unique($brand_ids))->column('brand_id,brand_name');
// 发货单金额
$orderDeliveryAmount = OrderDeliveryModel::where([['order_id','in',$order_ids],['status','in',[0,2,3,4,5]]])->group('order_id')->column('order_id,sum(order_amount)');
$order_change = OrderChange::where(['order_id'=>$order_ids,'status'=>1])->group('order_id')->column('change_id','order_id');
$items = [];
foreach ($orderListData as &$value)
{
$value['pay_name'] = $value['surplus_money'] > 0 ? '余额支付' : $value['pay_name'];
// 关闭可退回订单金额
$can_return_amount = $value['money_paid'] == 0 && $value['surplus_money'] > 0 ? $value['surplus_money'] : $value['money_paid'];
$delivery_amount = $orderDeliveryAmount[$value['order_id']] ?? 0;
if ($delivery_amount > 0) {
$can_return_amount = $can_return_amount - $delivery_amount;
}
if ($value['pay_type'] == 1) {
$can_return_amount = 0;
}
$value['can_return_amount'] = number_format($can_return_amount, 2);
$value['brand'] = isset($brands[$value['brand_id']]) ? ['brand_id'=>$value['brand_id'],'brand_name'=>$brands[$value['brand_id']]] : [];
if(isset($order_goods[$value['order_id']]))
{
$value['children'] = $order_goods[$value['order_id']];
foreach ($value['children'] as &$vc)
{
if ($vc['goods_type'] == 1) {
$vc['product_sn'] = $vc['goods_sn'];
}
}
unset($vc);
}
$value['add_time'] =date('Y-m-d H:i',$value['add_time']);
$value['pay_time'] = $value['pay_time'] ? date('Y-m-d H:i',$value['pay_time']) : '';
//是否有变更中的订单
$value['is_changing'] = isset($order_change[$value['order_id']]) ? 1 : 0;
$value['operation'] = false;
$region = Region::where('region_id', 'in', [$value['province'], $value['city'], $value['district']])
->column('region_name', 'region_id') ?: [];
$province = $region[$value['province']] ?? '';
$city = $region[$value['city']] ?? '';
$district = $region[$value['district']] ?? '';
$value['address'] = $province . $city . $district . $value['address'];
}
unset($value);
$exportData = [];
foreach ($orderListData as $key => $value)
{
$exportData[$key]['order_sn'] = $value['order_sn'];
$exportData[$key]['erp_sn'] = $value['erp_sn'];
$exportData[$key]['user_name'] = $value['company_name'];
$exportData[$key]['brand_name'] = $value['brand']['brand_name'] ?? '';
$exportData[$key]['order_status'] = $orderStatusFields[$value['order_status']];
$exportData[$key]['pay_status'] = $payStatusFields[$value['pay_status']];
$exportData[$key]['pay_name'] = $value['pay_name'];
$exportData[$key]['is_special_approval'] = $value['is_special_approval'] ? '是' : '否';
$exportData[$key]['prepare'] = $value['prepare'] ? '是' : '否';
$exportData[$key]['goods_amount'] = $value['goods_amount'];
$exportData[$key]['order_amount'] = $value['goods_amount'];
$exportData[$key]['money_paid'] = $value['money_paid'];
$exportData[$key]['cash_money'] = $value['cash_money'];
$exportData[$key]['add_time'] = $value['add_time'];
$exportData[$key]['pay_time'] = $value['pay_time'];
$exportData[$key]['address'] = $value['address'];
$exportData[$key]['tel'] = $value['tel'];
$exportData[$key]['mobile'] = $value['mobile'];
$exportData[$key]['consignee'] = $value['consignee'];
$exportData[$key]['close_reason'] = $value['close_reason'];
$exportData[$key]['remark'] = $value['remark'];
$exportData[$key]['action_user'] = $value['action_user'];
$exportData[$key]['email'] = $value['email'];
$exportData[$key]['service_name'] = $value['service_name'];
$exportData[$key]['pay_type'] = $value['pay_type'] ? '月结' : '现结';
}
foreach ($exportData as $val) {
$item = [];
foreach ($field as $key => $value) {
$item[$key] = $val[$key] ?? '';
}
$items[] = array_values($item);
}
unset($exportData);
!empty($items) && $fileObj->data($items);
$page++;
}
$filePath = $fileObj->output();
return ['file_path' => $filePath, 'task_id' => $params['task_id']];
}
public function exportTaskFailedHandle(\Exception $e, array $params, Job $job): array
{
AsyncTask::where('id', $params['task_id'])->update([
'status' => AsyncTaskEnum::TASK_STATUS_FAIL,
'exception_msg' => $e->getMessage()
]);
$job->delete();
return [];
}
public function taskFinishHandle(array $exportResult, Job $job): array
{
AsyncTask::where('id', $exportResult['task_id'])->update([
'status' => AsyncTaskEnum::TASK_STATUS_FINISH,
'download_url' => $exportResult['file_path']
]);
$job->delete();
return [];
}
}
定义ExportExcelJob
类
<?php
namespace app\job\jobs;
use app\common\MyLog;
use app\contract\excel\ExportTaskInterface;
use app\library\exception\ServiceLogicException;
use think\queue\Job;
class ExportExcelJob
{
public function fire(Job $job, $data)
{
if (!isset($data['handler'])){
$job->delete();
throw new ServiceLogicException('缺少处理类');
}
try {
$this->handleJob(new $data['handler'], $data, $job);
}catch (\Exception $e){
var_dump($e->getFile() . $e->getLine() . $e->getMessage());
MyLog::errorLog('error', $e->getFile() . $e->getLine() . $e->getMessage(), 'export_excel_task');
$job->delete();
}
}
protected function handleJob(ExportTaskInterface $exportTask, $data, Job $job)
{
MyLog::infoLog('export_excel_task', '接收到Excel导出任务:' . json_encode($data), 'export_excel_task');
try {
$result = $exportTask->doExportTask($data);
$exportTask->taskFinishHandle($result, $job);
MyLog::infoLog('export_excel_task_res', '执行成功:' . json_encode($result), 'export_excel_task');
}catch (\Exception $e){
MyLog::infoLog('export_excel_task_err', $e->getFile() . $e->getLine() . $e->getMessage(), 'export_excel_task');
$exportTask->exportTaskFailedHandle($e, $data, $job);
}
}
public function fail(Job $job, $data)
{
$job->delete();
}
}
调用顺序
用户点击导出按钮调用Order
实例的startExportTask
方法,startExportTask
方法把当前类以及相关参数传递到ExportExcelJob
实例,通过ExportExcelJob
实例调用handleJob
方法进行导出操作,具体实现逻辑请参考这篇文章 使用thinkphp、think-queue、redis、supervisor搭建异步任务中心 。