文件

330 行
12 KiB
PHP

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

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

<?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 = 0.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 = round((float)$m['score'], 2);
}
}
$score = $rev ? max(1, min(5, (int)$rev['score'])) : 0;
$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);
}
}
}