From bc8c15a3a9354ee221287a4f6602cf43bba90170 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 8 Sep 2025 10:37:06 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/SysCurrencyService.php | 18 +++++++ app/admin/service/SysDictItemService.php | 18 +++++++ app/admin/service/SysDictService.php | 18 +++++++ app/common/model/SysCurrencyTranslation.php | 49 +++++++++++++++++++ app/common/model/SysDictItemTranslation.php | 49 +++++++++++++++++++ app/common/model/SysDictTranslation.php | 49 +++++++++++++++++++ app/common/utils/translation/config/trans.php | 12 +++++ 7 files changed, 213 insertions(+) create mode 100644 app/common/model/SysCurrencyTranslation.php create mode 100644 app/common/model/SysDictItemTranslation.php create mode 100644 app/common/model/SysDictTranslation.php diff --git a/app/admin/service/SysCurrencyService.php b/app/admin/service/SysCurrencyService.php index 19f0529..c3086da 100644 --- a/app/admin/service/SysCurrencyService.php +++ b/app/admin/service/SysCurrencyService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysCurrencyValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysCurrency; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysCurrencyService @@ -58,4 +60,20 @@ class SysCurrencyService return $config; } + + public $translatFields = ['currency_name']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_currency', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } } \ No newline at end of file diff --git a/app/admin/service/SysDictItemService.php b/app/admin/service/SysDictItemService.php index 577b9f0..532cb61 100644 --- a/app/admin/service/SysDictItemService.php +++ b/app/admin/service/SysDictItemService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysDictItemValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDictItem; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysDictItemService @@ -72,4 +74,20 @@ class SysDictItemService return $config; } + + public $translatFields = ['item_label']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_dict_item', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } } \ No newline at end of file diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php index 859628c..e25c572 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysDictValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDict; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysDictService @@ -124,4 +126,20 @@ class SysDictService return $data; } + + public $translatFields = ['dict_name', 'description']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_dict', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } } \ No newline at end of file diff --git a/app/common/model/SysCurrencyTranslation.php b/app/common/model/SysCurrencyTranslation.php new file mode 100644 index 0000000..b6e5b68 --- /dev/null +++ b/app/common/model/SysCurrencyTranslation.php @@ -0,0 +1,49 @@ + \app\ticket\model\CategorieTranslation::class, 'foreign_key' => 'categorie_id', ], + 'sys_currency' => [ + 'translation_model' => app\common\model\SysCurrencyTranslation::class, + 'foreign_key' => 'currency_id', + ], + 'sys_dict' => [ + 'translation_model' => app\common\model\SysDictTranslation::class, + 'foreign_key' => 'dict_id', + ], + 'sys_dict_item' => [ + 'translation_model' => app\common\model\SysDictItemTranslation::class, + 'foreign_key' => 'dict_item_id', + ], ], // 默认支持的目标语种列表 From fe8bce4b4c5109780cbbfe848eb8735ca385311f Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 8 Sep 2025 17:16:00 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/SysDictItemService.php | 2 +- app/common/model/SysDictItemTranslation.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/admin/service/SysDictItemService.php b/app/admin/service/SysDictItemService.php index 532cb61..98430b9 100644 --- a/app/admin/service/SysDictItemService.php +++ b/app/admin/service/SysDictItemService.php @@ -75,7 +75,7 @@ class SysDictItemService return $config; } - public $translatFields = ['item_label']; + public $translatFields = ['item_label','remark']; public function afterServiceStore($res) { diff --git a/app/common/model/SysDictItemTranslation.php b/app/common/model/SysDictItemTranslation.php index e4446ff..8d33913 100644 --- a/app/common/model/SysDictItemTranslation.php +++ b/app/common/model/SysDictItemTranslation.php @@ -40,7 +40,7 @@ class SysDictItemTranslation extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'dict_id', 'locale', 'dict_name', 'description', 'created_at', 'updated_at']; + protected $fillable = ['id', 'dict_id', 'remark', 'locale', 'dict_name', 'description', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 From 1c45155cf6c6473abe53de242860227e06af5a96 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 9 Sep 2025 11:03:19 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=B8=AF=E5=8F=A3=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/SysPortService.php | 18 +++++++ app/admin/service/VehColorService.php | 18 +++++++ app/common/model/SysPortTranslation.php | 49 +++++++++++++++++++ app/common/model/VehColorTranslation.php | 49 +++++++++++++++++++ app/common/utils/translation/config/trans.php | 8 +++ 5 files changed, 142 insertions(+) create mode 100644 app/common/model/SysPortTranslation.php create mode 100644 app/common/model/VehColorTranslation.php diff --git a/app/admin/service/SysPortService.php b/app/admin/service/SysPortService.php index 6e8774c..a487848 100644 --- a/app/admin/service/SysPortService.php +++ b/app/admin/service/SysPortService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysPortValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysPort; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysPortService @@ -22,6 +24,8 @@ class SysPortService */ protected $validator = SysPortValidate::class; + public $translatFields = ['port_name','description']; + /** * 获取验证器 */ @@ -97,6 +101,20 @@ class SysPortService return $data; } + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_port_translation', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } + /** * 添加前置处理 */ diff --git a/app/admin/service/VehColorService.php b/app/admin/service/VehColorService.php index a701ef4..a50a262 100644 --- a/app/admin/service/VehColorService.php +++ b/app/admin/service/VehColorService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\VehColorValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\VehColor; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; class VehColorService { @@ -21,6 +23,8 @@ class VehColorService */ protected $validator = VehColorValidate::class; + public $translatFields = ['color_text']; + /** * 获取验证器 */ @@ -28,4 +32,18 @@ class VehColorService { return new $this->validator; } + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'veh_color_translation', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } } \ No newline at end of file diff --git a/app/common/model/SysPortTranslation.php b/app/common/model/SysPortTranslation.php new file mode 100644 index 0000000..8724caf --- /dev/null +++ b/app/common/model/SysPortTranslation.php @@ -0,0 +1,49 @@ + app\common\model\SysDictItemTranslation::class, 'foreign_key' => 'dict_item_id', ], + 'sys_port_translation' => [ + 'translation_model' => app\common\model\SysPortTranslation::class, + 'foreign_key' => 'port_id', + ], + 'veh_color_translation' => [ + 'translation_model' => app\common\model\VehColorTranslation::class, + 'foreign_key' => 'color_id', + ], ], // 默认支持的目标语种列表 From f5e93f2400a3636e06e34e6fe35ceea269eee378 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 9 Sep 2025 13:47:04 +0800 Subject: [PATCH 4/4] Merge branch 'feature/mzl_dev' into develop # Conflicts: # app/admin/service/SysDictService.php --- .../controller/ApprovalFlowsController.php | 16 +- .../ApprovalInstancesController.php | 33 + .../controller/ApprovalRecordsController.php | 75 ++ app/admin/logic/ApprovalInstancesLogic.php | 17 + app/admin/logic/ApprovalRecordsLogic.php | 17 + app/admin/model/ApprovalInstances.php | 107 ++ app/admin/model/ApprovalRecords.php | 84 ++ .../service/ApprovalInstancesService.php | 31 + app/admin/service/ApprovalRecordsService.php | 75 ++ app/admin/service/SysDictService.php | 88 ++ .../validate/ApprovalInstancesValidate.php | 29 + .../validate/ApprovalRecordsValidate.php | 29 + app/common/model/StockOrders.php | 6 + app/common/model/SysDict.php | 6 + app/common/model/SysDictCategory.php | 9 + app/common/service/StockOrdersService.php | 12 +- .../approval/service/ApprovalFlowService.php | 1101 ++++++++++------- .../service/ApprovalFlowService_bak.php | 962 +++++++++----- .../service/ApprovalFlowService_bak2.php | 620 ---------- app/common/utils/pdf/template/stockOrder.stub | 4 +- app/controller/IndexController.php | 26 +- config/route.php | 9 + public/pdf/BH20250908014.pdf | Bin 0 -> 65734 bytes 23 files changed, 1929 insertions(+), 1427 deletions(-) create mode 100644 app/admin/controller/ApprovalInstancesController.php create mode 100644 app/admin/controller/ApprovalRecordsController.php create mode 100644 app/admin/logic/ApprovalInstancesLogic.php create mode 100644 app/admin/logic/ApprovalRecordsLogic.php create mode 100644 app/admin/model/ApprovalInstances.php create mode 100644 app/admin/model/ApprovalRecords.php create mode 100644 app/admin/service/ApprovalInstancesService.php create mode 100644 app/admin/service/ApprovalRecordsService.php create mode 100644 app/admin/validate/ApprovalInstancesValidate.php create mode 100644 app/admin/validate/ApprovalRecordsValidate.php delete mode 100644 app/common/utils/approval/service/ApprovalFlowService_bak2.php create mode 100644 public/pdf/BH20250908014.pdf diff --git a/app/admin/controller/ApprovalFlowsController.php b/app/admin/controller/ApprovalFlowsController.php index 8f8ae24..c2babff 100644 --- a/app/admin/controller/ApprovalFlowsController.php +++ b/app/admin/controller/ApprovalFlowsController.php @@ -22,14 +22,14 @@ class ApprovalFlowsController extends BaseController * @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; - } + // 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 desc() diff --git a/app/admin/controller/ApprovalInstancesController.php b/app/admin/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..483ec40 --- /dev/null +++ b/app/admin/controller/ApprovalInstancesController.php @@ -0,0 +1,33 @@ +service = new ApprovalInstancesService(); + } + + /** + * 列表页数据格式化 + * + * @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/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..46712b3 --- /dev/null +++ b/app/admin/controller/ApprovalRecordsController.php @@ -0,0 +1,75 @@ +service = new ApprovalRecordsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['approvalInstance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['approvalInstance']['biz_id'], + 'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['approvalInstance']['flow_type'], + + 'initiator_name' => $record['approvalInstance']['initiator']['name'], + 'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'status' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['approvalInstance']['finished_at'], + 'deadline_at' => $record['approvalInstance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } + + + public function approve(Request $request) + { + try { + $res = $this->service->serviceApprove($request); + return $this->success([], $res['msg']); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/logic/ApprovalInstancesLogic.php b/app/admin/logic/ApprovalInstancesLogic.php new file mode 100644 index 0000000..bc8ce88 --- /dev/null +++ b/app/admin/logic/ApprovalInstancesLogic.php @@ -0,0 +1,17 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一个审批实例可以有多个审批记录 + */ + public function approvalRecords(): HasMany + { + return $this->hasMany(ApprovalRecords::class, 'instance_id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } + + public function flows() + { + return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type'); + } + + public function initiator() + { + return $this->belongsTo(User::class, 'initiator_id', 'id'); + } + + + public function bizInfo() + { + return $this->hasOne(StockOrders::class, 'id', 'biz_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/ApprovalRecords.php b/app/admin/model/ApprovalRecords.php new file mode 100644 index 0000000..a970ce4 --- /dev/null +++ b/app/admin/model/ApprovalRecords.php @@ -0,0 +1,84 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一条审批记录属于一个审批实例 + */ + public function approvalInstance(): BelongsTo + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id'); + } + + /** + * 关联审批人 + */ + public function approver() + { + return $this->belongsTo(User::class, 'approver_id'); + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalInstancesService.php b/app/admin/service/ApprovalInstancesService.php new file mode 100644 index 0000000..db9b0cb --- /dev/null +++ b/app/admin/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php new file mode 100644 index 0000000..0bc6325 --- /dev/null +++ b/app/admin/service/ApprovalRecordsService.php @@ -0,0 +1,75 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + + $config['with'] = [ + 'approvalInstance', + 'approver', + 'approvalInstance.flows', + 'approvalInstance.initiator', + 'approvalInstance.bizInfo' + ]; + 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 serviceApprove(Request $request) + { + $param = $request->all(); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']); + } +} \ No newline at end of file diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php index 68cd233..dfb7f53 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -5,6 +5,9 @@ namespace app\admin\service; use app\admin\validate\SysDictValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDict; +use app\common\model\SysDictCategory; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use app\common\utils\translation\service\TranslationService; use app\queue\utils\Translat; use support\Request; @@ -151,4 +154,89 @@ class SysDictService $traitsourceFields ); } + + + /* + + 示例用法: + + // 1) 获取分类为“车辆类型”的所有字典 + $list = $dictService->getDictsByCategoryName('车辆类型'); + // => [['dict_code' => 'SUV', 'dict_name' => '越野车'], …] + + // 2) 获取某分类下某个 code 的名称 + $name = $dictService->getDictName('车辆类型', 'SUV'); + // => "越野车" + + // 3) 获取某分类下拉选项 + $options = $dictService->getDictOptions('车辆类型'); + // => ['SUV' => '越野车', 'SEDAN' => '轿车', …] + + // 4) 通过 code 直接拿名称(不管分类) + $name = $dictService->getNameByCode('SUV'); + // => "越野车" + + */ + + + /** + * 根据分类名获取字典列表 + */ + public function getDictsByCategoryName(string $categoryName): array + { + $category = SysDictCategory::where('category_name', $categoryName) + ->where('status', 1) + ->first(); + + if (!$category) { + return []; + } + + return $category->dicts()->get(['dict_code', 'dict_name'])->toArray(); + } + + /** + * 根据分类名 + 字典code 获取字典项名称 + */ + public function getDictName(string $categoryName, string $dictCode): ?string + { + $dict = SysDict::whereHas('category', function ($q) use ($categoryName) { + $q->where('category_name', $categoryName)->where('status', 1); + }) + ->where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } + + /** + * 根据分类名获取下拉键值对 + * 返回格式:['code1' => '名称1', 'code2' => '名称2'] + */ + public function getDictOptions(string $categoryName): array + { + $dicts = $this->getDictsByCategoryName($categoryName); + + $options = []; + foreach ($dicts as $item) { + $options[$item['dict_code']] = $item['dict_name']; + } + + return $options; + } + + /** + * 根据字典 code 获取对应的名称(不依赖分类) + */ + public function getNameByCode(string $dictCode): ?string + { + $dict = SysDict::where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } } \ No newline at end of file diff --git a/app/admin/validate/ApprovalInstancesValidate.php b/app/admin/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..e6641be --- /dev/null +++ b/app/admin/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/ApprovalRecordsValidate.php b/app/admin/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..796768c --- /dev/null +++ b/app/admin/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index a52c791..815783e 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -89,4 +89,10 @@ class StockOrders extends Model { return $this->hasOne(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); } + + + public function salesperson() + { + return $this->hasOne(User::class, 'id', 'salesperson_id'); + } } \ No newline at end of file diff --git a/app/common/model/SysDict.php b/app/common/model/SysDict.php index ad7cf5c..02534df 100644 --- a/app/common/model/SysDict.php +++ b/app/common/model/SysDict.php @@ -56,4 +56,10 @@ class SysDict extends Model { return $this->hasMany(SysDictItem::class, 'dict_id', 'id'); } + + + public function category() + { + return $this->belongsTo(SysDictCategory::class, 'category_id', 'id'); + } } \ No newline at end of file diff --git a/app/common/model/SysDictCategory.php b/app/common/model/SysDictCategory.php index fef0219..cbca49a 100644 --- a/app/common/model/SysDictCategory.php +++ b/app/common/model/SysDictCategory.php @@ -46,4 +46,13 @@ class SysDictCategory extends Model * 不允许自动填充模型字段 */ protected $guarded = []; + + + public function dicts() + { + return $this->hasMany(SysDict::class, 'category_id', 'id') + ->where('status', 1) + ->whereNull('deleted_at') + ->orderBy('id', 'asc'); + } } \ No newline at end of file diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 402937c..fadd6cc 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -73,6 +73,7 @@ class StockOrdersService $config['with'][] = 'items.moneyValues'; $config['with'][] = 'accounts'; $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; break; case 'store': break; @@ -219,6 +220,10 @@ class StockOrdersService flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); } } + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); + var_dump($result); return $res; } @@ -242,7 +247,7 @@ class StockOrdersService $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; // 本地保存 - $filePath = \app\common\utils\pdf\service\PdfStorage::saveBinary( + \app\common\utils\pdf\service\PdfStorage::saveBinary( $pdfBin, $dir, $fileName @@ -354,6 +359,9 @@ class StockOrdersService $veh = [ 'order_no' => $item->order_no, 'salesperson_snapshot' => $item->salesperson_snapshot, + // 'sign_img' => $item->salesperson->sign_img, + 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), 'pi_no' => $item->pi_no, 'biz_date' => date('Y-m-d', $item->biz_date), @@ -381,7 +389,7 @@ class StockOrdersService 'acc_index' => $accIndex, 'cif_total_amount' => $cifTotalAmount ]; - + return $veh; } } \ 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 a44e1c4..0e32dfb 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -1,436 +1,232 @@ where('type', $flowType) - ->where('is_active', 1) - ->first(); + return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { + $flow = Db::table('approval_flows')->where('type', $flowType)->where('is_active', 1)->first(); + if (!$flow) throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - if (!$flow) { - throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - } + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) throw new BusinessException("流程步骤配置错误"); - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) { - throw new BusinessException("流程步骤配置错误"); - } + // 找首个有效步骤索引(考虑条件) + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤"); + $firstStep = $steps[$firstIndex]; - // 2. 查找第一个有效步骤(考虑 condition) - $firstStep = $this->getNextValidStep($steps, $formData); - if (!$firstStep) { - throw new BusinessException("未找到可执行的审批步骤"); - } + $now = date('Y-m-d H:i:s'); + $deadlineAt = null; + if (($flow->timeout_hours ?? 0) > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } - // 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', // 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'), - ]); - - // 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, - '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'), + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => $firstIndex, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, ]); - } - // 6. 通知审批人 - $instance = (object)[ - 'id' => $instanceId, - 'flow_type' => $flowType, - 'form_data' => json_encode($formData), - 'initiator_id' => $initiatorId - ]; - $this->sendNotification($approverIds, $instance, 'pending', false); + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + ]; - // 7. 通知抄送人(当前步骤) - $this->notifyCCs($firstStep, $instance, 'pending'); + // 解析首步审批人(支持 role_id 或 roles.name) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instanceId, $firstIndex, $uid); + } - return ['instance_id' => $instanceId]; + $this->sendNotification($approverIds, $instance, 'pending', false); + $this->notifyCCs($firstStep, $instance, 'pending'); + + return ['instance_id' => $instanceId]; + }); } - /** - * 执行审批 - */ + /** 执行审批(幂等友好) */ public function approve(int $instanceId, int $approverId, string $action, string $comment = '') { - // 验证操作是否有效 - if (!in_array($action, ['approve', 'reject'])) { + if (!in_array($action, ['approve','reject'], true)) { throw new BusinessException("无效的审批操作"); } - $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$instance) { - throw new BusinessException("审批实例未找到"); - } + return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException("审批实例未找到"); + if ($instance->status !== 'processing') { + // return ['status' => 'noop', 'reason' => 'instance_not_processing', 'instance_status' => $instance->status]; + // throw new BusinessException("实例未处理"); + return ['status' => 'noop', 'msg' => '实例未处理']; + } - // 获取流程定义 - $flow = Db::table('approval_flows') - ->where('type', $instance->flow_type) - ->first(); - - if (!$flow) { - 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; - $currentStep = $steps[$currentStepIndex] ?? null; + $steps = json_decode($flow->steps, true); + $idx = (int)$instance->current_step; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); - if (!$currentStep) { - throw new BusinessException("当前步骤配置错误"); - } + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); - // 检查用户是否有权限审批 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } + if (!$pendingRecord) { + // 已处理过? + $anyRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->first(); + if ($anyRecord && $anyRecord->status !== 'pending') { + // return ['status' => 'noop', 'reason' => 'already_handled', 'record_status' => $anyRecord->status]; + // throw new BusinessException("已处理"); + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 + if ((int)$instance->current_step !== $idx) { + // return ['status' => 'noop', 'reason' => 'not_current_step', 'current_step' => (int)$instance->current_step]; + // throw new BusinessException("非当前步骤"); + return ['status' => 'noop', 'msg' => '非当前步骤']; + } + // return ['status' => 'noop', 'reason' => 'no_pending_record']; + return ['status' => 'noop', 'msg' => '无待处理记录']; + } - // 检查用户是否已经审批过 - $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("您已经审批过此步骤"); - } + $now = date('Y-m-d H:i:s'); - // 更新审批记录 - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('step_index', $currentStepIndex) - ->where('status', 'pending') - ->update([ - 'action' => $action, - 'status' => $action, // 状态与操作一致 - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), + // 1) 更新当前人的审批记录 + Db::table('approval_records')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'status' => ($action === 'approve') ? 'approved' : 'rejected', + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, ]); - // 检查当前步骤是否完成(根据 type: any, all) - $stepCompleted = $this->isStepCompleted($instanceId, $currentStepIndex, $currentStep['type']); + // 2) 聚合本步结论(any / all) + $typeRaw = strtolower($step['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $outcome = $this->getStepOutcome($instanceId, $idx, $type); // 'pending' | 'approve' | 'reject' - if (!$stepCompleted) { - // 步骤未完成,不进入下一步 - return ['status' => 'step_pending']; - } + if ($outcome === 'pending') { + // return ['status' => 'step_pending']; + return ['status' => 'step_pending', 'msg' => '步骤待定']; + } - // 步骤已完成,进入下一步或结束 - $nextStepIndex = $this->getNextStepIndex($steps, json_decode($instance->form_data, true), $currentStepIndex); + // 3) 有结论:关闭本步其余 pending + $this->closeOtherPending($instanceId, $idx, [$approverId]); - if ($nextStepIndex === null) { - // 流程结束 + // 4) 拒绝 => 结束 + if ($outcome === 'reject') { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + // return ['status' => 'completed', 'result' => 'reject']; + return ['status' => 'completed', 'msg' => '已拒绝']; + } + + // 5) 通过 => 进入下一步或结束 + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + + if ($nextIndex === null) { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'approved', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); + // return ['status' => 'completed', 'result' => 'approve']; + return ['status' => 'completed', 'msg' => '已同意']; + } + + // 乐观推进 current_step + $affected = Db::table('approval_instances') + ->where('id', $instanceId) + ->where('current_step', $idx) + ->where('status', 'processing') + ->update(['current_step' => $nextIndex, 'updated_at' => $now]); + if (!$affected) { + // return ['status' => 'noop', 'reason' => 'concurrent_progressed']; + return ['status' => 'noop', 'msg' => '并发进展']; + } + + // 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instanceId, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + $this->notifyCCs($nextStep, $instance, 'pending'); + + // return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; + return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + /** 撤回(仅发起人在无已处理记录前可撤回) */ + public function revoke(int $instanceId, int $initiatorId, string $reason = '') + { + return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); + if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); + + $handled = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->whereIn('status', ['approved','rejected','timeout','skipped']) + ->count() > 0; + if ($handled) throw new BusinessException('当前进度不可撤回'); + + $now = date('Y-m-d H:i:s'); Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $action, // 最终状态与最后一次操作一致 - 'finished_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), + 'status' => 'revoked', + 'finished_at' => $now, + 'updated_at' => $now, ]); - // 通知发起人结果 - $this->sendNotification([$instance->initiator_id], $instance, $action, false); + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); - return ['status' => 'completed', 'result' => $action]; - } - - // 进入下一步 - 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, - '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'), - ]); - } - - // 通知下一步审批人 - $this->sendNotification($nextApprovers, $instance, 'pending', false); - - // 通知下一步抄送人 - $this->notifyCCs($nextStep, $instance, 'pending'); - - return ['status' => 'next_step', 'step_name' => $nextStep['name']]; + $this->sendNotification([$initiatorId], $instance, 'revoked', false); + return ['status' => 'completed', 'result' => 'revoked']; + }); } - /** - * 解析审批人 - */ - 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)) { - // 记录抄送信息到数据库 - 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); - } - } - - /** - * 评估条件 - */ - 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 '<': return $actualValue < $value; - case '>=': return $actualValue >= $value; - case '<=': return $actualValue <= $value; - default: throw new BusinessException("不支持的操作符: {$op}"); - } - } - - /** - * 获取下一个有效步骤索引(考虑 condition) - */ - private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int - { - for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { - $step = $steps[$i]; - if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { - return $i; - } - } - 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 - { - // 获取当前步骤的所有审批记录 - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('step_index', $stepIndex) - ->get(); - - if ($records->isEmpty()) { - return false; - } - - 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; - } - } - - /** - * 发送通知(你可以替换为你的通知系统) - */ - 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 { return Db::table('approval_records as ar') @@ -439,30 +235,23 @@ class ApprovalFlowService ->where('ar.status', 'pending') ->where('ai.status', 'processing') ->select('ai.*') + ->distinct() ->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') + ->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->toArray(); } - /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array - */ + /** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */ public function getFlowDiagramData(int $instanceId): array { $instance = Db::table('approval_instances')->find($instanceId); @@ -471,96 +260,496 @@ class ApprovalFlowService $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); if (!$flow) return []; - $steps = json_decode($flow->steps, true); + $steps = json_decode($flow->steps, true) ?: []; $records = Db::table('approval_records') ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') + ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->groupBy('step_index'); - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); + $formData = json_decode($instance->form_data, true) ?: []; + $data = []; + foreach ($steps as $i => $step) { + $actions = ($records->get($i) ?? collect())->toArray(); if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - $status = 'approved'; + $cntApproved = 0; $cntRejected = 0; $cntPending = 0; + foreach ($actions as $a) { + if ($a->status === 'approved') $cntApproved++; + elseif ($a->status === 'rejected') $cntRejected++; + elseif ($a->status === 'pending') $cntPending++; + // skipped/timeout 不阻断完结显示 + } + if ($cntRejected > 0) $status = 'rejected'; + elseif ($cntPending > 0) $status = 'processing'; + elseif ($cntApproved > 0) $status = 'approved'; + else $status = 'approved'; // 全是 skipped/timeout 也视为已过 + } else { + if ($i < (int)$instance->current_step) { + if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { + $status = 'skipped'; + } else { + $status = 'approved'; + } + } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { + $status = 'processing'; + } else { + $status = 'pending'; } - } 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 + $data[] = [ + 'index' => $i, + 'name' => $step['name'] ?? ('Step '.$i), + 'type' => $step['type'] ?? 'all', + 'status' => $status, + 'actions' => $actions, ]; } - return $diagramData; + return $data; } - /** - * 处理超时的审批实例 (定时任务调用) - */ + /** 定时任务:处理超时(实例级) */ public function handleTimeouts() { $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') + $expired = Db::table('approval_instances') ->where('status', 'processing') + ->whereNotNull('deadline_at') ->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 = [ + foreach ($expired as $instance) { + Db::transaction(function () use ($instance, $now) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return; + + $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; + $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; + + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => $final, 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'reject'; - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approve'; - } - - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, $flow->timeout_action); - } + 'updated_at' => $now, + ]); + + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + }); } } - // --- 辅助方法 --- + // ---------- 内部工具 ---------- - private function getUserIdsByRole(string $roleCode): array + /** 解析审批人(role/user/initiator_leader/field_user/dept_head) */ + private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array { - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) - ->pluck('ur.user_id') - ->toArray(); + $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + + $confs = $step['approvers'] ?? []; + foreach ($confs as $cfg) { + $typ = $cfg['type'] ?? ''; + $val = $cfg['value'] ?? null; + $ids = []; + + switch ($typ) { + case 'role': + $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; + $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; + $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); + break; + case 'user': + $ids = [(int)$val]; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $ids = [(int)$leaderId]; + break; + case 'field_user': + if (isset($formData[$val])) $ids = [(int)$formData[$val]]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $ids = [(int)$head]; + break; + default: + throw new BusinessException("不支持的审批人类型: {$typ}"); + } + $resolved = array_merge($resolved, $ids); + } + + $resolved = array_unique(array_filter(array_map('intval', $resolved))); + if ($avoidSelf) { + $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + } + if (empty($resolved)) { + throw new BusinessException("步骤未解析到任何审批人"); + } + return $resolved; + } + + /** 通知抄送人 */ + private function notifyCCs(array $step, object $instance, string $action = 'pending'): void + { + $ccConfig = $step['cc'] ?? []; + if (empty($ccConfig)) return; + + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $uids = []; + + foreach ($ccConfig as $cc) { + $typ = $cc['type'] ?? ''; + $val = $cc['value'] ?? null; + + switch ($typ) { + case 'role': + $uids = array_merge($uids, $this->getUserIdsByRole($val, null, true, true)); + break; + case 'user': $uids[] = (int)$val; break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $uids[] = (int)$leaderId; + break; + case 'field_user': + if (isset($formData[$val])) $uids[] = (int)$formData[$val]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $uids[] = (int)$head; + break; + default: break; + } + } + + $uids = array_unique(array_filter(array_map('intval', $uids))); + if (empty($uids)) return; + + $now = date('Y-m-d H:i:s'); + foreach ($uids as $uid) { + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $uid) + ->count() > 0; + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $uid, + 'notified_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->sendNotification($uids, $instance, $action, true); + } + + /** 条件求值(简单表达式) */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $m)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + $field = $m[1]; $op = $m[2]; $value = trim($m[3], "\"'"); + $actual = $formData[$field] ?? null; + if (is_numeric($value) && is_numeric($actual)) { $value=(float)$value; $actual=(float)$actual; } + return match ($op) { + '==' => $actual == $value, + '!=' => $actual != $value, + '>' => $actual > $value, + '<' => $actual < $value, + '>=' => $actual >= $value, + '<=' => $actual <= $value, + default => throw new BusinessException("不支持的操作符: {$op}"), + }; + } + + /** 获取下一个有效步骤索引(考虑 condition) */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { + return $i; + } + } + return null; + } + + /** + * 聚合本步骤结论(按类型) + * 返回:'pending' | 'approve' | 'reject' + * + * any:有 reject => reject;否则有 approve => approve;否则 pending + * all:有 reject => reject;有 pending => pending;否则(全 approved 或 approved+skipped/timeout)=> approve + */ + private function getStepOutcome(int $instanceId, int $stepIndex, string $type): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->get(); + + $pending = 0; $approved = 0; $rejected = 0; + foreach ($records as $r) { + if ($r->status === 'pending') { $pending++; } + elseif ($r->status === 'approved'){ $approved++; } + elseif ($r->status === 'rejected'){ $rejected++; } + // skipped/timeout 不影响聚合结论 + } + + $type = strtolower($type); + if ($type === 'any') { + if ($rejected > 0) return 'reject'; + if ($approved > 0) return 'approve'; + return 'pending'; + } + // all + if ($rejected > 0) return 'reject'; + if ($pending > 0) return 'pending'; + return $approved > 0 ? 'approve' : 'pending'; + } + + /** 关闭本步其余待办为 skipped */ + private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('status', 'pending'); + + if (!empty($keepApproverIds)) $q->whereNotIn('approver_id', $keepApproverIds); + + $q->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); + } + + /** 创建待办(去重) */ + private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void + { + $now = date('Y-m-d H:i:s'); + $exists = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->count() > 0; + if (!$exists) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'action' => null, + 'status' => 'pending', + 'comment' => null, + 'approved_at' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + /** 发送通知(接你自己的消息系统即可) */ + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void + { + $map = ['pending'=>'待审批','approve'=>'已通过','reject'=>'已拒绝','revoked'=>'已撤回']; + $prefix = $isCC ? '[抄送]' : ''; + $text = $map[$action] ?? $action; + $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; + // MessageService::send($userIds, $msg); + + $pushData = [ + 'sender_id' => 0, + 'sender_type' => 'system', + 'title' => $msg, + 'content' => $msg, + 'scene' => 1, + 'recipient' => $userIds, + 'push_method' => 1 + ]; + $msgId = (new MessageNotificationsService())->pushNotice($pushData); + var_dump($msgId); + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // ---------- 数据访问(roles / user_roles 结构) ---------- + + /** + * 从角色取用户ID + * @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name + * @param int|null $systemId 限定 roles.system_id;null 不限定 + * @param bool $onlyActive 仅取 status=1 角色,默认 true + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + } else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) $ids[] = (int)$row->user_id; + return array_values(array_unique($ids)); } private function getLeaderIdByUserId(int $userId): ?int { - return Db::table('user')->where('id', $userId)->value('report_to'); + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; } private function getDeptHeadId(int $deptId): ?int { - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; } - private function notifyInitiator(object $instance, string $result) + + /** + * 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名) + * + * @param int $instanceId + * @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表 + * @return array{ + * instance: array, + * steps: array, + * records: array, + * summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int} + * } + */ + public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array { - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; + // 1) 实例 + 流程步骤 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) { + 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) ?: []; + + // 2) 记录查询(可选带出审批人姓名) + if ($userNameColumn) { + // 注意:确保 user 表存在该列 + $records = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select( + 'ar.*', + Db::raw("u.`{$userNameColumn}` as approver_name") + ) + ->get() + ->toArray(); + } else { + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + } + + // 3) 附加步骤名/类型;统计汇总 + $summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0]; + foreach ($records as &$r) { + $idx = (int)$r->step_index; + $meta = $steps[$idx] ?? []; + $r->step_name = $meta['name'] ?? ('Step '.$idx); + $r->step_type = $meta['type'] ?? 'all'; + + if (isset($summary[$r->status])) { + $summary[$r->status]++; + } + $summary['total']++; + } + unset($r); + + return [ + 'instance' => [ + 'id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'current_step' => (int)$instance->current_step, + 'created_at' => $instance->created_at, + 'updated_at' => $instance->updated_at, + 'finished_at' => $instance->finished_at, + ], + 'steps' => array_map(function($s){ + return [ + 'name' => $s['name'] ?? '', + 'type' => $s['type'] ?? 'all', + 'condition' => $s['condition'] ?? null, + ]; + }, $steps), + 'records' => array_map(function($r){ + // 转成数组(Illuminate 返回的是 stdClass) + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'approver_id' => (int)$r->approver_id, + 'step_index' => (int)$r->step_index, + 'action' => $r->action, + 'status' => $r->status, + 'comment' => $r->comment, + 'approved_at' => $r->approved_at, + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null, + 'step_name' => $r->step_name, + 'step_type' => $r->step_type, + ]; + }, $records), + 'summary' => $summary, + ]; } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService_bak.php b/app/common/utils/approval/service/ApprovalFlowService_bak.php index 37d45c9..f988a48 100644 --- a/app/common/utils/approval/service/ApprovalFlowService_bak.php +++ b/app/common/utils/approval/service/ApprovalFlowService_bak.php @@ -1,195 +1,285 @@ 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}' 不存在或已停用"); - } + return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { + // 1) 流程定义 + $flow = Db::table('approval_flows') + ->where('type', $flowType) + ->where('is_active', 1) + ->first(); - // 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("无权撤回或状态不允许"); + if (!$flow) { + throw new BusinessException("审批流程未找到或已停用: {$flowType}"); } - $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; + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) { + throw new BusinessException("流程步骤配置错误"); + } - 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); - } + // 2) 寻找首个有效步骤索引(考虑条件) + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) { + throw new BusinessException("未找到可执行的审批步骤"); + } + $firstStep = $steps[$firstIndex]; - return ['result' => 'approved', 'next_step' => $nextStepIndex]; + // 3) 计算实例截止时间(实例级,可扩展到步骤级) + $deadlineAt = null; + if (($flow->timeout_hours ?? 0) > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } + + // 4) 创建审批实例 + $now = date('Y-m-d H:i:s'); + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => $firstIndex, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + // 5) 创建首步待办 + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + ]; + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + + foreach ($approverIds as $uid) { + // 唯一键会防重复 + $this->insertApprovalRecord($instanceId, $firstIndex, $uid); + } + + // 6) 通知审批人 & 抄送 + $this->sendNotification($approverIds, $instance, 'pending', false); + $this->notifyCCs($firstStep, $instance, 'pending'); + + return ['instance_id' => $instanceId]; + }); } /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array + * 执行审批 + */ + public function approve(int $instanceId, int $approverId, string $action, string $comment = '') + { + if (!in_array($action, ['approve', 'reject'], true)) { + throw new BusinessException("无效的审批操作"); + } + + return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { + // 锁实例,保证并发安全 + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) { + throw new BusinessException("审批实例未找到"); + } + if ($instance->status !== 'processing') { + throw new BusinessException("流程已结束或不可审批"); + } + + // 读取流程与当前步 + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException("审批流程定义未找到"); + + $steps = json_decode($flow->steps, true); + $idx = (int)$instance->current_step; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 锁当前步所有记录,避免同时推进 + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + if (!$pendingRecord) { + throw new BusinessException("未找到待审批记录"); + } + + $now = date('Y-m-d H:i:s'); + + // 1) 更新当前人的审批记录 + Db::table('approval_records')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'status' => ($action === 'approve') ? 'approved' : 'rejected', + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, + ]); + + // 2) 聚合判断本步骤结果 + $outcome = $this->getStepOutcome($instanceId, $idx); // 'pending'|'approve'|'reject' + $type = $step['type'] ?? 'all'; + + $stepDone = false; $finalReject = false; $finalApprove = false; + if ($type === 'any') { + if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } + elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } + } else { // all + if ($outcome === 'reject') { $stepDone = true; $finalReject = true; } + elseif ($outcome === 'approve') { $stepDone = true; $finalApprove = true; } + } + + if (!$stepDone) { + return ['status' => 'step_pending']; + } + + // 3) 本步已有结论:关闭其余 pending 待办为 skipped + $this->closeOtherPending($instanceId, $idx, [$approverId]); + + // 4) 步骤结论为拒绝 -> 结束 + if ($finalReject) { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + return ['status' => 'completed', 'result' => 'reject']; + } + + // 5) 进入下一步(基于表单快照与条件继续寻找) + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + if ($nextIndex === null) { + // 流程结束(全通过) + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'approved', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); + return ['status' => 'completed', 'result' => 'approve']; + } + + // 6) 推进实例 current_step(乐观条件) + $affected = Db::table('approval_instances') + ->where('id', $instanceId) + ->where('current_step', $idx) + ->where('status', 'processing') + ->update([ + 'current_step' => $nextIndex, + 'updated_at' => $now, + ]); + if (!$affected) { + throw new BusinessException('流程已被他人推进,请刷新后重试'); + } + + // 7) 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instanceId, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + $this->notifyCCs($nextStep, $instance, 'pending'); + + return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + /** + * 撤回流程(可选) + * 仅限发起人在第一条“已处理记录”出现前撤回 + */ + public function revoke(int $instanceId, int $initiatorId, string $reason = '') + { + return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); + + if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); + + // 已有处理记录则不允许撤回 + $handled = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->whereIn('status', ['approved','rejected','timeout','skipped']) + ->count() > 0; + if ($handled) throw new BusinessException('当前进度不可撤回'); + + $now = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'revoked', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + // 关闭所有 pending 待办 + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); + + $this->sendNotification([$initiatorId], $instance, 'revoked', false); + return ['status' => 'completed', 'result' => 'revoked']; + }); + } + + /** + * 我的待办 */ public function getPendingApprovals(int $userId): array { - // 简化查询:找到用户需要审批的实例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.*') + ->distinct() ->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') + ->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->toArray(); } /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array + * 流程图数据(识别 skipped) */ public function getFlowDiagramData(int $instanceId): array { @@ -199,233 +289,463 @@ class ApprovalFlowService_bak $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); if (!$flow) return []; - $steps = json_decode($flow->steps, true); + $steps = json_decode($flow->steps, true) ?: []; $records = Db::table('approval_records') ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') + ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') ->get() ->groupBy('step_index'); - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); + $formData = json_decode($instance->form_data, true) ?: []; + $data = []; + foreach ($steps as $i => $step) { + $actions = ($records->get($i) ?? collect())->toArray(); + $status = 'pending'; if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; + $hasReject = false; $allApproved = true; + foreach ($actions as $a) { + if ($a->status === 'rejected') $hasReject = true; + if ($a->status !== 'approved') $allApproved = false; + } + if ($hasReject) $status = 'rejected'; + elseif ($allApproved) $status = 'approved'; + else $status = 'processing'; + } else { + // 无记录:可能被跳过、未到达、已过 + if ($i < (int)$instance->current_step) { + // 如果条件不满足,认为 skipped,否则视为 approved(历史兼容) + if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { + $status = 'skipped'; } else { $status = 'approved'; } + } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { + $status = 'processing'; + } else { + // 还未到达 + $status = 'pending'; } - } 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 + $data[] = [ + 'index' => $i, + 'name' => $step['name'] ?? ('Step '.$i), + 'type' => $step['type'] ?? 'all', + 'status' => $status, + 'actions' => $actions, ]; } - return $diagramData; + return $data; } /** - * 处理超时的审批实例 (定时任务调用) + * 定时任务:处理超时实例(实例级) */ public function handleTimeouts() { $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') + $expired = Db::table('approval_instances') ->where('status', 'processing') + ->whereNotNull('deadline_at') ->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 = [ + foreach ($expired as $instance) { + Db::transaction(function () use ($instance, $now) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return; + + $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; + $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; + + // 更新实例 + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => ($final === 'approved') ? 'approved' : 'rejected', 'finished_at' => $now, - 'updated_at' => $now - ]; - 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'); - } - } + 'updated_at' => $now, + ]); + + // 关闭当前步所有 pending 记录为 timeout + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + }); } } - // --- 辅助方法 --- + // === 内部工具 === - 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 + /** + * 解析审批人 + * $avoidSelf:是否过滤发起人本人 + */ + private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array { $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; + $confs = $step['approvers'] ?? []; + foreach ($confs as $cfg) { + $typ = $cfg['type'] ?? ''; + $val = $cfg['value'] ?? null; + $ids = []; - switch ($approverConfig['type']) { - // 1. 按角色查找 + switch ($typ) { case 'role': - $roleCode = $approverConfig['value']; - // 所有拥有该角色的用户ID - $approverIds = $this->getUserIdsByRole($roleCode); - break; + // $ids = $this->getUserIdsByRole($val); - // 2. 按具体用户ID + // 支持: + // - {"type":"role","value":1} // 按 role_id + // - {"type":"role","value":"销售经理"} // 按 roles.name + // - {"type":"role","value":"销售经理","system_id": 2} // 限定系统;默认也包含全局(system_id=0) + $val = $cfg['value'] ?? null; + $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; + // include_global 默认 true(把 system_id=0 的“全局角色”也算上) + $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; + $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); + + break; case 'user': - $approverIds = [(int)$approverConfig['value']]; + $ids = [(int)$val]; break; - - // 3. 按发起人的直属领导 (业务一般是组长) case 'initiator_leader': - // 用户表中 report_to 字段 - $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; // 没有领导,跳过 - } + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $ids = [(int)$leaderId]; 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 = []; - } + if (isset($formData[$val])) $ids = [(int)$formData[$val]]; break; - - // 5. 按组织架构层级 (如发起人所在部门的负责人) case 'dept_head': - $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 - $approverIds = $this->getDeptHeadId($deptId); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; - } + $head = $this->getDeptHeadId((int)$val); + if ($head) $ids = [(int)$head]; break; - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); + throw new BusinessException("不支持的审批人类型: {$typ}"); } - - $resolved = array_merge($resolved, $approverIds); + $resolved = array_merge($resolved, $ids); } - return array_unique($resolved); + $resolved = array_unique(array_filter(array_map('intval', $resolved))); + if ($avoidSelf) { + $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + } + if (empty($resolved)) { + throw new BusinessException("步骤未解析到任何审批人"); + } + return $resolved; } - // 角色层 - private function getUserIdsByRole(string $roleCode): array + /** + * 通知抄送人 + */ + private function notifyCCs(array $step, object $instance, string $action = 'pending'): void { - // 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(); + $ccConfig = $step['cc'] ?? []; + if (empty($ccConfig)) return; + + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $uids = []; + + foreach ($ccConfig as $cc) { + $typ = $cc['type'] ?? ''; + $val = $cc['value'] ?? null; + + switch ($typ) { + case 'role': + $uids = array_merge($uids, $this->getUserIdsByRole($val)); + break; + case 'user': + $uids[] = (int)$val; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $uids[] = (int)$leaderId; + break; + case 'field_user': + if (isset($formData[$val])) $uids[] = (int)$formData[$val]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $uids[] = (int)$head; + break; + default: + // ignore + break; + } + } + + $uids = array_unique(array_filter(array_map('intval', $uids))); + if (empty($uids)) return; + + $now = date('Y-m-d H:i:s'); + foreach ($uids as $uid) { + // 唯一键防重;这里仍做一次 exists 判断 + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $uid) + ->count() > 0; + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $uid, + 'notified_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->sendNotification($uids, $instance, $action, true); } - // 领导层 + /** + * 条件求值:支持 {field} {op} {value} + * op: ==, !=, >, <, >=, <= + */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $m)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + + $field = $m[1]; + $op = $m[2]; + $value = trim($m[3], "\"'"); + + $actual = $formData[$field] ?? null; + + // 数值比较 + if (is_numeric($value) && is_numeric($actual)) { + $value = (float)$value; + $actual = (float)$actual; + } + + switch ($op) { + case '==': return $actual == $value; + case '!=': return $actual != $value; + case '>': return $actual > $value; + case '<': return $actual < $value; + case '>=': return $actual >= $value; + case '<=': return $actual <= $value; + } + throw new BusinessException("不支持的操作符: {$op}"); + } + + /** + * 获取下一个有效步骤索引(考虑 condition) + * @param int $currentStepIndex 从该索引之后开始找;如果传 -1 则从开头找“首个有效” + */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { + return $i; + } + } + return null; + } + + /** + * 聚合本步骤结论 + * 返回:'pending' | 'approve' | 'reject' + */ + private function getStepOutcome(int $instanceId, int $stepIndex): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->get(); + + $hasPending = false; $hasReject = false; $allApproved = true; + + foreach ($records as $r) { + if ($r->status === 'pending') { $hasPending = true; $allApproved = false; } + elseif ($r->status === 'rejected') { $hasReject = true; $allApproved = false; } + elseif ($r->status !== 'approved') { $allApproved = false; } + } + + if ($hasPending) return 'pending'; + if ($hasReject) return 'reject'; + return $allApproved ? 'approve' : 'reject'; + } + + /** + * 关闭本步其余待办(标记为 skipped) + */ + private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('status', 'pending'); + + if (!empty($keepApproverIds)) { + $q->whereNotIn('approver_id', $keepApproverIds); + } + + $q->update([ + 'status' => 'skipped', + 'approved_at' => $now, + 'updated_at' => $now, + ]); + } + + private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void + { + $now = date('Y-m-d H:i:s'); + + // 防重复:唯一键兜底,这里先查一次 + $exists = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->count() > 0; + + if (!$exists) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'action' => null, // 待办阶段无动作 + 'status' => 'pending', // 明确 pending + 'comment' => null, + 'approved_at' => null, // 未审批为 NULL + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void + { + $map = [ + 'pending' => '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝', + 'revoked' => '已撤回', + ]; + $prefix = $isCC ? '[抄送]' : ''; + $text = $map[$action] ?? $action; + $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; + // TODO: 接入你的消息系统(站内信/企微/飞书…) + // MessageService::send($userIds, $msg); + // echo "通知 ".implode(',', $userIds).":{$msg}\n"; + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // === 数据访问 === + + // private function getUserIdsByRole(string $roleCode): array + // { + // // 兼容 pluck 行为(不同版本可能返回 array 或 Collection) + // $rows = Db::table('user_roles as ur') + // ->join('roles as r', 'ur.role_id', '=', 'r.id') + // ->where('r.code', $roleCode) + // ->select('ur.user_id') + // ->get(); + + // $ids = []; + // foreach ($rows as $row) { + // $ids[] = (int)$row->user_id; + // } + // return array_values(array_unique($ids)); + // } + + /** + * 从角色取用户ID列表 + * + * @param mixed $roleIdOrName 数字 => role_id,字符串 => roles.name 精确匹配 + * @param int|null $systemId 可选:限定 roles.system_id;默认不传=不过滤 + * @param bool $onlyActive 是否仅取 status=1 的角色(默认 true) + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + * @return int[] + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + // 查询构造 + // 情况A:纯数字 => 当作 role_id,优先直接走 user_roles(role_id) 索引 + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + + // 如果不需要校验 roles 的 system/status,就不 join,性能更好 + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles') + ->where('role_id', $roleId) + ->select('user_id') + ->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + + if ($onlyActive) { + $q->where('r.status', 1); + } + if (!is_null($systemId)) { + if ($includeGlobal) { + $q->whereIn('r.system_id', [0, $systemId]); + } else { + $q->where('r.system_id', $systemId); + } + } + $rows = $q->get(); + } + } + // 情况B:字符串 => 当作 roles.name + else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + + if ($onlyActive) { + $q->where('r.status', 1); + } + if (!is_null($systemId)) { + if ($includeGlobal) { + $q->whereIn('r.system_id', [0, $systemId]); + } else { + $q->where('r.system_id', $systemId); + } + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) { + $ids[] = (int)$row->user_id; + } + return array_values(array_unique($ids)); + } + + private function getLeaderIdByUserId(int $userId): ?int { - // users 表 report_to 字段 - return Db::table('user')->where('id', $userId)->value('report_to'); + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; } - // 组织架构层 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"; + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService_bak2.php b/app/common/utils/approval/service/ApprovalFlowService_bak2.php deleted file mode 100644 index 9b6babd..0000000 --- a/app/common/utils/approval/service/ApprovalFlowService_bak2.php +++ /dev/null @@ -1,620 +0,0 @@ -where('type', $flowType) - ->where('is_active', 1) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程未找到或已停用: {$flowType}"); - } - - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) { - throw new BusinessException("流程步骤配置错误"); - } - - // 2. 查找第一个有效步骤(考虑 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 df20d14..6b93ecc 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -244,10 +244,10 @@
下单人: - +
-
下单时间:2025-09-08
+
下单时间:
diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 8039205..f7d924d 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -2,20 +2,10 @@ namespace app\controller; -use app\common\model\InquiryLog; -use app\common\model\User; -use app\common\model\UserRoles; -use app\common\model\UserSystems; -use app\common\service\MessageNotificationsService; -use app\common\traits\DataPermission; -use app\common\utils\email\MailSend; use app\common\utils\scaffold\Generate; use support\Request; use support\Db; use support\Cache; -use app\inquiry\service\InquiryLogService; -use Illuminate\Support\Facades\Mail; -use support\Redis; class IndexController { @@ -79,25 +69,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'model' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'service' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'logic' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], 'validate' => [ 'module' => 'admin', - 'name' => 'approval_flows' + 'name' => 'approval_instances' ], - 'table' => 'approval_flows' + 'table' => 'approval_instances' ]; $result = (new Generate($options))->doGenerate(); @@ -116,9 +106,9 @@ class IndexController $initiatorId = 74; $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); + $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); - $sp = $appro->approve(1, 1, 'approve'); + // $sp = $appro->approve(9, 1, 'reject'); var_dump($sp); return json([ diff --git a/config/route.php b/config/route.php index da3d540..8f693e3 100644 --- a/config/route.php +++ b/config/route.php @@ -298,6 +298,15 @@ Route::group('/admin', function () { Route::any('/update', [app\admin\controller\ApprovalFlowsController::class, 'update']); Route::any('/destroy', [app\admin\controller\ApprovalFlowsController::class, 'destroy']); }); + + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']); + Route::any('/show', [app\admin\controller\ApprovalRecordsController::class, 'show']); + Route::any('/store', [app\admin\controller\ApprovalRecordsController::class, 'store']); + Route::any('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']); + Route::any('/destroy', [app\admin\controller\ApprovalRecordsController::class, 'destroy']); + }); }); diff --git a/public/pdf/BH20250908014.pdf b/public/pdf/BH20250908014.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9b4b9bd658a02077087ca334b5dc5b909c0c57fd GIT binary patch literal 65734 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 M