diff --git a/app/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php index 4e963ea..1bf914e 100644 --- a/app/admin/controller/ApprovalRecordsController.php +++ b/app/admin/controller/ApprovalRecordsController.php @@ -47,7 +47,9 @@ class ApprovalRecordsController extends BaseController 'step_index' => $record['step_index'], 'action' => $record['action'], - 'status' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), 'comment' => $record['comment'], 'approved_at' => $record['approved_at'], diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 6a6d6b6..d936e5d 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -11,7 +11,6 @@ use app\common\model\StockOrders; use app\common\model\SysCurrency; use support\Request; use app\common\model\User; -use support\exception\BusinessException; use support\Db; class StockOrdersService @@ -63,6 +62,10 @@ class StockOrdersService } $config['order']['id'] = 'desc'; + + // $config['dataLimit']['type'] = 'relation'; + // $config['dataLimit']['userId'] = $request->user->uid; + // $config['dataLimit']['userField'] = 'salesperson_id'; break; case 'print': $config['where']['id'] = $p['id']; diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index b794532..9916b6e 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -112,7 +112,7 @@ class ApprovalFlowService if ((int)$instance->current_step !== $idx) { // return ['status' => 'noop', 'reason' => 'not_current_step', 'current_step' => (int)$instance->current_step]; // throw new BusinessException("非当前步骤"); - return ['status' => 'noop', 'msg' => '非当前步骤']; + return ['status' => 'noop', 'msg' => '非当前审批']; } // return ['status' => 'noop', 'reason' => 'no_pending_record']; return ['status' => 'noop', 'msg' => '无待处理记录']; @@ -136,7 +136,7 @@ class ApprovalFlowService if ($outcome === 'pending') { // return ['status' => 'step_pending']; - return ['status' => 'step_pending', 'msg' => '步骤待定']; + return ['status' => 'step_pending', 'msg' => '审批待定']; } // 3) 有结论:关闭本步其余 pending @@ -386,10 +386,11 @@ class ApprovalFlowService $resolved = array_unique(array_filter(array_map('intval', $resolved))); if ($avoidSelf) { - $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + // $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + $resolved = array_values($resolved); } if (empty($resolved)) { - throw new BusinessException("步骤未解析到任何审批人"); + throw new BusinessException("未解析到任何审批人"); } return $resolved; } diff --git a/app/common/utils/approval/service/ApprovalFlowService_bak.php b/app/common/utils/approval/service/ApprovalFlowService_bak.php deleted file mode 100644 index f988a48..0000000 --- a/app/common/utils/approval/service/ApprovalFlowService_bak.php +++ /dev/null @@ -1,751 +0,0 @@ -where('type', $flowType) - ->where('is_active', 1) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - } - - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) { - throw new BusinessException("流程步骤配置错误"); - } - - // 2) 寻找首个有效步骤索引(考虑条件) - $firstIndex = $this->getNextStepIndex($steps, $formData, -1); - if ($firstIndex === null) { - throw new BusinessException("未找到可执行的审批步骤"); - } - $firstStep = $steps[$firstIndex]; - - // 3) 计算实例截止时间(实例级,可扩展到步骤级) - $deadlineAt = null; - if (($flow->timeout_hours ?? 0) > 0) { - $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); - } - - // 4) 创建审批实例 - $now = date('Y-m-d H:i:s'); - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'initiator_id' => $initiatorId, - 'status' => 'processing', - 'current_step' => $firstIndex, - 'deadline_at' => $deadlineAt, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - // 5) 创建首步待办 - $instance = (object)[ - 'id' => $instanceId, - 'flow_type' => $flowType, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'initiator_id' => $initiatorId, - 'current_step' => $firstIndex, - 'status' => 'processing', - ]; - $approverIds = $this->resolveApprovers($firstStep, $instance, true); - - foreach ($approverIds as $uid) { - // 唯一键会防重复 - $this->insertApprovalRecord($instanceId, $firstIndex, $uid); - } - - // 6) 通知审批人 & 抄送 - $this->sendNotification($approverIds, $instance, 'pending', false); - $this->notifyCCs($firstStep, $instance, 'pending'); - - return ['instance_id' => $instanceId]; - }); - } - - /** - * 执行审批 - */ - public function approve(int $instanceId, int $approverId, string $action, string $comment = '') - { - if (!in_array($action, ['approve', 'reject'], true)) { - throw new BusinessException("无效的审批操作"); - } - - return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { - // 锁实例,保证并发安全 - $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); - if (!$instance) { - throw new BusinessException("审批实例未找到"); - } - if ($instance->status !== 'processing') { - throw new BusinessException("流程已结束或不可审批"); - } - - // 读取流程与当前步 - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) throw new BusinessException("审批流程定义未找到"); - - $steps = json_decode($flow->steps, true); - $idx = (int)$instance->current_step; - $step = $steps[$idx] ?? null; - if (!$step) throw new BusinessException("当前步骤配置错误"); - - // 锁当前步所有记录,避免同时推进 - $pendingRecord = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('step_index', $idx) - ->where('status', 'pending') - ->lockForUpdate() - ->first(); - if (!$pendingRecord) { - throw new BusinessException("未找到待审批记录"); - } - - $now = date('Y-m-d H:i:s'); - - // 1) 更新当前人的审批记录 - Db::table('approval_records')->where('id', $pendingRecord->id)->update([ - 'action' => $action, - 'status' => ($action === 'approve') ? 'approved' : 'rejected', - 'comment' => $comment, - 'approved_at' => $now, - 'updated_at' => $now, - ]); - - // 2) 聚合判断本步骤结果 - $outcome = $this->getStepOutcome($instanceId, $idx); // 'pending'|'approve'|'reject' - $type = $step['type'] ?? 'all'; - - $stepDone = false; $finalReject = false; $finalApprove = false; - if ($type === 'any') { - if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } - elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } - } else { // all - if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } - elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } - } - - if (!$stepDone) { - return ['status' => 'step_pending']; - } - - // 3) 本步已有结论:关闭其余 pending 待办为 skipped - $this->closeOtherPending($instanceId, $idx, [$approverId]); - - // 4) 步骤结论为拒绝 -> 结束 - if ($finalReject) { - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => 'rejected', - 'finished_at' => $now, - 'updated_at' => $now, - ]); - $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); - return ['status' => 'completed', 'result' => 'reject']; - } - - // 5) 进入下一步(基于表单快照与条件继续寻找) - $formData = json_decode($instance->form_data, true) ?: []; - $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); - if ($nextIndex === null) { - // 流程结束(全通过) - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => 'approved', - 'finished_at' => $now, - 'updated_at' => $now, - ]); - $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); - return ['status' => 'completed', 'result' => 'approve']; - } - - // 6) 推进实例 current_step(乐观条件) - $affected = Db::table('approval_instances') - ->where('id', $instanceId) - ->where('current_step', $idx) - ->where('status', 'processing') - ->update([ - 'current_step' => $nextIndex, - 'updated_at' => $now, - ]); - if (!$affected) { - throw new BusinessException('流程已被他人推进,请刷新后重试'); - } - - // 7) 生成下一步待办 + 通知 - $nextStep = $steps[$nextIndex]; - $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); - foreach ($nextApprovers as $uid) { - $this->insertApprovalRecord($instanceId, $nextIndex, $uid); - } - $this->sendNotification($nextApprovers, $instance, 'pending', false); - $this->notifyCCs($nextStep, $instance, 'pending'); - - return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; - }); - } - - /** - * 撤回流程(可选) - * 仅限发起人在第一条“已处理记录”出现前撤回 - */ - public function revoke(int $instanceId, int $initiatorId, string $reason = '') - { - return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { - $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); - if (!$instance) throw new BusinessException('审批实例未找到'); - if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); - - if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); - - // 已有处理记录则不允许撤回 - $handled = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->whereIn('status', ['approved','rejected','timeout','skipped']) - ->count() > 0; - if ($handled) throw new BusinessException('当前进度不可撤回'); - - $now = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => 'revoked', - 'finished_at' => $now, - 'updated_at' => $now, - ]); - - // 关闭所有 pending 待办 - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); - - $this->sendNotification([$initiatorId], $instance, 'revoked', false); - return ['status' => 'completed', 'result' => 'revoked']; - }); - } - - /** - * 我的待办 - */ - public function getPendingApprovals(int $userId): array - { - return Db::table('approval_records as ar') - ->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id') - ->where('ar.approver_id', $userId) - ->where('ar.status', 'pending') - ->where('ai.status', 'processing') - ->select('ai.*') - ->distinct() - ->get() - ->toArray(); - } - - /** - * 审批历史 - */ - public function getApprovalHistory(int $instanceId): array - { - return Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderByRaw('COALESCE(approved_at, created_at) asc') - ->get() - ->toArray(); - } - - /** - * 流程图数据(识别 skipped) - */ - public function getFlowDiagramData(int $instanceId): array - { - $instance = Db::table('approval_instances')->find($instanceId); - if (!$instance) return []; - - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) return []; - - $steps = json_decode($flow->steps, true) ?: []; - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') - ->get() - ->groupBy('step_index'); - - $formData = json_decode($instance->form_data, true) ?: []; - $data = []; - foreach ($steps as $i => $step) { - $actions = ($records->get($i) ?? collect())->toArray(); - - $status = 'pending'; - if (!empty($actions)) { - $hasReject = false; $allApproved = true; - foreach ($actions as $a) { - if ($a->status === 'rejected') $hasReject = true; - if ($a->status !== 'approved') $allApproved = false; - } - if ($hasReject) $status = 'rejected'; - elseif ($allApproved) $status = 'approved'; - else $status = 'processing'; - } else { - // 无记录:可能被跳过、未到达、已过 - if ($i < (int)$instance->current_step) { - // 如果条件不满足,认为 skipped,否则视为 approved(历史兼容) - if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { - $status = 'skipped'; - } else { - $status = 'approved'; - } - } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { - $status = 'processing'; - } else { - // 还未到达 - $status = 'pending'; - } - } - - $data[] = [ - 'index' => $i, - 'name' => $step['name'] ?? ('Step '.$i), - 'type' => $step['type'] ?? 'all', - 'status' => $status, - 'actions' => $actions, - ]; - } - return $data; - } - - /** - * 定时任务:处理超时实例(实例级) - */ - public function handleTimeouts() - { - $now = date('Y-m-d H:i:s'); - $expired = Db::table('approval_instances') - ->where('status', 'processing') - ->whereNotNull('deadline_at') - ->where('deadline_at', '<=', $now) - ->get(); - - foreach ($expired as $instance) { - Db::transaction(function () use ($instance, $now) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) return; - - $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; - $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; - - // 更新实例 - Db::table('approval_instances')->where('id', $instance->id)->update([ - 'status' => ($final === 'approved') ? 'approved' : 'rejected', - 'finished_at' => $now, - 'updated_at' => $now, - ]); - - // 关闭当前步所有 pending 记录为 timeout - Db::table('approval_records') - ->where('instance_id', $instance->id) - ->where('step_index', $instance->current_step) - ->where('status', 'pending') - ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); - - $this->notifyInitiator($instance, $finalAction); - }); - } - } - - // === 内部工具 === - - /** - * 解析审批人 - * $avoidSelf:是否过滤发起人本人 - */ - private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array - { - $resolved = []; - $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - - $confs = $step['approvers'] ?? []; - foreach ($confs as $cfg) { - $typ = $cfg['type'] ?? ''; - $val = $cfg['value'] ?? null; - $ids = []; - - switch ($typ) { - case 'role': - // $ids = $this->getUserIdsByRole($val); - - // 支持: - // - {"type":"role","value":1} // 按 role_id - // - {"type":"role","value":"销售经理"} // 按 roles.name - // - {"type":"role","value":"销售经理","system_id": 2} // 限定系统;默认也包含全局(system_id=0) - $val = $cfg['value'] ?? null; - $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; - // include_global 默认 true(把 system_id=0 的“全局角色”也算上) - $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; - $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); - - break; - case 'user': - $ids = [(int)$val]; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); - if ($leaderId) $ids = [(int)$leaderId]; - break; - case 'field_user': - if (isset($formData[$val])) $ids = [(int)$formData[$val]]; - break; - case 'dept_head': - $head = $this->getDeptHeadId((int)$val); - if ($head) $ids = [(int)$head]; - break; - default: - throw new BusinessException("不支持的审批人类型: {$typ}"); - } - $resolved = array_merge($resolved, $ids); - } - - $resolved = array_unique(array_filter(array_map('intval', $resolved))); - if ($avoidSelf) { - $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); - } - if (empty($resolved)) { - throw new BusinessException("步骤未解析到任何审批人"); - } - return $resolved; - } - - /** - * 通知抄送人 - */ - private function notifyCCs(array $step, object $instance, string $action = 'pending'): void - { - $ccConfig = $step['cc'] ?? []; - if (empty($ccConfig)) return; - - $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - $uids = []; - - foreach ($ccConfig as $cc) { - $typ = $cc['type'] ?? ''; - $val = $cc['value'] ?? null; - - switch ($typ) { - case 'role': - $uids = array_merge($uids, $this->getUserIdsByRole($val)); - break; - case 'user': - $uids[] = (int)$val; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); - if ($leaderId) $uids[] = (int)$leaderId; - break; - case 'field_user': - if (isset($formData[$val])) $uids[] = (int)$formData[$val]; - break; - case 'dept_head': - $head = $this->getDeptHeadId((int)$val); - if ($head) $uids[] = (int)$head; - break; - default: - // ignore - break; - } - } - - $uids = array_unique(array_filter(array_map('intval', $uids))); - if (empty($uids)) return; - - $now = date('Y-m-d H:i:s'); - foreach ($uids as $uid) { - // 唯一键防重;这里仍做一次 exists 判断 - $exists = Db::table('approval_ccs') - ->where('instance_id', $instance->id) - ->where('user_id', $uid) - ->count() > 0; - if (!$exists) { - Db::table('approval_ccs')->insert([ - 'instance_id' => $instance->id, - 'user_id' => $uid, - 'notified_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - $this->sendNotification($uids, $instance, $action, true); - } - - /** - * 条件求值:支持 {field} {op} {value} - * op: ==, !=, >, <, >=, <= - */ - private function evaluateCondition(?string $condition, array $formData): bool - { - if (!$condition) return true; - - $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; - if (!preg_match($pattern, $condition, $m)) { - throw new BusinessException("条件格式错误: {$condition}"); - } - - $field = $m[1]; - $op = $m[2]; - $value = trim($m[3], "\"'"); - - $actual = $formData[$field] ?? null; - - // 数值比较 - if (is_numeric($value) && is_numeric($actual)) { - $value = (float)$value; - $actual = (float)$actual; - } - - switch ($op) { - case '==': return $actual == $value; - case '!=': return $actual != $value; - case '>': return $actual > $value; - case '<': return $actual < $value; - case '>=': return $actual >= $value; - case '<=': return $actual <= $value; - } - throw new BusinessException("不支持的操作符: {$op}"); - } - - /** - * 获取下一个有效步骤索引(考虑 condition) - * @param int $currentStepIndex 从该索引之后开始找;如果传 -1 则从开头找“首个有效” - */ - private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int - { - for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { - $s = $steps[$i]; - if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { - return $i; - } - } - return null; - } - - /** - * 聚合本步骤结论 - * 返回:'pending' | 'approve' | 'reject' - */ - private function getStepOutcome(int $instanceId, int $stepIndex): string - { - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('step_index', $stepIndex) - ->get(); - - $hasPending = false; $hasReject = false; $allApproved = true; - - foreach ($records as $r) { - if ($r->status === 'pending') { $hasPending = true; $allApproved = false; } - elseif ($r->status === 'rejected') { $hasReject = true; $allApproved = false; } - elseif ($r->status !== 'approved') { $allApproved = false; } - } - - if ($hasPending) return 'pending'; - if ($hasReject) return 'reject'; - return $allApproved ? 'approve' : 'reject'; - } - - /** - * 关闭本步其余待办(标记为 skipped) - */ - private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void - { - $now = date('Y-m-d H:i:s'); - $q = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('step_index', $stepIndex) - ->where('status', 'pending'); - - if (!empty($keepApproverIds)) { - $q->whereNotIn('approver_id', $keepApproverIds); - } - - $q->update([ - 'status' => 'skipped', - 'approved_at' => $now, - 'updated_at' => $now, - ]); - } - - private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void - { - $now = date('Y-m-d H:i:s'); - - // 防重复:唯一键兜底,这里先查一次 - $exists = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('step_index', $stepIndex) - ->where('approver_id', $approverId) - ->count() > 0; - - if (!$exists) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'approver_id' => $approverId, - 'step_index' => $stepIndex, - 'action' => null, // 待办阶段无动作 - 'status' => 'pending', // 明确 pending - 'comment' => null, - 'approved_at' => null, // 未审批为 NULL - 'created_at' => $now, - 'updated_at' => $now, - ]); - } - } - - private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void - { - $map = [ - 'pending' => '待审批', - 'approve' => '已通过', - 'reject' => '已拒绝', - 'revoked' => '已撤回', - ]; - $prefix = $isCC ? '[抄送]' : ''; - $text = $map[$action] ?? $action; - $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; - // TODO: 接入你的消息系统(站内信/企微/飞书…) - // MessageService::send($userIds, $msg); - // echo "通知 ".implode(',', $userIds).":{$msg}\n"; - } - - private function notifyInitiator(object $instance, string $result): void - { - $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); - } - - // === 数据访问 === - - // private function getUserIdsByRole(string $roleCode): array - // { - // // 兼容 pluck 行为(不同版本可能返回 array 或 Collection) - // $rows = Db::table('user_roles as ur') - // ->join('roles as r', 'ur.role_id', '=', 'r.id') - // ->where('r.code', $roleCode) - // ->select('ur.user_id') - // ->get(); - - // $ids = []; - // foreach ($rows as $row) { - // $ids[] = (int)$row->user_id; - // } - // return array_values(array_unique($ids)); - // } - - /** - * 从角色取用户ID列表 - * - * @param mixed $roleIdOrName 数字 => role_id,字符串 => roles.name 精确匹配 - * @param int|null $systemId 可选:限定 roles.system_id;默认不传=不过滤 - * @param bool $onlyActive 是否仅取 status=1 的角色(默认 true) - * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true - * @return int[] - */ - private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array - { - // 查询构造 - // 情况A:纯数字 => 当作 role_id,优先直接走 user_roles(role_id) 索引 - if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { - $roleId = (int)$roleIdOrName; - - // 如果不需要校验 roles 的 system/status,就不 join,性能更好 - $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; - - if (!$needJoin) { - $rows = Db::table('user_roles') - ->where('role_id', $roleId) - ->select('user_id') - ->get(); - } else { - $q = Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('ur.role_id', $roleId) - ->select('ur.user_id'); - - if ($onlyActive) { - $q->where('r.status', 1); - } - if (!is_null($systemId)) { - if ($includeGlobal) { - $q->whereIn('r.system_id', [0, $systemId]); - } else { - $q->where('r.system_id', $systemId); - } - } - $rows = $q->get(); - } - } - // 情况B:字符串 => 当作 roles.name - else { - $name = (string)$roleIdOrName; - $q = Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.name', $name) - ->select('ur.user_id'); - - if ($onlyActive) { - $q->where('r.status', 1); - } - if (!is_null($systemId)) { - if ($includeGlobal) { - $q->whereIn('r.system_id', [0, $systemId]); - } else { - $q->where('r.system_id', $systemId); - } - } - $rows = $q->get(); - } - - $ids = []; - foreach ($rows as $row) { - $ids[] = (int)$row->user_id; - } - return array_values(array_unique($ids)); - } - - - private function getLeaderIdByUserId(int $userId): ?int - { - $id = Db::table('user')->where('id', $userId)->value('report_to'); - return $id ? (int)$id : null; - } - - private function getDeptHeadId(int $deptId): ?int - { - $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); - return $id ? (int)$id : null; - } -} \ No newline at end of file diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index 6b93ecc..850f31f 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -240,15 +240,61 @@ -
-
-
- 下单人: - + + +
+ +
+ +
+ 业务员签字: + + + + + +
+ +
+ 备发货负责人签字: + + + + +
-
下单时间:
+ + +
+
+ 组长签字: + + + + + +
+ +
+ 日期: + +
+
+ +
+
diff --git a/app/common/utils/pdf/template/stockOrder_bak.stub b/app/common/utils/pdf/template/stockOrder_bak.stub new file mode 100644 index 0000000..6b93ecc --- /dev/null +++ b/app/common/utils/pdf/template/stockOrder_bak.stub @@ -0,0 +1,255 @@ + + + + + 合塑实业-汽车事业部-备货单(客户下定) + + + +
+ +
+

