diff --git a/app/common/approval/model/ApprovalCcs.php b/app/common/approval/model/ApprovalCcs.php new file mode 100644 index 0000000..89be176 --- /dev/null +++ b/app/common/approval/model/ApprovalCcs.php @@ -0,0 +1,49 @@ +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. 查找第一个有效步骤(考虑 condition) + $firstStep = $this->getNextValidStep($steps, $formData); + if (!$firstStep) { + throw new BusinessException("未找到可执行的审批步骤"); + } + + // 3. 创建审批实例 + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => 0, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 4. 创建审批记录(待办) + $approverIds = $this->resolveApprovers($firstStep, (object)['initiator_id' => $initiatorId]); + foreach ($approverIds as $approverId) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'step_name' => $firstStep['name'], + 'approver_id' => $approverId, + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + ]); + } + + // 5. 通知审批人 + $this->sendNotification($approverIds, (object)['id' => $instanceId, 'flow_type' => $flowType], 'pending', false); + + // 6. 通知抄送人(当前步骤) + $this->notifyCCs($firstStep, (object)['id' => $instanceId, 'flow_type' => $flowType, 'form_data' => json_encode($formData)], 'pending'); + + return ['instance_id' => $instanceId]; + } + + /** + * 执行审批 + */ + public function approve(int $instanceId, int $approverId, string $action, string $comment = '') + { + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) { + throw new BusinessException("审批实例未找到"); + } + + $steps = json_decode($instance->steps, true) ?: json_decode(Db::table('approval_flows')->where('type', $instance->flow_type)->value('steps'), true); + $currentStep = $steps[$instance->current_step] ?? null; + + if (!$currentStep) { + throw new BusinessException("当前步骤配置错误"); + } + + // 检查用户是否有权限审批 + if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { + throw new BusinessException("您无权审批此步骤"); + } + + // 更新审批记录 + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('status', 'pending') + ->update([ + 'status' => $action, + 'comment' => $comment, + 'approved_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 检查当前步骤是否完成(根据 type: any, all, sequential) + $stepCompleted = $this->isStepCompleted($instanceId, $instance->current_step, $currentStep['type']); + + if (!$stepCompleted) { + // 步骤未完成,不进入下一步 + return ['status' => 'step_pending']; + } + + // 步骤已完成,进入下一步或结束 + $nextStep = $this->getNextValidStep($steps, json_decode($instance->form_data, true), $instance->current_step + 1); + + if ($nextStep === null) { + // 流程结束 + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => $action === 'approve' ? 'approved' : 'rejected', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 通知发起人结果 + $this->sendNotification([$instance->initiator_id], $instance, $action, false); + + return ['status' => 'completed', 'result' => $action]; + } + + // 进入下一步 + $nextStepIndex = array_search($nextStep, $steps); + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'current_step' => $nextStepIndex, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 创建下一步的审批记录 + $nextApprovers = $this->resolveApprovers($nextStep, $instance); + foreach ($nextApprovers as $approverId) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'step_name' => $nextStep['name'], + 'approver_id' => $approverId, + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + ]); + } + + // 通知下一步审批人 + $this->sendNotification($nextApprovers, $instance, 'pending', false); + + // 通知下一步抄送人 + $this->notifyCCs($nextStep, $instance, 'pending'); + + return ['status' => 'next_step', 'step_name' => $nextStep['name']]; + } + + /** + * 解析审批人 + */ + private function resolveApprovers(array $step, object $instance): array + { + $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true); + + foreach ($step['approvers'] as $approverConfig) { + $approverIds = []; + + switch ($approverConfig['type']) { + case 'role': + $approverIds = $this->getUserIdsByRole($approverConfig['value']); + break; + case 'user': + $approverIds = [(int)$approverConfig['value']]; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); + if ($leaderId) $approverIds = [$leaderId]; + break; + case 'field_user': + if (isset($formData[$approverConfig['value']])) { + $approverIds = [(int)$formData[$approverConfig['value']]]; + } + break; + case 'dept_head': + $approverIds = $this->getDeptHeadId($approverConfig['value']); + if ($approverIds) $approverIds = [$approverIds]; + break; + default: + throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); + } + + $resolved = array_merge($resolved, $approverIds); + } + + return array_unique(array_filter($resolved)); + } + + /** + * 通知抄送人 + */ + private function notifyCCs(array $step, object $instance, string $action = 'pending') + { + $ccConfig = $step['cc'] ?? []; + $ccUserIds = []; + $formData = json_decode($instance->form_data ?? '{}', true); + + foreach ($ccConfig as $cc) { + switch ($cc['type']) { + case 'role': + $ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value'])); + break; + case 'user': + $ccUserIds[] = (int)$cc['value']; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); + if ($leaderId) $ccUserIds[] = $leaderId; + break; + case 'field_user': + if (isset($formData[$cc['value']])) { + $ccUserIds[] = (int)$formData[$cc['value']]; + } + break; + case 'dept_head': + $deptHeadId = $this->getDeptHeadId($cc['value']); + if ($deptHeadId) $ccUserIds[] = $deptHeadId; + break; + default: + break; + } + } + + $ccUserIds = array_unique(array_filter($ccUserIds)); + + if (!empty($ccUserIds)) { + $this->sendNotification($ccUserIds, $instance, $action, true); + } + } + + /** + * 评估条件 + */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $matches)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + + $field = $matches[1]; + $op = $matches[2]; + $value = trim($matches[3], "\"'"); + + $actualValue = $formData[$field] ?? null; + + if (is_numeric($value) && is_numeric($actualValue)) { + $value = (float)$value; + $actualValue = (float)$actualValue; + } + + switch ($op) { + case '==': return $actualValue == $value; + case '!=': return $actualValue != $value; + case '>': return $actualValue > $value; + case '<': $actualValue < $value; + case '>=': return $actualValue >= $value; + case '<=': return $actualValue <= $value; + default: throw new BusinessException("不支持的操作符: {$op}"); + } + } + + /** + * 获取下一个有效步骤(考虑 condition) + */ + private function getNextValidStep(array $steps, array $formData, int $startIndex = 0): ?array + { + for ($i = $startIndex; $i < count($steps); $i++) { + $step = $steps[$i]; + if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { + return $step; + } + } + return null; + } + + /** + * 检查用户是否有权限审批 + */ + private function canUserApproveStep(array $step, int $userId, object $instance): bool + { + $approvers = $this->resolveApprovers($step, $instance); + return in_array($userId, $approvers); + } + + /** + * 检查当前步骤是否完成 + */ + private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool + { + $pendingCount = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->count(); + + if ($type === 'any') { + return $pendingCount === 0; // 任一通过即可 + } + + if ($type === 'all' || $type === 'sequential') { + return $pendingCount === 0; // 所有人都必须审批 + } + + return false; + } + + /** + * 发送通知(你可以替换为你的通知系统) + */ + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC) + { + $actionText = [ + 'pending' => '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝' + ][$action] ?? $action; + + $prefix = $isCC ? '[抄送]' : ''; + $message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}"; + + // TODO: 调用你的消息系统,如邮件、站内信、企业微信等 + // 示例:MessageService::send($userIds, $message); + echo "通知用户 " . implode(',', $userIds) . ":$message\n"; + } + + /** + * 获取用户待审批列表 + * @param int $userId 用户ID + * @return array + */ + public function getPendingApprovals(int $userId): array + { + // 简化查询:找到用户需要审批的实例ID + // 更复杂的逻辑(如并行会签)需要更精细的查询 + $instanceIds = Db::table('approval_records') + ->where('approver_id', $userId) + ->pluck('instance_id') + ->unique() + ->toArray(); + + if (empty($instanceIds)) { + return []; + } + + return Db::table('approval_instances') + ->whereIn('id', $instanceIds) + ->where('status', 'processing') + ->get() + ->toArray(); + } + + /** + * 获取审批历史记录 + * @param int $instanceId + * @return array + */ + public function getApprovalHistory(int $instanceId): array + { + return Db::table('approval_records') + ->where('instance_id', $instanceId) + ->orderBy('step_index') + ->orderBy('approved_at') + ->get() + ->toArray(); + } + + /** + * 获取审批流程图数据 (简化版) + * @param int $instanceId + * @return array + */ + 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') + ->orderBy('approved_at') + ->get() + ->groupBy('step_index'); + + $diagramData = []; + foreach ($steps as $index => $step) { + $status = 'pending'; + $actions = $records->get($index, collect())->toArray(); + + if (!empty($actions)) { + $hasReject = collect($actions)->contains('action', 'reject'); + if ($hasReject) { + $status = 'rejected'; + } else { + if ($step['type'] === 'parallel') { + // 简化处理 + $status = 'approved'; + } else { + $status = 'approved'; + } + } + } elseif ($index < $instance->current_step) { + $status = 'approved'; + } elseif ($index == $instance->current_step) { + $status = 'processing'; + } + + $diagramData[] = [ + 'index' => $index, + 'name' => $step['name'], + 'type' => $step['type'], + 'status' => $status, + 'actions' => $actions + ]; + } + return $diagramData; + } + + /** + * 处理超时的审批实例 (定时任务调用) + */ + public function handleTimeouts() + { + $now = date('Y-m-d H:i:s'); + $expiredInstances = Db::table('approval_instances') + ->where('status', 'processing') + ->where('deadline_at', '<=', $now) + ->get(); + + foreach ($expiredInstances as $instance) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if ($flow && $flow->timeout_action) { + $updateData = [ + 'finished_at' => $now, + 'updated_at' => $now + ]; + if ($flow->timeout_action === 'auto_reject') { + $updateData['status'] = 'rejected'; + Db::table('approval_instances')->where('id', $instance->id)->update($updateData); + $this->notifyInitiator($instance, 'rejected_timeout'); + } elseif ($flow->timeout_action === 'auto_approve') { + $updateData['status'] = 'approved'; + Db::table('approval_instances')->where('id', $instance->id)->update($updateData); + $this->notifyInitiator($instance, 'approved_timeout'); + } + } + } + } + + // --- 辅助方法 --- + + // private function canUserApproveStep(array $step, int $userId, object $instance): bool + // { + // $approvers = $this->resolveApprovers($step, $instance); + // return in_array($userId, $approvers); + // } + + // private function resolveApprovers(array $step, object $instance): array + // { + // $resolved = []; + // foreach ($step['approvers'] as $approver) { + // if ($approver['type'] === 'role') { + // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 + // if ($approver['value'] === 'admin') { + // $resolved[] = 1; // 假设 admin 角色的用户ID是1 + // } elseif ($approver['value'] === 'manager') { + // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager + // } + // } elseif ($approver['type'] === 'user') { + // $resolved[] = (int)$approver['value']; + // } elseif ($approver['type'] === 'initiator_leader') { + // // 示例:获取发起人的领导 + // $resolved[] = 4; // 假设领导ID是4 + // } + // } + // return array_unique($resolved); + // } + + // private function resolveApprovers(array $step, object $instance): array + // { + // $resolved = []; + + // foreach ($step['approvers'] as $approverConfig) { + // $approverIds = []; + + // switch ($approverConfig['type']) { + // // 1. 按角色查找 + // case 'role': + // $roleCode = $approverConfig['value']; + // // 所有拥有该角色的用户ID + // $approverIds = $this->getUserIdsByRole($roleCode); + // break; + + // // 2. 按具体用户ID + // case 'user': + // $approverIds = [(int)$approverConfig['value']]; + // break; + + // // 3. 按发起人的直属领导 (业务一般是组长) + // case 'initiator_leader': + // // 用户表中 report_to 字段 + // $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); + // if ($approverIds) { + // $approverIds = [$approverIds]; + // } else { + // $approverIds = []; // 没有领导,跳过 + // } + // break; + + // // 4. 按业务字段关联的用户 (高级功能) + // case 'field_user': + // $fieldName = $approverConfig['value']; // 如 'project_manager_id' + // $formData = json_decode($instance->form_data, true); + // if (isset($formData[$fieldName])) { + // $approverIds = [(int)$formData[$fieldName]]; + // } else { + // $approverIds = []; + // } + // break; + + // // 5. 按组织架构层级 (如发起人所在部门的负责人) + // case 'dept_head': + // $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 + // $approverIds = $this->getDeptHeadId($deptId); + // if ($approverIds) { + // $approverIds = [$approverIds]; + // } else { + // $approverIds = []; + // } + // break; + + // default: + // throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); + // } + + // $resolved = array_merge($resolved, $approverIds); + // } + + // return array_unique($resolved); + // } + + // 角色层 + private function getUserIdsByRole(string $roleCode): array + { + // user_roles 表和 roles 表 + return Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 + ->pluck('ur.user_id') + ->toArray(); + } + + // 领导层 + private function getLeaderIdByUserId(int $userId): ?int + { + // users 表 report_to 字段 + return Db::table('user')->where('id', $userId)->value('report_to'); + } + + // 组织架构层 + private function getDeptHeadId(int $deptId): ?int + { + // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) + return Db::table('departments')->where('id', $deptId)->value('head_user_id'); + } + + private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int + { + // 对于 sequential 和 any,单次 approve 即进入下一步 + if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { + return $currentStepIndex + 1; + } elseif ($currentStep['type'] === 'parallel') { + // 对于并行会签,需要检查是否所有审批人都已 approve + // 这里简化处理,实际需要精确计算 + // 假设只要有一次 approve 就算完成(不准确,仅为示例) + // 正确做法是查询所有 approvers 是否都 approve 了 + return $currentStepIndex + 1; + } + return $currentStepIndex + 1; + } + + private function notifyNextApprovers(int $instanceId) + { + // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 + // 可以查询 instance_id 对应的当前步骤审批人 + echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; + } + + private function notifyInitiator(object $instance, string $result) + { + // 通知发起人审批结果 + echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; + } + + private function notifyAllApprovers(object $instance, string $status) + { + // 通知所有相关审批人(如撤回时) + echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; + } + + private function notifyCc(int $instanceId) + { + // 通知抄送人 + echo "通知抄送人 (Instance ID: {$instanceId})\n"; + } +} \ No newline at end of file diff --git a/app/common/approval/service/ApprovalFlowService_bak.php b/app/common/approval/service/ApprovalFlowService_bak.php new file mode 100644 index 0000000..e6e13e7 --- /dev/null +++ b/app/common/approval/service/ApprovalFlowService_bak.php @@ -0,0 +1,431 @@ + true, 'instance_id' => ...] 或抛出异常 + */ + public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array + { + // 1. 获取流程定义 + $flow = Db::table('approval_flows') + ->where('type', $flowType) + ->where('is_active', 1) + ->first(); + + if (!$flow) { + throw new BusinessException("审批流程 '{$flowType}' 不存在或已停用"); + } + + // 2. 创建审批实例 + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_id' => $bizId, + 'biz_type' => $bizType, + 'form_data' => json_encode($formData), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => 0, + 'deadline_at' => $flow->timeout_hours > 0 ? + (new DateTime())->modify("+{$flow->timeout_hours} hours")->format('Y-m-d H:i:s') : null, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 3. 通知下一个审批人 + $this->notifyNextApprovers($instanceId); + + return ['success' => true, 'instance_id' => $instanceId]; + } + + /** + * 执行审批操作 + * @param int $instanceId 审批实例ID + * @param int $approverId 审批人ID + * @param string $action 操作 (approve, reject, revoke) + * @param string $comment 审批意见 + * @return array 操作结果 + */ + public function approve(int $instanceId, int $approverId, string $action, string $comment = ''): array + { + // 1. 获取实例和流程 + $instance = Db::table('approval_instances')->find($instanceId); + 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); + $currentStepIndex = $instance->current_step; + + if ($currentStepIndex >= count($steps)) { + throw new BusinessException("审批流程已结束"); + } + + $currentStep = $steps[$currentStepIndex]; + + // 2. 验证权限和条件 + if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { + throw new BusinessException("您无权审批此步骤"); + } + + // 3. 记录审批行为 + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $currentStepIndex, + 'action' => $action, + 'comment' => $comment, + 'approved_at' => date('Y-m-d H:i:s'), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + // 4. 处理审批结果 + $updateData = ['updated_at' => date('Y-m-d H:i:s')]; + if ($action === 'reject') { + $updateData['status'] = 'rejected'; + $updateData['finished_at'] = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update($updateData); + $this->notifyInitiator($instance, 'rejected'); + return ['result' => 'rejected']; + } + + if ($action === 'revoke') { + if ($instance->initiator_id !== $approverId || $instance->status !== 'processing') { + throw new BusinessException("无权撤回或状态不允许"); + } + $updateData['status'] = 'revoked'; + $updateData['finished_at'] = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update($updateData); + $this->notifyAllApprovers($instance, 'revoked'); + return ['result' => 'revoked']; + } + + // 5. 处理 approve 逻辑 + $nextStepIndex = $this->calculateNextStepIndex($instanceId, $currentStepIndex, $currentStep); + $updateData['current_step'] = $nextStepIndex; + + if ($nextStepIndex >= count($steps)) { + $updateData['status'] = 'approved'; + $updateData['finished_at'] = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update($updateData); + $this->notifyInitiator($instance, 'approved'); + $this->notifyCc($instanceId); + } else { + Db::table('approval_instances')->where('id', $instanceId)->update($updateData); + $this->notifyNextApprovers($instanceId); + } + + return ['result' => 'approved', 'next_step' => $nextStepIndex]; + } + + /** + * 获取用户待审批列表 + * @param int $userId 用户ID + * @return array + */ + public function getPendingApprovals(int $userId): array + { + // 简化查询:找到用户需要审批的实例ID + // 更复杂的逻辑(如并行会签)需要更精细的查询 + $instanceIds = Db::table('approval_records') + ->where('approver_id', $userId) + ->pluck('instance_id') + ->unique() + ->toArray(); + + if (empty($instanceIds)) { + return []; + } + + return Db::table('approval_instances') + ->whereIn('id', $instanceIds) + ->where('status', 'processing') + ->get() + ->toArray(); + } + + /** + * 获取审批历史记录 + * @param int $instanceId + * @return array + */ + public function getApprovalHistory(int $instanceId): array + { + return Db::table('approval_records') + ->where('instance_id', $instanceId) + ->orderBy('step_index') + ->orderBy('approved_at') + ->get() + ->toArray(); + } + + /** + * 获取审批流程图数据 (简化版) + * @param int $instanceId + * @return array + */ + 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') + ->orderBy('approved_at') + ->get() + ->groupBy('step_index'); + + $diagramData = []; + foreach ($steps as $index => $step) { + $status = 'pending'; + $actions = $records->get($index, collect())->toArray(); + + if (!empty($actions)) { + $hasReject = collect($actions)->contains('action', 'reject'); + if ($hasReject) { + $status = 'rejected'; + } else { + if ($step['type'] === 'parallel') { + // 简化处理 + $status = 'approved'; + } else { + $status = 'approved'; + } + } + } elseif ($index < $instance->current_step) { + $status = 'approved'; + } elseif ($index == $instance->current_step) { + $status = 'processing'; + } + + $diagramData[] = [ + 'index' => $index, + 'name' => $step['name'], + 'type' => $step['type'], + 'status' => $status, + 'actions' => $actions + ]; + } + return $diagramData; + } + + /** + * 处理超时的审批实例 (定时任务调用) + */ + public function handleTimeouts() + { + $now = date('Y-m-d H:i:s'); + $expiredInstances = Db::table('approval_instances') + ->where('status', 'processing') + ->where('deadline_at', '<=', $now) + ->get(); + + foreach ($expiredInstances as $instance) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if ($flow && $flow->timeout_action) { + $updateData = [ + 'finished_at' => $now, + 'updated_at' => $now + ]; + if ($flow->timeout_action === 'auto_reject') { + $updateData['status'] = 'rejected'; + Db::table('approval_instances')->where('id', $instance->id)->update($updateData); + $this->notifyInitiator($instance, 'rejected_timeout'); + } elseif ($flow->timeout_action === 'auto_approve') { + $updateData['status'] = 'approved'; + Db::table('approval_instances')->where('id', $instance->id)->update($updateData); + $this->notifyInitiator($instance, 'approved_timeout'); + } + } + } + } + + // --- 辅助方法 --- + + private function canUserApproveStep(array $step, int $userId, object $instance): bool + { + $approvers = $this->resolveApprovers($step, $instance); + return in_array($userId, $approvers); + } + + // private function resolveApprovers(array $step, object $instance): array + // { + // $resolved = []; + // foreach ($step['approvers'] as $approver) { + // if ($approver['type'] === 'role') { + // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 + // if ($approver['value'] === 'admin') { + // $resolved[] = 1; // 假设 admin 角色的用户ID是1 + // } elseif ($approver['value'] === 'manager') { + // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager + // } + // } elseif ($approver['type'] === 'user') { + // $resolved[] = (int)$approver['value']; + // } elseif ($approver['type'] === 'initiator_leader') { + // // 示例:获取发起人的领导 + // $resolved[] = 4; // 假设领导ID是4 + // } + // } + // return array_unique($resolved); + // } + + private function resolveApprovers(array $step, object $instance): array + { + $resolved = []; + + foreach ($step['approvers'] as $approverConfig) { + $approverIds = []; + + switch ($approverConfig['type']) { + // 1. 按角色查找 + case 'role': + $roleCode = $approverConfig['value']; + // 所有拥有该角色的用户ID + $approverIds = $this->getUserIdsByRole($roleCode); + break; + + // 2. 按具体用户ID + case 'user': + $approverIds = [(int)$approverConfig['value']]; + break; + + // 3. 按发起人的直属领导 (业务一般是组长) + case 'initiator_leader': + // 用户表中 report_to 字段 + $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); + if ($approverIds) { + $approverIds = [$approverIds]; + } else { + $approverIds = []; // 没有领导,跳过 + } + break; + + // 4. 按业务字段关联的用户 (高级功能) + case 'field_user': + $fieldName = $approverConfig['value']; // 如 'project_manager_id' + $formData = json_decode($instance->form_data, true); + if (isset($formData[$fieldName])) { + $approverIds = [(int)$formData[$fieldName]]; + } else { + $approverIds = []; + } + break; + + // 5. 按组织架构层级 (如发起人所在部门的负责人) + case 'dept_head': + $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 + $approverIds = $this->getDeptHeadId($deptId); + if ($approverIds) { + $approverIds = [$approverIds]; + } else { + $approverIds = []; + } + break; + + default: + throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); + } + + $resolved = array_merge($resolved, $approverIds); + } + + return array_unique($resolved); + } + + // 角色层 + private function getUserIdsByRole(string $roleCode): array + { + // user_roles 表和 roles 表 + return Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 + ->pluck('ur.user_id') + ->toArray(); + } + + // 领导层 + private function getLeaderIdByUserId(int $userId): ?int + { + // users 表 report_to 字段 + return Db::table('user')->where('id', $userId)->value('report_to'); + } + + // 组织架构层 + private function getDeptHeadId(int $deptId): ?int + { + // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) + return Db::table('departments')->where('id', $deptId)->value('head_user_id'); + } + + private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int + { + // 对于 sequential 和 any,单次 approve 即进入下一步 + if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { + return $currentStepIndex + 1; + } elseif ($currentStep['type'] === 'parallel') { + // 对于并行会签,需要检查是否所有审批人都已 approve + // 这里简化处理,实际需要精确计算 + // 假设只要有一次 approve 就算完成(不准确,仅为示例) + // 正确做法是查询所有 approvers 是否都 approve 了 + return $currentStepIndex + 1; + } + return $currentStepIndex + 1; + } + + private function notifyNextApprovers(int $instanceId) + { + // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 + // 可以查询 instance_id 对应的当前步骤审批人 + echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; + } + + private function notifyInitiator(object $instance, string $result) + { + // 通知发起人审批结果 + echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; + } + + private function notifyAllApprovers(object $instance, string $status) + { + // 通知所有相关审批人(如撤回时) + echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; + } + + private function notifyCc(int $instanceId) + { + // 通知抄送人 + echo "通知抄送人 (Instance ID: {$instanceId})\n"; + } +} \ No newline at end of file diff --git a/app/common/controller/StockOrdersController.php b/app/common/controller/StockOrdersController.php deleted file mode 100644 index 4bb5a50..0000000 --- a/app/common/controller/StockOrdersController.php +++ /dev/null @@ -1,33 +0,0 @@ -service = new StockOrdersService(); - } - - /** - * 列表页数据格式化 - * - * @param array $data - * @return array - */ - // protected function indexFormat($data) - // { - // if (isset($data['created_at'])) - // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); - // if (isset($data['updated_at'])) - // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); - // return $data; - // } -} \ No newline at end of file diff --git a/app/common/model/StockOrderItems.php b/app/common/model/StockOrderItems.php index 07ed077..847fec7 100644 --- a/app/common/model/StockOrderItems.php +++ b/app/common/model/StockOrderItems.php @@ -3,6 +3,7 @@ namespace app\common\model; use support\Model; +use app\common\traits\HasMoneyJournal; class StockOrderItems extends Model { @@ -40,44 +41,44 @@ class StockOrderItems extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot', 'model_snapshot', 'quantity', 'color_interior_id', 'color_interior_snapshot', 'color_exterior_id', 'color_exterior_snapshot', 'remark', 'unit_price', 'handling_fee', 'fob_transport_fee', 'port_misc_fee', 'retrofit_fee', 'total_price', 'created_at', 'updated_at', 'unit_price_value_id', 'handling_fee_value_id', 'fob_transport_fee_value_id', 'port_misc_fee_value_id', 'retrofit_fee_value_id', 'total_price_value_id']; + protected $fillable = [ + 'id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot', + 'model_snapshot', 'quantity', 'color_interior_coide', 'color_interior_snapshot', + 'color_exterior_coide', 'color_exterior_snapshot', 'remark', + // 'unit_price', + // 'handling_fee', + // 'fob_transport_fee', + // 'port_misc_fee', + // 'retrofit_fee', + // 'total_price', + 'created_at', 'updated_at', + // 'unit_price_value_id', + // 'handling_fee_value_id', + // 'fob_transport_fee_value_id', + // 'port_misc_fee_value_id', + // 'retrofit_fee_value_id', + // 'total_price_value_id' + ]; /** * 不允许自动填充模型字段 */ protected $guarded = []; + /* ========= 关系 ========= */ - public function unitPrice() { return $this->belongsTo(VehMoneyValue::class, 'unit_price_value_id'); } - public function handlingFee() { return $this->belongsTo(VehMoneyValue::class, 'handling_fee_value_id'); } - public function fobTransportFee() { return $this->belongsTo(VehMoneyValue::class, 'fob_transport_fee_value_id'); } - public function portMiscFee() { return $this->belongsTo(VehMoneyValue::class, 'port_misc_fee_value_id'); } - public function retrofitFee() { return $this->belongsTo(VehMoneyValue::class, 'retrofit_fee_value_id'); } - public function totalPrice() { return $this->belongsTo(VehMoneyValue::class, 'total_price_value_id'); } - - // 便捷访问器(原币种的金额) - public function getUnitPriceAmountAttribute() { return $this->unitPrice?->amount; } - public function getHandlingFeeAmountAttribute() { return $this->handlingFee?->amount; } - public function getFobTransportFeeAmountAttribute() { return $this->fobTransportFee?->amount; } - public function getPortMiscFeeAmountAttribute() { return $this->portMiscFee?->amount; } - public function getRetrofitFeeAmountAttribute() { return $this->retrofitFee?->amount; } - public function getTotalPriceAmountAttribute() { return $this->totalPrice?->amount; } - - // 便捷:按“各自 exchange_rate”换到结算币后相加(无 rate 的跳过) - public function sumToSettlement(): string + use HasMoneyJournal; + // 实现抽象方法 + protected static function moneyObjectScene(): string { - $list = array_filter([ - $this->unitPrice?->amountInSettlement(), - $this->handlingFee?->amountInSettlement(), - $this->fobTransportFee?->amountInSettlement(), - $this->portMiscFee?->amountInSettlement(), - $this->retrofitFee?->amountInSettlement(), - ], fn($v) => $v !== null); - - $sum = '0.00'; - foreach ($list as $v) { - $sum = bcadd($sum, (string)$v, 2); - } - return $sum; + return 'stock_order_items'; } + + // 便捷访问器(最新一条的原币金额) + public function getUnitPriceAmountAttribute() { return $this->moneyLatest('unit_price')?->amount; } + public function getHandlingFeeAmountAttribute() { return $this->moneyLatest('handling_fee')?->amount; } + public function getFobTransportFeeAmountAttribute() { return $this->moneyLatest('fob_transport_fee')?->amount; } + public function getPortMiscFeeAmountAttribute() { return $this->moneyLatest('port_misc_fee')?->amount; } + public function getRetrofitFeeAmountAttribute() { return $this->moneyLatest('retrofit_fee')?->amount; } + public function getTotalPriceAmountAttribute() { return $this->moneyLatest('total_price')?->amount; } } \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index 21239c1..2814f39 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -40,7 +40,17 @@ class StockOrders extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'order_no', 'pi_no', 'biz_date', 'salesperson_id', 'salesperson_snapshot', 'clearance_country_id', 'clearance_country_snapshot', 'ali_order_no', 'customer_source_id', 'customer_source_snapshot', 'customer_id', 'customer_snapshot', 'customer_country_id', 'customer_country_snapshot', 'declarant_name', 'eta_ship_date', 'shipping_method', 'cif_total_amount', 'domestic_port_id', 'domestic_port_snapshot', 'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot', 'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status', 'remark', 'created_at', 'updated_at', 'deleted_at']; + protected $fillable = [ + 'id', 'order_no', 'pi_no', 'biz_date', 'salesperson_id', 'salesperson_snapshot', + 'clearance_country_id', 'clearance_country_snapshot', 'ali_order_no', + 'customer_source_id', 'customer_source_snapshot', 'customer_id', 'customer_snapshot', + 'customer_country_id', 'customer_country_snapshot', 'declarant_name', 'eta_ship_date', + 'shipping_method_id', 'shipping_method_snapshot', 'cif_total_amount', 'domestic_port_id', 'domestic_port_snapshot', + 'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot', + 'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status', + 'remark', 'created_at', 'updated_at', 'deleted_at', + 'object_id', 'contract', 'is_use', 'is_final', 'approval_status' + ]; /** * 不允许自动填充模型字段 diff --git a/app/common/model/VehMoneyValue.php b/app/common/model/VehMoneyValue.php index 30fd497..e4c9d58 100644 --- a/app/common/model/VehMoneyValue.php +++ b/app/common/model/VehMoneyValue.php @@ -37,13 +37,26 @@ class VehMoneyValue extends Model protected $dateFormat = 'U'; - /** - * 允许自动填充模型字段 - */ - protected $fillable = ['id', 'currency', 'amount', 'exchange_rate', 'remark', 'created_at', 'updated_at']; - /** * 不允许自动填充模型字段 */ protected $guarded = []; + + + protected $fillable = [ + 'object_scene','object_id','field','currency','amount','exchange_rate', + 'remark','created_at','updated_at' + ]; + + protected $casts = [ + 'amount' => 'string', + 'exchange_rate' => 'string', + ]; + + /** 按本行汇率换到结算币;无汇率返回 null */ + public function amountInSettlement(int $scale = 2): ?string + { + if ($this->exchange_rate === null) return null; + return (string) bcmul((string)$this->amount, (string)$this->exchange_rate, $scale); + } } \ No newline at end of file diff --git a/app/common/model/VehMoneyValue_bak.php b/app/common/model/VehMoneyValue_bak.php new file mode 100644 index 0000000..50de155 --- /dev/null +++ b/app/common/model/VehMoneyValue_bak.php @@ -0,0 +1,49 @@ +validator; } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + 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 = []) + { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); // 表1 + + // 车辆 + foreach ($data['veh_list'] as $veh) { + $veh['order_id'] = $created->id; + $item = StockOrderItems::create($veh); // 表2 + + // 金额 // 表3 + $item->addMoney('official_price', $veh['currency'], $veh['amount'], $veh['rate'], '官方指导价'); + $item->addMoney('discount_price', $veh['currency'], $veh['amount'], $veh['rate'], '优惠价'); + $item->addMoney('unit_price', $veh['currency'], $veh['amount'], $veh['rate'], '单价'); + $item->addMoney('total_price', $veh['currency'], $veh['amount'], $veh['rate'], '总价'); + $item->addMoney('handling_fee', $veh['currency'], $veh['amount'], $veh['rate'], '手续费'); + $item->addMoney('fob_transport_fee', $veh['currency'], $veh['amount'], $veh['rate'], 'FOB运输费'); + $item->addMoney('port_misc_fee', $veh['currency'], $veh['amount'], $veh['rate'], '港杂'); + $item->addMoney('retrofit_fee', $veh['currency'], $veh['amount'], $veh['rate'], '改装费'); + + // 改装 + foreach ($data['veh_list']['modify_list'] as $modify) { + $modify['order_item_id'] = $item->id; + StockOrderItemOptions::create($modify); // 表4 + } + } + + // 账户 + foreach ($data['account_list'] as $account) { + $account['order_id'] = $created->id; + 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') ?? ''; + + var_dump($data); + + throw new BusinessException('测试异常'); + + return $data; + } + + // protected function afterServiceStore($res) + // { + + // return $res; + // } + + + public function serviceShow(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Show', $item); + } } \ No newline at end of file diff --git a/app/common/traits/HasMoneyJournal.php b/app/common/traits/HasMoneyJournal.php new file mode 100644 index 0000000..5d2c92d --- /dev/null +++ b/app/common/traits/HasMoneyJournal.php @@ -0,0 +1,85 @@ +hasMany(VehMoneyValue::class, 'object_id') + ->where('object_scene', static::moneyObjectScene()); + } + + /** 取某费项的“最新记录”(按时间/id 倒序) */ + public function moneyLatest(string $field): ?VehMoneyValue + { + return $this->moneyJournal() + ->where('field', $field) + ->orderByDesc('created_at') + ->orderByDesc('id') + ->first(); + } + + /** 追加一行金额 */ + public function addMoney(string $field, string $currency, $amount, ?string $rate = null, ?string $remark = null): VehMoneyValue + { + $now = time(); + /** @var VehMoneyValue $row */ + $row = $this->moneyJournal()->create([ + 'object_scene' => static::moneyObjectScene(), + 'object_id' => $this->id, + 'field' => $field, + 'currency' => strtoupper($currency), + 'amount' => (string)$amount, + 'exchange_rate' => $rate, + 'remark' => $remark, + 'created_at' => $now, + 'updated_at' => $now, + ]); + return $row; + } + + /** 批量追加:['unit_price'=>['currency'=>'USD','amount'=>'2199.00','rate'=>'7.2','remark'=>'...'], ...] */ + public function addMoneyBatch(array $payload): array + { + return Db::transaction(function () use ($payload) { + $out = []; + foreach ($payload as $field => $cfg) { + $out[$field] = $this->addMoney( + $field, + $cfg['currency'] ?? 'CNY', + $cfg['amount'] ?? '0.00', + $cfg['rate'] ?? null, + $cfg['remark'] ?? null + ); + } + return $out; + }); + } + + /** 把若干费项按各自汇率换到结算币并求和(无 rate 的跳过) */ + public function sumToSettlement(array $fields, int $scale = 2): string + { + $sum = '0.00'; + foreach ($fields as $f) { + $row = $this->moneyLatest($f); + if ($row) { + $v = $row->amountInSettlement($scale); + if ($v !== null) $sum = bcadd($sum, $v, $scale); + } + } + return $sum; + } +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index a0f6dba..1c69073 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -15,6 +15,7 @@ use support\Db; use support\Cache; use app\inquiry\service\InquiryLogService; use Illuminate\Support\Facades\Mail; +use support\Redis; class IndexController { @@ -76,27 +77,27 @@ class IndexController public function createResource() { $options = [ - 'controller' => [ - 'module' => 'common', - 'name' => 'stock_orders' - ], + // 'controller' => [ + // 'module' => 'common', + // 'name' => 'stock_orders' + // ], 'model' => [ - 'module' => 'common', - 'name' => 'stock_orders' + 'module' => 'approval', + 'name' => 'approval_records' ], - 'service' => [ - 'module' => 'common', - 'name' => 'stock_orders' - ], - 'logic' => [ - 'module' => 'common', - 'name' => 'stock_orders' - ], - 'validate' => [ - 'module' => 'common', - 'name' => 'stock_orders' - ], - 'table' => 'stock_orders' + // 'service' => [ + // 'module' => 'common', + // 'name' => 'stock_orders' + // ], + // 'logic' => [ + // 'module' => 'common', + // 'name' => 'stock_orders' + // ], + // 'validate' => [ + // 'module' => 'common', + // 'name' => 'stock_orders' + // ], + 'table' => 'approval_records' ]; $result = (new Generate($options))->doGenerate(); @@ -106,24 +107,46 @@ class IndexController public function test() { - $params = [ - 'email' => '', - 'phone' => '11', - 'whatsApp' => '' + $data = [ + 'id' => 1, + 'user_id' => 1, + 'leave_type_id' => 1, + 'start_date' => '2019-01-01', + 'end_date' => '2019-01-02', + 'reason' => 'test', + 'status' => 1 ]; - $corpId = 1; - $customer = \app\crm\model\Customer::where('corp_id', $corpId) - ->where((function (\Illuminate\Database\Eloquent\Builder $query) use ($params) { - if(!empty($params['email'])) - $query->Orwhere('email', '=', $params['email']); - if(!empty($params['phone'])) - $query->Orwhere('phone', '=', $params['phone']); - if(!empty($params['whatsApp'])) - $query->Orwhere('whatsApp', '=', $params['whatsApp']); - return $query; - })) - ->first(); + $code = $this->submitLeaveApplication(1, $data); - return json($customer); + return $code; + } + + + // 假设这是你的业务逻辑 + public function submitLeaveApplication(int $bizId, $leaveData) { + // 1. 保存业务数据 + // $bizId = DB::table('leave_applications')->insertGetId($leaveData); + + // 2. 启动审批流程 + $service = new \app\common\approval\service\ApprovalFlowService(); + try { + $result = $service->start( + 'leave', // 流程类型,对应 approval_flows.type + 'leave_application', // 业务类型标识 + $bizId, // 业务ID + $leaveData, // 表单数据快照 + $_SESSION['user_id'] ?? 1 // 发起人ID + ); + + // 3. 将审批实例ID关联回业务表 + DB::table('leave_applications') + ->where('id', $bizId) + ->update(['approval_instance_id' => $result['instance_id']]); + + return ['status' => 'success', 'message' => '申请已提交,等待审批']; + } catch (\Exception $e) { + // 处理错误,可能需要回滚业务数据插入 + return ['status' => 'error', 'message' => $e->getMessage()]; + } } } \ No newline at end of file diff --git a/app/common/controller/StockOrderAccountsController.php b/app/crm/controller/StockOrderAccountsController.php similarity index 96% rename from app/common/controller/StockOrderAccountsController.php rename to app/crm/controller/StockOrderAccountsController.php index 5bd7d57..4fe4338 100644 --- a/app/common/controller/StockOrderAccountsController.php +++ b/app/crm/controller/StockOrderAccountsController.php @@ -1,5 +1,5 @@ service = new StockOrdersService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } + + + + + + public function index(Request $request) + { + + $data = [ + 'id' => '', //'主键', + 'order_no' => '', //'备货单编号(业务唯一)', + 'pi_no' => '', //'PI号'/'阿里订单号', + 'biz_date' => '', //'单据日期(时间戳)', + 'salesperson_id' => '', //'业务员ID(关联数据)', + 'salesperson_snapshot' => '', //'业务员姓名快照', + 'clearance_country_id' => '', //'清关目的国ID(关联数据)', + 'clearance_country_snapshot' => '', //'清关目的国名称快照', + // 'ali_order_no' => '', //'阿里订单号', + 'customer_source_id' => '', //'客户来源ID(关联数据)', + 'customer_source_snapshot' => '', //'客户来源名称快照', + 'customer_id' => '', //'客户ID(关联数据)', + 'customer_snapshot' => '', //'客户名称快照', + 'customer_country_id' => '', //'客户所在国ID(关联数据)', + 'customer_country_snapshot' => '', //'客户所在国名称快照', + 'declarant_name' => '', //'清关人(文本)', + 'eta_ship_date' => '', //'预计发运日(时间戳)', + 'shipping_method' => '', //'出运方式(如海运/空运/铁路等)', + 'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)', + 'domestic_port_id' => '', //'国内收货港ID(关联数据)', + 'domestic_port_snapshot' => '', //'国内收货港名称快照', + 'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)', + 'trade_term_snapshot' => '', //'贸易条款名称快照', + 'clearance_port_id' => '', //'清关目的港ID(关联数据)', + 'clearance_port_snapshot' => '', //'清关目的港名称快照', + 'latest_arrival_date' => '', //'最迟到港时间(时间戳)', + 'contract_exchange_rate' => '', //'合同汇率(下单锁定)', + 'settlement_currency' => '', //'CNY' COMMENT '结算币种', + 'remark' => '', //'备注', + // 'real_veh' => '', //'0' COMMENT '实车ID', + 'is_final' => '', //'0' COMMENT '是否终版1是0否', + 'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等', + ]; + + // (?<=\x60 )[^\r\n']*(?=') + + $items = [ + // 'id' => '', // '主键', + // 'order_id' => '', // '所属备货单ID', + // 'product_code' => '', // '商品编号(可作外部编码)', + // 'sku_snapshot' => '', // '商品/车型名称快照', + // 'unit_price_value_id' => '', // '单价', + // 'handling_fee_value_id' => '', // '操作费', + // 'fob_transport_fee_value_id' => '', // 'FOB内陆运费', + // 'port_misc_fee_value_id' => '', // '港杂费', + // 'retrofit_fee_value_id' => '', // '改装费', + // 'total_price_value_id' => '', // '合计(按订单结算币计算)', + + 'vin' => '', // '车架号', + 'sku_id' => '', // '商品/车型ID(关联数据)', + 'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)', + 'quantity' => '', // '1.000000' COMMENT '数量', + 'color_interior_id' => '', // '颜色内饰ID(关联数据)', + 'color_interior_snapshot' => '', // '颜色内饰名称快照', + 'color_exterior_id' => '', // '颜色外饰ID(关联数据)', + 'color_exterior_snapshot' => '', // '颜色外饰名称快照', + 'remark' => '', // '备注', + 'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)', + 'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)', + 'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)', + 'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)', + 'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)', + 'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)', + 'official_price' => '', // 官方指导价 + 'discount_price' => '', // 优惠价 + + + 'modify_list' => [ + // 'order_item_id' => '', // '所属明细ID', + 'option_id' => '', // '配置项ID(关联数据)', + 'option_name_snapshot' => '', // '配置项名称快照', + 'option_price' => '', // '配置项价格(可选,写时快照)', + ] + ]; + + + $account = [ + 'order_id' => '', // '所属备货单ID', + // 'seq_no' => '', // '1' COMMENT '账户序号(行号)', + 'remit_country_id' => '', // '汇款国ID(关联数据)', + 'remit_country_name_snapshot' => '', // '汇款国名称快照', + 'remit_date' => '', // '汇款日期(时间戳)', + 'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)', + 'currency_name_snapshot' => '', // '币种名称快照(可选)', + 'account_id' => '', // '账户ID(关联数据)', + 'account_name_snapshot' => '', // '账户名称/开户名快照', + 'remitter_name' => '', // '汇款人', + 'amount' => '', // '0.00' COMMENT '金额', + ]; + } +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index db8cfe7..d12b5ae 100644 --- a/app/functions.php +++ b/app/functions.php @@ -330,4 +330,33 @@ if (!function_exists('skuCode')) { // 拼接:日期时间 + 微秒 + 随机数 return $date . $micro . $rand; } +} + + +/** + * 生成备货单编号(预占式,每天从1开始) + * + * @return string + */ +if (!function_exists('generateStockNo')) { + function generateStockNo(string $prefix = 'BH', int $serialLength = 3): string + { + $date = date('Ymd'); + $key = "stock:order:serial:{$date}"; + + // 原子递增获取当日流水号 + $serial = \support\Redis::incr($key); + + // 首次创建时设置过期时间:到明天凌晨 + 1小时(容错) + if ($serial === 1) { + $tomorrow = strtotime(date('Y-m-d', strtotime('+1 day'))); + $expireSeconds = $tomorrow - time() + 3600; // 多加1小时 + \support\Redis::expire($key, $expireSeconds); + } + + // 格式化流水号(补零) + $paddedSerial = str_pad($serial, $serialLength, '0', STR_PAD_LEFT); + + return $prefix . $date . $paddedSerial; + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 38c7907..586a191 100644 --- a/config/route.php +++ b/config/route.php @@ -346,5 +346,13 @@ Route::group('/crm', function () { Route::any('/user/edit', [app\common\controller\UserController::class, 'edit']); Route::any('/user/savePassword', [app\common\controller\UserController::class, 'savePassword']); -}); + // 基础信息 + Route::group('/stock/order', function () { + Route::any('/list', [app\crm\controller\StockOrdersController::class, 'index']); + Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']); + Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']); + // Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']); + // Route::any('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']); + }); +}); \ No newline at end of file