文件
yjyf/server/application/admin/controller/Order.php
MiAn fafddceaf2 feat: 新增发票抬头、晒单、钱包日志等模型及后台管理功能
- 新增 invoice_title、showcase、wallet_log 模型及对应语言包
- 新增后台控制器、视图及搜索字段配置
- 修复服务类目删除保护逻辑
- 优化后台登录页面文案及样式
- 新增前端角色切换逻辑及页面拦截
- 新增 SVG 图标及样式变量
- 修复接口权限及下拉框默认值问题
2026-11-11 17:02:35 +08:00

571 行
24 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\admin\controller;
use app\common\controller\Backend;
use think\Db;
use Exception;
/**
* 订单管理与调度中心
*
* @icon fa fa-file-text-o
*/
class Order extends Backend
{
/**
* Order模型对象
* @var \app\admin\model\Order
*/
protected $model = null;
protected $relationSearch = true;
protected $searchFields = 'id,order_no,service_name,contact,mobile';
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\Order;
$this->view->assign("priceTypeList", $this->model->getPriceTypeList());
$this->view->assign("statusList", $this->model->getStatusList());
$this->view->assign("payStatusList", $this->model->getPayStatusList());
$this->view->assign("settleStatusList", $this->model->getSettleStatusList());
$this->view->assign("isReviewList", $this->model->getIsReviewList());
$this->view->assign("hasElevatorList", $this->model->getHasElevatorList());
$this->view->assign("urgentList", $this->model->getUrgentList());
}
public function index()
{
$this->request->filter(['strip_tags', 'trim']);
if (false === $this->request->isAjax()) {
return $this->view->fetch();
}
if ($this->request->request('keyField')) {
return $this->selectpage();
}
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
$list = $this->model
->with(['user', 'master'])
->where($where)
->order($sort, $order)
->paginate($limit);
$rows = $list->items();
if (!empty($rows)) {
$orderIds = array_column($rows, 'id');
$merLinks = Db::name('merchant_order')
->alias('mo')
->join('merchant m', 'm.id = mo.merchant_id', 'LEFT')
->where('mo.order_id', 'in', $orderIds)
->field('mo.order_id, mo.batch_no, m.name as merchant_name')
->select();
$merMap = [];
foreach ($merLinks as $ml) {
$merMap[(int)$ml['order_id']] = $ml;
}
foreach ($rows as &$r) {
$oid = (int)$r['id'];
$r['merchant_name'] = isset($merMap[$oid]) ? (string)$merMap[$oid]['merchant_name'] : '';
$r['batch_no'] = isset($merMap[$oid]) ? (string)$merMap[$oid]['batch_no'] : '';
}
unset($r);
}
return json(['total' => $list->total(), 'rows' => $rows]);
}
/**
* 禁止后台手工凭空新增订单
*/
public function add()
{
$this->error('订单须由客户端或企业商家工作台真实发起,不支持后台手工伪造');
}
/**
* 禁止物理删除业务订单
*/
public function del($ids = "")
{
$this->error('订单关联资金流水与结算凭证,禁止物理删除;如需终止履约请编辑取消工单');
}
/**
* 编辑订单(支持后台派单/改派师傅通知与轨迹联动,以及取消订单自动退回优惠券)
*/
public function edit($ids = null)
{
$row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find();
if (!$row) {
$this->error(__('No Results were found'));
}
if (false === $this->request->isPost()) {
$catName = (int)$row['cat_id'] > 0 ? (string)Db::name('service_cat')->where('id', (int)$row['cat_id'])->value('name') : '';
$this->view->assign('catName', $catName);
$this->view->assign('row', $row);
return $this->view->fetch();
}
$row = $this->model->get($ids);
$params = $this->request->post('row/a', []);
if (!$params) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
// 锁定订单归属与财务结算核心字段,禁止后台编辑工单时意外篡改账务
unset(
$params['order_no'],
$params['user_id'],
$params['cat_id'],
$params['service_id'],
$params['price_type'],
$params['base_amount'],
$params['distance_amount'],
$params['floor_amount'],
$params['diff_amount'],
$params['discount_amount'],
$params['total_amount'],
$params['pay_amount'],
$params['pay_status'],
$params['pay_type'],
$params['pay_time'],
$params['settle_status'],
$params['settle_amount'],
$params['coupon_id'],
$params['verify_code']
);
// 统一转换时间节点字段(留空转为 0,日期字符串转为 Unix 时间戳)
foreach (['appoint_time', 'accept_time', 'arrive_time', 'start_time', 'finish_time'] as $tField) {
if (array_key_exists($tField, $params)) {
$tv = trim((string)$params[$tField]);
$params[$tField] = $tv === '' ? 0 : (is_numeric($tv) ? (int)$tv : ((int)strtotime($tv) ?: 0));
}
}
if (array_key_exists('master_id', $params) && trim((string)$params['master_id']) === '') {
$params['master_id'] = 0;
}
$oldMasterId = (int)$row['master_id'];
$oldStatus = (int)$row['status'];
$newMasterId = isset($params['master_id']) ? (int)$params['master_id'] : $oldMasterId;
$newStatus = isset($params['status']) ? (int)$params['status'] : $oldStatus;
$now = time();
// 若 city-picker 提交空串而原库有城市,保留原城市
if (isset($params['city']) && trim((string)$params['city']) === '' && !empty($row['city'])) {
unset($params['city']);
}
$targetMaster = null;
if ($newMasterId > 0 && $newMasterId !== $oldMasterId && $newStatus !== 9) {
$targetMaster = Db::name('master')->where('id', $newMasterId)->find();
if (!$targetMaster || $targetMaster['verify_status'] !== 'pass') {
$this->error('所选承接师傅不存在或尚未通过实名认证');
}
if (in_array($newStatus, [1, 2], true)) {
$newStatus = 3;
$params['status'] = 3;
}
if (empty($params['accept_time']) || (string)$params['accept_time'] === (string)$row['accept_time']) {
$params['accept_time'] = $now;
}
}
$result = false;
Db::startTrans();
try {
$result = $row->allowField(true)->save($params);
// 1. 后台通过编辑指派或改派了师傅:写轨迹日志与三方站内信
if ($targetMaster && $newMasterId !== $oldMasterId && $newStatus !== 9) {
$actionLabel = $oldMasterId > 0 ? '平台编辑改派师傅' : '平台编辑指派师傅';
$logRemark = $actionLabel . ':指派给【' . $targetMaster['realname'] . '(' . $targetMaster['mobile'] . ')】';
Db::name('order_log')->insert([
'order_id' => (int)$row['id'],
'status' => $newStatus,
'remark' => mb_substr($logRemark, 0, 255),
'operator_type' => 'admin',
'operator_id' => (int)$this->auth->id,
'createtime' => $now,
]);
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => $newMasterId,
'type' => 'order',
'title' => '平台派单通知',
'content' => '平台已为您指派工单【' . $row['order_no'] . '】(' . $row['service_name'] . '),请及时联系客户预约上门。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
if ($oldMasterId > 0 && $oldMasterId !== $newMasterId) {
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => $oldMasterId,
'type' => 'order',
'title' => '工单改派通知',
'content' => '工单【' . $row['order_no'] . '】已由平台调度改派给其他师傅承接。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
if ((int)$row['user_id'] > 0) {
Db::name('message')->insert([
'receive_type' => 'user',
'receive_id' => (int)$row['user_id'],
'type' => 'order',
'title' => '师傅指派成功通知',
'content' => '您的订单【' . $row['order_no'] . '】已安排' . $targetMaster['realname'] . '师傅(' . $targetMaster['mobile'] . ')为您服务。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
}
// 2. 后台将订单变更为取消状态(status=9):自动退回已锁定的优惠券并记录轨迹通知
if ($oldStatus !== 9 && $newStatus === 9) {
$this->refundLockedCoupon((int)$row['id'], (int)$row['coupon_id'], $now);
$cancelReason = trim((string)($params['cancel_reason'] ?? $row['cancel_reason'] ?? ''));
Db::name('order_log')->insert([
'order_id' => (int)$row['id'],
'status' => 9,
'remark' => mb_substr('平台运营后台取消工单' . ($cancelReason !== '' ? (':' . $cancelReason) : ''), 0, 255),
'operator_type' => 'admin',
'operator_id' => (int)$this->auth->id,
'createtime' => $now,
]);
if ($oldMasterId > 0) {
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => $oldMasterId,
'type' => 'order',
'title' => '工单已取消通知',
'content' => '工单【' . $row['order_no'] . '】已由平台取消' . ($cancelReason !== '' ? (',原因:' . $cancelReason) : '') . '。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
if ((int)$row['user_id'] > 0) {
Db::name('message')->insert([
'receive_type' => 'user',
'receive_id' => (int)$row['user_id'],
'type' => 'order',
'title' => '订单取消通知',
'content' => '您的订单【' . $row['order_no'] . '】已取消' . ($cancelReason !== '' ? (',原因:' . $cancelReason) : '') . ',如使用了优惠券已自动退回账户。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
} elseif ($oldStatus !== $newStatus && !($targetMaster && $newMasterId !== $oldMasterId)) {
$statusMap = $this->model->getStatusList();
Db::name('order_log')->insert([
'order_id' => (int)$row['id'],
'status' => $newStatus,
'remark' => '平台运营变更工单状态为:' . ($statusMap[$newStatus] ?? ('状态' . $newStatus)),
'operator_type' => 'admin',
'operator_id' => (int)$this->auth->id,
'createtime' => $now,
]);
}
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result !== false) {
$this->success();
}
$this->error(__('No rows were updated'));
}
/**
* 订单取消时退回已锁定的用户优惠券
*/
protected function refundLockedCoupon($orderId, $userCouponId, $now)
{
$ucRows = Db::name('user_coupon')
->where(function ($q) use ($orderId, $userCouponId) {
if ($userCouponId > 0) {
$q->where('id', $userCouponId)->whereOr('order_id', $orderId);
} else {
$q->where('order_id', $orderId);
}
})
->where('status', 'used')
->select();
foreach ($ucRows as $uc) {
$couponTpl = Db::name('coupon')->where('id', (int)$uc['coupon_id'])->find();
$isExpired = $couponTpl && (int)$couponTpl['end_time'] > 0 && (int)$couponTpl['end_time'] < $now;
Db::name('user_coupon')->where('id', (int)$uc['id'])->update([
'status' => $isExpired ? 'expired' : 'unused',
'order_id' => 0,
'use_time' => null,
]);
}
}
/**
* 人工指派 / 改派师傅
*/
public function dispatch($ids = null)
{
$row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find();
if (!$row) {
$this->error(__('No Results were found'));
}
if ($this->request->isPost()) {
if (in_array((int)$row['status'], [8, 9], true)) {
$this->error('已完成或已取消的工单不支持指派/改派');
}
if ((int)$row['pay_status'] !== 1 && (int)$row['status'] === 0) {
$this->error('该工单尚未完成支付,无法派单');
}
$masterId = (int)$this->request->post('master_id', 0);
$remark = trim((string)$this->request->post('dispatch_remark', ''));
if ($masterId <= 0) {
$this->error('请选择要指派的认证师傅');
}
$targetMaster = Db::name('master')->where('id', $masterId)->find();
if (!$targetMaster || $targetMaster['verify_status'] !== 'pass') {
$this->error('所选师傅不存在或未通过实名认证');
}
$oldMasterId = (int)$row['master_id'];
if ($oldMasterId === $masterId && (int)$row['status'] >= 3) {
$this->error('当前工单已由该师傅承接,无需重复指派');
}
$now = time();
$newStatus = in_array((int)$row['status'], [1, 2], true) ? 3 : (int)$row['status'];
$actionLabel = $oldMasterId > 0 ? '平台改派师傅' : '平台人工指派师傅';
$logRemark = $actionLabel . ':指派给【' . $targetMaster['realname'] . '(' . $targetMaster['mobile'] . ')】' . ($remark !== '' ? (',备注:' . $remark) : '');
Db::startTrans();
try {
$updateData = [
'master_id' => $masterId,
'status' => $newStatus,
'updatetime' => $now,
];
if (empty($row['accept_time']) || $oldMasterId !== $masterId) {
$updateData['accept_time'] = $now;
}
Db::name('order')->where('id', $row['id'])->update($updateData);
Db::name('order_log')->insert([
'order_id' => $row['id'],
'status' => $newStatus,
'remark' => mb_substr($logRemark, 0, 255),
'operator_type' => 'admin',
'operator_id' => (int)$this->auth->id,
'createtime' => $now,
]);
// 通知新接单师傅
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => $masterId,
'type' => 'order',
'title' => '平台派单通知',
'content' => '平台已为您指派工单【' . $row['order_no'] . '】(' . $row['service_name'] . '),请及时联系客户预约上门。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
// 若为改派,通知原师傅
if ($oldMasterId > 0 && $oldMasterId !== $masterId) {
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => $oldMasterId,
'type' => 'order',
'title' => '工单改派通知',
'content' => '工单【' . $row['order_no'] . '】已由平台调度改派给其他师傅承接。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
// 通知下单业主
if ((int)$row['user_id'] > 0) {
Db::name('message')->insert([
'receive_type' => 'user',
'receive_id' => (int)$row['user_id'],
'type' => 'order',
'title' => '师傅指派成功通知',
'content' => '您的订单【' . $row['order_no'] . '】已安排' . $targetMaster['realname'] . '师傅(' . $targetMaster['mobile'] . ')为您服务。',
'order_id' => (int)$row['id'],
'is_read' => 0,
'createtime' => $now,
]);
}
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
$this->success('派单调度成功');
}
// 推荐候选师傅:优先同城在线且实名通过的师傅
$cityKeyword = '';
if (!empty($row['city'])) {
$parts = explode('/', (string)$row['city']);
$cityKeyword = trim((string)(count($parts) > 1 ? $parts[1] : $parts[0]));
$cityKeyword = preg_replace('/市$/u', '', $cityKeyword);
}
$candidateMasters = Db::name('master')
->where('verify_status', 'pass')
->orderRaw("work_status = 'on' DESC, score DESC, order_count DESC")
->limit(12)
->select();
if ($cityKeyword !== '') {
$sameCity = [];
$otherCity = [];
foreach ($candidateMasters as $cm) {
if (mb_strpos((string)$cm['city'], $cityKeyword) !== false) {
$sameCity[] = $cm;
} else {
$otherCity[] = $cm;
}
}
$candidateMasters = array_slice(array_merge($sameCity, $otherCity), 0, 8);
} else {
$candidateMasters = array_slice($candidateMasters, 0, 8);
}
$cmIds = array_column($candidateMasters, 'id');
$cmSkillMap = [];
if (!empty($cmIds)) {
$skRows = Db::name('master_skill')->where('master_id', 'in', $cmIds)->where('status', 'normal')->select();
foreach ($skRows as $sk) {
if (!empty($sk['skill_name'])) {
$cmSkillMap[(int)$sk['master_id']][] = $sk['skill_name'];
}
}
}
foreach ($candidateMasters as &$cm) {
$cm['skills'] = array_slice($cmSkillMap[(int)$cm['id']] ?? [], 0, 3);
}
unset($cm);
$this->view->assign('row', $row);
$this->view->assign('candidateMasters', $candidateMasters);
return $this->view->fetch();
}
/**
* 工单全景详情
*/
public function detail($ids = null)
{
$row = $this->model->with(['user', 'master'])->where('order.id', (int)$ids)->find();
if (!$row) {
$this->error(__('No Results were found'));
}
// 1. 多商品/多工项明细
$goodsList = Db::name('order_goods')->where('order_id', $row['id'])->select();
// 2. 现场施工取证图片 (区分签到、过程、完工、报损)
$mediaList = Db::name('order_media')->where('order_id', $row['id'])->order('id asc')->select();
// 3. 履约轨迹日志
$statusMap = $this->model->getStatusList();
$logList = Db::name('order_log')->where('order_id', $row['id'])->order('createtime asc, id asc')->select();
foreach ($logList as &$lg) {
$lg['status_label'] = isset($statusMap[(int)$lg['status']]) ? $statusMap[(int)$lg['status']] : '状态变更';
}
unset($lg);
// 4. 师傅报价列表
$quoteList = Db::name('order_quote')
->alias('q')
->join('master m', 'q.master_id = m.id', 'LEFT')
->field('q.*, m.realname as master_name, m.mobile as master_mobile, m.score as master_score')
->where('q.order_id', $row['id'])
->order('q.id desc')
->select();
// 5. 客户评价与回复(兼容 JSON 数组或逗号分隔标签)
$review = Db::name('review')->where('order_id', $row['id'])->find();
if ($review) {
$tagsArr = [];
if (!empty($review['tags'])) {
$decoded = json_decode($review['tags'], true);
if (is_array($decoded)) {
$tagsArr = array_filter($decoded);
} else {
$tagsArr = array_filter(explode(',', (string)$review['tags']));
}
}
$review['tags_arr'] = $tagsArr;
}
// 6. 财务结算记录
$settle = Db::name('settle')->where('order_id', $row['id'])->find();
// 7. 企业商家协同发包来源
$merchantOrder = Db::name('merchant_order')
->alias('mo')
->join('merchant m', 'm.id = mo.merchant_id', 'LEFT')
->where('mo.order_id', $row['id'])
->field('mo.batch_no, mo.createtime as bind_time, m.id as merchant_id, m.name as merchant_name, m.contact as merchant_contact, m.mobile as merchant_mobile, m.settle_type')
->find();
// 8. 发票开具关联信息
$invoiceInfo = null;
if (!empty($row['invoice_id'])) {
$invoiceInfo = Db::name('invoice')
->field('id,invoice_sn,title,type,amount,status,invoice_no,issue_time')
->where('id', (int)$row['invoice_id'])
->find();
}
// 9. 售后申诉争议记录
$appealList = Db::name('appeal')
->alias('a')
->join('master m', 'm.id = a.master_id', 'LEFT')
->where('a.order_id', $row['id'])
->field('a.*, m.realname as master_name')
->order('a.id desc')
->select();
$this->view->assign([
'row' => $row,
'goodsList' => $goodsList,
'mediaList' => $mediaList,
'logList' => $logList,
'quoteList' => $quoteList,
'review' => $review,
'settle' => $settle,
'merchantOrder' => $merchantOrder,
'invoiceInfo' => $invoiceInfo,
'appealList' => $appealList,
]);
return $this->view->fetch();
}
}