合塑实业-汽车事业部-备货单(客户下定)

+
+ 备货单号: + 业务员: +
+
+ + + + + + + + + + + + + + + + + + + + + +
阿里订单号/PI号: 日期: 关目的国: 客户来源: 客户名称: 客户所在国:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + +
编号车型数量/台颜色/外内饰改装单价总价官方指导价/二手挂牌价优惠/二手减价
+ 合同汇率: + + 合计到岸价: +
+ + + + + + + + + + + + + + + + + + + + + +
清关人: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /最迟到港时间:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
计划收款信息序号汇款国日期币种账户汇款人金额
+ +
+
+
+ 下单人: + +
+
+
下单时间:
+
+ +
+ + \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 28cb1d0..289d5b7 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -127,5 +127,8 @@ class IndexController // $up = new UploadService(); // $res = $up->uploadLocalFile($param); // var_dump($res); + + $service = new \app\common\service\StockOrdersService(); + $service->createdPdfFile(60); } } \ No newline at end of file diff --git a/app/crm/controller/ApprovalRecordsController.php b/app/crm/controller/ApprovalRecordsController.php index 0c55e0d..3edc6b5 100644 --- a/app/crm/controller/ApprovalRecordsController.php +++ b/app/crm/controller/ApprovalRecordsController.php @@ -46,7 +46,9 @@ class ApprovalRecordsController extends BaseController 'step_index' => $record['step_index'], 'action' => $record['action'], - 'status' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), 'comment' => $record['comment'], 'approved_at' => $record['approved_at'], diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index cd2b382..45a3632 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -3,7 +3,7 @@ namespace app\crm\controller; use app\common\controller\BaseController; use app\common\traits\crud\CrudControllerTrait; -use app\common\service\StockOrdersService; +use app\crm\service\StockOrdersService; use support\Request; class StockOrdersController extends BaseController diff --git a/app/crm/service/StockOrdersService.php b/app/crm/service/StockOrdersService.php new file mode 100644 index 0000000..70bb4ec --- /dev/null +++ b/app/crm/service/StockOrdersService.php @@ -0,0 +1,434 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + + // ====== ====== 筛选 ====== ====== + // 业务id + if (isset($p['salesperson_id']) && $p['salesperson_id']) { + $config['where']['salesperson_id'] = $p['salesperson_id']; + } + // 初、终版 + $config['where']['is_final'] = $p['is_final'] ?? 0; + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['id'] = 'desc'; + + $config['where']['salesperson_id'] = $request->user->uid; + break; + case 'print': + $config['where']['id'] = $p['id']; + break; + case 'show': + $config['where']['id'] = $p['id']; + + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function serviceStore(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); // 表1 + + // 车辆 + $cifTotalAmount = 0; + $cifTotalAmountRate = 0; + $cifTotalAmountCurrency = 'CNY'; + // $profit = 0; + foreach ($data['veh_list'] as $veh) { + $veh['order_id'] = $created->id; + + // 唯一标识 + $uniqueIdStr = ""; + $uniqueIdStr .= $veh['sku_id']; + $uniqueIdStr .= $veh['model_snapshot']; + $uniqueIdStr .= $veh['color_interior_code']; + $uniqueIdStr .= $veh['color_exterior_code']; + $uniqueIdStr .= $veh['mileage']; + $uniqueIdStr .= json($veh['modify_list']); + $veh['unique_id'] = md5($uniqueIdStr); + $item = StockOrderItems::create($veh); // 表2 + + // 金额 // 表3 + // $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + // $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + if ($veh['unit_price']['amount']) { + $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); + } + if ($veh['total_price']['amount']) { + $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); + } + if ($veh['handling_fee']['amount']) { + $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); + } + if ($veh['fob_transport_fee']['amount']) { + $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); + } + if ($veh['port_misc_fee']['amount']) { + $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); + } + if ($veh['retrofit_fee']['amount']) { + $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); + } + + // 二手车 or 新车 + if ($veh['mileage'] > 0) { + if ($veh['listing_price']['amount']) { + $item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价'); + } + if ($veh['reduction_price']['amount']) { + $item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价'); + } + + $ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['reduction_price']['amount']; + } else { + if ($veh['official_price']['amount']) { + $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + } + if ($veh['discount_price']['amount'] ) { + $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + } + + $ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['discount_price']['amount']; + } + + // 利润 + $profit = (float)$veh['unit_price']['amount'] - (float)$ttPrice - (float)$ddPrice - (float)$veh['handling_fee']['amount'] - (float)$veh['fob_transport_fee']['amount'] - (float)$veh['port_misc_fee']['amount'] - (float)$veh['retrofit_fee']['amount']; + $item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润'); + + // cif_total_amount + $cifTotalAmount += $veh['total_price']['amount']; + $cifTotalAmountRate = $veh['retrofit_fee']['rate']; + $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; + + // 改装 + if (count($veh['modify_list']) > 0) { + foreach ($veh['modify_list'] as $modify) { + $modify['order_item_id'] = $item->id; + StockOrderItemOptions::create($modify); // 表4 + } + } + } + + // 计算&&插入 表1 + $created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价'); + + // 账户 + if (count($data['account_list']) > 0) { + foreach ($data['account_list'] as $account) { + $account['order_id'] = $created->id; + $account['remit_date'] = strtotime($account['remit_date']); + StockOrderAccounts::create($account); // 表5 + } + } + + return $this->postProcess('Store', $created); + }); + } + + public function beforeServiceStore($data, Request $request) + { + $userId = $request->user->uid; + + $data['order_no'] = generateStockNo(); + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['salesperson_id'] = $userId; + $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; + + return $data; + } + + protected function afterServiceStore($res) + { + // 处理原单 + if ($res->is_final == 1 && $res->object_id > 0) { + $order = StockOrders::find($res->object_id); + $order->is_use = 1; + $order->object_id = $res->id; + if (!$order->save()) { + flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); + } + } + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); + var_dump($result); + + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); + return $res; + } + + + public function createdPdfFile($id) + { + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + + // 渲染模板为最终HTML + $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); + $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); + + // 生成PDF + $driver = new \app\common\utils\pdf\service\MpdfDriver(); + $pdfBin = $driver->htmlToPdf($html); + + // 存储 + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + + // 本地保存 + \app\common\utils\pdf\service\PdfStorage::saveBinary( + $pdfBin, + $dir, + $fileName + ); + + // // 在线预览 + // return new \support\Response(200, [ + // 'Content-Type' => 'application/pdf', + // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', + // ], $pdfBin); + + // $url = [ + // 'file_name' => $fileName, + // 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + // ]; + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName + ]; + $up = new \app\common\service\UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); + } + + + public function pdfData($item) + { + $vehList = []; + $index = 0; + foreach ($item->items as $k => $v) { + // 存在则数量加1 + if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { + $vehList[$v->unique_id]['num'] += 1; + $vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price']; + $vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ','); + continue; + } + $index += 1; + // 处理金额 + $mv = []; + foreach ($v->moneyValues as $money) { + $mv[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'), + ]; + } + // 处理改装 + $mfy = ''; + foreach ($v->options as $my) { + $mfy .= $my->option_name_snapshot.' '; + } + + // 处理结果集 + $vehList[$v->unique_id] = [ + 'index' => $index, + 'model_snapshot' => $v->model_snapshot, + 'color_interior_snapshot' => $v->color_interior_snapshot, + 'color_exterior_snapshot' => $v->color_exterior_snapshot, + 'num' => 1, + 'modify' => $mfy, + 'mileage' => $v->mileage, + 'unit_price' => $mv['unit_price']['amount'], + 'total_price' => $mv['total_price']['amount'], + 'unit_price_symbol' => $mv['unit_price']['currency_symbol'], + 'total_price_symbol' => $mv['total_price']['currency_symbol'], + 'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','), + 'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',') + ]; + + // 根据里程处理 新车or二手车 + if ($v->mileage > 0) { + if (isset($mv['reduction_price'])) { + $vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount']; + $vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol']; + $vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount']; + $vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol']; + $vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ','); + } else { + if (isset($mv['discount_price'])) { + $vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount']; + $vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol']; + $vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount']; + $vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol']; + $vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','); + } + } + + $accList = []; + $accIndex = 1; + foreach ($item->accounts as $k => $v) { + $accList[] = [ + 'index' => '收款'.$k + 1, + 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $v->remit_date), + 'currency_name_snapshot' => $v->currency_name_snapshot, + 'account_name_snapshot' => $v->account_name_snapshot, + 'remitter_name' => $v->remitter_name, + 'amount' => $v->amount, + ]; + $accIndex += 1; + } + + $cifTotalAmount = 0.00; + if ($item->moneyValues) { + $cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol'); + $cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate); + } + + $veh = [ + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + // 'sign_img' => $item->salesperson->sign_img, + 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), + + 'pi_no' => $item->pi_no, + 'biz_date' => date('Y-m-d', $item->biz_date), + 'clearance_country_snapshot' => $item->clearance_country_snapshot, + 'customer_source_snapshot' => $item->customer_source_snapshot, + 'customer_snapshot' => $item->customer_snapshot, + 'customer_country_snapshot' => $item->customer_country_snapshot, + + // 订单项 + 'veh_list' => $vehList, + + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $item->cif_total_amount, + + 'declarant_name' => $item->declarant_name, + 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), + 'shipping_method_snapshot' => $item->shipping_method_snapshot, + 'domestic_port_snapshot' => $item->domestic_port_snapshot, + 'trade_term_snapshot' => $item->trade_term_snapshot, + 'clearance_port_snapshot' => $item->clearance_port_snapshot, + 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), + + // 账户 + 'account_list' => $accList, + 'acc_index' => $accIndex, + 'cif_total_amount' => $cifTotalAmount + ]; + + return $veh; + } +} \ No newline at end of file