From 84c588496b19ff48c92f78be485f2459eced7536 Mon Sep 17 00:00:00 2001 From: mzl Date: Mon, 8 Sep 2025 20:00:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=A4=87=E8=B4=A7=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/UserController.php | 12 + app/admin/service/SysDictService.php | 13 +- app/admin/service/UserService.php | 10 + app/common/model/StockOrderItems.php | 3 +- app/common/model/StockOrders.php | 2 +- app/common/service/StockOrdersService.php | 127 +++- app/common/service/UserService.php | 7 +- .../approval/service/ApprovalFlowService.php | 330 ++++------ .../service/ApprovalFlowService_bak2.php | 620 ++++++++++++++++++ app/common/utils/pdf/template/stockOrder.stub | 30 +- app/controller/IndexController.php | 22 +- app/crm/controller/StockOrdersController.php | 15 +- config/route.php | 1 + public/01.jpg | Bin 0 -> 36265 bytes 14 files changed, 945 insertions(+), 247 deletions(-) create mode 100644 app/common/utils/approval/service/ApprovalFlowService_bak2.php create mode 100644 public/01.jpg diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php index 25f2385..8c24c6c 100644 --- a/app/admin/controller/UserController.php +++ b/app/admin/controller/UserController.php @@ -154,4 +154,16 @@ class UserController extends BaseController return $this->error($e->getMessage()); } } + + + public function updateSign(Request $request) + { + try { + $data = $request->post(); + $this->service->serviceUpdateSign($request, $data); + return $this->success([], '签名成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php index 859628c..2d29b21 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -80,8 +80,17 @@ class SysDictService break; case 'scope': $config['order']['id'] = 'desc'; + // 决定查id还是code + $scopeType = 0; + if (isset($p['scope_type']) && $p['scope_type'] > 0) { + $scopeType = 1; + } if (isset($p['scope']) && !empty($p['scope'])) { - $config['where']['id'] = ['in', explode(',', $p['scope'])]; + if ($scopeType == 1) { + $config['where']['dict_code'] = ['in', explode(',', $p['scope'])]; + } else { + $config['where']['id'] = ['in', explode(',', $p['scope'])]; + } } $config['with']['sysDictItem'][] = 'dict_id'; $config['with']['sysDictItem'][] = 'item_label'; @@ -95,7 +104,7 @@ class SysDictService $config['with']['sysDictItem'][] = 'item_value'; break; } - + return $config; } diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php index 6675a89..4484552 100644 --- a/app/admin/service/UserService.php +++ b/app/admin/service/UserService.php @@ -113,6 +113,7 @@ class UserService $config['where']['id'] = $p['id']; break; case 'edit': + case 'updateSign': $config['where']['id'] = $request->user->uid; break; } @@ -154,4 +155,13 @@ class UserService $user = $this->logicUpdateBy($config,$data); return $user; } + + + + public function serviceUpdateSign(Request $request, array $data) + { + $config = $this->buildQueryConfig($request, 'updateSign'); + $user = $this->logicUpdateBy($config,$data); + return $user; + } } \ No newline at end of file diff --git a/app/common/model/StockOrderItems.php b/app/common/model/StockOrderItems.php index da57006..f45c532 100644 --- a/app/common/model/StockOrderItems.php +++ b/app/common/model/StockOrderItems.php @@ -44,7 +44,8 @@ class StockOrderItems extends Model protected $fillable = [ 'id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot', 'model_snapshot', 'quantity', 'color_interior_code', 'color_interior_snapshot', - 'color_exterior_code', 'color_exterior_snapshot', 'remark', 'vin', 'created_at', 'updated_at' + 'color_exterior_code', 'color_exterior_snapshot', 'remark', 'vin', 'created_at', 'updated_at', + 'mileage', 'unique_id' ]; /** diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index 2c4c8d3..a52c791 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -87,6 +87,6 @@ class StockOrders extends Model */ public function moneyValues() { - return $this->hasMany(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); + return $this->hasOne(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); } } \ No newline at end of file diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 17e9fbd..402937c 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -103,20 +103,65 @@ class StockOrdersService // $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'], '优惠价'); - $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); - $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); - $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); - $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); - $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); - $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); + // $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 = $veh['unit_price']['amount'] - ($veh['official_price']['amount'] / $created->contract_exchange_rate) - $veh['discount_price']['amount'] - $veh['handling_fee']['amount'] - $veh['fob_transport_fee']['amount'] - $veh['port_misc_fee']['amount'] - $veh['retrofit_fee']['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 @@ -193,8 +238,8 @@ class StockOrdersService $pdfBin = $driver->htmlToPdf($html); // 存储 - $dir = public_path('pdf'); - $fileName = '/'.($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; // 本地保存 $filePath = \app\common\utils\pdf\service\PdfStorage::saveBinary( @@ -209,7 +254,11 @@ class StockOrdersService // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', // ], $pdfBin); - return ['file_url' => $dir.$fileName]; + $url = [ + 'file_name' => $fileName, + 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + ]; + return $url; } @@ -219,10 +268,10 @@ class StockOrdersService $index = 0; foreach ($item->items as $k => $v) { // 存在则数量加1 - if (count($vehList) > 0 && isset($vehList[$v->sku_id])) { - $vehList[$v->sku_id]['num'] += 1; - $vehList[$v->sku_id]['total_price'] += $vehList[$v->sku_id]['unit_price']; - $vehList[$v->sku_id]['total_price_text'] = $vehList[$v->sku_id]['total_price_symbol'].number_format($vehList[$v->sku_id]['total_price'], 2, '.', ','); + 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; @@ -232,7 +281,7 @@ class StockOrdersService $mv[$money->field] = [ 'currency' => $money->currency, 'amount' => $money->amount, - 'rate' => $money->rate, + 'rate' => $money->exchange_rate, 'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'), ]; } @@ -243,29 +292,42 @@ class StockOrdersService } // 处理结果集 - $vehList[$v->sku_id] = [ + $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'], - 'official_price' => $mv['official_price']['amount'], - 'discount_price' => $mv['discount_price']['amount'], - 'unit_price_symbol' => $mv['unit_price']['currency_symbol'], 'total_price_symbol' => $mv['total_price']['currency_symbol'], - 'official_price_symbol' => $mv['official_price']['currency_symbol'], - 'discount_price_symbol' => $mv['discount_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, '.', ','), - 'official_price_text' => $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','), - 'discount_price_text' => $mv['discount_price']['currency_symbol'].number_format($mv['discount_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 = []; @@ -283,6 +345,12 @@ class StockOrdersService $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, @@ -310,7 +378,8 @@ class StockOrdersService // 账户 'account_list' => $accList, - 'acc_index' => $accIndex + 'acc_index' => $accIndex, + 'cif_total_amount' => $cifTotalAmount ]; return $veh; diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 53507b2..74c12c4 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -52,6 +52,9 @@ class UserService 'sn' => $user->sn, 'corp_id' => $user->corp_id, 'corp_name' => $corp_name, + 'report_to' => $user->report_to, + 'sign_img' => $user->sign_img, + 'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'', ], // 'menu' => $menuTree, // 'permissions' => $permissionCodes @@ -84,7 +87,9 @@ class UserService 'email' => $user->email, 'sex' => $user->sex, 'status' => $user->status, - 'roles' => $user->roles->pluck('name') + 'roles' => $user->roles->pluck('name'), + 'sign_img' => $user->sign_img, + 'sign_img_url' => (!empty($user->sign_img))?$ossservice->generatePrivateUrl($user->sign_img):'', ]; } diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 100cdec..a44e1c4 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -34,36 +34,56 @@ class ApprovalFlowService throw new BusinessException("未找到可执行的审批步骤"); } - // 3. 创建审批实例 + // 3. 计算截止时间 + $deadlineAt = null; + if ($flow->timeout_hours > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } + + // 4. 创建审批实例 $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', + 'status' => 'processing', // processing 表示正在进行中 'current_step' => 0, + 'deadline_at' => $deadlineAt, '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]); + // 5. 创建审批记录(待办) + $approverIds = $this->resolveApprovers($firstStep, (object)[ + 'initiator_id' => $initiatorId, + 'form_data' => json_encode($formData) + ]); + foreach ($approverIds as $approverId) { Db::table('approval_records')->insert([ 'instance_id' => $instanceId, - 'step_name' => $firstStep['name'], 'approver_id' => $approverId, + 'step_index' => 0, + 'action' => '', // 未操作时为空 'status' => 'pending', + 'approved_at' => date('Y-m-d H:i:s', 0), // 未审批时设置为初始时间 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), ]); } - // 5. 通知审批人 - $this->sendNotification($approverIds, (object)['id' => $instanceId, 'flow_type' => $flowType], 'pending', false); + // 6. 通知审批人 + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData), + 'initiator_id' => $initiatorId + ]; + $this->sendNotification($approverIds, $instance, 'pending', false); - // 6. 通知抄送人(当前步骤) - $this->notifyCCs($firstStep, (object)['id' => $instanceId, 'flow_type' => $flowType, 'form_data' => json_encode($formData)], 'pending'); + // 7. 通知抄送人(当前步骤) + $this->notifyCCs($firstStep, $instance, 'pending'); return ['instance_id' => $instanceId]; } @@ -73,13 +93,28 @@ class ApprovalFlowService */ public function approve(int $instanceId, int $approverId, string $action, string $comment = '') { + // 验证操作是否有效 + if (!in_array($action, ['approve', 'reject'])) { + throw new BusinessException("无效的审批操作"); + } + $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; + // 获取流程定义 + $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; + $currentStep = $steps[$currentStepIndex] ?? null; if (!$currentStep) { throw new BusinessException("当前步骤配置错误"); @@ -90,20 +125,33 @@ class ApprovalFlowService throw new BusinessException("您无权审批此步骤"); } + // 检查用户是否已经审批过 + $existingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $currentStepIndex) + ->first(); + + if ($existingRecord && $existingRecord->status !== 'pending') { + throw new BusinessException("您已经审批过此步骤"); + } + // 更新审批记录 Db::table('approval_records') ->where('instance_id', $instanceId) ->where('approver_id', $approverId) + ->where('step_index', $currentStepIndex) ->where('status', 'pending') ->update([ - 'status' => $action, + 'action' => $action, + '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']); + // 检查当前步骤是否完成(根据 type: any, all) + $stepCompleted = $this->isStepCompleted($instanceId, $currentStepIndex, $currentStep['type']); if (!$stepCompleted) { // 步骤未完成,不进入下一步 @@ -111,12 +159,13 @@ class ApprovalFlowService } // 步骤已完成,进入下一步或结束 - $nextStep = $this->getNextValidStep($steps, json_decode($instance->form_data, true), $instance->current_step + 1); + $nextStepIndex = $this->getNextStepIndex($steps, json_decode($instance->form_data, true), $currentStepIndex); - if ($nextStep === null) { + if ($nextStepIndex === null) { // 流程结束 Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $action === 'approve' ? 'approved' : 'rejected', + 'status' => $action, // 最终状态与最后一次操作一致 + 'finished_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); @@ -127,21 +176,26 @@ class ApprovalFlowService } // 进入下一步 - $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'), ]); + // 获取下一步信息 + $nextStep = $steps[$nextStepIndex]; + // 创建下一步的审批记录 $nextApprovers = $this->resolveApprovers($nextStep, $instance); foreach ($nextApprovers as $approverId) { Db::table('approval_records')->insert([ 'instance_id' => $instanceId, - 'step_name' => $nextStep['name'], 'approver_id' => $approverId, + 'step_index' => $nextStepIndex, + 'action' => '', 'status' => 'pending', + 'approved_at' => date('Y-m-d H:i:s', 0), 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), ]); } @@ -233,6 +287,26 @@ class ApprovalFlowService $ccUserIds = array_unique(array_filter($ccUserIds)); if (!empty($ccUserIds)) { + // 记录抄送信息到数据库 + foreach ($ccUserIds as $userId) { + // 检查是否已存在抄送记录 + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $userId) + ->exists(); + + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $userId, + 'notified_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'), + ]); + } + } + + // 发送通知 $this->sendNotification($ccUserIds, $instance, $action, true); } } @@ -264,7 +338,7 @@ class ApprovalFlowService case '==': return $actualValue == $value; case '!=': return $actualValue != $value; case '>': return $actualValue > $value; - case '<': $actualValue < $value; + case '<': return $actualValue < $value; case '>=': return $actualValue >= $value; case '<=': return $actualValue <= $value; default: throw new BusinessException("不支持的操作符: {$op}"); @@ -272,14 +346,14 @@ class ApprovalFlowService } /** - * 获取下一个有效步骤(考虑 condition) + * 获取下一个有效步骤索引(考虑 condition) */ - private function getNextValidStep(array $steps, array $formData, int $startIndex = 0): ?array + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int { - for ($i = $startIndex; $i < count($steps); $i++) { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { $step = $steps[$i]; if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { - return $step; + return $i; } } return null; @@ -299,20 +373,38 @@ class ApprovalFlowService */ private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool { - $pendingCount = Db::table('approval_records') + // 获取当前步骤的所有审批记录 + $records = Db::table('approval_records') ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->count(); + ->where('step_index', $stepIndex) + ->get(); - if ($type === 'any') { - return $pendingCount === 0; // 任一通过即可 + if ($records->isEmpty()) { + return false; } - if ($type === 'all' || $type === 'sequential') { - return $pendingCount === 0; // 所有人都必须审批 + switch ($type) { + case 'any': + // 任意一人审批即可(approve或reject都算完成) + foreach ($records as $record) { + if ($record->status !== 'pending') { + return true; + } + } + return false; + + case 'all': + // 所有人都必须审批 + foreach ($records as $record) { + if ($record->status === 'pending') { + return false; + } + } + return true; + + default: + return false; } - - return false; } /** @@ -341,21 +433,12 @@ class ApprovalFlowService */ 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') + 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.*') ->get() ->toArray(); } @@ -406,12 +489,7 @@ class ApprovalFlowService if ($hasReject) { $status = 'rejected'; } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; - } else { - $status = 'approved'; - } + $status = 'approved'; } } elseif ($index < $instance->current_step) { $status = 'approved'; @@ -449,172 +527,40 @@ class ApprovalFlowService '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'); + $updateData['status'] = 'reject'; } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approved'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'approved_timeout'); + $updateData['status'] = 'approve'; } + + Db::table('approval_instances')->where('id', $instance->id)->update($updateData); + $this->notifyInitiator($instance, $flow->timeout_action); } } } // --- 辅助方法 --- - // 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 字段 + ->where('r.code', $roleCode) ->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/utils/approval/service/ApprovalFlowService_bak2.php b/app/common/utils/approval/service/ApprovalFlowService_bak2.php new file mode 100644 index 0000000..9b6babd --- /dev/null +++ b/app/common/utils/approval/service/ApprovalFlowService_bak2.php @@ -0,0 +1,620 @@ +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/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index a6aa700..df20d14 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -140,10 +140,10 @@ 颜色/外 内饰 改装 - 单价(美金) - 总价(美金) - 官方指导价 - 优惠 + 单价 + 总价 + 官方指导价/二手挂牌价 + 优惠/二手减价 @@ -157,8 +157,15 @@ - - + + 0): ?> + + + + + + + @@ -232,6 +239,17 @@ + +
+
+
+ 下单人: + +
+
+
下单时间:2025-09-08
+
+ \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 8f6428f..8039205 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -107,13 +107,23 @@ class IndexController public function test() { - return json([ + + + $flowType = 'stock_order_flow'; + $bizType = 'stock_orders'; + $bizId = 32; + $formData = []; + $initiatorId = 74; + + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); + + $sp = $appro->approve(1, 1, 'approve'); + var_dump($sp); + + return json([ 'code' => 0, - 'msg' => 'success', - 'data' => [ - 'time' => time(), - 'date' => date('Y-m-d H:i:s') - ] + 'msg' => 'success' ]); } } \ No newline at end of file diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index f32f03f..0930a1a 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -51,16 +51,13 @@ class StockOrdersController extends BaseController } if (count($data['moneyValues']) > 0) { - foreach ($data['moneyValues'] as $mvs) { - $data['cif_total_amount'] = [ - 'exchange_rate' => $mvs['exchange_rate'], - 'amount' => $mvs['amount'], - 'currency' => $mvs['currency'], - 'id' => $mvs['id'] - ]; - } + $data['cif_total_amount'] = [ + 'exchange_rate' => $data['moneyValues']['exchange_rate'], + 'amount' => $data['moneyValues']['amount'], + 'currency' => $data['moneyValues']['currency'], + 'id' => $data['moneyValues']['id'] + ]; } - unset($data['moneyValues']); foreach ($data['items'] as &$item) { foreach ($item['moneyValues'] as $mv) { diff --git a/config/route.php b/config/route.php index cd2c03c..a46227c 100644 --- a/config/route.php +++ b/config/route.php @@ -153,6 +153,7 @@ Route::group('/admin', function () { Route::any('/authorize', [app\admin\controller\UserController::class, 'authorize']); Route::any('/set/resources', [app\admin\controller\UserController::class, 'updateResources']); Route::any('/update/password', [app\admin\controller\UserController::class, 'savePassword']); + Route::any('/update/sign', [app\admin\controller\UserController::class, 'updateSign']); }); // 询单 diff --git a/public/01.jpg b/public/01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d646fb21ecd05aeb56afeabebd4abb89699eb22b GIT binary patch literal 36265 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 C7