- 下单页新增计价方式选择;自主定价填写总价,师傅报价单进入报价大厅 - 用户订单详情展示师傅报价列表并可选定师傅;选定后同步师傅、金额与订单状态 - 师傅大厅新增用户定价筛选与按价接单;报价单禁止直接抢单 - 后端订单新增 custom 价态、金额口径统一由 calcAmount 输出 - 报价中状态纳入用户可取消范围;超时回流重新开放报价 - 新增 fa_order.price_type custom 枚举迁移脚本(schema_phase6)
666 行
28 KiB
PHP
666 行
28 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\library\OrderStatus;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 师傅端:入驻 / 订单大厅 / 抢单 / 履约流转 / 收入
|
||
*/
|
||
class Master extends Base
|
||
{
|
||
/**
|
||
* 师傅入驻申请
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function apply()
|
||
{
|
||
$data = [
|
||
'realname' => trim((string)$this->request->post('realname')),
|
||
'mobile' => trim((string)$this->request->post('mobile')),
|
||
'idcard_no' => trim((string)$this->request->post('idcard_no')),
|
||
'idcard_front' => (string)$this->request->post('idcard_front'),
|
||
'idcard_back' => (string)$this->request->post('idcard_back'),
|
||
'cert_images' => (string)$this->request->post('cert_images'),
|
||
'city' => trim((string)$this->request->post('city')),
|
||
'skills' => (array)$this->request->post('skills/a', []),
|
||
];
|
||
|
||
if ($data['realname'] === '' || $data['mobile'] === '' || $data['idcard_no'] === '') {
|
||
$this->error('请填写真实姓名、手机号和身份证号');
|
||
}
|
||
if (!preg_match('/^1\d{10}$/', $data['mobile'])) {
|
||
$this->error('手机号格式不正确');
|
||
}
|
||
if (empty($data['skills'])) {
|
||
$this->error('请至少选择一个服务类目');
|
||
}
|
||
|
||
$exist = Db::name('master')->where('user_id', $this->uid())->find();
|
||
if ($exist && $exist['verify_status'] === 'pass') {
|
||
$this->error('您已是认证师傅,无需重复入驻');
|
||
}
|
||
|
||
$save = [
|
||
'realname' => $data['realname'],
|
||
'mobile' => $data['mobile'],
|
||
'idcard_no' => substr($data['idcard_no'], 0, 6) . '********' . substr($data['idcard_no'], -4),
|
||
'idcard_front' => $data['idcard_front'],
|
||
'idcard_back' => $data['idcard_back'],
|
||
'cert_images' => $data['cert_images'],
|
||
'city' => $data['city'],
|
||
'verify_status' => 'pending',
|
||
'audit_msg' => '',
|
||
'updatetime' => time(),
|
||
];
|
||
|
||
Db::startTrans();
|
||
try {
|
||
if ($exist) {
|
||
$masterId = (int)$exist['id'];
|
||
Db::name('master')->where('id', $masterId)->update($save);
|
||
Db::name('master_skill')->where('master_id', $masterId)->delete();
|
||
} else {
|
||
$save['user_id'] = $this->uid();
|
||
$save['createtime'] = time();
|
||
$masterId = Db::name('master')->insertGetId($save);
|
||
}
|
||
foreach ($data['skills'] as $catId) {
|
||
Db::name('master_skill')->insert([
|
||
'master_id' => $masterId,
|
||
'cat_id' => (int)$catId,
|
||
'status' => 'normal',
|
||
'createtime' => time(),
|
||
]);
|
||
}
|
||
Db::commit();
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
$this->error('提交失败:' . $e->getMessage());
|
||
}
|
||
|
||
$this->success('提交成功,请等待审核', ['master_id' => (int)$masterId]);
|
||
}
|
||
|
||
/**
|
||
* 我的师傅档案
|
||
*/
|
||
public function info()
|
||
{
|
||
$master = $this->masterInfo();
|
||
if (!$master) {
|
||
$this->success('未入驻', ['applied' => false]);
|
||
}
|
||
$skills = Db::name('master_skill')->alias('s')
|
||
->join('service_cat c', 'c.id = s.cat_id', 'LEFT')
|
||
->field('s.cat_id,c.name,c.icon')
|
||
->where('s.master_id', $master['id'])
|
||
->select();
|
||
|
||
$todo = Db::name('order')->where('master_id', $master['id'])
|
||
->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING])
|
||
->count();
|
||
$pendingVerify = Db::name('order')->where('master_id', $master['id'])
|
||
->where('status', OrderStatus::PENDING_VERIFY)->count();
|
||
|
||
$this->success('', [
|
||
'applied' => true,
|
||
'master' => [
|
||
'id' => (int)$master['id'],
|
||
'realname' => $master['realname'],
|
||
'mobile' => $master['mobile'],
|
||
'avatar' => $master['avatar'],
|
||
'city' => $master['city'],
|
||
'level' => (int)$master['level'],
|
||
'score' => (float)$master['score'],
|
||
'order_count' => (int)$master['order_count'],
|
||
'accept_rate' => (float)$master['accept_rate'],
|
||
'ontime_rate' => (float)$master['ontime_rate'],
|
||
'work_status' => $master['work_status'],
|
||
'verify_status' => $master['verify_status'],
|
||
'audit_msg' => $master['audit_msg'],
|
||
'money' => (float)$master['money'],
|
||
'todo_count' => (int)$todo,
|
||
'verify_count' => (int)$pendingVerify,
|
||
],
|
||
'skills' => $skills,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 接单开关
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function toggleWork()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$status = $this->request->post('work_status', '');
|
||
if (!in_array($status, ['on', 'off'], true)) {
|
||
$this->error('参数错误');
|
||
}
|
||
Db::name('master')->where('id', $master['id'])->update(['work_status' => $status, 'updatetime' => time()]);
|
||
$this->success($status === 'on' ? '已开启接单' : '已暂停接单', ['work_status' => $status]);
|
||
}
|
||
|
||
/**
|
||
* 订单大厅:支持排序与单类型筛选(一口价 / 师傅报价 / 用户自主定价)
|
||
*/
|
||
public function hall()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$this->recycleExpired();
|
||
|
||
$catIds = Db::name('master_skill')->where('master_id', $master['id'])->column('cat_id');
|
||
$page = max(1, (int)$this->request->param('page', 1));
|
||
$limit = min(30, max(1, (int)$this->request->param('limit', 10)));
|
||
$sort = $this->request->param('sort', 'new'); // new 最新 / amount 报酬 / urgent 急单 / time 时效
|
||
$type = $this->request->param('type', 'all'); // all / fixed 一口价 / quote 师傅报价 / custom 用户自主定价
|
||
|
||
$build = function () use ($catIds, $type) {
|
||
$q = Db::name('order')->where('master_id', 0)
|
||
->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING]);
|
||
if (!empty($catIds)) {
|
||
$q->where('cat_id', 'in', $catIds);
|
||
}
|
||
if ($type === 'fixed') {
|
||
$q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'fixed');
|
||
} elseif ($type === 'quote') {
|
||
$q->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING])->where('price_type', 'quote');
|
||
} elseif ($type === 'custom') {
|
||
$q->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'custom');
|
||
}
|
||
return $q;
|
||
};
|
||
|
||
if ($sort === 'amount') {
|
||
$order = 'pay_amount desc, id desc';
|
||
} elseif ($sort === 'urgent') {
|
||
$order = 'urgent desc, id desc';
|
||
} elseif ($sort === 'time') {
|
||
$order = 'appoint_time asc, id desc';
|
||
} else {
|
||
$order = 'id desc';
|
||
}
|
||
|
||
$total = $build()->count();
|
||
$list = $build()->order($order)->page($page, $limit)->select();
|
||
|
||
$now = time();
|
||
foreach ($list as &$row) {
|
||
$row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门';
|
||
$row['pay_amount'] = (float)$row['pay_amount'];
|
||
$row['urgent'] = (int)$row['urgent'];
|
||
$row['mobile'] = substr_replace($row['mobile'], '****', 3, 4);
|
||
$row['address'] = $row['city'] . $row['district'] . $row['address'];
|
||
$row['status_text'] = OrderStatus::label($row['status']);
|
||
$row['left_seconds'] = max(0, (int)$row['createtime'] + 3600 - $now);
|
||
$row['quote_count'] = (int)Db::name('order_quote')->where('order_id', $row['id'])->count();
|
||
$row['price_type_text'] = $row['price_type'] === 'fixed' ? '一口价'
|
||
: ($row['price_type'] === 'quote' ? '师傅报价'
|
||
: ($row['price_type'] === 'custom' ? '用户自主定价' : '按面积计价'));
|
||
$row['user_price'] = $row['price_type'] === 'custom' ? (float)$row['base_amount'] : 0;
|
||
}
|
||
unset($row);
|
||
|
||
$this->success('', [
|
||
'list' => $list, 'total' => (int)$total,
|
||
'page' => $page, 'sort' => $sort, 'type' => $type,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 抢单(条件更新保证并发安全)
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function grab()
|
||
{
|
||
$master = $this->requireMaster();
|
||
if ($master['work_status'] !== 'on') {
|
||
$this->error('您已暂停接单,请先开启');
|
||
}
|
||
$orderId = (int)$this->request->post('order_id');
|
||
$order = Db::name('order')->where('id', $orderId)->find();
|
||
if (!$order) {
|
||
$this->error('订单不存在');
|
||
}
|
||
if ($order['price_type'] === 'quote') {
|
||
$this->error('该订单为师傅报价订单,请提交报价');
|
||
}
|
||
if ((int)$order['status'] !== OrderStatus::PENDING_ACCEPT || (int)$order['master_id'] !== 0) {
|
||
$this->error('手慢了,该订单已被接走');
|
||
}
|
||
|
||
$affected = Db::name('order')
|
||
->where('id', $orderId)
|
||
->where('status', OrderStatus::PENDING_ACCEPT)
|
||
->where('master_id', 0)
|
||
->update([
|
||
'master_id' => (int)$master['id'],
|
||
'status' => OrderStatus::ACCEPTED,
|
||
'accept_time' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
if (!$affected) {
|
||
$this->error('手慢了,该订单已被接走');
|
||
}
|
||
|
||
$this->addLog($orderId, OrderStatus::ACCEPTED, '师傅接单:' . $master['realname'], 'master', (int)$master['id']);
|
||
$this->notify('user', (int)$order['user_id'], '师傅已接单', '师傅 ' . $master['realname'] . ' 已接单,将尽快与您联系', 'order', $orderId);
|
||
|
||
$this->success('抢单成功,请在 2 小时内联系客户', [
|
||
'order_id' => $orderId,
|
||
'deadline' => time() + 7200,
|
||
'user_mobile' => $order['mobile'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 标记已联系客户(2 小时考核)
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function contact()
|
||
{
|
||
$order = $this->ownOrder((int)$this->request->post('order_id'), 'master');
|
||
if ((int)$order['contact_time'] > 0) {
|
||
$this->success('已记录');
|
||
}
|
||
Db::name('order')->where('id', $order['id'])->update(['contact_time' => time(), 'updatetime' => time()]);
|
||
$this->addLog((int)$order['id'], (int)$order['status'], '师傅已联系客户', 'master', (int)$order['master_id']);
|
||
$this->success('已记录联系时间');
|
||
}
|
||
|
||
/**
|
||
* 设置预约上门时间
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function schedule()
|
||
{
|
||
$order = $this->ownOrder((int)$this->request->post('order_id'), 'master');
|
||
$appointTime = (int)$this->request->post('appoint_time');
|
||
if ($appointTime <= 0) {
|
||
$this->error('请选择上门时间');
|
||
}
|
||
if ((int)$order['status'] === OrderStatus::ACCEPTED) {
|
||
$this->transit($order, OrderStatus::SCHEDULED, '已与客户约定上门时间', [
|
||
'appoint_time' => $appointTime,
|
||
'contact_time' => (int)$order['contact_time'] ?: time(),
|
||
], 'master', (int)$order['master_id']);
|
||
} else {
|
||
Db::name('order')->where('id', $order['id'])->update(['appoint_time' => $appointTime, 'updatetime' => time()]);
|
||
}
|
||
$this->notify('user', (int)$order['user_id'], '师傅已预约上门', '约定时间:' . date('m-d H:i', $appointTime), 'order', (int)$order['id']);
|
||
$this->success('预约成功');
|
||
}
|
||
|
||
/**
|
||
* 到达现场(定位打卡)
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function arrive()
|
||
{
|
||
$order = $this->ownOrder((int)$this->request->post('order_id'), 'master');
|
||
$lng = (string)$this->request->post('lng', '');
|
||
$lat = (string)$this->request->post('lat', '');
|
||
$extra = ['arrive_time' => time()];
|
||
if ($lng !== '' && $lat !== '') {
|
||
$extra['lng'] = $lng;
|
||
$extra['lat'] = $lat;
|
||
}
|
||
$this->transit($order, OrderStatus::ARRIVED, '师傅已到达现场', $extra, 'master', (int)$order['master_id']);
|
||
$this->notify('user', (int)$order['user_id'], '师傅已到达', '师傅已到达服务地址,请做好准备', 'order', (int)$order['id']);
|
||
$this->success('已打卡到达');
|
||
}
|
||
|
||
/**
|
||
* 开始服务
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function startJob()
|
||
{
|
||
$order = $this->ownOrder((int)$this->request->post('order_id'), 'master');
|
||
$this->transit($order, OrderStatus::WORKING, '师傅开始施工', ['start_time' => time()], 'master', (int)$order['master_id']);
|
||
$this->notify('user', (int)$order['user_id'], '师傅已开工', '师傅已开始服务,请留意进度', 'order', (int)$order['id']);
|
||
$this->success('已开工');
|
||
}
|
||
|
||
/**
|
||
* 完工并提交现场取证 → 进入待验收
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function finish()
|
||
{
|
||
$order = $this->ownOrder((int)$this->request->post('order_id'), 'master');
|
||
$images = (array)$this->request->post('images/a', []);
|
||
$damage = trim((string)$this->request->post('damage_remark', ''));
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$this->transit($order, OrderStatus::PENDING_VERIFY, '师傅完工,等待客户验收', [
|
||
'finish_time' => time(),
|
||
], 'master', (int)$order['master_id']);
|
||
|
||
foreach ($images as $url) {
|
||
if (!$url) {
|
||
continue;
|
||
}
|
||
Db::name('order_media')->insert([
|
||
'order_id' => (int)$order['id'],
|
||
'master_id' => (int)$order['master_id'],
|
||
'stage' => $damage !== '' ? 'damage' : 'finish',
|
||
'type' => 'image',
|
||
'url' => (string)$url,
|
||
'watermark' => date('Y-m-d H:i') . ' ' . $order['address'],
|
||
'remark' => $damage !== '' ? mb_substr($damage, 0, 255) : '',
|
||
'createtime' => time(),
|
||
]);
|
||
}
|
||
Db::commit();
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
$this->error('提交失败:' . $e->getMessage());
|
||
}
|
||
|
||
$code = (string)$order['verify_code'];
|
||
$this->notify('user', (int)$order['user_id'], '师傅已完工,待您验收', '验收码:' . $code . ',请核对后确认完成', 'order', (int)$order['id']);
|
||
$this->success('已提交完工,等待客户验收', ['verify_code' => $code]);
|
||
}
|
||
|
||
/**
|
||
* 我的订单(师傅视角)
|
||
*/
|
||
public function orders()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$status = $this->request->param('status', 'active');
|
||
$page = max(1, (int)$this->request->param('page', 1));
|
||
$limit = min(30, max(1, (int)$this->request->param('limit', 10)));
|
||
|
||
// TP5 中 count() 会重置查询条件,故用闭包重建
|
||
$build = function () use ($master, $status) {
|
||
$q = Db::name('order')->where('master_id', $master['id']);
|
||
if ($status === 'active') {
|
||
$q->where('status', 'in', [OrderStatus::ACCEPTED, OrderStatus::SCHEDULED, OrderStatus::ARRIVED, OrderStatus::WORKING]);
|
||
} elseif ($status === 'verify') {
|
||
$q->where('status', OrderStatus::PENDING_VERIFY);
|
||
} elseif ($status === 'completed') {
|
||
$q->where('status', OrderStatus::COMPLETED);
|
||
}
|
||
return $q;
|
||
};
|
||
|
||
$total = $build()->count();
|
||
$list = $build()->order('id desc')->page($page, $limit)->select();
|
||
foreach ($list as &$row) {
|
||
$row['status_text'] = OrderStatus::label($row['status']);
|
||
$row['pay_amount'] = (float)$row['pay_amount'];
|
||
$row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门';
|
||
$row['urgent'] = (int)$row['urgent'];
|
||
$row['masked_mobile'] = substr_replace($row['mobile'], '****', 3, 4);
|
||
}
|
||
unset($row);
|
||
|
||
$this->success('', ['list' => $list, 'total' => (int)$total, 'page' => $page]);
|
||
}
|
||
|
||
/**
|
||
* 收入概览
|
||
*/
|
||
public function income()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$todayStart = strtotime(date('Y-m-d'));
|
||
$todayIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id'])
|
||
->where('type', 'settle')->where('createtime', '>=', $todayStart)->sum('amount');
|
||
$totalIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id'])
|
||
->where('type', 'settle')->sum('amount');
|
||
$logs = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id'])
|
||
->order('id desc')->limit(20)->select();
|
||
|
||
$this->success('', [
|
||
'money' => (float)$master['money'],
|
||
'frozen_money' => (float)$master['frozen_money'],
|
||
'today_income' => round((float)$todayIncome, 2),
|
||
'total_income' => round((float)$totalIncome, 2),
|
||
'order_count' => (int)$master['order_count'],
|
||
'logs' => $logs,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 报价(报价竞价单)
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function quote()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$orderId = (int)$this->request->post('order_id');
|
||
$amount = (float)$this->request->post('amount');
|
||
$desc = trim((string)$this->request->post('description', ''));
|
||
$hours = min(72, max(1, (int)$this->request->post('valid_hours', 24)));
|
||
|
||
$order = Db::name('order')->where('id', $orderId)->find();
|
||
if (!$order) {
|
||
$this->error('订单不存在');
|
||
}
|
||
if ($order['price_type'] !== 'quote') {
|
||
$this->error('该订单不是报价竞价订单');
|
||
}
|
||
if (!in_array((int)$order['status'], [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) {
|
||
$this->error('该订单已不在报价阶段');
|
||
}
|
||
if ((int)$order['master_id'] > 0) {
|
||
$this->error('该订单已选定师傅');
|
||
}
|
||
if ($amount <= 0) {
|
||
$this->error('请填写报价金额');
|
||
}
|
||
if (Db::name('order_quote')->where('order_id', $orderId)->where('master_id', $master['id'])->find()) {
|
||
$this->error('您已报价,请勿重复提交');
|
||
}
|
||
|
||
Db::name('order_quote')->insert([
|
||
'order_id' => $orderId,
|
||
'master_id' => (int)$master['id'],
|
||
'amount' => round($amount, 2),
|
||
'description' => mb_substr($desc, 0, 500),
|
||
'valid_hours' => $hours,
|
||
'expire_time' => time() + $hours * 3600,
|
||
'status' => 'wait',
|
||
'createtime' => time(),
|
||
]);
|
||
|
||
// 兼容旧流程:已支付但仍在待接单的报价单,收到首个报价时进入「报价中」
|
||
if ((int)$order['status'] === OrderStatus::PENDING_ACCEPT) {
|
||
$this->transit($order, OrderStatus::QUOTING, '收到师傅报价,订单进入报价中', [], 'master', (int)$master['id']);
|
||
}
|
||
$this->notify('user', (int)$order['user_id'], '收到新报价', '师傅 ' . $master['realname'] . ' 对订单 ' . $order['order_no'] . ' 报价 ¥' . round($amount, 2) . ',请前往订单详情选择', 'order', $orderId);
|
||
|
||
$this->success('报价已提交');
|
||
}
|
||
|
||
/**
|
||
* 申请提现
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function withdraw()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$amount = round((float)$this->request->post('amount'), 2);
|
||
if ($amount < 10) {
|
||
$this->error('单次提现金额不少于 10 元');
|
||
}
|
||
if ($amount > (float)$master['money']) {
|
||
$this->error('可提现余额不足');
|
||
}
|
||
$fee = round($amount * 0.006, 2);
|
||
$arrive = round($amount - $fee, 2);
|
||
|
||
Db::startTrans();
|
||
try {
|
||
Db::name('withdraw')->insert([
|
||
'master_id' => (int)$master['id'],
|
||
'amount' => $amount,
|
||
'fee' => $fee,
|
||
'arrive_amount' => $arrive,
|
||
'channel' => $this->request->post('channel', 'wechat'),
|
||
'account' => (string)$this->request->post('account', ''),
|
||
'status' => 'wait',
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
$newMoney = round((float)$master['money'] - $amount, 2);
|
||
Db::name('master')->where('id', $master['id'])->update([
|
||
'money' => $newMoney,
|
||
'frozen_money' => round((float)$master['frozen_money'] + $amount, 2),
|
||
'updatetime' => time(),
|
||
]);
|
||
Db::name('wallet_log')->insert([
|
||
'user_type' => 'master', 'user_id' => (int)$master['id'], 'type' => 'withdraw',
|
||
'amount' => -$amount, 'balance' => $newMoney, 'order_id' => 0,
|
||
'remark' => '提现申请(手续费 ¥' . $fee . ')', 'createtime' => time(),
|
||
]);
|
||
Db::commit();
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
$this->error('提现申请失败:' . $e->getMessage());
|
||
}
|
||
|
||
$this->success('提现申请已提交,等待审核', ['arrive_amount' => $arrive]);
|
||
}
|
||
|
||
/**
|
||
* 超 2 小时未联系客户 → 订单自动回流大厅
|
||
*/
|
||
protected function recycleExpired()
|
||
{
|
||
$rows = Db::name('order')
|
||
->where('status', OrderStatus::ACCEPTED)
|
||
->where('contact_time', 0)
|
||
->where('accept_time', '<', time() - 7200)
|
||
->limit(50)
|
||
->select();
|
||
foreach ($rows as $row) {
|
||
$toStatus = $row['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT;
|
||
Db::name('order')->where('id', $row['id'])->update([
|
||
'master_id' => 0,
|
||
'status' => $toStatus,
|
||
'accept_time' => null,
|
||
'updatetime' => time(),
|
||
]);
|
||
if ($row['price_type'] === 'quote') {
|
||
Db::name('order_quote')->where('order_id', $row['id'])->where('status', 'accept')->update(['status' => 'wait']);
|
||
}
|
||
$this->addLog($row['id'], $toStatus, $row['price_type'] === 'quote' ? '师傅超 2 小时未联系客户,订单重新开放报价' : '超 2 小时未联系客户,订单自动回流大厅', 'system', 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 信用体系:综合评分 / 接单率 / 准时率 / 好评率 / 投诉率
|
||
*/
|
||
public function credit()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$mid = (int)$master['id'];
|
||
|
||
$reviewTotal = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->count();
|
||
$reviewGood = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->where('score', '>=', 4)->count();
|
||
$appealCount = (int)Db::name('appeal')->where('master_id', $mid)->count();
|
||
$orderCount = max(1, (int)$master['order_count']);
|
||
|
||
$this->success('', [
|
||
'score' => (float)$master['score'],
|
||
'level' => (int)$master['level'],
|
||
'accept_rate' => (float)$master['accept_rate'],
|
||
'ontime_rate' => (float)$master['ontime_rate'],
|
||
'good_rate' => $reviewTotal ? round($reviewGood * 100 / $reviewTotal, 1) : 100,
|
||
'complaint_rate' => round($appealCount * 100 / $orderCount, 1),
|
||
'order_count' => (int)$master['order_count'],
|
||
'review_total' => $reviewTotal,
|
||
'appeal_total' => $appealCount,
|
||
'rules' => [
|
||
'综合评分 = 客户评价星级平均值(5 分制)',
|
||
'接单率 = 已联系客户订单 / 接单总数',
|
||
'准时率 = 按预约时间到达 / 上门总数',
|
||
'好评率 = 4 星及以上评价占比',
|
||
'投诉率 = 申诉工单数 / 完成订单数',
|
||
],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 申诉中心:列表
|
||
*/
|
||
public function appeals()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$list = Db::name('appeal')->where('master_id', $master['id'])->order('id desc')->limit(30)->select();
|
||
$typeText = ['misjudge' => '误判申诉', 'review' => '恶意评价', 'deduct' => '扣款申诉', 'other' => '其他'];
|
||
$statusText = ['wait' => '待处理', 'handling' => '处理中', 'done' => '已处理', 'reject' => '已驳回'];
|
||
foreach ($list as &$row) {
|
||
$row['type_text'] = isset($typeText[$row['type']]) ? $typeText[$row['type']] : '其他';
|
||
$row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status'];
|
||
$row['time_text'] = date('Y-m-d H:i', (int)$row['createtime']);
|
||
}
|
||
unset($row);
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
|
||
/**
|
||
* 申诉中心:提交
|
||
* @ApiMethod (POST)
|
||
*/
|
||
public function appeal()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$type = $this->request->post('type', 'other');
|
||
$orderId = (int)$this->request->post('order_id', 0);
|
||
$content = trim((string)$this->request->post('content'));
|
||
if (!in_array($type, ['misjudge', 'review', 'deduct', 'other'], true)) {
|
||
$this->error('申诉类型错误');
|
||
}
|
||
if (mb_strlen($content) < 5) {
|
||
$this->error('请填写至少 5 个字的申诉说明');
|
||
}
|
||
if (Db::name('appeal')->where('master_id', $master['id'])->where('status', 'wait')->count() >= 5) {
|
||
$this->error('您有多个待处理申诉,请等待处理完成');
|
||
}
|
||
Db::name('appeal')->insert([
|
||
'master_id' => $master['id'],
|
||
'order_id' => $orderId,
|
||
'type' => $type,
|
||
'content' => mb_substr($content, 0, 500),
|
||
'images' => (string)$this->request->post('images', ''),
|
||
'status' => 'wait',
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
$this->success('申诉已提交,平台将在 24 小时内处理');
|
||
}
|
||
|
||
/**
|
||
* 我的报价记录
|
||
*/
|
||
public function quotes()
|
||
{
|
||
$master = $this->requireMaster();
|
||
$list = Db::name('order_quote')->alias('q')
|
||
->join('order o', 'o.id = q.order_id', 'LEFT')
|
||
->field('q.*, o.order_no, o.service_name, o.pay_amount as order_amount, o.status as order_status')
|
||
->where('q.master_id', $master['id'])
|
||
->order('q.id desc')
|
||
->limit(30)
|
||
->select();
|
||
$statusText = ['wait' => '等待客户选择', 'accept' => '已被选中', 'reject' => '未被选中', 'expired' => '已过期'];
|
||
foreach ($list as &$row) {
|
||
$row['status_text'] = isset($statusText[$row['status']]) ? $statusText[$row['status']] : $row['status'];
|
||
$row['amount'] = (float)$row['amount'];
|
||
$row['time_text'] = date('m-d H:i', (int)$row['createtime']);
|
||
$row['expire_text'] = date('m-d H:i', (int)$row['expire_time']);
|
||
}
|
||
unset($row);
|
||
$this->success('', ['list' => $list]);
|
||
}
|
||
}
|
||
|