فهرست منبع

提交自动派单相关的功能

dongxiaoqin 1 سال پیش
والد
کامیت
06ad747c5f

+ 108 - 0
app/adminapi/controller/goods_time/GoodsTimeController.php

@@ -0,0 +1,108 @@
+<?php
+// +----------------------------------------------------------------------
+// | likeadmin快速开发前后端分离管理后台(PHP版)
+// +----------------------------------------------------------------------
+// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
+// | 开源版本可自由商用,可去除界面版权logo
+// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
+// | github下载:https://github.com/likeshop-github/likeadmin
+// | 访问官网:https://www.likeadmin.cn
+// | likeadmin团队 版权所有 拥有最终解释权
+// +----------------------------------------------------------------------
+// | author: likeadminTeam
+// +----------------------------------------------------------------------
+
+
+namespace app\adminapi\controller\goods_time;
+
+
+use app\adminapi\controller\BaseAdminController;
+use app\adminapi\lists\goods_time\GoodsTimeLists;
+use app\adminapi\logic\goods_time\GoodsTimeLogic;
+use app\adminapi\validate\goods_time\GoodsTimeValidate;
+
+
+/**
+ * GoodsTime控制器
+ * Class GoodsTimeController
+ * @package app\adminapi\controller\goods_time
+ */
+class GoodsTimeController extends BaseAdminController
+{
+
+
+    /**
+     * @notes 获取列表
+     * @return \think\response\Json
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function lists()
+    {
+        return $this->dataLists(new GoodsTimeLists());
+    }
+
+
+    /**
+     * @notes 添加
+     * @return \think\response\Json
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function add()
+    {
+        $params = (new GoodsTimeValidate())->post()->goCheck('add');
+        $result = GoodsTimeLogic::add($params);
+        if (true === $result) {
+            return $this->success('添加成功', [], 1, 1);
+        }
+        return $this->fail(GoodsTimeLogic::getError());
+    }
+
+
+    /**
+     * @notes 编辑
+     * @return \think\response\Json
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function edit()
+    {
+        $params = (new GoodsTimeValidate())->post()->goCheck('edit');
+        $result = GoodsTimeLogic::edit($params);
+        if (true === $result) {
+            return $this->success('编辑成功', [], 1, 1);
+        }
+        return $this->fail(GoodsTimeLogic::getError());
+    }
+
+
+    /**
+     * @notes 删除
+     * @return \think\response\Json
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function delete()
+    {
+        $params = (new GoodsTimeValidate())->post()->goCheck('delete');
+        GoodsTimeLogic::delete($params);
+        return $this->success('删除成功', [], 1, 1);
+    }
+
+
+    /**
+     * @notes 获取详情
+     * @return \think\response\Json
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function detail()
+    {
+        $params = (new GoodsTimeValidate())->goCheck('detail');
+        $result = GoodsTimeLogic::detail($params);
+        return $this->data($result);
+    }
+
+
+}

+ 106 - 0
app/adminapi/lists/goods_time/GoodsTimeLists.php

@@ -0,0 +1,106 @@
+<?php
+// +----------------------------------------------------------------------
+// | likeadmin快速开发前后端分离管理后台(PHP版)
+// +----------------------------------------------------------------------
+// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
+// | 开源版本可自由商用,可去除界面版权logo
+// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
+// | github下载:https://github.com/likeshop-github/likeadmin
+// | 访问官网:https://www.likeadmin.cn
+// | likeadmin团队 版权所有 拥有最终解释权
+// +----------------------------------------------------------------------
+// | author: likeadminTeam
+// +----------------------------------------------------------------------
+
+namespace app\adminapi\lists\goods_time;
+
+
+use app\adminapi\lists\BaseAdminDataLists;
+use app\common\model\goods_time\GoodsTime;
+use app\common\lists\ListsSearchInterface;
+
+
+/**
+ * GoodsTime列表
+ * Class GoodsTimeLists
+ * @package app\adminapi\listsgoods_time
+ */
+class GoodsTimeLists extends BaseAdminDataLists implements ListsSearchInterface
+{
+
+
+    /**
+     * @notes 设置搜索条件
+     * @return \string[][]
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function setSearch(): array
+    {
+        return [
+            '=' => ['service_time'],
+        ];
+    }
+    
+    public function queryWhereRaw(){
+        $where = '1=1';
+
+        if(isset($this->params['goods_category_ids']) && !empty($this->params['goods_category_ids'])){
+            $sqls = [];
+            $goods_category_ids =[];
+            foreach ($this->params['goods_category_ids'] as $val){
+                ($val = json_decode($val,true))?($goods_category_ids[] = end($val)):($goods_category_ids[] = $val);
+            }
+            foreach ($goods_category_ids as $item) {
+                $sqls[] = "FIND_IN_SET({$item}, goods_category_ids) > 0";
+            }
+            $where = implode(' OR ', $sqls);
+        }
+        return $where;
+    }
+
+    public function queryWhere(){
+        $where = [];
+        if(isset($this->params['title']) && !empty($this->params['title'])){
+            $where[] = ['title', 'like','%' .$this->params['title'] . '%'];
+        }
+        return $where;
+    }
+
+    /**
+     * @notes 获取列表
+     * @return array
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function lists(): array
+    {
+        $list = GoodsTime::where($this->searchWhere)
+            ->where($this->queryWhere())
+            ->whereRaw($this->queryWhereRaw())
+            ->field(['id', 'title','service_time', 'goods_category_ids'])
+            ->limit($this->limitOffset, $this->limitLength)
+            ->order(['id' => 'desc'])
+            ->select()
+            ->toArray();
+        foreach($list as &$item) {
+            $item['goods_category_ids'] = explode(',', $item['goods_category_ids']);
+        }
+        return $list;
+    }
+
+    /**
+     * @notes 获取数量
+     * @return int
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function count(): int
+    {
+        return GoodsTime::where($this->searchWhere)->count();
+    }
+
+}

+ 1 - 1
app/adminapi/lists/works/ServiceWorkLists.php

@@ -177,7 +177,7 @@ class ServiceWorkLists extends BaseAdminDataLists implements ListsSearchInterfac
             ->where($this->searchWhere)
             ->where($this->queryWhere())
             ->where($this->queryDataWhere())
-            ->field(['id', 'work_sn', 'real_name', 'mobile', 'address', 'title', 'category_type', 'goods_category_ids', 'goods_category_id', 'base_service_fee', 'service_fee', 'work_status','work_pay_status', 'service_status', 'dispatch_time', 'receive_time', 'appointment_time', 'finished_images', 'finished_time', 'master_worker_id', 'work_amount', 'work_type', 'create_time', 'update_time','lon', 'lat','appoint_approval','refund_approval','finally_door_time','property_activity_id','order_effective_id'])
+            ->field(['id', 'work_sn', 'real_name', 'mobile', 'address', 'title', 'category_type', 'goods_category_ids', 'goods_category_id', 'base_service_fee', 'service_fee', 'work_status','work_pay_status', 'service_status', 'dispatch_time', 'receive_time', 'appointment_time', 'finished_images', 'finished_time', 'master_worker_id', 'work_amount', 'work_type', 'create_time', 'update_time','lon', 'lat','appoint_approval','refund_approval','finally_door_time','property_activity_id','order_effective_id,estimated_finish_time'])
             ->limit($this->limitOffset, $this->limitLength)
             ->order(['id' => 'desc'])
             ->select()

+ 127 - 0
app/adminapi/logic/goods_time/GoodsTimeLogic.php

@@ -0,0 +1,127 @@
+<?php
+// +----------------------------------------------------------------------
+// | likeadmin快速开发前后端分离管理后台(PHP版)
+// +----------------------------------------------------------------------
+// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
+// | 开源版本可自由商用,可去除界面版权logo
+// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
+// | github下载:https://github.com/likeshop-github/likeadmin
+// | 访问官网:https://www.likeadmin.cn
+// | likeadmin团队 版权所有 拥有最终解释权
+// +----------------------------------------------------------------------
+// | author: likeadminTeam
+// +----------------------------------------------------------------------
+
+namespace app\adminapi\logic\goods_time;
+
+
+use app\common\model\goods_time\GoodsTime;
+use app\common\logic\BaseLogic;
+use think\facade\Db;
+
+
+/**
+ * GoodsTime逻辑
+ * Class GoodsTimeLogic
+ * @package app\adminapi\logic\goods_time
+ */
+class GoodsTimeLogic extends BaseLogic
+{
+
+
+    /**
+     * @notes 添加
+     * @param array $params
+     * @return bool
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public static function add(array $params): bool
+    {
+        if($params['goods_category_ids']){
+            foreach ($params['goods_category_ids'] as $val){
+                is_array($val)?($ids[] = end($val)):($ids[] = $val);
+            }
+            $params['goods_category_ids'] = implode(',',$ids);
+        }
+        Db::startTrans();
+        try {
+            GoodsTime::create([
+                'title' => $params['title'],
+                'service_time' => $params['service_time'],
+                'goods_category_ids' => $params['goods_category_ids'],
+            ]);
+
+            Db::commit();
+            return true;
+        } catch (\Exception $e) {
+            Db::rollback();
+            self::setError($e->getMessage());
+            return false;
+        }
+    }
+
+
+    /**
+     * @notes 编辑
+     * @param array $params
+     * @return bool
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public static function edit(array $params): bool
+    {
+        if($params['goods_category_ids']){
+            foreach ($params['goods_category_ids'] as $val){
+                is_array($val)?($ids[] = end($val)):($ids[] = $val);
+            }
+            $params['goods_category_ids'] = implode(',',$ids);
+        }
+        Db::startTrans();
+        try {
+            GoodsTime::where('id', $params['id'])->update([
+                'title' => $params['title'],
+                'service_time' => $params['service_time'],
+                'goods_category_ids' => $params['goods_category_ids'],
+            ]);
+
+            Db::commit();
+            return true;
+        } catch (\Exception $e) {
+            Db::rollback();
+            self::setError($e->getMessage());
+            return false;
+        }
+    }
+
+
+    /**
+     * @notes 删除
+     * @param array $params
+     * @return bool
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public static function delete(array $params): bool
+    {
+        return GoodsTime::destroy($params['id']);
+    }
+
+
+    /**
+     * @notes 获取详情
+     * @param $params
+     * @return array
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public static function detail($params): array
+    {
+        $info = GoodsTime::findOrEmpty($params['id'])->toArray();
+        if(!empty($info['goods_category_ids'])){
+            $info['goods_category_ids'] = explode(',',$info['goods_category_ids']);
+            $info['goods_category_ids'] = array_map('intval', $info['goods_category_ids']);
+        }
+        return $info;
+    }
+}

+ 18 - 1
app/adminapi/logic/master_worker/MasterWorkerLogic.php

@@ -17,6 +17,7 @@ namespace app\adminapi\logic\master_worker;
 
 use app\common\model\master_worker\MasterWorker;
 use app\common\logic\BaseLogic;
+use app\common\model\master_worker\MasterWorkerScore;
 use app\common\model\master_worker\MasterWorkerRetentionMoneyLog;
 use think\db\Query;
 use think\facade\Config;
@@ -43,7 +44,7 @@ class MasterWorkerLogic extends BaseLogic
     {
         Db::startTrans();
         try {
-            MasterWorker::create([
+            $masterWorker = MasterWorker::create([
                 'sn' => $params['sn'],
                 'avatar' => $params['avatar'],
                 'real_avatar' => $params['real_avatar'],
@@ -76,6 +77,11 @@ class MasterWorkerLogic extends BaseLogic
                 'labels' => (isset($params['labels']) && $params['labels'])?implode(',',$params['labels']):'',
                 'remark' => $params['remark']??'',
             ]);
+            
+            //添加工程师汇总评分数据
+            MasterWorkerScore::create([
+                'worker_id' => $masterWorker->id
+            ]);
 
             Db::commit();
             return true;
@@ -145,6 +151,17 @@ class MasterWorkerLogic extends BaseLogic
                 'labels' => (isset($params['labels']) && $params['labels'])?implode(',',$params['labels']):'',
                 'remark' => $params['remark']??'',
             ]);
+            if (is_numeric($params['weight_score'])) {
+                $exists = MasterWorkerScore::where('worker_id', $params['id'])->value('id');
+                if ($exists) {
+                    MasterWorkerScore::where('worker_id',$params['id'])->update(['weight_score' => $params['weight_score']]);
+                } else {
+                    MasterWorkerScore::create([
+                        'worker_id' => $params['id'],
+                       'weight_score' => $params['weight_score'],
+                    ]);
+                }
+            }
             Db::commit();
             return true;
         } catch (\Exception $e) {

+ 5 - 0
app/adminapi/logic/master_worker_register/MasterWorkerRegisterLogic.php

@@ -18,6 +18,7 @@ namespace app\adminapi\logic\master_worker_register;
 use app\common\enum\notice\NoticeEnum;
 use app\common\model\master_worker\MasterWorker;
 use app\common\model\master_worker\MasterWorkerAuth;
+use app\common\model\master_worker\MasterWorkerScore;
 use app\common\model\master_worker_register\MasterWorkerRegister;
 use app\common\logic\BaseLogic;
 use app\common\service\ConfigService;
@@ -153,6 +154,10 @@ class MasterWorkerRegisterLogic extends BaseLogic
                 'city' => $params['city'],
                 'area_name' => $params['area_name'],
             ]);
+            //添加工程师汇总评分数据
+            MasterWorkerScore::create([
+                'worker_id' => $masterWorker->id
+            ]);
         }
         return $masterWorker->id;
     }

+ 31 - 0
app/adminapi/logic/works/ServiceWorkLogic.php

@@ -40,6 +40,7 @@ use app\common\model\works\ServiceWorkLog;
 use app\common\model\works\ServiceWorkSpare;
 use app\workerapi\logic\ServiceWorkerAllocateWorkerLogic;
 use app\workerapi\logic\ServiceWorkLogLogic;
+use app\workerapi\logic\ServiceWorkReceiveLogLogic;
 use think\db\Query;
 use think\Exception;
 use think\facade\Db;
@@ -103,6 +104,8 @@ class ServiceWorkLogic extends BaseLogic
                 'opera_log'=>'编号['.$params['user_info']['worker_number'].']'.$params['user_info']['real_name'].'于'.date('y-m-d H:i:s',$receive_time).'领取了工单',
             ];
             ServiceWorkLogLogic::add($work_log);
+            //添加领单日志
+            ServiceWorkReceiveLogLogic::add($work_log);
             Db::commit();
         }
         catch (\Exception $e) {
@@ -360,6 +363,7 @@ class ServiceWorkLogic extends BaseLogic
             $work_log = [
                 'work_id'=>$work->id,
                 'master_worker_id'=>$work->master_worker_id,
+                'type' => 0,
                 'opera_log'=>'后台用户['.$userInfo['admin_id'].']'.$userInfo['name'].'于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker->worker_number.']'.$worker->real_name
             ];
             ServiceWorkerAllocateWorkerLogic::add($work_log);
@@ -852,11 +856,15 @@ class ServiceWorkLogic extends BaseLogic
             $work->master_worker_id = 0;
             $work->work_status = 0;
             $work->dispatch_time = 0;
+            $work->first_contact_time = 0;
+            $work->estimated_finish_time = 0;
+            $work->exec = 0;
             $work->save();
 
             $work_log = [
                 'work_id'=>$work->id,
                 'master_worker_id'=>$params['master_worker_id'],
+                'type' => 1,
                 'opera_log'=>'后台用户['.$userInfo['admin_id'].']'.$userInfo['name'].'于'.date('Y-m-d H:i:s',time()).'取消了工程师'.'编号['.$worker->worker_number.']'.$worker->real_name
             ];
             ServiceWorkerAllocateWorkerLogic::add($work_log);
@@ -896,5 +904,28 @@ class ServiceWorkLogic extends BaseLogic
         }
     }
 
+    /**
+     *
+     * @return false|void
+     */
+    public static function contactCustomer($params)
+    {
+        try {
+            $work = ServiceWork::where(['master_worker_id'=>$params['user_id'],'work_sn'=>$params['work_sn']])->findOrEmpty();
+            if($work->isEmpty()){
+                throw new Exception('工单不存在');
+            }
+            if (empty($work->first_contact_time)) {
+                $work->first_contact_time = time();
+                $work->save();
+            }
+            return true;
+        }
+        catch (\Exception $e) {
+            self::setError($e->getMessage());
+            return false;
+        }
+    }
+
 
 }

+ 101 - 0
app/adminapi/validate/goods_time/GoodsTimeValidate.php

@@ -0,0 +1,101 @@
+<?php
+// +----------------------------------------------------------------------
+// | likeadmin快速开发前后端分离管理后台(PHP版)
+// +----------------------------------------------------------------------
+// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
+// | 开源版本可自由商用,可去除界面版权logo
+// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
+// | github下载:https://github.com/likeshop-github/likeadmin
+// | 访问官网:https://www.likeadmin.cn
+// | likeadmin团队 版权所有 拥有最终解释权
+// +----------------------------------------------------------------------
+// | author: likeadminTeam
+// +----------------------------------------------------------------------
+
+namespace app\adminapi\validate\goods_time;
+
+
+use app\common\validate\BaseValidate;
+
+
+/**
+ * GoodsTime验证器
+ * Class GoodsTimeValidate
+ * @package app\adminapi\validate\goods_time
+ */
+class GoodsTimeValidate extends BaseValidate
+{
+
+     /**
+      * 设置校验规则
+      * @var string[]
+      */
+    protected $rule = [
+        'id' => 'require',
+        'title' => 'require',
+        'service_time' => 'require',
+        'goods_category_ids' => 'require',
+
+    ];
+
+
+    /**
+     * 参数描述
+     * @var string[]
+     */
+    protected $field = [
+        'id' => 'id',
+        'title' => '标题',
+        'service_time' => '服务时长',
+        'goods_category_ids' => '商品分类'
+    ];
+
+
+    /**
+     * @notes 添加场景
+     * @return GoodsTimeValidate
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function sceneAdd()
+    {
+        return $this->only(['title','service_time','goods_category_ids']);
+    }
+
+
+    /**
+     * @notes 编辑场景
+     * @return GoodsTimeValidate
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function sceneEdit()
+    {
+        return $this->only(['id','title','service_time','goods_category_ids']);
+    }
+
+
+    /**
+     * @notes 删除场景
+     * @return GoodsTimeValidate
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function sceneDelete()
+    {
+        return $this->only(['id']);
+    }
+
+
+    /**
+     * @notes 详情场景
+     * @return GoodsTimeValidate
+     * @author likeadmin
+     * @date 2025/02/23 11:02
+     */
+    public function sceneDetail()
+    {
+        return $this->only(['id']);
+    }
+
+}

+ 2 - 0
app/api/logic/GoodsReviewsLogic.php

@@ -37,6 +37,8 @@ class GoodsReviewsLogic extends BaseLogic
             GoodsReviews::create([
                 'goods_id'=>$goods_id,
                 'goods_category_id'=>$work['goods_category_id'],
+                'sn'=>$params['sn'],
+                'work_id'=>$order['work_id'],
                 'user_id'=>$params['user_id'],
                 'nickname'=>$params['user_info']['nickname'],
                 'avatar'=>$params['user_info']['avatar'],

+ 24 - 0
app/common.php

@@ -4,6 +4,30 @@ use app\common\model\setting\PostageRegion;
 use app\common\service\FileService;
 use think\helper\Str;
 
+
+/**
+ * 计算两点之间的距离
+ */
+function haversineDistance($lat1, $lon1, $lat2, $lon2) {
+    // 地球平均半径,单位:千米
+    $R = 6371;
+    // 将角度转换为弧度
+    $lat1 = deg2rad($lat1);
+    $lon1 = deg2rad($lon1);
+    $lat2 = deg2rad($lat2);
+    $lon2 = deg2rad($lon2);
+    // 计算纬度和经度的差值
+    $dLat = $lat2 - $lat1;
+    $dLon = $lon2 - $lon1;
+    // Haversine 公式的计算步骤
+    $a = sin($dLat / 2) * sin($dLat / 2) +
+         cos($lat1) * cos($lat2) * sin($dLon / 2) * sin($dLon / 2);
+    $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
+    // 计算距离
+    $distance = $R * $c;
+    return $distance;
+}
+
 /**
  * @notes 生成密码加密密钥
  * @param string $plaintext

+ 200 - 0
app/common/command/AutomaticDispatch.php

@@ -0,0 +1,200 @@
+<?php
+namespace app\common\command;
+
+use think\facade\Db;
+use think\facade\Log;
+use think\console\Input;
+use think\console\Output;
+use think\console\Command;
+use app\common\model\works\ServiceWork;
+use app\common\model\goods_time\GoodsTime;
+use app\common\model\master_worker\MasterWorker;
+use app\common\model\works\ServiceWorkAllocateWorkerLog;
+use app\workerapi\logic\ServiceWorkerAllocateWorkerLogic;
+use app\common\model\master_worker\MasterWorkerServiceTime;
+
+class AutomaticDispatch extends Command
+{
+    protected $defaultServiceTime = 300; //默认服务时长 300 分钟
+    protected function configure()
+    {
+        $this->setName('automatic_dispatch')
+            ->setDescription('自动派单');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $this->autoDispatch();
+    }
+
+    /*
+     * 自动派单总分100分
+        1、地理效率得分(distance score)25%
+        2、工程师权重「自定义是否有证」(weight score)20%
+        3、工程师综合服务分 55%
+    */
+    protected function autoDispatch()
+    {
+        $size = 30;
+        $distanceRate = 0.25;
+        $weightRate = 0.2;
+        $comprehensiveRate = 0.55;
+        // 获取当前时间的前五分钟时间戳
+        $fiveMinutesAgo = time() - 300; // 300 秒 = 5 分钟
+
+        while(true) {
+            $list = ServiceWork::where('work_status',0)
+                        ->where(function ($query) use ($fiveMinutesAgo) {
+                            $query->where('exec_time', 0)->whereOr('exec_time', '<', $fiveMinutesAgo);
+                        })
+                        ->where('create_time','>', strtotime("-1 days"))
+                        ->field('id,category_type,goods_category_id,lon,lat,province,city,title,appointment_time,address,mobile')
+                        ->limit($size)
+                        ->select()
+                        ->toArray();
+            if (!$list) {
+                sleep(5);
+            }
+
+            $isExec = 0;
+ 
+            foreach($list as $item) {     
+                try {
+                    // 获取符合条件的工程师
+                    $worker = MasterWorker::alias('a')->leftJoin('master_worker_score b','a.id = b.worker_id')
+                    ->where([
+                        ['is_disable','=',0],
+                        ['work_status','=',0],
+                        ['accept_order_status','=',0],
+                        ['city','=',$item['city']],
+                    ])
+                    ->whereRaw('FIND_IN_SET('.$item['goods_category_id'].', a.category_ids)')
+                    ->field('a.id,a.distance,a.lon,a.lat,a.worker_number,a.real_name,b.comprehensive_score,b.weight_score')
+                    ->orderRaw('(b.comprehensive_score + b.weight_score) desc')
+                    ->limit(100)
+                    ->select()
+                    ->toArray();
+                    $queue = [];
+                    foreach($worker as $value) {  
+                        //过滤已接过此单的师傅
+                        $exists = ServiceWorkAllocateWorkerLog::where('work_id', $item['id'])->where('master_worker_id',$value['id'])->count();
+                        if ($exists) {
+                            continue;
+                        }
+                        
+                        if ( $value['distance'] > 0) {
+                            //校验客户的地址是否在工程师的接单区域内
+                            $realDistance = haversineDistance($item['lat'],$item['lon'], $value['lat'],$value['lon'],$value['distance']);
+                            if ($realDistance > $value['distance']) {
+                                continue;
+                            }
+                        }
+
+                        //计算地理效率得分
+                        $realDistance = ceil($realDistance / 1000);
+                        $travelTime = $realDistance * 2;//预计每公里行驶2分钟
+                        
+                        $distanceScore = 100 - ($travelTime * 1.5) - ($realDistance * 5);
+                        $distanceScore = bcadd($distanceScore, 0, 2);
+                        $tmpDistanceRate = bcmul($distanceScore, $distanceRate, 2);
+                        $tmpRate = 0;
+                        $value['travelTime'] = $travelTime;
+                        $value['comprehensive_score'] = isset($value['comprehensive_score']) ? $value['comprehensive_score'] : 0;
+                        $value['weight_score'] = isset($value['weight_score']) ? $value['weight_score'] : 0;
+                        $tmpRate = bcmul($value['comprehensive_score'], $comprehensiveRate, 2) + bcmul($value['weight_score'], $weightRate, 2);
+                        $tmpRate = bcadd($tmpRate, $tmpDistanceRate,2);
+                        $queue[$tmpRate."_".$value['id']] = $value;
+                       
+                    }
+                    //按照工程师的总分值倒序排序
+                    krsort($queue);
+                    foreach($queue as $worker) {
+                        $serviceTime = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$item['goods_category_id'])->value('service_time');
+                        if (empty($serviceTime)) {
+                            $serviceTime = GoodsTime::whereRaw('FIND_IN_SET('.$item['goods_category_id'].', goods_category_ids)')->value('service_time');
+                            $serviceTime = $serviceTime ?? $this->defaultServiceTime;//默认服务时长
+                        }
+
+                        //预约开始时间和结束时间
+                        $appointment_time = is_numeric($item['appointment_time']) ? $item['appointment_time'] : strtotime($item['appointment_time']);
+                        $estimated_finish_time = $appointment_time + $serviceTime * 60 + $worker['travelTime'] * 60;
+                        //校验客户的预约时间是否在工程师的空挡期内
+                        $count = ServiceWork::where([
+                            ['master_worker_id','=',$worker['id']],
+                            ['work_status','>=',1],
+                            ['work_status','<=',5],
+                        ])
+                        ->where(function ($query) use ($appointment_time,$estimated_finish_time) {
+                            $query->where('appointment_time', 'between',[$appointment_time, $estimated_finish_time])
+                                ->whereOr('estimated_finish_time', 'between', [$appointment_time, $estimated_finish_time]);
+                        })
+                        ->count();
+                        if ($count == 0) {
+                            $res = $this->allocateWorker($item,$worker,$estimated_finish_time);
+                            if ($res === true) {
+                                $isExec = 1;
+                            }
+                            break;
+                        }
+                    } 
+
+                } catch (\Exception $e) {
+                    Log::write('自动派单异常:'.$e->getMessage());
+                    sleep(5);
+                }
+
+                if ($isExec == 0) {
+                    ServiceWork::where('id',$item['id'])->update([
+                        'exec_time' => time(),
+                    ]);
+                }
+            }
+
+            
+        }
+    }
+
+    /**
+     * 分配工程师
+     */
+    protected function allocateWorker($workDetail, $worker, $estimated_finish_time)
+    {
+        Db::startTrans();
+        try {
+            ServiceWork::where('id',$workDetail['id'])->update([
+                'master_worker_id'=>$worker['id'],
+                'work_status'=>1,
+                'estimated_finish_time' => $estimated_finish_time,
+                'dispatch_time'=>time(),
+                'exec_time' => time(),
+            ]);
+            MasterWorker::setWorktotal('inc',$worker['id']);
+            $work_log = [
+                'work_id'=>$workDetail['id'],
+                'master_worker_id'=>$worker['id'],
+                'type' => 0,
+                'opera_log'=>'系统自动派单于'.date('Y-m-d H:i:s',time()).'分配了工程师'.'编号['.$worker['worker_number'].']'.$worker['real_name']
+            ];
+            ServiceWorkerAllocateWorkerLogic::add($work_log);
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            Log::write('自动派单分配工程师异常:'.$e->getMessage());
+            return false;
+        }
+        // 工程师派单通知【给工程师的通知】【公众号通知,不发短信】
+        $res = event('Notice',  [
+            'scene_id' => 113,
+            'params' => [
+                'user_id' => $worker['id'],
+                'order_id' => $workDetail['id'],
+                'thing9' => $workDetail['title'],
+                'time7' => $workDetail['appointment_time'],
+                'thing8' => (iconv_strlen($workDetail['address'])>15)?(mb_substr($workDetail['address'],0,15,'UTF-8').'...'):$workDetail['address'],
+                'phone_number6' => asteriskString($workDetail['mobile']),
+            ]
+        ]);
+        return true;
+    }
+
+}

+ 39 - 0
app/common/command/CancelDispatch.php

@@ -0,0 +1,39 @@
+<?php
+namespace app\common\command;
+
+use think\console\Input;
+use think\console\Output;
+use think\console\Command;
+use app\common\model\works\ServiceWork;
+
+class CancelDispatch extends Command
+{
+    protected function configure()
+    {
+        $this->setName('cancel_dispatch')
+            ->setDescription('派单5分钟未领取的工单自动取消');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $this->cancelDispatch();
+
+    }
+
+    /*
+    * 超过5分钟未领取工单的,取消分配
+    */
+    protected function cancelDispatch()
+    {
+        ServiceWork::where('work_status',1)
+        ->where('dispatch_time','<',strtotime('-5 minutes'))
+        ->update([
+            'master_worker_id' => 0,
+            'work_status' => 0,
+            'dispatch_time' => 0,
+            'first_contact_time' => 0,
+            'estimated_finish_time' => 0,
+        ]);
+    }
+
+}

+ 197 - 0
app/common/command/UpdateWorkerScore.php

@@ -0,0 +1,197 @@
+<?php
+namespace app\common\command;
+
+use think\facade\Log;
+use think\console\Input;
+use think\console\Output;
+use think\console\Command;
+use think\console\input\Argument;
+use app\common\model\works\IssueWork;
+use app\common\model\works\ReturnWork;
+use app\common\model\works\ServiceWork;
+use app\common\model\goods_time\GoodsTime;
+use app\common\model\reviews\GoodsReviews;
+use app\common\model\master_worker\MasterWorker;
+use app\common\model\effective\OrderEffectiveLog;
+use app\common\model\works\ServiceWorkReceiveLog;
+use app\common\model\master_worker\MasterWorkerScore;
+use app\common\model\works\ServiceWorkAllocateWorkerLog;
+use app\common\model\master_worker\MasterWorkerServiceTime;
+
+class UpdateWorkerScore extends Command
+{
+    protected $defaultServiceTime = 300; //默认服务时长 300 分钟
+    protected function configure()
+    {
+        $this->setName('update_worker_score')
+            ->setDescription('更新工程师综合评分和服务时长')
+            ->addArgument('type', Argument::OPTIONAL, '类型可选');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        // 获取传递的参数
+        $type = $input->getArgument('type');
+        
+        if ($type == 'init') {
+            $this->initMasterWorkerScore();
+        } else {
+            $this->changeWorderScore();
+        }
+
+    }
+
+    /**
+     * 初始化工程师汇总评分数据,只执行一次即可
+     */
+    protected function initMasterWorkerScore()
+    {
+        $masterWorker = MasterWorker::field('id')->order('id','asc')->select()->toArray();
+        foreach($masterWorker as $item) {
+            //添加工程师汇总评分数据
+            MasterWorkerScore::create([
+                'worker_id' => $item['id']
+            ]);
+        }
+    }
+
+    /*
+     * 每周统计并更新一次工程师的综合评分
+    1、用户评分 
+    2、完单率 = 完结工单/总工单
+    3、接单率 = 已领的工单/总工单
+    4、工单投诉率 = 投诉的工单(判定工程师原因) /已完成工单
+    5、客户粘性率 = 工程师第一次联系客户时间-领单时间 和定义的时间(10分钟)进行比较 
+    6、保修率 = 保修工单/已完成工单(去除保修单)
+    7、返修率 = 返修工单/已完成工单
+    8、加单率 = 加单个数/派单数
+    */
+    protected function changeWorderScore()
+    {
+        $startTime = date('Y-m-d 00:00:00', strtotime('-7 days'));
+        $endTime = date('Y-m-d 23:59:59', strtotime('-1 days'));
+        $page = 0;
+        $size = 50;
+        while(true) {
+            $page++;
+            $offset = ($page - 1) * $size;
+            $list = MasterWorker::field('id,category_ids')
+                    ->limit($offset, $size)
+                    ->select()
+                    ->toArray();
+            if (!$list) {
+                break;
+            }
+            foreach($list as $item) {
+                $workId = $item['id'];
+                $this->updateComprehensiveScore($startTime,$endTime,$workId);
+
+                //更新工程师平均服务时长
+                $this->updateServiceTime($startTime,$endTime,$item);
+            }
+        }
+    }
+
+    /**
+     * 更新工程师服务类目的平均服务时长
+     */
+    protected function updateServiceTime($startTime,$endTime,$worker) {
+        if ($worker['category_ids']) {
+            $category_ids = explode(",",$worker['category_ids']);
+            foreach($category_ids as $categoryId) {
+                $avgTime = ServiceWork::where('master_worker_id',$worker['id'])->where('service_status',3)->whereIn('work_type',[0,1])->whereBetweenTime('create_time', $startTime, $endTime)->field('AVG(finished_time - appointment_time) as avg_time')->find();
+                if (isset($avgTime['avg_time']) && $avgTime['avg_time'] > 0) {
+                    $avgTime = $avgTime['avg_time'] / 60 ;
+                } else {
+                    $avgTime = GoodsTime::whereRaw('FIND_IN_SET('.$categoryId.', goods_category_ids)')->value('service_time');
+                    $avgTime = $avgTime ?? $this->defaultServiceTime;//默认服务时长
+                }
+                $exists = MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$categoryId)->value('id');
+                if ($exists) {
+                    MasterWorkerServiceTime::where('master_worker_id',$worker['id'])->where('goods_category_id',$categoryId)->update([
+                        'service_time' => $avgTime
+                    ]);
+                } else {
+                    MasterWorkerServiceTime::create([
+                        'master_worker_id' => $worker['id'],
+                        'goods_category_id' => $categoryId,
+                        'service_time' => $avgTime
+                    ]);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * 更新工程师综合评分
+     */
+    protected function updateComprehensiveScore($startTime,$endTime,$workId) {
+        try {
+            //查询本周平均评分值
+            $goodsReviewsAvg = GoodsReviews::alias('a')->leftJoin("service_work b","a.work_id = b.id")->whereBetweenTime('a.create_time', $startTime, $endTime)->avg('rating');
+            $commentScore = $goodsReviewsAvg > 0 ? bcdiv($goodsReviewsAvg, 5, 2) : 0;
+            
+            //总工单:统计派单日志记录
+            $allOrder = ServiceWorkAllocateWorkerLog::where('master_worker_id',$workId)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+            //完结工单:统计工单表已完成的工单
+            $completeOrder = ServiceWork::where('master_worker_id',$workId)->where('service_status',3)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+            
+            if ($allOrder == 0) {
+                $completionRate = 0;
+                $acceptRate = 0;
+            } else {
+                //完单率
+                $completionRate = bcdiv($completeOrder, $allOrder ,2);
+                //接单率
+                $acceptOrder = ServiceWorkReceiveLog::where('master_worker_id',$workId)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+                $acceptRate = bcdiv($acceptOrder, $allOrder ,2);
+            }
+
+            if ($completeOrder == 0) {
+                $issueRate = 0;
+                $returnRate = 0;
+                $addRate = 0;
+            } else {
+                //工单投诉率
+                $issueWork = IssueWork::where('master_worker_id',$workId)->where('responsible',2)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+                $issueRate = bcdiv($issueWork, $completeOrder ,2);
+                $issueRate = 1 - ($issueRate > 1 ? 1 : $issueRate);
+                //返修率
+                $returnWord = ReturnWork::where('master_worker_id',$workId)->whereBetweenTime('create_time', date('Y-m-d 00:00:00', strtotime('-30 days')), $endTime)->count();
+                $returnRate = bcdiv($returnWord, $completeOrder ,2);
+                $returnRate =  1 - ($returnRate > 1 ? 1 : $returnRate);
+                //加单率
+                $addWord = ServiceWork::where('master_worker_id',$workId)->where('work_type',2)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+                $addRate = bcdiv($addWord, $completeOrder ,2);
+                $addRate = $addRate > 1 ? 1 : $addRate;
+            }
+         
+            //客户粘性率(10分钟内)
+            $avgTime = ServiceWork::where('master_worker_id',$workId)->where('work_status','>',1)->where('first_contact_time','>',0)->whereBetweenTime('create_time', $startTime, $endTime)->field('AVG(first_contact_time - receive_time) as avg_time')->find();
+            $avgTime = $avgTime['avg_time'] ? $avgTime['avg_time'] / 60 : 0;
+            $viscosityRate = $avgTime <= 10  && $avgTime > 0 ? 1 : 0;
+
+            $partOrder = ServiceWork::where('master_worker_id',$workId)->where('work_status','>',1)->where('order_effective_id',0)->whereBetweenTime('create_time', $startTime, $endTime)->count();
+            if ($partOrder == 0) {
+                $warrantyRate = 0;
+            } else {
+                //保修率
+                $effectiveOder = OrderEffectiveLog::alias('a')->leftJoin('service_work b', 'a.work_id = b.id')->where('b.id',$workId)->whereBetweenTime('a.create_time', date('Y-m-d 00:00:00', strtotime('-30 days')), $endTime)->count();
+                $warrantyRate = bcdiv($effectiveOder, $partOrder ,2);
+                $warrantyRate = 1 - ($warrantyRate > 1 ? 1 : $warrantyRate);
+            }
+            //工程师汇总评分 
+            $comprehensiveScore = $commentScore + $completionRate + $acceptRate + $issueRate + $viscosityRate + $warrantyRate + $returnRate + $addRate;
+            $comprehensiveScore = bcdiv($comprehensiveScore, 8, 2) * 100;
+            
+            MasterWorkerScore::where('worker_id',$workId)->update(['comprehensive_score' => $comprehensiveScore]);
+            
+        } catch (\Exception $e) {
+            
+            Log::write('更新工程师综合评分异常:'.$e->getMessage());
+            return false;
+        }
+    }
+
+}

+ 34 - 0
app/common/model/goods_time/GoodsTime.php

@@ -0,0 +1,34 @@
+<?php
+// +----------------------------------------------------------------------
+// | likeadmin快速开发前后端分离管理后台(PHP版)
+// +----------------------------------------------------------------------
+// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
+// | 开源版本可自由商用,可去除界面版权logo
+// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
+// | github下载:https://github.com/likeshop-github/likeadmin
+// | 访问官网:https://www.likeadmin.cn
+// | likeadmin团队 版权所有 拥有最终解释权
+// +----------------------------------------------------------------------
+// | author: likeadminTeam
+// +----------------------------------------------------------------------
+
+namespace app\common\model\goods_time;
+
+
+use app\common\model\BaseModel;
+
+
+
+/**
+ * GoodsTime模型
+ * Class GoodsTime
+ * @package app\common\model\goods_time
+ */
+class GoodsTime extends BaseModel
+{
+    
+    protected $name = 'goods_time';
+    
+
+    
+}

+ 9 - 0
app/common/model/master_worker/MasterWorkerScore.php

@@ -0,0 +1,9 @@
+<?php
+namespace app\common\model\master_worker;
+use app\common\model\BaseModel;
+
+
+class MasterWorkerScore extends BaseModel
+{
+    protected $name = 'master_worker_score';
+}

+ 9 - 0
app/common/model/master_worker/MasterWorkerServiceTime.php

@@ -0,0 +1,9 @@
+<?php
+namespace app\common\model\master_worker;
+use app\common\model\BaseModel;
+
+
+class MasterWorkerServiceTime extends BaseModel
+{
+    protected $name = 'master_worker_service_time';
+}

+ 10 - 0
app/common/model/works/ServiceWork.php

@@ -207,6 +207,16 @@ class ServiceWork extends BaseModel
         return !empty($data['finished_time'])?date('Y-m-d H:i:s',$data['finished_time']):'';
     }
 
+    public function getEstimatedFinishTimeAttr($value,$data)
+    {
+        return !empty($data['estimated_finish_time'])?date('Y-m-d H:i:s',$data['estimated_finish_time']):'';
+    }
+
+    public function getFirstContactTimeAttr($value,$data)
+    {
+        return !empty($data['first_contact_time'])?date('Y-m-d H:i:s',$data['first_contact_time']):'';
+    }
+
     public function getWorkImagesAttr($value)
     {
         return !empty($value)?json_decode($value,true):'';

+ 17 - 0
app/common/model/works/ServiceWorkReceiveLog.php

@@ -0,0 +1,17 @@
+<?php
+namespace app\common\model\works;
+
+
+use app\common\model\BaseModel;
+
+
+/**
+ * ServiceWorkReceiveLog模型
+ * Class ServiceWorkReceiveLog
+ * @package app\common\model\works
+ */
+class ServiceWorkReceiveLog extends BaseModel
+{
+    protected $name = 'service_work_receive_log';
+
+}

+ 19 - 0
app/workerapi/controller/WorksController.php

@@ -330,5 +330,24 @@ class WorksController extends BaseApiController
         return $this->success('操作成功,已确定新的预约时间', [], 1, 1);
     }
 
+    
+    /**
+     * 第一次电话联系客户
+     * @return \think\response\Json
+     */
+    public function contactCustomer()
+    {
+        $params = (new ServiceWorkValidate())->post()->goCheck('contact', [
+            'user_id' => $this->userId,
+            'user_info' => $this->userInfo
+        ]);
+        $result = ServiceWorkLogic::contactCustomer($params);
+        if (false === $result) {
+            return $this->fail(ServiceWorkLogic::getError());
+        }
+        
+        return $this->success('成功', [], 1, 1);
+    }
+
 
 }

+ 1 - 0
app/workerapi/logic/MasterWorkerTeamLogic.php

@@ -115,6 +115,7 @@ class MasterWorkerTeamLogic extends  BaseLogic
             $work_log = [
                 'work_id'=>$work->id,
                 'master_worker_id'=>$work->master_worker_id,
+                'type' => 0,
                 'opera_log'=>'团队负责人['.$userInfo['user_id'].']'.$userInfo['real_name'].'于'.date('Y-m-d H:i:s',time()).'分配了工程师成员'.'编号['.$worker->worker_number.']'.$worker->real_name
             ];
             ServiceWorkerAllocateWorkerLogic::add($work_log);

+ 36 - 0
app/workerapi/logic/ServiceWorkReceiveLogLogic.php

@@ -0,0 +1,36 @@
+<?php
+namespace app\workerapi\logic;
+use app\common\logic\BaseLogic;
+use app\common\model\works\ServiceWorkReceiveLog;
+use think\Exception;
+
+
+/**
+ * ServiceWorkReceiveLog逻辑
+ * Class ServiceWorkLogLogic
+ * @package app\workerapi\logic\works
+ */
+class ServiceWorkReceiveLogLogic extends BaseLogic
+{
+    /**
+     * @notes 添加
+     * @param array $params
+     * @return bool
+     * @throws Exception
+     * @author whitef
+     * @date 2024/07/10 15:06
+     */
+    public static function add(array $params): bool
+    {
+        if(empty($params['work_id']) || empty($params['master_worker_id']) || empty($params['opera_log'])) {
+            throw new Exception('参数错误');
+        }
+        ServiceWorkReceiveLog::create([
+            'work_id' => $params['work_id'],
+            'master_worker_id' => $params['master_worker_id'],
+            'opera_log'=>$params['opera_log']
+        ]);
+
+        return true;
+    }
+}

+ 9 - 0
app/workerapi/validate/ServiceWorkValidate.php

@@ -129,4 +129,13 @@ class ServiceWorkValidate extends BaseValidate
     {
         return $this->only(['work_sn','appointment_time']);
     }
+
+    /**
+     * 电话联系客户
+     * @return ServiceWorkValidate
+     */
+    public function sceneContact()
+    {
+        return $this->only(['work_sn']);
+    }
 }

+ 6 - 0
config/console.php

@@ -13,5 +13,11 @@ return [
         'query_add_agreement' => 'app\common\command\AddAgreementPdf',
         //工程师每日开启接单的通知
         'open_obtain_order' => 'app\common\command\OpenObtainOrder',
+        //工程师自动派单
+        'automatic_dispatch' => 'app\common\command\AutomaticDispatch',
+        //自动取消超时未领的工单
+        'cancel_dispatch' => 'app\common\command\CancelDispatch',
+        //工程师每周更新一次综合评分和服务时长
+        'update_worker_score' => 'app\common\command\UpdateWorkerScore',
     ],
 ];