diff --git a/app/admin/controller/StockOrdersController.php b/app/admin/controller/StockOrdersController.php index 39806a5..47e694a 100644 --- a/app/admin/controller/StockOrdersController.php +++ b/app/admin/controller/StockOrdersController.php @@ -81,19 +81,22 @@ class StockOrdersController extends BaseController if (!isset($groupedFiles[$file['file_code']])) { $groupedFiles[$file['file_code']] = []; } - $groupedFiles[$file['file_code']][] = $file['file_url']; + $groupedFiles[$file['file_code']][] = [ + 'url' => $file['file_url'], + 'original_name' => $file['original_name'] + ]; } // 映射到对应字段 foreach ($fileTypes as $field => $fileCode) { if (isset($groupedFiles[$fileCode])) { - // 原始URL数组 + // 文件信息数组,包含url和original_name $data[$field] = $groupedFiles[$fileCode]; // 完整URL数组 $fullUrls = []; - foreach ($groupedFiles[$fileCode] as $url) { - $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + foreach ($groupedFiles[$fileCode] as $fileInfo) { + $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($fileInfo['url']); } $data[$field . '_url'] = $fullUrls; } diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php index 8c24c6c..e9ef8d4 100644 --- a/app/admin/controller/UserController.php +++ b/app/admin/controller/UserController.php @@ -118,6 +118,11 @@ class UserController extends BaseController $data['system_ids'][] = $system['id']; } } + if ($data['reportToInfo']) { + $data['report_to_name'] = $data['reportToInfo']['id'] == $data['id'] ? '/' : $data['reportToInfo']['name']; + } else { + $data['report_to_name'] = '-'; + } return $data; } diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php index 7f2fadd..9d850a5 100644 --- a/app/admin/service/UserService.php +++ b/app/admin/service/UserService.php @@ -100,6 +100,8 @@ class UserService $config['with']['roles'][] = 'name'; $config['with']['systems'][] = 'id'; $config['with']['systems'][] = 'name'; + $config['with']['reportToInfo'][] = 'id'; + $config['with']['reportToInfo'][] = 'name'; break; case 'show': $config['where']['id'] = $p['id']; diff --git a/app/common/model/StockOrdersFiles.php b/app/common/model/StockOrdersFiles.php index 98daa3f..3b78e5c 100644 --- a/app/common/model/StockOrdersFiles.php +++ b/app/common/model/StockOrdersFiles.php @@ -40,7 +40,7 @@ class StockOrdersFiles extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'order_id', 'file_name', 'file_code', 'file_url', 'created_at', 'updated_at']; + protected $fillable = ['id', 'order_id', 'file_name', 'file_code', 'file_url', 'original_name', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 76d2ab5..9d5e4df 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -73,6 +73,7 @@ class StockOrdersService case 'print': case 'approval': $config['where']['id'] = $p['id']; + $config['with'][] = 'files'; // 添加文件附件关联 break; case 'detail': case 'show': @@ -102,22 +103,14 @@ class StockOrdersService 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') ?? ''; - $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; - - // if ($data['is_final'] == 1) { - $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; - $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; - // } - + // 检查绑定 + (new UserService())->checkBind($userId); + // 保存不检查文件上传 + if ($data['approval_status'] != 0) { + $this->validateRequiredFiles($data); + } + // 终版备货单时,至少一个车辆填写了vin if ($data['is_final'] == 1) { - // 检查是否至少有一个车辆填写了vin $hasVin = false; foreach ($data['veh_list'] as $k => $v) { if ($v['vin'] != '') { @@ -125,17 +118,105 @@ class StockOrdersService break; } } - // 如果没有任何车辆填写vin,则抛出异常 if (!$hasVin) { throw new BusinessException('至少需要为一辆车辆填写vin,才能提交审核'); } } - - // 检查绑定 - (new UserService())->checkBind($userId); + $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') ?? ''; + $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; + $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; + $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; return $data; } + /** + * 验证创建备货单时必需的文件是否已提供 + * + * @param array $data 提交的数据 + * @throws BusinessException 当必需的文件缺失或为空时抛出异常 + */ + private function validateRequiredFiles(array $data): void + { + $requiredFiles = [ + 'contract' => '合同', + 'transaction_slip' => '水单', + 'pi_doc' => 'PI' + ]; + + foreach ($requiredFiles as $fileType => $fileLabel) { + if (!isset($data[$fileType]) || !is_array($data[$fileType]) || empty($data[$fileType])) { + throw new BusinessException("创建备货单时{$fileLabel}文件不能为空"); + } + + // 检查文件数组中的每个条目是否都有有效的URL + $hasValidFile = false; + foreach ($data[$fileType] as $fileInfo) { + if (isset($fileInfo['url']) && !empty($fileInfo['url'])) { + $hasValidFile = true; + break; + } + } + + if (!$hasValidFile) { + throw new BusinessException("创建备货单时{$fileLabel}文件不能为空"); + } + } + } + + /** + * 验证备货单审批时必需的文件是否存在 + * + * @param StockOrders $item 备货单对象 + * @throws BusinessException 当必需的文件缺失或为空时抛出异常 + */ + private function validateRequiredFilesForApproval($item): void + { + // 确保已加载文件关联 + if (!isset($item->files)) { + $item->load('files'); + } + + $requiredFiles = [ + \app\common\model\StockOrdersFiles::CONTRACT => '合同', + \app\common\model\StockOrdersFiles::TRANSACTION_SLIP => '水单', + \app\common\model\StockOrdersFiles::PI_DOC => 'PI' + ]; + + // 按文件类型分组现有文件 + $existingFiles = []; + foreach ($item->files as $file) { + if (!isset($existingFiles[$file->file_code])) { + $existingFiles[$file->file_code] = []; + } + $existingFiles[$file->file_code][] = $file; + } + + // 检查每种必需文件是否存在 + foreach ($requiredFiles as $fileCode => $fileLabel) { + if (!isset($existingFiles[$fileCode]) || empty($existingFiles[$fileCode])) { + throw new BusinessException("提交审批前必须上传{$fileLabel}文件"); + } + + // 检查文件中是否有有效的URL + $hasValidFile = false; + foreach ($existingFiles[$fileCode] as $file) { + if (!empty($file->file_url)) { + $hasValidFile = true; + break; + } + } + + if (!$hasValidFile) { + throw new BusinessException("提交审批前必须上传有效的{$fileLabel}文件"); + } + } + } + protected function afterServiceStore($res) { // 处理原单 @@ -161,7 +242,8 @@ class StockOrdersService { // 启动审批 $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->start('stock_orders', 'stock_orders', $item->id, [], $item->salesperson_id); + $formData = $this->checkNewOrUsedVehicles($item->id); + $appro->start('stock_orders', 'stock_orders', $item->id, $formData, $item->salesperson_id); // 异步生成pdf文件 \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); } @@ -270,10 +352,14 @@ class StockOrdersService public function serviceApproval(Request $request) { $config = $this->buildQueryConfig($request, 'approval'); + $item = $this->logicShow($config); + + // 检查备货单是否包含必需的文件 + $this->validateRequiredFilesForApproval($item); + $this->logicUpdateBy($config, [ 'approval_status' => 1 ]); - $item = $this->logicShow($config); $this->startApproval($item); return $this->postProcess('Approval', $item); } @@ -295,7 +381,8 @@ class StockOrdersService public function approvalNullify($item) { $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, [], $item->salesperson_id); + $formData = $this->checkNewOrUsedVehicles($item->id); + $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, $formData, $item->salesperson_id); } /** @@ -364,6 +451,9 @@ class StockOrdersService */ public function beforeServiceUpdate($data, Request $request) { + // 检查必填文件是否已上传 + // $this->validateRequiredFiles($data); + $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']); @@ -479,14 +569,19 @@ class StockOrdersService // 检查数据中是否存在该字段且为数组 if (!empty($data[$field]) && is_array($data[$field])) { // 处理该字段下的每个文件 - foreach ($data[$field] as $fileUrl) { + foreach ($data[$field] as $fileInfo) { $fileData = [ 'order_id' => $order->id, 'file_code' => $fileCode, - 'file_url' => $fileUrl, + 'file_url' => $fileInfo['url'] ?? '', 'file_name' => StockOrdersFiles::FILE_CODE[$fileCode], + 'original_name' => $fileInfo['original_name'] ?? StockOrdersFiles::FILE_CODE[$fileCode], ]; - StockOrdersFiles::create($fileData); + + // 只有当file_url不为空时才创建记录 + if (!empty($fileData['file_url'])) { + StockOrdersFiles::create($fileData); + } } } } @@ -905,6 +1000,7 @@ class StockOrdersService 'id' => $file->id, 'file_url' => $file->file_url, 'file_full_url' => $this->getFileUrl($file->file_url), + 'original_name' => $file->original_name, ]; } @@ -978,4 +1074,45 @@ class StockOrdersService } return (new \app\common\service\OssService)->generatePrivateUrl($fileUrl); } + + /** + * 检查备货单中的车辆是新车还是二手车 + * @param int $stockOrderId 备货单ID + * @return array 返回包含has_new和has_used的数组 + */ + public function checkNewOrUsedVehicles(int $stockOrderId): array + { + // 获取备货单关联的所有车辆项 + $vehicles = StockOrderItems::where('order_id', $stockOrderId)->get(); + + $hasNew = false; + $hasUsed = false; + $submitNote = ''; + + foreach ($vehicles as $vehicle) { + // 判断是否为二手车(mileage > 0) + if ($vehicle->mileage > 0) { + $hasUsed = true; + $submitNote = '本单只有二手车'; + } else { + $hasNew = true; + $submitNote = '本单只有新车'; + } + + // 如果两种类型都已经存在,可以提前退出循环 + if ($hasNew && $hasUsed) { + $submitNote = '本单包含新车和二手车'; + break; + } + } + + return [ + 'has_new' => $hasNew ? 1 : 0, + 'has_used' => $hasUsed ? 1 : 0, + 'submit_note' => $submitNote + ]; + } + + + } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 6eae8d8..a26b79a 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -68,7 +68,7 @@ class ApprovalFlowService } $this->sendNotification($approverIds, $instance, 'pending', false, $extra); - $this->notifyCCs($firstStep, $instance, 'pending'); + // $this->notifyCCs($firstStep, $instance, 'pending'); $this->pushTodoChanged($approverIds); @@ -193,6 +193,12 @@ class ApprovalFlowService 'updated_at' => $now, ]); $this->onFlowFinalized($instance, $finalStatus); + + // 新增:在流程最终完成时才发本步骤的抄送 + if (!empty($step['cc'])) { + $this->notifyCCs($step, $instance, $notifyAction); + } + $this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false); $this->pushTodoChanged([$approverId]); return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')]; @@ -215,7 +221,7 @@ class ApprovalFlowService $this->insertApprovalRecord($instance, $nextIndex, $uid); } $this->sendNotification($nextApprovers, $instance, 'pending', false); - $this->notifyCCs($nextStep, $instance, 'pending'); + // $this->notifyCCs($nextStep, $instance, 'pending'); $this->pushTodoChanged(array_merge([$approverId], $nextApprovers)); return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; }); diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index 3716c69..3f6c6e4 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -61,43 +61,49 @@ class StockOrdersController extends BaseController if (isset($data['pick_up_date']) && $data['pick_up_date'] > 0) $data['pick_up_date'] = date('Y-m-d', $data['pick_up_date']); if (isset($data['collect_all_date']) && $data['collect_all_date'] > 0) $data['collect_all_date'] = date('Y-m-d', $data['collect_all_date']); - // 处理文件附件 + // 处理文件附件:只允许 _url 字段调用 OSS 生成私有链接 $fileTypes = [ 'contract' => \app\common\model\StockOrdersFiles::CONTRACT, 'transaction_slip' => \app\common\model\StockOrdersFiles::TRANSACTION_SLIP, 'pi_doc' => \app\common\model\StockOrdersFiles::PI_DOC ]; - + // 初始化文件字段 foreach (array_keys($fileTypes) as $field) { - $data[$field] = []; - $data[$field . '_url'] = []; + $data[$field] = []; // 存储原始文件信息(不包含签名链接) + $data[$field . '_url'] = []; // 存储通过OSS生成的私有链接 } - - // 按file_code分组文件 + + // 按 file_code 分组文件 if (!empty($data['files'])) { $groupedFiles = []; foreach ($data['files'] as $file) { - if (!isset($groupedFiles[$file['file_code']])) { - $groupedFiles[$file['file_code']] = []; + $code = $file['file_code']; + if (!isset($groupedFiles[$code])) { + $groupedFiles[$code] = []; } - $groupedFiles[$file['file_code']][] = $file['file_url']; + $groupedFiles[$code][] = [ + 'url' => $file['file_url'], // 原始路径 + 'original_name' => $file['original_name'] // 原始文件名 + ]; } - - // 映射到对应字段 + + // 映射到对应字段,并仅对 _url 调用 OSS 服务 foreach ($fileTypes as $field => $fileCode) { if (isset($groupedFiles[$fileCode])) { - // 原始URL数组 + // 主字段存储基本信息(无签名链接) $data[$field] = $groupedFiles[$fileCode]; - - // 完整URL数组 - $fullUrls = []; - foreach ($groupedFiles[$fileCode] as $url) { - $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + + // 只有 *_url 字段才生成带签名的完整 URL + $signedUrls = []; + foreach ($groupedFiles[$fileCode] as $fileInfo) { + $signedUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($fileInfo['url']); } - $data[$field . '_url'] = $fullUrls; + $data[$field . '_url'] = $signedUrls; } } + + // 清理临时数据 unset($data['files']); }