feat: 新增发票抬头、晒单、钱包日志等模型及后台管理功能
- 新增 invoice_title、showcase、wallet_log 模型及对应语言包 - 新增后台控制器、视图及搜索字段配置 - 修复服务类目删除保护逻辑 - 优化后台登录页面文案及样式 - 新增前端角色切换逻辑及页面拦截 - 新增 SVG 图标及样式变量 - 修复接口权限及下拉框默认值问题
这个提交包含在:
@@ -3,20 +3,23 @@
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use think\Db;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* 师傅档案
|
||||
* 师傅档案与资质管理
|
||||
*
|
||||
* @icon fa fa-circle-o
|
||||
* @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()
|
||||
{
|
||||
@@ -26,8 +29,6 @@ class Master extends Backend
|
||||
$this->view->assign("verifyStatusList", $this->model->getVerifyStatusList());
|
||||
}
|
||||
|
||||
protected $relationSearch = true;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->request->filter(['strip_tags', 'trim']);
|
||||
@@ -44,12 +45,11 @@ class Master extends Backend
|
||||
->order($sort, $order)
|
||||
->paginate($limit);
|
||||
|
||||
// 合并查出当前页每个师傅拥有的技能
|
||||
$items = $list->items();
|
||||
$masterIds = array_column($items, 'id');
|
||||
$skillsGroup = [];
|
||||
if (!empty($masterIds)) {
|
||||
$allSkills = \think\Db::name('master_skill')
|
||||
$allSkills = Db::name('master_skill')
|
||||
->where('master_id', 'in', $masterIds)
|
||||
->where('status', 'normal')
|
||||
->select();
|
||||
@@ -63,13 +63,169 @@ class Master extends Backend
|
||||
foreach ($items as &$it) {
|
||||
$it['skills_list'] = $skillsGroup[$it['id']] ?? [];
|
||||
}
|
||||
unset($it);
|
||||
|
||||
$result = ['total' => $list->total(), 'rows' => $items];
|
||||
return json($result);
|
||||
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)
|
||||
{
|
||||
@@ -82,83 +238,96 @@ class Master extends Backend
|
||||
$selectedSkills = (array)$this->request->post('skills/a', []);
|
||||
$customText = trim((string)$this->request->post('custom_text', ''));
|
||||
|
||||
// 1. 处理自定义输入的技能
|
||||
$customNames = [];
|
||||
if (!empty($customText)) {
|
||||
$parts = preg_split('/[,,\s]+/u', $customText);
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if (!empty($p) && !in_array($p, $customNames) && !in_array($p, $selectedSkills)) {
|
||||
if (!empty($p) && !in_array($p, $customNames, true) && !in_array($p, $selectedSkills, true)) {
|
||||
$customNames[] = $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理当前师傅原有的技能
|
||||
\think\Db::name('master_skill')->where('master_id', $row['id'])->delete();
|
||||
// 缓存师傅原有技能的证书图片与分类ID,防止保存时丢失证书
|
||||
$existingRows = Db::name('master_skill')->where('master_id', $row['id'])->select();
|
||||
$existingMap = [];
|
||||
foreach ($existingRows as $er) {
|
||||
$existingMap[(string)$er['skill_name']] = $er;
|
||||
}
|
||||
|
||||
// 3. 写入选中的技能
|
||||
$dictRows = \think\Db::name('skill_dict')->where('name', 'in', $selectedSkills)->select();
|
||||
$dictRows = !empty($selectedSkills)
|
||||
? Db::name('skill_dict')->where('name', 'in', $selectedSkills)->select()
|
||||
: [];
|
||||
$dictMap = [];
|
||||
foreach ($dictRows as $dr) {
|
||||
$dictMap[$dr['name']] = $dr;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
foreach ($selectedSkills as $sName) {
|
||||
$dict = $dictMap[$sName] ?? null;
|
||||
\think\Db::name('master_skill')->insert([
|
||||
'master_id' => $row['id'],
|
||||
'cat_id' => 0,
|
||||
'skill_id' => $dict ? $dict['id'] : 0,
|
||||
'skill_name' => $sName,
|
||||
'category' => $dict ? $dict['category'] : '服务技能',
|
||||
'is_custom' => 0,
|
||||
'cert_image' => '',
|
||||
'status' => 'normal',
|
||||
'createtime' => $now
|
||||
]);
|
||||
}
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('master_skill')->where('master_id', $row['id'])->delete();
|
||||
|
||||
// 4. 处理自定义输入的技能(沉淀入库)
|
||||
foreach ($customNames as $cName) {
|
||||
$exist = \think\Db::name('skill_dict')->where('name', $cName)->find();
|
||||
if (!$exist) {
|
||||
$skillDictId = \think\Db::name('skill_dict')->insertGetId([
|
||||
'category_id'=> 0,
|
||||
'category' => '其他技能',
|
||||
'name' => $cName,
|
||||
'source' => 'custom',
|
||||
foreach ($selectedSkills as $sName) {
|
||||
$dict = $dictMap[$sName] ?? null;
|
||||
$old = $existingMap[$sName] ?? null;
|
||||
Db::name('master_skill')->insert([
|
||||
'master_id' => $row['id'],
|
||||
'weigh' => 0,
|
||||
'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' => $now,
|
||||
'updatetime' => $now
|
||||
'createtime' => $old ? (int)$old['createtime'] : $now,
|
||||
]);
|
||||
} else {
|
||||
$skillDictId = $exist['id'];
|
||||
}
|
||||
|
||||
\think\Db::name('master_skill')->insert([
|
||||
'master_id' => $row['id'],
|
||||
'cat_id' => 0,
|
||||
'skill_id' => $skillDictId,
|
||||
'skill_name' => $cName,
|
||||
'category' => '其他技能',
|
||||
'is_custom' => 1,
|
||||
'cert_image' => '',
|
||||
'status' => 'normal',
|
||||
'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 = \think\Db::name('skill_category')->where('status', 'normal')->order('weigh desc')->select();
|
||||
$allDictSkills = \think\Db::name('skill_dict')->where('status', 'normal')->order('weigh desc')->select();
|
||||
|
||||
$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']] = [];
|
||||
@@ -171,12 +340,11 @@ class Master extends Backend
|
||||
$catTree[$cName][] = $item;
|
||||
}
|
||||
|
||||
// 读取当前师傅已拥有的技能
|
||||
$mySkills = \think\Db::name('master_skill')->where('master_id', $row['id'])->select();
|
||||
$mySkills = Db::name('master_skill')->where('master_id', $row['id'])->select();
|
||||
$selectedSkills = [];
|
||||
$customArr = [];
|
||||
foreach ($mySkills as $ms) {
|
||||
if ($ms['is_custom'] == 1) {
|
||||
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'];
|
||||
@@ -195,45 +363,78 @@ class Master extends Backend
|
||||
*/
|
||||
public function detail($ids = null)
|
||||
{
|
||||
$row = $this->model->with(['user'])->find($ids);
|
||||
$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 = \think\Db::name('order')
|
||||
$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();
|
||||
|
||||
// 2. 提现流水
|
||||
$recentWithdraws = \think\Db::name('withdraw')
|
||||
->where('master_id', $row['id'])
|
||||
->order('id desc')
|
||||
// 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();
|
||||
|
||||
// 3. 服务好评记录
|
||||
$recentReviews = \think\Db::name('review')
|
||||
->where('master_id', $row['id'])
|
||||
->order('id desc')
|
||||
->limit(5)
|
||||
->select();
|
||||
|
||||
// 4. 服务技能与认证证书
|
||||
$skills = \think\Db::name('master_skill')
|
||||
// 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();
|
||||
|
||||
$this->view->assign('row', $row);
|
||||
$this->view->assign('recentOrders', $recentOrders);
|
||||
$this->view->assign('recentWithdraws', $recentWithdraws);
|
||||
$this->view->assign('recentReviews', $recentReviews);
|
||||
$this->view->assign('skills', $skills);
|
||||
// 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();
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户