diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php index 1cad6b4..a371f41 100644 --- a/app/admin/service/ApprovalRecordsService.php +++ b/app/admin/service/ApprovalRecordsService.php @@ -49,9 +49,9 @@ class ApprovalRecordsService 'approvalInstance.bizInfo' ]; - $config['dataLimit']['type'] = 'relation'; - $config['dataLimit']['userId'] = $request->user->uid; - $config['dataLimit']['userField'] = 'approver_id'; + // $config['dataLimit']['type'] = 'relation'; + // $config['dataLimit']['userId'] = $request->user->uid; + // $config['dataLimit']['userField'] = 'approver_id'; break; case 'show': $config['where']['id'] = $p['id']; diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php index dfb7f53..e46b07b 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -8,8 +8,6 @@ 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; class SysDictService diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 2adf96b..b794532 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -1017,4 +1017,125 @@ class ApprovalFlowService return $timeline; } + + /** + * 按业务ID获取“最新进展”的极简状态 + * - 已完结:直接返回实例最终状态 + * - 进行中:返回当前步骤(索引、名称、类型)与当前待处理审批者列表(pending) + * + * @param string $bizId + * @param string|null $bizType 可选限定业务类型 + * @param string|null $userNameColumn 可选:审批人姓名字段(如 user.name / nickname) + * @return array{ + * instance_id:int, flow_type:string, biz_type:string, biz_id:string, + * status:string, // 实例状态:approved|rejected|revoked|processing + * finished_at:?string, updated_at:?string, + * step?: array{ + * index:int, name:string, type:string, // any|all + * status:string, // processing|timeout(进行中场景) + * pending_approvers: array + * } + * } + */ + public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array + { + // 1) 最新实例 + $q = Db::table('approval_instances')->where('biz_id', $bizId); + if ($bizType !== null && $bizType !== '') { + $q->where('biz_type', $bizType); + } + $instance = $q->orderByDesc('id')->first(); + if (!$instance) { + // throw new BusinessException('未找到对应的审批实例'); + return []; + } + + $base = [ + 'instance_id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'finished_at' => $instance->finished_at, + 'updated_at' => $instance->updated_at, + ]; + + // 2) 已完结:直接返回 + if ($instance->status !== 'processing') { + return $base; + } + + // 3) 进行中:取当前步骤 + 当前 pending 审批者 + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + // 容错:没有流程定义 + return $base; + } + $steps = json_decode($flow->steps, true) ?: []; + $idx = (int)$instance->current_step; + $meta = $steps[$idx] ?? []; + + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + + // 当前步的所有记录 + $recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx); + + // 待处理人(只列出 pending) + if ($userNameColumn) { + $pendingRows = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instance->id) + ->where('ar.step_index', $idx) + ->where('ar.status', 'pending') + ->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name")) + ->get() + ->toArray(); + } else { + $pendingRows = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->where('status', 'pending') + ->select('approver_id') + ->get() + ->toArray(); + } + + $pendingApprovers = []; + foreach ($pendingRows as $r) { + $row = ['approver_id' => (int)$r->approver_id]; + if ($userNameColumn && property_exists($r, 'approver_name')) { + $row['approver_name'] = $r->approver_name; + } + $pendingApprovers[] = $row; + } + + // 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout) + // 先快速统计是否纯 timeout + $stats = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending") + ->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved") + ->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected") + ->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout") + ->first(); + + $stepStatus = 'processing'; + if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) { + $stepStatus = 'timeout'; + } + + $base['step'] = [ + 'index' => $idx, + 'name' => $meta['name'] ?? ('Step '.$idx), + 'type' => $type, // any|all + 'status' => $stepStatus, // processing|timeout + 'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者 + ]; + + return $base; + } + + } \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 54ffb3d..28cb1d0 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -69,26 +69,26 @@ class IndexController { $options = [ 'controller' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'model' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'service' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'logic' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'validate' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], - 'table' => 'approval_instances' + 'table' => 'approval_flows' ]; $result = (new Generate($options))->doGenerate(); diff --git a/app/crm/controller/ApprovalFlowsController.php b/app/crm/controller/ApprovalFlowsController.php new file mode 100644 index 0000000..7eacb12 --- /dev/null +++ b/app/crm/controller/ApprovalFlowsController.php @@ -0,0 +1,33 @@ +service = new ApprovalFlowsService(); + } + + /** + * 列表页数据格式化 + * + * @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/crm/controller/ApprovalInstancesController.php b/app/crm/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..e854af6 --- /dev/null +++ b/app/crm/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/crm/controller/ApprovalRecordsController.php b/app/crm/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..0c55e0d --- /dev/null +++ b/app/crm/controller/ApprovalRecordsController.php @@ -0,0 +1,87 @@ +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()); + } + } + + + public function logs(Request $request) + { + try { + $instanceId = $request->get('instance_id'); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend'); + return $this->success($res); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index 75fe8fc..cd2b382 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -34,6 +34,19 @@ class StockOrdersController extends BaseController if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (isset($data['id'])) { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = $appro->getBriefStatusByBiz($data['id'], null, 'name'); + $map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回']; + if ($appro) { + $data['appro_status'] = [ + 'status' => $appro['status'], + 'status_desc' => $map[$appro['status']], + 'name' => isset($appro['step'])?$appro['step']['name']:'' + ]; + } + } return $data; } diff --git a/app/crm/logic/ApprovalFlowsLogic.php b/app/crm/logic/ApprovalFlowsLogic.php new file mode 100644 index 0000000..8a5260f --- /dev/null +++ b/app/crm/logic/ApprovalFlowsLogic.php @@ -0,0 +1,17 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/crm/model/ApprovalInstances.php b/app/crm/model/ApprovalInstances.php new file mode 100644 index 0000000..b5a9a7b --- /dev/null +++ b/app/crm/model/ApprovalInstances.php @@ -0,0 +1,107 @@ + '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/crm/model/ApprovalRecords.php b/app/crm/model/ApprovalRecords.php new file mode 100644 index 0000000..4f4ea06 --- /dev/null +++ b/app/crm/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/crm/service/ApprovalFlowsService.php b/app/crm/service/ApprovalFlowsService.php new file mode 100644 index 0000000..e373c07 --- /dev/null +++ b/app/crm/service/ApprovalFlowsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalInstancesService.php b/app/crm/service/ApprovalInstancesService.php new file mode 100644 index 0000000..86e6e57 --- /dev/null +++ b/app/crm/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalRecordsService.php b/app/crm/service/ApprovalRecordsService.php new file mode 100644 index 0000000..16c070d --- /dev/null +++ b/app/crm/service/ApprovalRecordsService.php @@ -0,0 +1,91 @@ +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' + ]; + + $config['dataLimit']['type'] = 'relation'; + $config['dataLimit']['userId'] = $request->user->uid; + $config['dataLimit']['userField'] = 'approver_id'; + 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; + case 'logs': + $config['where']['instance_id'] = $p['instance_id']; + $config['order']['id'] = 'desc'; + 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']); + } + + + public function serviceLogs(Request $request) + { + $config = $this->buildQueryConfig($request, 'logs'); + $item = $this->logicListAll($config); + return $this->postProcess('Logs', $item); + } +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalFlowsValidate.php b/app/crm/validate/ApprovalFlowsValidate.php new file mode 100644 index 0000000..fef36f4 --- /dev/null +++ b/app/crm/validate/ApprovalFlowsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalInstancesValidate.php b/app/crm/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..bbcbad5 --- /dev/null +++ b/app/crm/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalRecordsValidate.php b/app/crm/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..6a91cb6 --- /dev/null +++ b/app/crm/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index afc379f..ad4d5ae 100644 --- a/config/route.php +++ b/config/route.php @@ -303,9 +303,7 @@ Route::group('/admin', function () { Route::group('/approval/records', function () { Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']); Route::any('/logs', [app\admin\controller\ApprovalRecordsController::class, 'logs']); - // 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']); }); }); @@ -377,12 +375,12 @@ Route::group('/crm', function () { Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']); Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']); Route::any('/print', [app\crm\controller\StockOrdersController::class, 'print']); - // Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']); - // Route::any('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']); }); - // Route::group('/pdf', function () { - // Route::any('/bx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'downloadReimbursementPdf']); - // Route::any('/ckbx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'viewReimbursementPdf']); - // }); + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\crm\controller\ApprovalRecordsController::class, 'index']); + Route::any('/logs', [app\crm\controller\ApprovalRecordsController::class, 'logs']); + Route::any('/action', [app\crm\controller\ApprovalRecordsController::class, 'approve']); + }); }); \ No newline at end of file