- 新增 invoice_title、showcase、wallet_log 模型及对应语言包 - 新增后台控制器、视图及搜索字段配置 - 修复服务类目删除保护逻辑 - 优化后台登录页面文案及样式 - 新增前端角色切换逻辑及页面拦截 - 新增 SVG 图标及样式变量 - 修复接口权限及下拉框默认值问题
330 行
12 KiB
PHP
330 行
12 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\controller\Api;
|
||
use app\common\library\OrderStatus;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 业务接口基类
|
||
* 统一处理:登录用户注入 / 师傅档案 / 订单归属校验 / 状态流转 / 通知
|
||
*/
|
||
class Base extends Api
|
||
{
|
||
protected $noNeedLogin = [];
|
||
protected $noNeedRight = '*';
|
||
|
||
/** @var mixed 当前登录用户 */
|
||
protected $user = null;
|
||
|
||
/** @var array|false|null 师傅档案:null未查询 / false未入驻 / array已入驻 */
|
||
protected $master = null;
|
||
|
||
protected function _initialize()
|
||
{
|
||
parent::_initialize();
|
||
$this->user = $this->auth->getUser();
|
||
}
|
||
|
||
/**
|
||
* 当前用户ID
|
||
*/
|
||
protected function uid()
|
||
{
|
||
return (int)$this->auth->id;
|
||
}
|
||
|
||
/**
|
||
* 师傅档案
|
||
*/
|
||
protected function masterInfo($reload = false)
|
||
{
|
||
if ($this->uid() <= 0) {
|
||
return false;
|
||
}
|
||
if ($this->master === null || $reload) {
|
||
$row = Db::name('master')->where('user_id', $this->uid())->find();
|
||
$this->master = $row ? $row : false;
|
||
}
|
||
return $this->master;
|
||
}
|
||
|
||
/**
|
||
* 要求是已通过审核的师傅
|
||
*/
|
||
protected function requireMaster()
|
||
{
|
||
$master = $this->masterInfo();
|
||
if (!$master) {
|
||
$this->error('请先完成师傅入驻', ['need_apply' => 1], 2001);
|
||
}
|
||
if ($master['verify_status'] === 'pending') {
|
||
$this->error('师傅资质审核中,请耐心等待', ['verify_status' => 'pending'], 2002);
|
||
}
|
||
if ($master['verify_status'] === 'reject') {
|
||
$this->error('资质未通过:' . $master['audit_msg'], ['verify_status' => 'reject'], 2003);
|
||
}
|
||
return $master;
|
||
}
|
||
|
||
/**
|
||
* 取本人订单(越权拦截)
|
||
* @param int $orderId
|
||
* @param string $side user|master
|
||
*/
|
||
protected function ownOrder($orderId, $side = 'user')
|
||
{
|
||
$orderId = (int)$orderId;
|
||
if ($orderId <= 0) {
|
||
$this->error('订单参数错误');
|
||
}
|
||
$order = Db::name('order')->where('id', $orderId)->find();
|
||
if (!$order) {
|
||
$this->error('订单不存在');
|
||
}
|
||
if ($side === 'user' && (int)$order['user_id'] !== $this->uid()) {
|
||
$team = $this->currentMerchantTeam();
|
||
if (!$team || !in_array((int)$order['user_id'], $team['user_ids'], true)) {
|
||
$this->error('无权操作该订单', null, 403);
|
||
}
|
||
}
|
||
if ($side === 'master') {
|
||
$master = $this->requireMaster();
|
||
if ((int)$order['master_id'] !== (int)$master['id']) {
|
||
$this->error('无权操作该订单', null, 403);
|
||
}
|
||
}
|
||
return $order;
|
||
}
|
||
|
||
/**
|
||
* 获取当前登录用户所属的已认证商家团队(主账号 + 关联子账号)
|
||
* @return array|null ['merchant' => array, 'user_ids' => int[]]
|
||
*/
|
||
protected function currentMerchantTeam()
|
||
{
|
||
$uid = $this->uid();
|
||
if ($uid <= 0) {
|
||
return null;
|
||
}
|
||
$merchant = Db::name('merchant')->where('user_id', $uid)->find();
|
||
if (!$merchant) {
|
||
$bind = Db::name('merchant_member')->where('user_id', $uid)->find();
|
||
if ($bind) {
|
||
$merchant = Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find();
|
||
}
|
||
}
|
||
if (!$merchant || $merchant['verify_status'] !== 'pass') {
|
||
return null;
|
||
}
|
||
$ids = [(int)$merchant['user_id']];
|
||
$rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id');
|
||
foreach ($rows as $id) {
|
||
$id = (int)$id;
|
||
if ($id > 0 && !in_array($id, $ids, true)) {
|
||
$ids[] = $id;
|
||
}
|
||
}
|
||
return [
|
||
'merchant' => $merchant,
|
||
'user_ids' => $ids,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 订单状态流转:校验 → 更新 → 写流水
|
||
*/
|
||
protected function transit(array $order, $to, $remark = '', $extra = [], $operatorType = 'system', $operatorId = 0)
|
||
{
|
||
$from = (int)$order['status'];
|
||
if (!OrderStatus::can($from, $to)) {
|
||
$this->error('订单状态「' . OrderStatus::label($from) . '」不允许变更为「' . OrderStatus::label($to) . '」');
|
||
}
|
||
$data = array_merge($extra, [
|
||
'status' => (int)$to,
|
||
'updatetime' => time(),
|
||
]);
|
||
Db::name('order')->where('id', (int)$order['id'])->update($data);
|
||
$this->addLog((int)$order['id'], (int)$to, $remark, $operatorType, $operatorId);
|
||
return array_merge($order, $data);
|
||
}
|
||
|
||
/**
|
||
* 写订单流水
|
||
*/
|
||
protected function addLog($orderId, $status, $remark = '', $operatorType = 'system', $operatorId = 0)
|
||
{
|
||
Db::name('order_log')->insert([
|
||
'order_id' => (int)$orderId,
|
||
'status' => (int)$status,
|
||
'remark' => mb_substr((string)$remark, 0, 255),
|
||
'operator_type' => $operatorType,
|
||
'operator_id' => (int)$operatorId,
|
||
'createtime' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 消息通知
|
||
*/
|
||
protected function notify($receiveType, $receiveId, $title, $content, $type = 'order', $orderId = 0)
|
||
{
|
||
if (!$receiveId) {
|
||
return;
|
||
}
|
||
Db::name('message')->insert([
|
||
'receive_type' => $receiveType,
|
||
'receive_id' => (int)$receiveId,
|
||
'type' => $type,
|
||
'title' => mb_substr((string)$title, 0, 100),
|
||
'content' => mb_substr((string)$content, 0, 500),
|
||
'order_id' => (int)$orderId,
|
||
'is_read' => 0,
|
||
'createtime' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 生成订单号
|
||
*/
|
||
protected function makeOrderNo()
|
||
{
|
||
return date('YmdHis') . str_pad((string)mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
|
||
}
|
||
|
||
/**
|
||
* 同步写入或隐藏独立晒单表 fa_showcase(仅存储脱敏后的必要公开信息)
|
||
* @param array $order 订单行
|
||
* @param int $isShowcase 1=公开晒单, 0=取消晒单
|
||
* @return int 晒单表主键 ID(取消时返回 0)
|
||
*/
|
||
protected function syncOrderShowcase(array $order, $isShowcase = 1)
|
||
{
|
||
$orderId = (int)$order['id'];
|
||
$now = time();
|
||
if (!$isShowcase) {
|
||
Db::name('showcase')->where('order_id', $orderId)->update([
|
||
'status' => 'hidden',
|
||
'updatetime' => $now,
|
||
]);
|
||
return 0;
|
||
}
|
||
|
||
$rev = Db::name('review')->where('order_id', $orderId)->where('status', 'normal')->find();
|
||
$u = Db::name('user')->where('id', (int)$order['user_id'])->find();
|
||
$cat = Db::name('service_cat')->where('id', (int)$order['cat_id'])->find();
|
||
$service = Db::name('service')->where('id', (int)$order['service_id'])->find();
|
||
|
||
$isAnon = $rev ? (int)$rev['is_anonymous'] : 0;
|
||
$userName = '匿名业主';
|
||
$userAvatar = '';
|
||
if (!$isAnon && $u) {
|
||
$nick = trim((string)$u['nickname']);
|
||
if ($nick !== '' && !preg_match('/^[0-9]/', $nick)) {
|
||
$userName = mb_substr($nick, 0, 1) . '业主';
|
||
} else {
|
||
$userName = '优匠业主';
|
||
}
|
||
$userAvatar = !empty($u['avatar']) ? (string)$u['avatar'] : '';
|
||
}
|
||
|
||
// 地区仅保留区县或城市,绝不保存具体街道/小区/门牌
|
||
$district = trim((string)$order['district']);
|
||
$city = trim((string)$order['city']);
|
||
$region = $district ?: ($city ?: '同城服务');
|
||
|
||
// 收集晒单实拍图(评价晒图 > 完工实况图,过滤无效测试域名)
|
||
$imgList = [];
|
||
if ($rev && !empty($rev['images'])) {
|
||
$dec = json_decode((string)$rev['images'], true);
|
||
$arr = is_array($dec) ? $dec : array_filter(explode(',', (string)$rev['images']));
|
||
foreach ($arr as $it) {
|
||
$it = trim((string)$it);
|
||
if ($it !== '' && substr($it, -4) !== '.svg' && strpos($it, 'example.com') === false) {
|
||
$imgList[] = $it;
|
||
}
|
||
}
|
||
}
|
||
if (empty($imgList)) {
|
||
$mediaUrls = Db::name('order_media')->where('order_id', $orderId)->order('id desc')->limit(6)->column('url');
|
||
foreach ($mediaUrls as $mu) {
|
||
$mu = trim((string)$mu);
|
||
if ($mu !== '' && substr($mu, -4) !== '.svg' && strpos($mu, 'example.com') === false) {
|
||
$imgList[] = $mu;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 封面图提取
|
||
$cover = !empty($imgList) ? $imgList[0] : '';
|
||
if ($cover === '' && !empty($order['service_image']) && substr($order['service_image'], -4) !== '.svg') {
|
||
$cover = (string)$order['service_image'];
|
||
}
|
||
if ($cover === '' && $service && !empty($service['image']) && substr($service['image'], -4) !== '.svg') {
|
||
$cover = (string)$service['image'];
|
||
}
|
||
if ($cover === '') {
|
||
$fallback = Db::name('service')
|
||
->where('cat_id', (int)$order['cat_id'])
|
||
->where('image', '<>', '')
|
||
->where('image', 'not like', '%.svg%')
|
||
->order('weigh desc, id asc')
|
||
->value('image');
|
||
if ($fallback) {
|
||
$cover = (string)$fallback;
|
||
}
|
||
}
|
||
|
||
// 师傅脱敏称呼与评分
|
||
$masterName = '优匠师傅';
|
||
$masterScore = 5.00;
|
||
if ((int)$order['master_id'] > 0) {
|
||
$m = Db::name('master')->where('id', (int)$order['master_id'])->find();
|
||
if ($m) {
|
||
$rName = trim((string)$m['realname']);
|
||
if ($rName !== '' && strpos($rName, '师傅') === false) {
|
||
$masterName = mb_substr($rName, 0, 1) . '师傅';
|
||
} elseif ($rName !== '') {
|
||
$masterName = $rName;
|
||
}
|
||
$masterScore = (float)$m['score'] > 0 ? round((float)$m['score'], 2) : 5.00;
|
||
}
|
||
}
|
||
|
||
$score = $rev ? max(1, min(5, (int)$rev['score'])) : 5;
|
||
$tags = ($rev && !empty($rev['tags'])) ? (string)$rev['tags'] : '准时,专业,值得推荐';
|
||
$content = ($rev && !empty($rev['content'])) ? (string)$rev['content'] : '师傅准时上门,施工专业细致,安装效果很满意!';
|
||
|
||
$payload = [
|
||
'order_id' => $orderId,
|
||
'user_id' => (int)$order['user_id'],
|
||
'user_name' => $userName,
|
||
'user_avatar' => $userAvatar,
|
||
'cat_id' => (int)$order['cat_id'],
|
||
'cat_name' => $cat ? (string)$cat['name'] : '上门服务',
|
||
'service_id' => (int)$order['service_id'],
|
||
'service_name' => $order['service_name'] ?: ($service ? (string)$service['name'] : '上门服务'),
|
||
'region' => $region,
|
||
'cover_image' => $cover,
|
||
'images' => !empty($imgList) ? json_encode(array_values($imgList), JSON_UNESCAPED_UNICODE) : '',
|
||
'score' => $score,
|
||
'tags' => $tags,
|
||
'content' => mb_substr($content, 0, 500),
|
||
'master_name' => $masterName,
|
||
'master_score' => $masterScore,
|
||
'status' => 'normal',
|
||
'updatetime' => $now,
|
||
];
|
||
|
||
$existing = Db::name('showcase')->where('order_id', $orderId)->find();
|
||
if ($existing) {
|
||
Db::name('showcase')->where('id', (int)$existing['id'])->update($payload);
|
||
return (int)$existing['id'];
|
||
} else {
|
||
$payload['createtime'] = !empty($order['showcase_time']) ? (int)$order['showcase_time'] : $now;
|
||
return (int)Db::name('showcase')->insertGetId($payload);
|
||
}
|
||
}
|
||
}
|