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

442 行
17 KiB
PHP

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

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

<?php
namespace app\admin\controller;
use app\common\controller\Backend;
use think\Db;
use Exception;
/**
* 师傅档案与资质管理
*
* @icon fa fa-wrench
*/
class Master extends Backend
{
/**
* Master模型对象
* @var \app\admin\model\Master
*/
protected $model = null;
protected $relationSearch = true;
protected $searchFields = 'id,realname,mobile,city';
public function _initialize()
{
parent::_initialize();
$this->model = new \app\admin\model\Master;
$this->view->assign("workStatusList", $this->model->getWorkStatusList());
$this->view->assign("verifyStatusList", $this->model->getVerifyStatusList());
}
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'])
->where($where)
->order($sort, $order)
->paginate($limit);
$items = $list->items();
$masterIds = array_column($items, 'id');
$skillsGroup = [];
if (!empty($masterIds)) {
$allSkills = Db::name('master_skill')
->where('master_id', 'in', $masterIds)
->where('status', 'normal')
->select();
foreach ($allSkills as $sk) {
$name = !empty($sk['skill_name']) ? $sk['skill_name'] : '';
if ($name) {
$skillsGroup[$sk['master_id']][] = $name;
}
}
}
foreach ($items as &$it) {
$it['skills_list'] = $skillsGroup[$it['id']] ?? [];
}
unset($it);
return json(['total' => $list->total(), 'rows' => $items]);
}
/**
* 新增认证师傅档案(校验用户唯一绑定,强制初始化资金与履约统计为 0)
*/
public function add()
{
if (false === $this->request->isPost()) {
return $this->view->fetch();
}
$params = $this->request->post('row/a', []);
if (!$params) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
$uid = (int)($params['user_id'] ?? 0);
if ($uid <= 0) {
$this->error('请选择关联的平台注册用户账号');
}
if (Db::name('master')->where('user_id', $uid)->count() > 0) {
$this->error('该用户已绑定过师傅档案,请勿重复创建');
}
$params['money'] = 0.00;
$params['frozen_money'] = 0.00;
$params['order_count'] = 0;
$params['level'] = max(1, (int)($params['level'] ?? 1));
$params['score'] = 5.00;
$params['accept_rate'] = 100.00;
$params['ontime_rate'] = 100.00;
$result = $this->model->allowField(true)->save($params);
if ($result !== false) {
$this->success();
}
$this->error($this->model->getError() ?: __('No rows were inserted'));
}
/**
* 删除保护:已产生承接工单、钱包余额或结算流水的师傅禁止物理删除
*/
public function del($ids = null)
{
if (false === $this->request->isPost()) {
$this->error(__('Invalid parameters'));
}
$ids = $ids ?: $this->request->post('ids');
$idArr = array_values(array_filter(array_map('intval', explode(',', (string)$ids))));
if (!$idArr) {
$this->error(__('Parameter %s can not be empty', 'ids'));
}
$hasOrder = Db::name('order')->where('master_id', 'in', $idArr)->count();
$hasFund = Db::name('master')->where('id', 'in', $idArr)->where('money > 0 OR frozen_money > 0')->count();
$hasSettle = Db::name('settle')->where('master_id', 'in', $idArr)->count();
if ($hasOrder > 0 || $hasFund > 0 || $hasSettle > 0) {
$this->error('所选师傅已关联历史工单、结算记录或存在钱包资金,禁止物理删除;如需停用请关闭听单状态');
}
Db::startTrans();
try {
Db::name('master_skill')->where('master_id', 'in', $idArr)->delete();
$deleted = Db::name('master')->where('id', 'in', $idArr)->delete();
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($deleted) {
$this->success();
}
$this->error(__('No rows were deleted'));
}
/**
* 编辑师傅资料与入驻资质审核(状态变更自动触发通知)
*/
public function edit($ids = null)
{
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
$oldVerify = (string)$row['verify_status'];
if ($this->request->isPost()) {
$params = $this->request->post("row/a");
if ($params) {
$params = $this->preExcludeFields($params);
// 禁止在师傅档案编辑中直接篡改钱包可用余额、冻结资金与累计完单数
unset($params['money'], $params['frozen_money'], $params['order_count']);
// 若提交的身份证号包含脱敏星号 *,保留原数据库记录不覆盖;若为新填写明文则做合法性校验
if (isset($params['idcard_no'])) {
$idcardInput = trim((string)$params['idcard_no']);
if (strpos($idcardInput, '*') !== false) {
unset($params['idcard_no']);
} elseif ($idcardInput !== '' && !preg_match('/^(^\d{15}$)|(^\d{17}([0-9]|X|x)$)$/', $idcardInput)) {
$this->error('请输入正确的15位或18位身份证号码');
}
}
// 若 city-picker 因省市格式差异未选新值导致提交为空而原库有城市,保留原城市不清空
if (isset($params['city']) && trim((string)$params['city']) === '' && !empty($row['city'])) {
unset($params['city']);
}
if (isset($params['verify_status']) && $params['verify_status'] === 'reject' && empty(trim((string)($params['audit_msg'] ?? '')))) {
$this->error('驳回实名认证时请填写审核意见说明');
}
$result = false;
Db::startTrans();
try {
$result = $row->allowField(true)->save($params);
$newVerify = isset($params['verify_status']) ? (string)$params['verify_status'] : $oldVerify;
if ($newVerify !== $oldVerify && in_array($newVerify, ['pass', 'reject'], true)) {
$now = time();
$title = $newVerify === 'pass' ? '师傅入驻审核通过' : '师傅入驻审核未通过';
$content = $newVerify === 'pass'
? '您的师傅实名认证已审核通过,现在可开启听单承接同城服务工单。'
: ('您的师傅认证申请未通过,原因:' . (trim((string)($params['audit_msg'] ?? '')) ?: '请核对资料后重新提交'));
Db::name('message')->insert([
'receive_type' => 'master',
'receive_id' => (int)$row['id'],
'type' => 'audit',
'title' => $title,
'content' => $content,
'order_id' => 0,
'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' => 'audit',
'title' => $title,
'content' => $content,
'order_id' => 0,
'is_read' => 0,
'createtime' => $now,
]);
}
}
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result !== false) {
$this->success();
}
$this->error(__('No rows were updated'));
}
$this->error(__('Parameter %s can not be empty', ''));
}
$this->view->assign("row", $row);
return $this->view->fetch();
}
/**
* 师傅服务技能管理(保留既有证书与元数据)
*/
public function skills($ids = null)
{
$row = $this->model->find($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
if ($this->request->isPost()) {
$selectedSkills = (array)$this->request->post('skills/a', []);
$customText = trim((string)$this->request->post('custom_text', ''));
$customNames = [];
if (!empty($customText)) {
$parts = preg_split('/[,,\s]+/u', $customText);
foreach ($parts as $p) {
$p = trim($p);
if (!empty($p) && !in_array($p, $customNames, true) && !in_array($p, $selectedSkills, true)) {
$customNames[] = $p;
}
}
}
// 缓存师傅原有技能的证书图片与分类ID,防止保存时丢失证书
$existingRows = Db::name('master_skill')->where('master_id', $row['id'])->select();
$existingMap = [];
foreach ($existingRows as $er) {
$existingMap[(string)$er['skill_name']] = $er;
}
$dictRows = !empty($selectedSkills)
? Db::name('skill_dict')->where('name', 'in', $selectedSkills)->select()
: [];
$dictMap = [];
foreach ($dictRows as $dr) {
$dictMap[$dr['name']] = $dr;
}
$now = time();
Db::startTrans();
try {
Db::name('master_skill')->where('master_id', $row['id'])->delete();
foreach ($selectedSkills as $sName) {
$dict = $dictMap[$sName] ?? null;
$old = $existingMap[$sName] ?? null;
Db::name('master_skill')->insert([
'master_id' => $row['id'],
'cat_id' => $old ? (int)$old['cat_id'] : 0,
'skill_id' => $dict ? (int)$dict['id'] : ($old ? (int)$old['skill_id'] : 0),
'skill_name' => $sName,
'category' => $dict ? $dict['category'] : ($old && !empty($old['category']) ? $old['category'] : '服务技能'),
'is_custom' => 0,
'cert_image' => $old ? (string)$old['cert_image'] : '',
'status' => 'normal',
'createtime' => $old ? (int)$old['createtime'] : $now,
]);
}
foreach ($customNames as $cName) {
$exist = Db::name('skill_dict')->where('name', $cName)->find();
if (!$exist) {
$skillDictId = Db::name('skill_dict')->insertGetId([
'category_id' => 0,
'category' => '其他技能',
'name' => $cName,
'source' => 'custom',
'master_id' => $row['id'],
'weigh' => 0,
'status' => 'normal',
'createtime' => $now,
'updatetime' => $now,
]);
} else {
$skillDictId = (int)$exist['id'];
}
$old = $existingMap[$cName] ?? null;
Db::name('master_skill')->insert([
'master_id' => $row['id'],
'cat_id' => $old ? (int)$old['cat_id'] : 0,
'skill_id' => $skillDictId,
'skill_name' => $cName,
'category' => $exist && !empty($exist['category']) ? $exist['category'] : '其他技能',
'is_custom' => 1,
'cert_image' => $old ? (string)$old['cert_image'] : '',
'status' => 'normal',
'createtime' => $old ? (int)$old['createtime'] : $now,
]);
}
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
$this->success('技能配置更新成功');
}
$categories = Db::name('skill_category')->where('status', 'normal')->order('weigh desc')->select();
$allDictSkills = Db::name('skill_dict')->where('status', 'normal')->order('weigh desc')->select();
$catTree = [];
foreach ($categories as $cat) {
$catTree[$cat['name']] = [];
}
foreach ($allDictSkills as $item) {
$cName = $item['category'];
if (!isset($catTree[$cName])) {
$catTree[$cName] = [];
}
$catTree[$cName][] = $item;
}
$mySkills = Db::name('master_skill')->where('master_id', $row['id'])->select();
$selectedSkills = [];
$customArr = [];
foreach ($mySkills as $ms) {
if ((int)$ms['is_custom'] === 1 && !Db::name('skill_dict')->where('name', $ms['skill_name'])->where('source', '<>', 'custom')->count()) {
$customArr[] = $ms['skill_name'];
} else {
$selectedSkills[] = $ms['skill_name'];
}
}
$this->view->assign('row', $row);
$this->view->assign('catTree', $catTree);
$this->view->assign('selectedSkills', $selectedSkills);
$this->view->assign('customText', implode(' ', $customArr));
return $this->view->fetch();
}
/**
* 师傅全景档案画像
*/
public function detail($ids = null)
{
$row = $this->model->with(['user'])->where('master.id', (int)$ids)->find();
if (!$row) {
$this->error(__('No Results were found'));
}
$orderModel = new \app\admin\model\Order;
$statusMap = $orderModel->getStatusList();
// 1. 最近接单记录
$recentOrders = Db::name('order')
->where('master_id', $row['id'])
->order('id desc')
->limit(8)
->select();
foreach ($recentOrders as &$o) {
$o['status_label'] = $statusMap[(int)$o['status']] ?? ('状态' . $o['status']);
}
unset($o);
// 2. 提现流水
$recentWithdraws = Db::name('withdraw')
->where('master_id', $row['id'])
->order('id desc')
->limit(6)
->select();
// 3. 工单佣金结算记录
$recentSettles = Db::name('settle')
->alias('s')
->join('order o', 'o.id = s.order_id', 'LEFT')
->where('s.master_id', $row['id'])
->field('s.*, o.order_no, o.service_name')
->order('s.id desc')
->limit(6)
->select();
// 4. 客户评价记录
$recentReviews = Db::name('review')
->alias('r')
->join('order o', 'o.id = r.order_id', 'LEFT')
->join('user u', 'u.id = r.user_id', 'LEFT')
->where('r.master_id', $row['id'])
->field('r.*, o.order_no, o.service_name, u.nickname as user_nickname')
->order('r.id desc')
->limit(5)
->select();
// 5. 服务技能与认证证书
$skills = Db::name('master_skill')
->alias('s')
->join('service_cat c', 'c.id = s.cat_id', 'LEFT')
->where('s.master_id', $row['id'])
->field('s.*, c.name as cat_name')
->select();
// 6. 钱包资金变动流水
$walletLogs = Db::name('wallet_log')
->where('user_type', 'master')
->where('user_id', $row['id'])
->order('id desc')
->limit(6)
->select();
$this->view->assign([
'row' => $row,
'recentOrders' => $recentOrders,
'recentWithdraws' => $recentWithdraws,
'recentSettles' => $recentSettles,
'recentReviews' => $recentReviews,
'skills' => $skills,
'walletLogs' => $walletLogs,
]);
return $this->view->fetch();
}
}