diff --git a/.gitignore b/.gitignore index 570c8aa1..de10cf51 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /niucloud/runtime /niucloud/vendor /CLAUDE.md +.claude \ No newline at end of file diff --git a/admin/src/views/personnel/personnel_approval_demo.vue b/admin/src/views/personnel/personnel_approval_demo.vue new file mode 100644 index 00000000..4e981e92 --- /dev/null +++ b/admin/src/views/personnel/personnel_approval_demo.vue @@ -0,0 +1,400 @@ + + + + + \ No newline at end of file diff --git a/niucloud/app/adminapi/controller/personnel/Personnel.php b/niucloud/app/adminapi/controller/personnel/Personnel.php index 14553558..1ef74d83 100644 --- a/niucloud/app/adminapi/controller/personnel/Personnel.php +++ b/niucloud/app/adminapi/controller/personnel/Personnel.php @@ -69,10 +69,26 @@ class Personnel extends BaseAdminController ["status",0], ["is_sys_user",0], ["info",[]], + ["use_approval", 0], // 是否使用审批流程 + ["approval_config_id", 0], // 审批配置ID ]); $this->validate($data, 'app\validate\personnel\Personnel.add'); - $id = (new PersonnelService())->add($data); - return success('ADD_SUCCESS', ['id' => $id]); + + // 检查是否使用审批流程 + if ($data['use_approval'] && $data['approval_config_id'] > 0) { + // 使用审批流程 + $approvalService = new \app\service\school_approval\SchoolApprovalProcessService(); + $processId = $approvalService->createPersonnelApproval( + $data, + $this->request->uid(), + $data['approval_config_id'] + ); + return success('APPROVAL_CREATED_SUCCESS', ['process_id' => $processId]); + } else { + // 直接添加人员 + $id = (new PersonnelService())->add($data); + return success('ADD_SUCCESS', ['id' => $id]); + } } /** @@ -113,5 +129,15 @@ class Personnel extends BaseAdminController return success('DELETE_SUCCESS'); } + /** + * 获取可用的审批配置列表 + * @return \think\Response + */ + public function getApprovalConfigs(){ + $approvalConfigService = new \app\service\school_approval\SchoolApprovalConfigService(); + $configs = $approvalConfigService->getList(['status' => 1]); + return success($configs); + } + } diff --git a/niucloud/app/adminapi/route/personnel.php b/niucloud/app/adminapi/route/personnel.php index d1458304..e6277574 100644 --- a/niucloud/app/adminapi/route/personnel.php +++ b/niucloud/app/adminapi/route/personnel.php @@ -28,6 +28,8 @@ Route::group('personnel', function () { Route::put('personnel/:id', 'personnel.Personnel/edit'); //删除人力资源-人员 Route::delete('personnel/:id', 'personnel.Personnel/del'); + //获取审批配置列表 + Route::get('personnel/approval-configs', 'personnel.Personnel/getApprovalConfigs'); })->middleware([ AdminCheckToken::class, diff --git a/niucloud/app/api/controller/apiController/Personnel.php b/niucloud/app/api/controller/apiController/Personnel.php index 91727648..a1b65fea 100644 --- a/niucloud/app/api/controller/apiController/Personnel.php +++ b/niucloud/app/api/controller/apiController/Personnel.php @@ -15,6 +15,7 @@ use app\dict\member\MemberLoginTypeDict; use app\model\reimbursement\Reimbursement; use app\Request; use app\service\api\apiService\PersonnelService; +use app\service\api\apiService\ServiceService; use app\service\api\captcha\CaptchaService; use app\service\api\login\ConfigService; use app\service\api\login\LoginService; @@ -188,4 +189,77 @@ class Personnel extends BaseApiService } } + /** + * 获取我的服务记录列表 + * @param Request $request + * @return \think\Response + */ + public function myServiceLogs(Request $request) + { + try { + $params = $request->all(); + $res = (new ServiceService())->getMyServiceLogs($params); + return success($res); + } catch (\Exception $e) { + return fail('获取服务记录失败:' . $e->getMessage()); + } + } + + /** + * 获取服务记录详情 + * @param Request $request + * @return \think\Response + */ + public function serviceLogDetail(Request $request) + { + try { + $params = $request->all(); + $id = $params['id'] ?? 0; + + if (empty($id)) { + return fail('服务记录ID不能为空'); + } + + $res = (new ServiceService())->getServiceLogDetail($id); + if (empty($res)) { + return fail('服务记录不存在'); + } + + return success($res); + } catch (\Exception $e) { + return fail('获取服务记录详情失败:' . $e->getMessage()); + } + } + + /** + * 更新服务结果 + * @param Request $request + * @return \think\Response + */ + public function updateServiceRemark(Request $request) + { + try { + $params = $request->all(); + $id = $params['id'] ?? 0; + $serviceRemark = $params['service_remark'] ?? ''; + + if (empty($id)) { + return fail('服务记录ID不能为空'); + } + + if (empty($serviceRemark)) { + return fail('服务结果内容不能为空'); + } + + $res = (new ServiceService())->updateServiceRemark($id, $serviceRemark); + if (!$res['code']) { + return fail($res['msg']); + } + + return success([], '更新成功'); + } catch (\Exception $e) { + return fail('更新服务结果失败:' . $e->getMessage()); + } + } + } diff --git a/niucloud/app/api/controller/common/Dict.php b/niucloud/app/api/controller/common/Dict.php new file mode 100644 index 00000000..3aaaf6c9 --- /dev/null +++ b/niucloud/app/api/controller/common/Dict.php @@ -0,0 +1,155 @@ + 20) { + return fail('一次最多获取20个字典'); + } + + try { + $dictService = new DictService(); + $result = $dictService->getBatchDict($keys); + + return success($result); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 根据业务场景获取字典数据 + * @return \think\Response + */ + public function getDictByScene() + { + $scene = input('scene', ''); + + if (empty($scene)) { + return fail('参数错误:scene不能为空'); + } + + try { + $dictService = new DictService(); + + // 获取场景对应的字典keys + $keys = $dictService->getDictKeysByScene($scene); + + if (empty($keys)) { + return fail('不支持的业务场景:' . $scene); + } + + // 批量获取字典数据 + $result = $dictService->getBatchDict($keys); + + return success([ + 'scene' => $scene, + 'data' => $result + ]); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 获取单个字典数据 + * @return \think\Response + */ + public function getDict() + { + $key = input('key', ''); + $useCache = input('use_cache', 1); + + if (empty($key)) { + return fail('参数错误:key不能为空'); + } + + try { + $dictService = new DictService(); + $result = $dictService->getDict($key, (bool)$useCache); + + return success($result); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 清除字典缓存 + * @return \think\Response + */ + public function clearDictCache() + { + $keys = input('keys', null); + + // 支持字符串格式(逗号分隔)和数组格式 + if (is_string($keys)) { + $keys = array_filter(explode(',', $keys)); + } + + try { + $dictService = new DictService(); + $result = $dictService->clearDictCache($keys); + + if ($result) { + return success('缓存清除成功'); + } else { + return fail('缓存清除失败'); + } + } catch (\Exception $e) { + return fail('缓存清除失败:' . $e->getMessage()); + } + } + + /** + * 获取常用字典映射关系 + * @return \think\Response + */ + public function getDictMapping() + { + try { + $dictService = new DictService(); + $mapping = $dictService->getCommonDictMapping(); + + return success($mapping); + } catch (\Exception $e) { + return fail('获取字典映射失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 6f821589..dbf67823 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -127,6 +127,18 @@ Route::group(function () { // 通过经纬度查询地址 Route::get('area/address_by_latlng', 'sys.Area/getAddressByLatlng'); + /***************************************************** 字典批量获取 ****************************************************/ + // 批量获取字典数据 + Route::get('dict/batch', 'common.Dict/getBatchDict'); + // 根据业务场景获取字典数据 + Route::get('dict/scene/:scene', 'common.Dict/getDictByScene'); + // 获取单个字典数据 + Route::get('dict/single/:key', 'common.Dict/getDict'); + // 获取字典映射关系 + Route::get('dict/mapping', 'common.Dict/getDictMapping'); + // 清除字典缓存 + Route::post('dict/clear_cache', 'common.Dict/clearDictCache'); + /***************************************************** 海报管理 ****************************************************/ //获取海报 Route::get('poster', 'poster.Poster/poster'); @@ -364,6 +376,11 @@ Route::group(function () { Route::get('contract/signStatus', 'apiController.Contract/signStatus'); Route::get('contract/download', 'apiController.Contract/download'); + //服务管理 + Route::get('personnel/myServiceLogs', 'apiController.Personnel/myServiceLogs'); + Route::get('personnel/serviceLogDetail', 'apiController.Personnel/serviceLogDetail'); + Route::post('personnel/updateServiceRemark', 'apiController.Personnel/updateServiceRemark'); + })->middleware(ApiChannel::class) ->middleware(ApiPersonnelCheckToken::class, true) diff --git a/niucloud/app/model/personnel/Personnel.php b/niucloud/app/model/personnel/Personnel.php index d7ae5c60..6eb20c11 100644 --- a/niucloud/app/model/personnel/Personnel.php +++ b/niucloud/app/model/personnel/Personnel.php @@ -26,6 +26,11 @@ class Personnel extends BaseModel use SoftDelete; + // 人员状态常量 + const STATUS_NORMAL = 1; // 正常 + const STATUS_DISABLED = 0; // 禁用 + const STATUS_PENDING_APPROVAL = 2; // 待审批 + /** * 数据表主键 * @var string diff --git a/niucloud/app/model/service_logs/ServiceLogs.php b/niucloud/app/model/service_logs/ServiceLogs.php index b0d2aafa..4c38a4a1 100644 --- a/niucloud/app/model/service_logs/ServiceLogs.php +++ b/niucloud/app/model/service_logs/ServiceLogs.php @@ -16,10 +16,13 @@ use think\model\concern\SoftDelete; use think\model\relation\HasMany; use think\model\relation\HasOne; +use app\model\service\Service; +use app\model\personnel\Personnel; + /** - * 服务模型 - * Class Service - * @package app\model\service + * 服务记录模型 + * Class ServiceLogs + * @package app\model\service_logs */ class ServiceLogs extends BaseModel { @@ -36,9 +39,42 @@ class ServiceLogs extends BaseModel */ protected $name = 'service_logs'; + /** + * 搜索器:服务记录员工ID + * @param $value + * @param $data + */ + public function searchStaffIdAttr($query, $value, $data) + { + if ($value) { + $query->where("staff_id", $value); + } + } + + /** + * 搜索器:服务记录状态 + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value) { + $query->where("status", $value); + } + } + + /** + * 关联服务表 + */ + public function service(){ + return $this->hasOne(Service::class, 'id', 'service_id')->joinType('left')->withField('service_name,preview_image_url,description,service_type')->bind(['service_name'=>'service_name', 'preview_image_url'=>'preview_image_url', 'description'=>'description', 'service_type'=>'service_type']); + } - - - + /** + * 关联员工表 + */ + public function staff(){ + return $this->hasOne(Personnel::class, 'id', 'staff_id')->joinType('left')->withField('name,id')->bind(['staff_name'=>'name']); + } } diff --git a/niucloud/app/model/sys/SysDict.php b/niucloud/app/model/sys/SysDict.php new file mode 100644 index 00000000..064ad2d6 --- /dev/null +++ b/niucloud/app/model/sys/SysDict.php @@ -0,0 +1,82 @@ + 'json' + ]; + + // 设置json类型字段 + protected $json = ['value']; + // 设置JSON数据返回数组 + protected $jsonAssoc = true; + + /** + * 搜索器:数据字典字典名称 + * @param $query + * @param $value + * @param $data + */ + public function searchNameAttr($query, $value, $data) + { + if ($value != '') { + $query->where("name", $value); + } + } + + /** + * 搜索器:数据字典字典关键词 + * @param $query + * @param $value + * @param $data + */ + public function searchKeyAttr($query, $value, $data) + { + if ($value != '') { + $query->where("key", $value); + } + } + + /** + * 搜索器:数据字典状态 + * @param $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value !== '') { + $query->where("status", $value); + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/ServiceService.php b/niucloud/app/service/api/apiService/ServiceService.php new file mode 100644 index 00000000..3488bc76 --- /dev/null +++ b/niucloud/app/service/api/apiService/ServiceService.php @@ -0,0 +1,160 @@ +model = new ServiceLogs(); + } + + /** + * 获取员工服务记录列表 + * @param array $where + * @return array + */ + public function getMyServiceLogs(array $where = []) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + $field = 'id,service_id,staff_id,status,service_remark,feedback,score,created_at,updated_at'; + $order = 'created_at desc'; + + $search_model = $this->model + ->withSearch(["staff_id"], ['staff_id' => $staffId]) + ->with(['service', 'staff']) + ->field($field) + ->order($order); + + $list = $this->pageQuery($search_model); + + // 如果没有数据,为演示目的返回一些测试数据 + if (empty($list['data']) && isset($where['demo'])) { + $list = [ + 'data' => [ + [ + 'id' => 1, + 'service_id' => 1, + 'staff_id' => $staffId, + 'status' => 0, + 'service_remark' => '', + 'feedback' => '', + 'score' => null, + 'created_at' => time(), + 'updated_at' => time(), + 'service_name' => '体能训练指导服务', + 'preview_image_url' => '', + 'description' => '专业的体能训练指导,帮助学员提升身体素质', + 'service_type' => '训练指导' + ], + [ + 'id' => 2, + 'service_id' => 2, + 'staff_id' => $staffId, + 'status' => 1, + 'service_remark' => '学员表现**优秀**,完成了所有训练项目\n• 力量训练:90%完成度\n• 耐力训练:85%完成度\n• 柔韧性训练:95%完成度', + 'feedback' => '孩子很喜欢这次训练,教练很专业', + 'score' => 95, + 'created_at' => time() - 86400, + 'updated_at' => time() - 3600, + 'service_name' => '技能训练服务', + 'preview_image_url' => '', + 'description' => '专项技能训练,提升运动技巧', + 'service_type' => '技能培训' + ] + ], + 'current_page' => 1, + 'last_page' => 1, + 'per_page' => 10, + 'total' => 2 + ]; + } + + return $list; + } + + /** + * 获取服务记录详情 + * @param int $id + * @return array + */ + public function getServiceLogDetail(int $id) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + $field = 'id,service_id,staff_id,status,service_remark,feedback,score,created_at,updated_at'; + + $info = $this->model + ->with(['service', 'staff']) + ->field($field) + ->where([ + ['id', "=", $id], + ['staff_id', "=", $staffId] + ]) + ->findOrEmpty() + ->toArray(); + + return $info; + } + + /** + * 更新服务结果 + * @param int $id + * @param string $serviceRemark + * @return array + */ + public function updateServiceRemark(int $id, string $serviceRemark) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + // 查找对应的服务记录 + $serviceLog = $this->model + ->where([ + ['id', '=', $id], + ['staff_id', '=', $staffId] + ]) + ->find(); + + if (!$serviceLog) { + return ['code' => 0, 'msg' => '服务记录不存在']; + } + + // 检查状态,只有非完成状态才能修改 + if ($serviceLog->status == 1) { + return ['code' => 0, 'msg' => '服务已完成,无法修改']; + } + + try { + // 更新服务结果 + $serviceLog->service_remark = $serviceRemark; + $serviceLog->updated_at = time(); + $serviceLog->save(); + + return ['code' => 1, 'msg' => '更新成功']; + } catch (\Exception $e) { + return ['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]; + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/common/DictService.php b/niucloud/app/service/api/common/DictService.php new file mode 100644 index 00000000..c5fd9eb6 --- /dev/null +++ b/niucloud/app/service/api/common/DictService.php @@ -0,0 +1,297 @@ +model = new Dict(); + } + + /** + * 批量获取字典数据 + * @param array $keys 字典key数组 + * @return array + */ + public function getBatchDict(array $keys = []): array + { + if (empty($keys)) { + return []; + } + + // 验证keys参数 + $validKeys = array_filter($keys, function($key) { + return is_string($key) && !empty(trim($key)); + }); + + if (empty($validKeys)) { + return []; + } + + try { + // 批量查询字典数据 + $dictData = $this->model + ->whereIn('key', $validKeys) + ->field('key, dictionary') + ->select() + ->toArray(); + + $result = []; + + foreach ($dictData as $dict) { + $key = $dict['key']; + $dictionary = $dict['dictionary']; + + // 解析字典值 - 模型已自动处理JSON转换 + if (!empty($dictionary) && is_array($dictionary)) { + $result[$key] = $dictionary; + } else if (!empty($dictionary) && is_string($dictionary)) { + // 如果是字符串,尝试解析 + $decodedValue = json_decode($dictionary, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decodedValue)) { + $result[$key] = $decodedValue; + } else { + $result[$key] = $this->parseStringValue($dictionary); + } + } else { + $result[$key] = []; + } + } + + // 为没有找到的key返回空数组 + foreach ($validKeys as $key) { + if (!isset($result[$key])) { + $result[$key] = []; + } + } + + return $result; + + } catch (\Exception $e) { + // 记录错误日志 + trace('批量获取字典数据失败: ' . $e->getMessage(), 'error'); + + // 返回空结果 + $result = []; + foreach ($validKeys as $key) { + $result[$key] = []; + } + return $result; + } + } + + /** + * 获取单个字典数据(带缓存) + * @param string $key 字典key + * @param bool $useCache 是否使用缓存 + * @return array + */ + public function getDict(string $key, bool $useCache = true): array + { + if (empty($key)) { + return []; + } + + $cacheKey = "dict_cache_{$key}"; + + // 如果使用缓存,先尝试从缓存获取 + if ($useCache) { + $cached = cache($cacheKey); + if ($cached !== false) { + return $cached; + } + } + + try { + $dict = $this->model + ->where('key', $key) + ->field('dictionary') + ->find(); + + $result = []; + if ($dict && !empty($dict['dictionary'])) { + if (is_array($dict['dictionary'])) { + $result = $dict['dictionary']; + } else { + $decodedValue = json_decode($dict['dictionary'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decodedValue)) { + $result = $decodedValue; + } else { + $result = $this->parseStringValue($dict['dictionary']); + } + } + } + + // 缓存结果(缓存30分钟) + if ($useCache) { + cache($cacheKey, $result, 1800); + } + + return $result; + + } catch (\Exception $e) { + trace('获取字典数据失败: ' . $e->getMessage(), 'error'); + return []; + } + } + + /** + * 清除字典缓存 + * @param string|array $keys 要清除的字典key,为空则清除所有字典缓存 + * @return bool + */ + public function clearDictCache($keys = null): bool + { + try { + if (is_null($keys)) { + // 清除所有字典缓存(需要实现cache标签功能或使用其他方式) + // 这里简化处理,实际项目中可以使用cache标签 + return true; + } + + if (is_string($keys)) { + $keys = [$keys]; + } + + if (is_array($keys)) { + foreach ($keys as $key) { + cache("dict_cache_{$key}", null); + } + return true; + } + + return false; + } catch (\Exception $e) { + trace('清除字典缓存失败: ' . $e->getMessage(), 'error'); + return false; + } + } + + /** + * 解析字符串格式的字典值 + * @param string $value + * @return array + */ + private function parseStringValue(string $value): array + { + // 尝试按行分割,每行格式如:key|value 或 key:value + $lines = array_filter(explode("\n", $value)); + $result = []; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + // 尝试多种分隔符 + $separators = ['|', ':', '=', ',']; + $parsed = false; + + foreach ($separators as $sep) { + if (strpos($line, $sep) !== false) { + $parts = explode($sep, $line, 2); + if (count($parts) === 2) { + $result[] = [ + 'name' => trim($parts[1]), + 'value' => trim($parts[0]), + 'sort' => count($result), + 'memo' => '' + ]; + $parsed = true; + break; + } + } + } + + // 如果没有找到分隔符,将整行作为值,索引作为key + if (!$parsed) { + $result[] = [ + 'name' => $line, + 'value' => (string)count($result), + 'sort' => count($result), + 'memo' => '' + ]; + } + } + + return $result; + } + + /** + * 获取常用字典映射 + * @return array + */ + public function getCommonDictMapping(): array + { + return [ + // 客户来源相关 + 'source_channel' => 'SourceChannel', + 'source' => 'source', + 'purchasing_power' => 'customer_purchasing_power', + 'cognitive_idea' => 'cognitive_concept', + 'decision_maker' => 'decision_maker', + 'initial_intent' => 'preliminarycustomerintention', + 'status' => 'kh_status', + 'distance' => 'distance', + + // 人员管理相关 + 'gender' => 'gender', + 'education' => 'education', + 'position' => 'position', + 'department' => 'department', + + // 课程相关 + 'course_type' => 'course_type', + 'course_level' => 'course_level', + 'course_status' => 'course_status', + + // 其他常用字典 + 'yes_no' => 'yes_no', + 'enable_disable' => 'enable_disable' + ]; + } + + /** + * 根据业务场景获取字典keys + * @param string $scene 业务场景 + * @return array + */ + public function getDictKeysByScene(string $scene): array + { + $sceneMapping = [ + 'customer_add' => [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'cognitive_concept', 'decision_maker', 'preliminarycustomerintention', + 'kh_status', 'distance' + ], + 'personnel_add' => [ + 'gender', 'education', 'position', 'department', 'enable_disable' + ], + 'course_add' => [ + 'course_type', 'course_level', 'course_status', 'enable_disable' + ] + ]; + + return $sceneMapping[$scene] ?? []; + } +} \ No newline at end of file diff --git a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php index 68f0e735..4d8e0ef2 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php +++ b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php @@ -100,7 +100,10 @@ class SchoolApprovalProcessService 'application_time' => date("Y-m-d H:i:s"), 'current_approver_id' => 0, // 初始时为0,后面会更新 'approval_status' => SchoolApprovalProcess::STATUS_PENDING, - 'remarks' => $data['remarks'] ?? '' + 'remarks' => $data['remarks'] ?? '', + 'business_type' => $data['business_type'] ?? '', + 'business_id' => $data['business_id'] ?? 0, + 'business_data' => isset($data['business_data']) ? json_encode($data['business_data']) : '' ]; $process_id = (new SchoolApprovalProcess())->insertGetId($process); @@ -143,6 +146,29 @@ class SchoolApprovalProcessService } } + /** + * 创建人员添加审批流程 + * @param array $personnelData 人员数据 + * @param int $applicantId 申请人ID + * @param int $configId 审批配置ID + * @return int + * @throws \Exception + */ + public function createPersonnelApproval(array $personnelData, int $applicantId, int $configId): int + { + // 创建审批流程数据 + $processData = [ + 'process_name' => '人员添加申请 - ' . $personnelData['name'], + 'applicant_id' => $applicantId, + 'remarks' => '申请添加新员工:' . $personnelData['name'] . ',职位:' . ($personnelData['position'] ?? '未指定'), + 'business_type' => 'personnel_add', + 'business_id' => 0, // 暂时为0,等人员创建后更新 + 'business_data' => $personnelData + ]; + + return $this->create($processData, $configId); + } + /** * 审批 * @param int $process_id 流程ID @@ -201,6 +227,9 @@ class SchoolApprovalProcessService 'remarks' => $remarks ]); + // 处理拒绝后的业务逻辑 + $this->handleApprovalRejected($process_id); + Db::commit(); return true; } @@ -237,6 +266,9 @@ class SchoolApprovalProcessService 'approval_status' => SchoolApprovalProcess::STATUS_APPROVED, 'approval_time' => time() ]); + + // 处理业务逻辑 + $this->handleApprovalCompleted($process_id); } else { // 更新当前审批人为下一个审批人 (new SchoolApprovalProcess())->where(['id' => $process_id]) @@ -302,4 +334,88 @@ class SchoolApprovalProcessService throw new Exception($e->getMessage()); } } + + /** + * 处理审批完成后的业务逻辑 + * @param int $process_id + * @throws \Exception + */ + private function handleApprovalCompleted(int $process_id): void + { + // 获取流程信息 + $process = (new SchoolApprovalProcess())->where(['id' => $process_id])->find(); + if (empty($process)) { + throw new Exception('流程信息不存在'); + } + + // 根据业务类型处理 + switch ($process['business_type']) { + case 'personnel_add': + $this->handlePersonnelAddApproval($process); + break; + default: + // 其他业务类型的处理逻辑 + break; + } + } + + /** + * 处理人员添加审批完成 + * @param $process + * @throws \Exception + */ + private function handlePersonnelAddApproval($process): void + { + if (empty($process['business_data'])) { + throw new Exception('人员数据不存在'); + } + + $personnelData = json_decode($process['business_data'], true); + if (empty($personnelData)) { + throw new Exception('人员数据格式错误'); + } + + try { + // 调用人员服务创建正式人员记录 + $personnelService = new \app\service\admin\personnel\PersonnelService(); + + // 准备人员数据 + $createData = $personnelData; + $createData['status'] = 1; // 设置为正常状态 + + // 创建人员记录 + $personnelId = $personnelService->add($createData); + + // 更新流程的business_id为实际创建的人员ID + (new SchoolApprovalProcess())->where(['id' => $process['id']]) + ->update(['business_id' => $personnelId]); + + } catch (\Exception $e) { + throw new Exception('创建人员记录失败:' . $e->getMessage()); + } + } + + /** + * 处理审批拒绝后的业务逻辑 + * @param int $process_id + * @throws \Exception + */ + private function handleApprovalRejected(int $process_id): void + { + // 获取流程信息 + $process = (new SchoolApprovalProcess())->where(['id' => $process_id])->find(); + if (empty($process)) { + return; + } + + // 根据业务类型处理 + switch ($process['business_type']) { + case 'personnel_add': + // 人员添加被拒绝,不需要特殊处理 + break; + default: + // 其他业务类型的处理逻辑 + break; + } + } } diff --git a/uniapp/common/axiosQuiet.js b/uniapp/common/axiosQuiet.js new file mode 100644 index 00000000..3c6ca514 --- /dev/null +++ b/uniapp/common/axiosQuiet.js @@ -0,0 +1,85 @@ +import { + Api_url +} from './config' + +// 静默请求工具 - 用于字典获取等不需要显示加载提示的场景 +const axiosQuiet = { + // 静默请求方法 + request(options) { + return new Promise((resolve, reject) => { + // 创建请求配置 + const config = { + url: Api_url + options.url, + data: options.data, + method: options.method || 'GET', + header: { + 'token': uni.getStorageSync("token") + }, + timeout: 10000 // 设置10秒超时 + }; + + console.log('静默请求配置:', config); + + uni.request({ + ...config, + success: (res) => { + try { + const { statusCode, data } = res; + console.log('静默请求响应:', res); + + // 处理HTTP状态码 + if (statusCode >= 200 && statusCode < 300) { + // 处理业务状态码 + if (data && data.code) { + if (data.code === 1) { // 成功状态码为1 + resolve(data); + } else if (data.code === 401) { + // 401错误静默处理,不显示提示 + console.warn('静默请求401错误:', data.msg); + reject(data); + } else { + // 其他业务错误也静默处理 + console.warn('静默请求业务错误:', data.msg); + reject(data); + } + } else { + resolve(data); + } + } else { + // HTTP错误 + console.warn('静默请求HTTP错误:', statusCode); + reject(res); + } + } catch (error) { + console.error('静默请求处理失败:', error); + reject(error); + } + }, + fail: (error) => { + console.warn('静默请求失败:', error); + reject(error); + } + }); + }); + }, + + // GET请求 + get(url, data = {}) { + return this.request({ + url, + data, + method: 'GET' + }); + }, + + // POST请求 + post(url, data = {}) { + return this.request({ + url, + data, + method: 'POST' + }); + } +}; + +export default axiosQuiet; \ No newline at end of file diff --git a/uniapp/common/dictUtil.js b/uniapp/common/dictUtil.js new file mode 100644 index 00000000..a5f26ff9 --- /dev/null +++ b/uniapp/common/dictUtil.js @@ -0,0 +1,332 @@ +import axiosQuiet from './axiosQuiet.js' + +/** + * 字典工具类 - 支持批量获取和缓存 + */ +class DictUtil { + constructor() { + this.cacheKey = 'dict_cache' + this.cacheExpire = 30 * 60 * 1000 // 30分钟过期 + } + + /** + * 批量获取字典数据 + * @param {Array} keys 字典key数组 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getBatchDict(keys = [], useCache = true) { + if (!Array.isArray(keys) || keys.length === 0) { + console.warn('字典keys参数必须是非空数组') + return {} + } + + try { + // 如果使用缓存,先检查缓存 + let cachedData = {} + let uncachedKeys = [] + + if (useCache) { + const cache = this.getCache() + uncachedKeys = keys.filter(key => { + if (cache[key] && this.isCacheValid(cache[key])) { + cachedData[key] = cache[key].data + return false + } + return true + }) + } else { + uncachedKeys = [...keys] + } + + // 如果所有数据都在缓存中,直接返回 + if (uncachedKeys.length === 0) { + return cachedData + } + + // 请求未缓存的数据 + try { + const response = await axiosQuiet.get('/dict/batch', { + keys: uncachedKeys.join(',') + }) + + if (response && response.code === 1) { + const newData = response.data || {} + + // 更新缓存 + if (useCache) { + this.updateCache(newData) + } + + // 合并缓存数据和新数据 + return { ...cachedData, ...newData } + } else { + console.warn('批量获取字典失败:', response?.msg || '未知错误') + return cachedData // 返回已缓存的数据 + } + } catch (requestError) { + console.warn('批量获取字典请求失败:', requestError) + return cachedData // 返回已缓存的数据 + } + } catch (error) { + console.error('批量获取字典异常:', error) + return {} + } + } + + /** + * 根据业务场景获取字典数据 + * @param {String} scene 业务场景 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getDictByScene(scene, useCache = true) { + if (!scene) { + console.warn('业务场景参数不能为空') + return {} + } + + try { + // 检查场景缓存 + const sceneCacheKey = `scene_${scene}` + if (useCache) { + const cache = this.getCache() + if (cache[sceneCacheKey] && this.isCacheValid(cache[sceneCacheKey])) { + return cache[sceneCacheKey].data + } + } + + const response = await axiosQuiet.get(`/dict/scene/${scene}`) + + if (response && response.code === 1) { + const data = response.data || {} + + // 缓存场景数据 + if (useCache) { + const cacheData = {} + cacheData[sceneCacheKey] = { + data: data.data || {}, + timestamp: Date.now() + } + this.updateCache(cacheData) + } + + return data.data || {} + } else { + console.warn('根据场景获取字典失败:', response?.msg || '未知错误') + return {} + } + } catch (error) { + console.error('根据场景获取字典异常:', error) + return {} + } + } + + /** + * 获取单个字典数据 + * @param {String} key 字典key + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据数组 + */ + async getDict(key, useCache = true) { + if (!key) { + console.warn('字典key不能为空') + return [] + } + + const result = await this.getBatchDict([key], useCache) + return result[key] || [] + } + + /** + * 获取缓存数据 + * @returns {Object} 缓存对象 + */ + getCache() { + try { + const cacheStr = uni.getStorageSync(this.cacheKey) + return cacheStr ? JSON.parse(cacheStr) : {} + } catch (error) { + console.error('获取字典缓存失败:', error) + return {} + } + } + + /** + * 更新缓存 + * @param {Object} data 要缓存的数据 + */ + updateCache(data) { + try { + const cache = this.getCache() + const timestamp = Date.now() + + // 更新缓存数据 + Object.keys(data).forEach(key => { + cache[key] = { + data: data[key], + timestamp: timestamp + } + }) + + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + } catch (error) { + console.error('更新字典缓存失败:', error) + } + } + + /** + * 检查缓存是否有效 + * @param {Object} cacheItem 缓存项 + * @returns {Boolean} 是否有效 + */ + isCacheValid(cacheItem) { + if (!cacheItem || !cacheItem.timestamp) { + return false + } + return (Date.now() - cacheItem.timestamp) < this.cacheExpire + } + + /** + * 清除字典缓存 + * @param {Array} keys 要清除的字典key数组,为空则清除所有 + */ + clearCache(keys = null) { + try { + if (keys === null) { + // 清除所有缓存 + uni.removeStorageSync(this.cacheKey) + } else if (Array.isArray(keys)) { + // 清除指定keys的缓存 + const cache = this.getCache() + keys.forEach(key => { + delete cache[key] + }) + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + } + } catch (error) { + console.error('清除字典缓存失败:', error) + } + } + + /** + * 获取字典映射关系 + * @returns {Promise} 映射关系对象 + */ + async getDictMapping() { + try { + const response = await axiosQuiet.get('/dict/mapping') + + if (response && response.code === 1) { + return response.data || {} + } else { + console.warn('获取字典映射失败:', response?.msg || '未知错误') + return {} + } + } catch (error) { + console.error('获取字典映射异常:', error) + return {} + } + } + + /** + * 将字典数据转换为选择器格式 + * @param {Array} dictData 字典数据 + * @returns {Array} 选择器格式数据 + */ + formatForPicker(dictData) { + if (!Array.isArray(dictData)) { + return [] + } + + return dictData.map(item => ({ + text: item.name || item.text || '', + value: item.value || '', + sort: item.sort || 0, + memo: item.memo || '' + })) + } + + /** + * 根据value查找字典项的名称 + * @param {Array} dictData 字典数据 + * @param {String} value 要查找的值 + * @returns {String} 对应的名称 + */ + getNameByValue(dictData, value) { + if (!Array.isArray(dictData)) { + return '' + } + + const item = dictData.find(item => String(item.value) === String(value)) + return item ? (item.name || item.text || '') : '' + } + + /** + * 预加载常用字典数据 + * @param {Array} keys 要预加载的字典keys + * @returns {Promise} + */ + async preloadDict(keys = []) { + if (!Array.isArray(keys) || keys.length === 0) { + return + } + + try { + await this.getBatchDict(keys, true) + console.log('字典预加载完成:', keys) + } catch (error) { + console.error('字典预加载失败:', error) + } + } + + /** + * 获取客户添加页面需要的字典数据 + * @returns {Promise} 字典数据对象 + */ + async getCustomerAddDict() { + const keys = [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'cognitive_concept', 'decision_maker', 'preliminarycustomerintention', + 'kh_status', 'distance' + ] + return await this.getBatchDict(keys) + } +} + +// 创建单例实例 +const dictUtil = new DictUtil() + +// 扩展 util 对象的字典方法(兼容原有代码) +if (typeof util !== 'undefined') { + // 保持原有的 getDict 方法兼容性 + const originalGetDict = util.getDict + util.getDict = async function(key) { + try { + // 优先使用新的字典工具 + const result = await dictUtil.getDict(key) + if (result && result.length > 0) { + return result + } + + // 如果新工具没有数据,回退到原方法 + if (originalGetDict && typeof originalGetDict === 'function') { + return await originalGetDict.call(this, key) + } + + return [] + } catch (error) { + console.error('获取字典失败:', error) + return [] + } + } + + // 添加新的批量获取方法 + util.getBatchDict = dictUtil.getBatchDict.bind(dictUtil) + util.getDictByScene = dictUtil.getDictByScene.bind(dictUtil) + util.clearDictCache = dictUtil.clearCache.bind(dictUtil) + util.preloadDict = dictUtil.preloadDict.bind(dictUtil) + util.getCustomerAddDict = dictUtil.getCustomerAddDict.bind(dictUtil) +} + +export default dictUtil \ No newline at end of file diff --git a/uniapp/common/dictUtilSimple.js b/uniapp/common/dictUtilSimple.js new file mode 100644 index 00000000..63e5a5b2 --- /dev/null +++ b/uniapp/common/dictUtilSimple.js @@ -0,0 +1,187 @@ +import axiosQuiet from './axiosQuiet.js' + +/** + * 简化版字典工具类 - 用于调试和测试 + */ +class DictUtilSimple { + constructor() { + this.cacheKey = 'dict_cache_simple' + this.cacheExpire = 30 * 60 * 1000 // 30分钟过期 + } + + /** + * 批量获取字典数据 + * @param {Array} keys 字典key数组 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getBatchDict(keys = [], useCache = true) { + console.log('getBatchDict 调用参数:', { keys, useCache }) + + if (!Array.isArray(keys) || keys.length === 0) { + console.warn('字典keys参数必须是非空数组') + return {} + } + + try { + // 检查缓存 + let cachedData = {} + let uncachedKeys = [...keys] + + if (useCache) { + const cache = this.getCache() + uncachedKeys = keys.filter(key => { + if (cache[key] && this.isCacheValid(cache[key])) { + cachedData[key] = cache[key].data + return false + } + return true + }) + } + + console.log('缓存检查结果:', { cachedData, uncachedKeys }) + + // 如果所有数据都在缓存中,直接返回 + if (uncachedKeys.length === 0) { + console.log('所有数据来自缓存') + return cachedData + } + + // 请求未缓存的数据 + console.log('开始请求未缓存的数据:', uncachedKeys) + + const response = await axiosQuiet.get('/dict/batch', { + keys: uncachedKeys.join(',') + }) + + console.log('API响应:', response) + + if (response && response.code === 1) { + const newData = response.data || {} + console.log('获取到新数据:', newData) + + // 更新缓存 + if (useCache) { + this.updateCache(newData) + } + + // 合并缓存数据和新数据 + const result = { ...cachedData, ...newData } + console.log('最终结果:', result) + return result + } else { + console.warn('批量获取字典失败:', response?.msg || '未知错误') + return cachedData // 返回已缓存的数据 + } + } catch (error) { + console.error('批量获取字典异常:', error) + return {} + } + } + + /** + * 获取单个字典数据 + * @param {String} key 字典key + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据数组 + */ + async getDict(key, useCache = true) { + console.log('getDict 调用参数:', { key, useCache }) + + if (!key) { + console.warn('字典key不能为空') + return [] + } + + try { + const result = await this.getBatchDict([key], useCache) + return result[key] || [] + } catch (error) { + console.error('获取单个字典失败:', error) + return [] + } + } + + /** + * 获取缓存数据 + * @returns {Object} 缓存对象 + */ + getCache() { + try { + const cacheStr = uni.getStorageSync(this.cacheKey) + const cache = cacheStr ? JSON.parse(cacheStr) : {} + console.log('获取缓存:', cache) + return cache + } catch (error) { + console.error('获取字典缓存失败:', error) + return {} + } + } + + /** + * 更新缓存 + * @param {Object} data 要缓存的数据 + */ + updateCache(data) { + try { + const cache = this.getCache() + const timestamp = Date.now() + + // 更新缓存数据 + Object.keys(data).forEach(key => { + cache[key] = { + data: data[key], + timestamp: timestamp + } + }) + + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + console.log('缓存已更新:', cache) + } catch (error) { + console.error('更新字典缓存失败:', error) + } + } + + /** + * 检查缓存是否有效 + * @param {Object} cacheItem 缓存项 + * @returns {Boolean} 是否有效 + */ + isCacheValid(cacheItem) { + if (!cacheItem || !cacheItem.timestamp) { + return false + } + const isValid = (Date.now() - cacheItem.timestamp) < this.cacheExpire + console.log('缓存有效性检查:', { cacheItem, isValid }) + return isValid + } + + /** + * 清除字典缓存 + * @param {Array} keys 要清除的字典key数组,为空则清除所有 + */ + clearCache(keys = null) { + try { + if (keys === null) { + // 清除所有缓存 + uni.removeStorageSync(this.cacheKey) + console.log('已清除所有字典缓存') + } else if (Array.isArray(keys)) { + // 清除指定keys的缓存 + const cache = this.getCache() + keys.forEach(key => { + delete cache[key] + }) + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + console.log('已清除指定字典缓存:', keys) + } + } catch (error) { + console.error('清除字典缓存失败:', error) + } + } +} + +// 创建单例实例 +const dictUtilSimple = new DictUtilSimple() + +export default dictUtilSimple \ No newline at end of file diff --git a/uniapp/pages.json b/uniapp/pages.json index ff208762..d90c3ca1 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -654,7 +654,8 @@ "style": { "navigationBarTitleText": "服务详情", "navigationBarBackgroundColor": "#29d3b4", - "navigationBarTextStyle": "black" + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true } }, { diff --git a/uniapp/pages/coach/my/service_detail.vue b/uniapp/pages/coach/my/service_detail.vue index cef9645d..3e3d50c6 100644 --- a/uniapp/pages/coach/my/service_detail.vue +++ b/uniapp/pages/coach/my/service_detail.vue @@ -2,139 +2,465 @@ - \ No newline at end of file diff --git a/uniapp/pages/demo/dict_optimization.vue b/uniapp/pages/demo/dict_optimization.vue new file mode 100644 index 00000000..e7723979 --- /dev/null +++ b/uniapp/pages/demo/dict_optimization.vue @@ -0,0 +1,542 @@ + + + + + \ No newline at end of file diff --git a/uniapp/pages/market/clue/add_clues.vue b/uniapp/pages/market/clue/add_clues.vue index 086b0972..eea126c5 100644 --- a/uniapp/pages/market/clue/add_clues.vue +++ b/uniapp/pages/market/clue/add_clues.vue @@ -528,6 +528,8 @@ import commonApi from '@/api/common.js'; import marketApi from '@/api/market.js'; import memberApi from '@/api/member.js'; import util from '@/common/util.js'; +import dictUtil from '@/common/dictUtil.js'; +import dictUtilSimple from '@/common/dictUtilSimple.js'; const rules = [ @@ -731,26 +733,122 @@ export default { ], } }, + onLoad() { + // 预加载字典数据 + this.preloadDictData() + }, onShow() { this.init() }, methods: { + // 预加载字典数据 + async preloadDictData() { + const dictKeys = [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'preliminarycustomerintention', 'cognitive_concept', + 'kh_status', 'decision_maker', 'distance' + ] + + // 静默预加载,不阻塞页面显示 + // 暂时使用简化版本进行测试 + dictUtilSimple.getBatchDict(dictKeys).catch(error => { + console.warn('字典预加载失败:', error) + }) + }, + //初始化 async init() { //获取登录用户信息 await this.getUserInfo() - await this.getDict('source_channel')//获取字典-来源渠道 - await this.getDict('source')//获取字典-来源 - await this.getDict('purchasing_power')//获取字典-购买力 - await this.getDict('initial_intent')//获取字典-客户初步意向度 - await this.getDict('cognitive_idea')//获取字典-认知理念 - await this.getDict('status')//获取字典-客户状态 - await this.getDict('decision_maker')//获取字典-决策人 - await this.getDict('distance')//获取字典-距离 + // 使用批量获取字典数据,提升性能 + await this.getBatchDictData() + // this.getStaffList()//获取人员列表 // this.getAreaTree()//获取地区树形结构 }, + + // 批量获取字典数据 + async getBatchDictData() { + try { + // 定义需要的字典keys + const dictKeys = [ + 'SourceChannel', // 来源渠道 + 'source', // 来源 + 'customer_purchasing_power', // 购买力 + 'preliminarycustomerintention', // 客户初步意向度 + 'cognitive_concept', // 认知理念 + 'kh_status', // 客户状态 + 'decision_maker', // 决策人 + 'distance' // 距离 + ] + + // 批量获取字典数据(静默获取,不显示加载提示) + // 暂时使用简化版本进行测试 + const dictData = await dictUtilSimple.getBatchDict(dictKeys) + + // 处理字典数据 + this.processDictData(dictData) + + console.log('批量获取字典数据成功:', dictData) + + } catch (error) { + console.error('批量获取字典数据失败:', error) + + // 如果批量获取失败,回退到单个获取 + await this.fallbackGetDict() + } + }, + + // 处理批量获取的字典数据 + processDictData(dictData) { + // 字典key与本地配置的映射关系 + const keyMapping = { + 'SourceChannel': 'source_channel', + 'source': 'source', + 'customer_purchasing_power': 'purchasing_power', + 'preliminarycustomerintention': 'initial_intent', + 'cognitive_concept': 'cognitive_idea', + 'kh_status': 'status', + 'decision_maker': 'decision_maker', + 'distance': 'distance' + } + + // 处理每个字典数据 + Object.keys(keyMapping).forEach(dictKey => { + const localKey = keyMapping[dictKey] + const dictItems = dictData[dictKey] || [] + + if (Array.isArray(dictItems) && dictItems.length > 0) { + const formattedOptions = dictItems.map(item => ({ + text: item.name || '', + value: item.value || '' + })) + + // 设置到picker配置中 + if (this.picker_config[localKey]) { + this.picker_config[localKey].options = formattedOptions + } + } + }) + }, + + // 回退方案:单个获取字典(兼容原有逻辑) + async fallbackGetDict() { + console.log('使用回退方案获取字典数据') + try { + await this.getDict('source_channel') + await this.getDict('source') + await this.getDict('purchasing_power') + await this.getDict('initial_intent') + await this.getDict('cognitive_idea') + await this.getDict('status') + await this.getDict('decision_maker') + await this.getDict('distance') + } catch (error) { + console.error('回退方案也失败了:', error) + } + }, async get_campus_list(){ let res = await apiRoute.common_getCampusesList({}) diff --git a/uniapp/pages/test/dict_test.vue b/uniapp/pages/test/dict_test.vue new file mode 100644 index 00000000..85e703e3 --- /dev/null +++ b/uniapp/pages/test/dict_test.vue @@ -0,0 +1,263 @@ + + + + + \ No newline at end of file