Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c6e43170f | |||
| a0d5a766a1 | |||
| 403ac7a271 | |||
| 433b2dd915 | |||
| bed833f790 | |||
| 5570e025b4 | |||
| 67139e4161 | |||
| 9f96d4da3f | |||
| e408f7e20e | |||
| 736df0ebdf | |||
| 4c9853f72a | |||
| 90dca83487 | |||
| 7cafb61797 | |||
| 35f80aab1a | |||
| b35e536c79 | |||
| facf0b4c10 | |||
| 5a7a6a45d4 | |||
| 1221e42917 | |||
| 5841b342de | |||
| 4dc708e133 | |||
| 63011a0351 | |||
| 5bcca5cb12 | |||
| 2b529a27c8 | |||
| 9b36bbf227 | |||
| cab15c4fab | |||
| 502740e106 | |||
| 139acd9e70 | |||
| 52eac05d43 | |||
| c38a53f053 | |||
| fec079ef58 | |||
| 07a40bf688 | |||
| 134ced1a31 | |||
| 472f8e39a7 | |||
| c2e402722d | |||
| 90aebf9956 | |||
| de9484f570 | |||
| edee50aaf9 | |||
| 615e9745a0 | |||
| ca757e67dd | |||
| fd4fa0fbf3 | |||
| 8003ef49d1 | |||
| fddcbc2df8 | |||
| f60b365361 | |||
| ded377a758 | |||
| 4537df63c2 | |||
| d76fec4624 | |||
| 62d86c634c | |||
| 9fc890f8cd | |||
| 6289b545f5 | |||
| 376411dd79 | |||
| 83ff52c1a0 | |||
| db7f5d568c | |||
| bcf39b6037 | |||
| 9d050f4211 | |||
| 034b278b40 | |||
| ebcabb23df | |||
| 4acb89e56a | |||
| d16bddcc32 | |||
| 6434c8769e | |||
| c796d01703 | |||
| ab6a8806af | |||
| 9f8a241e35 | |||
| 8f699d5ca9 | |||
| e6f68a9946 | |||
| 7ac6201925 | |||
| 358cb64fe4 | |||
| aede027ae2 | |||
| dbe5e2e96f | |||
| 91c313f2ca | |||
| f99b2a84ca | |||
| 909f169aae | |||
| cdd6d78995 | |||
| 133cd0a801 | |||
| 8cdf9e9e39 | |||
| e21f41701a | |||
| 9f77b93b25 | |||
| 70fa843970 | |||
| 0050ef6829 | |||
| ba7c00e942 | |||
| 460de92912 | |||
| c85f07cff4 | |||
| 638be73988 | |||
| 725710abcb | |||
| cab55847e4 | |||
| 07f9280327 | |||
| 399eaa2b5c | |||
| a3da2cfc23 | |||
| 521cfec00a | |||
| ac982b0957 | |||
| d9e2c89c0e | |||
| 81445a7096 | |||
| 1413a93d6d | |||
| 65f0be0a5d | |||
| b65f8090cf | |||
| 5c1304e239 | |||
| 253616b2bb | |||
| beabfd96fb | |||
| cc93979e9a | |||
| f336d1c4af | |||
| 35836edbfa | |||
| 32a7752f8b | |||
| f182684378 | |||
| d53f18181a | |||
| 75e31a7e9f | |||
| c4a2b39edd | |||
| 45169572b5 | |||
| 631d433b7e | |||
| 9fbe094a30 | |||
| 1ba1d5763f | |||
| 5086344742 | |||
| 63cca12a8d | |||
| eb4813b77b | |||
| f664653eb9 | |||
| 00bd5ada53 | |||
| 2c295cd631 | |||
| 134505be8c | |||
| 79d19a53e6 | |||
| 1d9fbb9032 | |||
| 76bd46a0d7 | |||
| 20188521fd | |||
| 1d4224bf4a | |||
| 8b8336437a | |||
| c157b3ee12 | |||
| 2d7456b74c | |||
| 8740918785 | |||
| d515bc07fe | |||
| 39820844f7 | |||
| 6ab4505e17 | |||
| 663d29dc61 | |||
| 862a87d66f | |||
| 32452dc29a | |||
| f19ec4edc4 | |||
| 7e90b05dde | |||
| 29dd6a2954 | |||
| f73bad8082 | |||
| be4dcb4dd4 | |||
| 1eccabb806 | |||
| 013fc83362 | |||
| 42bf32a9a5 | |||
| 2f8f7dccf9 | |||
| 49e7709ae3 | |||
| 5b77b8ed83 | |||
| a98769fb4c | |||
| 56ad551707 | |||
| 2711ed8f6a | |||
| f1d5fae122 | |||
| 4f965f8b5f | |||
| 4dbd72acce | |||
| 0c789b7a1a | |||
| 8674063e6e | |||
| 1094bc406f | |||
| 596313ed5a | |||
| 74ac8941ea | |||
| 6493bdebb1 | |||
| 07a3d66079 | |||
| 18eb3e7c5d | |||
| 262add20f8 | |||
| 139d3509f4 | |||
| f7faf79809 | |||
| 29ba35f9b6 | |||
| 0e38f0f9bb | |||
| 43fe263df1 | |||
| 58a38d1463 | |||
| 821f455e47 | |||
| 24805257c7 | |||
| b4ce4fe672 | |||
| 206eff87e2 | |||
| 2e85b46a3d | |||
| 72fa638cf5 | |||
| 5f25ac9895 | |||
| ad67a88d0f | |||
| 4c3f8c9cf7 | |||
| 3f8dbf97e4 | |||
| e9636513ec | |||
| 9f167ad526 | |||
| d737281283 | |||
| 695c398962 | |||
| 0110e393ce | |||
| ea0d5b14bc | |||
| a3819bcba7 | |||
| 3d6aa5f365 | |||
| 79d0ae2cbd | |||
| 659f6d4866 | |||
| 1b4914f7b7 | |||
| c8cdc5ba00 | |||
| 94e9d6eb7e | |||
| 3d1a4ceeb5 | |||
| 7db7e652de | |||
| 9fbe77a3a9 | |||
| 4c63c1b669 | |||
| f64324fba5 | |||
| 499b0894b7 | |||
| 88ebef4fc3 | |||
| 15df6ad9a3 | |||
| 02fe911a44 | |||
| 5facb1abba | |||
| 077edc5fd6 | |||
| ef0c236130 | |||
| 8c3a445a39 | |||
| a7d623656d | |||
| f004d99802 | |||
| ea3d511c4d | |||
| 25f8017b3c | |||
| 59f474a037 | |||
| 672258e594 | |||
| 9f944ae92c | |||
| c2d291bed4 | |||
| f3b1c9bae4 | |||
| 346d67c084 | |||
| eacdb93be2 | |||
| cf45acbf0f | |||
| b235747a9a | |||
| b440c03241 | |||
| 2b2e821779 | |||
| 07c0b2a2f3 | |||
| 6baee549c0 | |||
| 8a1b2f2369 | |||
| 2127473290 | |||
| 77dad7daed | |||
| 38effed458 | |||
| 1bd2eee8db | |||
| 36c7d19319 | |||
| 19cb169abd | |||
| a85fa8f072 | |||
| 83d76b16b4 | |||
| 2f9f483661 | |||
| 7fd28461fe | |||
| 5cc67b78f1 | |||
| 770664e70f | |||
| 6bd4ed9ec3 | |||
| 211fe575b8 | |||
| 18786591b1 | |||
| 1c3585b079 | |||
| 8db33a40a4 | |||
| c1ab40031d | |||
| 2c8efc18f9 | |||
| 03ab55bd63 | |||
| 4326bcd68b | |||
| 7036e3b849 | |||
| 0f505d5c56 | |||
| 33efdc8534 | |||
| 6644cc1c58 | |||
| 797256e707 | |||
| 448d151106 | |||
| 7486803468 | |||
| 4c1ac68a2b | |||
| 1cb5a1d5b2 | |||
| 55850aef81 | |||
| 13ab4b7868 | |||
| 67ef59f9fd | |||
| 07f9a4306f | |||
| f17646238e | |||
| 03d010ec88 | |||
| 13d24d041d | |||
| 4c94392ef6 | |||
| 5c6978c803 | |||
| 73ed4a413c | |||
| d05106f9df | |||
| 90cce108b0 | |||
| a3a6cfefc0 | |||
| 40dfd99f4e | |||
| ae6cd4ec78 | |||
| ec2502cd58 | |||
| e0b11718a7 | |||
| 82e4435a2c | |||
| 435d3ce677 | |||
| abc1142e88 | |||
| 54f83679ea | |||
| db10ece6b7 | |||
| 870bca5e57 | |||
| 1d6caad7b0 | |||
| e6d3cc3f04 | |||
| 4717bb01ca | |||
| 36d1d333b1 |
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\User;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvItemService;
|
||||
use support\Request;
|
||||
|
||||
class InvItemController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvItemService();
|
||||
}
|
||||
|
||||
public function preset(Request $request){
|
||||
try {
|
||||
$res = $this->service->getList();
|
||||
$result = [];
|
||||
foreach ($res as $value) {
|
||||
$value['company_name'] = Corp::where('id', $value['company_id'])->value('name');
|
||||
$value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name');
|
||||
$result[] = $value;
|
||||
}
|
||||
return $this->success($result, '获取物品列表');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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']);
|
||||
if (isset($data['company_id']))
|
||||
$data['company_name'] = Corp::where('id', $data['company_id'])->value('name');
|
||||
if (isset($data['manage_user_id']))
|
||||
$data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name');
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionService;
|
||||
use support\Request;
|
||||
|
||||
class InvRequisitionController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -31,7 +31,8 @@ class AuthController extends BaseController
|
||||
public function feishuScope(Request $request)
|
||||
{
|
||||
try {
|
||||
$result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code);
|
||||
$is_mob = $request->get('is_mob',0);
|
||||
$result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code, $is_mob);
|
||||
return $this->success($result);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionApproveService;
|
||||
|
||||
class InvRequisitionApproveController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionApproveService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionAttachmentService;
|
||||
|
||||
class InvRequisitionAttachmentController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionAttachmentService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionItemService;
|
||||
|
||||
class InvRequisitionItemController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionItemService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionItemReturnAttachmentService;
|
||||
|
||||
class InvRequisitionItemReturnAttachmentController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionItemReturnAttachmentService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionItemReturnService;
|
||||
|
||||
class InvRequisitionItemReturnController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionItemReturnService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionReturnService;
|
||||
|
||||
class InvRequisitionReturnController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionReturnService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -196,7 +196,15 @@ class AuthLogic
|
||||
{
|
||||
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
|
||||
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
|
||||
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
|
||||
|
||||
//判断移动端或pc端回调地址
|
||||
if(!empty($data['is_mob']) && $data['is_mob'] == 1){
|
||||
$redirect_uri = $certificate['m_url'];
|
||||
}else{
|
||||
$redirect_uri = $certificate['r_url'];
|
||||
}
|
||||
|
||||
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $redirect_uri);
|
||||
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
|
||||
|
||||
if ($userInfo['code'] != 0) {
|
||||
@@ -298,6 +306,7 @@ class AuthLogic
|
||||
'app_id' => $fsAppInfo->app_id,
|
||||
'app_secret' => $fsAppInfo->app_secret,
|
||||
'r_url' => $fsAppInfo->r_url,
|
||||
'm_url' => $fsAppInfo->m_url,
|
||||
'scope' => $fsAppInfo->scopes,
|
||||
'state' => $fsAppInfo->state,
|
||||
'abbr' => $corpAbbr,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvItem;
|
||||
|
||||
class InvItemLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvItem::class;
|
||||
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return InvItem::where('status',1)->orderBy('sort','desc')->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionApprove;
|
||||
|
||||
class InvRequisitionApproveLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionApprove::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionAttachment;
|
||||
|
||||
class InvRequisitionAttachmentLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionAttachment::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
|
||||
class InvRequisitionItemLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItem::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionItemReturnAttachment;
|
||||
|
||||
class InvRequisitionItemReturnAttachmentLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItemReturnAttachment::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionItemReturn;
|
||||
|
||||
class InvRequisitionItemReturnLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItemReturn::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisition;
|
||||
|
||||
class InvRequisitionLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisition::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InvRequisitionReturn;
|
||||
|
||||
class InvRequisitionReturnLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionReturn::class;
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class CorpFeishuApp extends Model
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at'];
|
||||
protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'm_url', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvItem extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_item';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'company_id', 'item_code', 'item_name',
|
||||
'category_id', 'spec_model', 'unit', 'is_consumable', 'stock_qty', 'manage_user_id', 'location', 'remark',
|
||||
'sort', 'status',
|
||||
'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisition extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'order_no', 'applicant_company_id', 'dept_id', 'applicant_id', 'applicant_name', 'leader_id', 'manager_id', 'borrow_reason', 'use_start_date', 'use_end_date', 'status', 'is_returned', 'remark', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
const STATUS_DRAFT = 0; // 草稿
|
||||
const STATUS_LEADER = 10; // 待直属上级审批
|
||||
const STATUS_MANAGE = 20; // 待管理员审批
|
||||
const STATUS_APPROVED = 30; // 已通过
|
||||
const STATUS_REJECTEDT = 40; // 已驳回
|
||||
const STATUS_CANCELED = 50; // 已取消
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionApprove extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_approve';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'requisition_id', 'node_type', 'approver_id', 'approver_name', 'action', 'comment', 'action_at', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionAttachment extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_attachment';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'requisition_id', 'file_name', 'file_path', 'file_size', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionItem extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_item';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'requisition_id', 'item_id', 'item_name_snap', 'spec_model_snap', 'unit_snap', 'is_consumable_snap', 'request_qty', 'approved_qty', 'returned_qty', 'is_finished', 'remark', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionItemReturn extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_item_return';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'requisition_id', 'requisition_item_id', 'item_id', 'apply_user_id', 'apply_user_name', 'return_qty', 'status', 'handler_id', 'handler_name', 'return_status', 'apply_remark', 'handle_remark', 'return_at', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionItemReturnAttachment extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_item_return_attachment';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'return_id', 'file_name', 'file_path', 'file_size', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionReturn extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inv_requisition_return';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'order_no', 'requisition_id', 'applicant_id', 'applicant_name', 'apply_remark', 'handler_id', 'handler_name', 'status', 'handle_remark', 'return_at', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -13,12 +13,12 @@ class AuthService
|
||||
/**
|
||||
* 飞书授权
|
||||
*/
|
||||
public function feishuAuthorize(string $corpAbbr, string $systemCcode)
|
||||
public function feishuAuthorize(string $corpAbbr, string $systemCcode, $mobile)
|
||||
{
|
||||
$logic = new AuthLogic();
|
||||
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
|
||||
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
|
||||
return ['auth_url' => $feishu->authorize($certificate)];
|
||||
return ['auth_url' => $feishu->authorize($certificate, $mobile)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\InvItemLogic;
|
||||
use app\common\validate\InvItemValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvItem;
|
||||
use support\Request;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class InvItemService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvItem::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvItemValidate::class;
|
||||
|
||||
protected $logic;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logic = (new InvItemLogic());
|
||||
}
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 CrudUtil 查询配置
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
|
||||
// 关键字
|
||||
if (isset($p['keyword']) && $p['keyword']) {
|
||||
$k = '%' . $p['keyword'] . '%';
|
||||
foreach (['item_name'] as $col) {
|
||||
$config['orWhere'][] = [$col, 'like', $k];
|
||||
}
|
||||
}
|
||||
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 8-12 位的物品编码(默认10位)
|
||||
*
|
||||
* @param int $length 编码长度,默认为10(范围 8-12)
|
||||
* @return string 返回大写的字母数字组合编码
|
||||
*/
|
||||
private function generateItemCode($length = 10)
|
||||
{
|
||||
// 排除易混淆字符:0, O, o, 1, I, l
|
||||
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
$code = '';
|
||||
$maxIndex = strlen($chars) - 1;
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$code .= $chars[mt_rand(0, $maxIndex)];
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
|
||||
public function beforeServiceStore($data, Request $request): array
|
||||
{
|
||||
$data['item_code'] = $this->generateItemCode();
|
||||
$data['category_id'] = 0;
|
||||
$data['spec_model'] = '';
|
||||
$data['unit'] = '件';
|
||||
$data['is_consumable'] = 0;
|
||||
$data['stock_qty'] = $data['stock_qty']??1;
|
||||
$data['location'] = '';
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 变动物品库存数量
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param int $changeQty 变动数量:>0 入库,<0 出库
|
||||
* @param bool $allowNegative 是否允许库存变成负数,默认不允许
|
||||
*
|
||||
* @return InvItem 最新的物品记录
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false): InvItem
|
||||
{
|
||||
if ($changeQty === 0) {
|
||||
throw new BusinessException('库存变动数量不能为 0');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
// 构建查询
|
||||
$query = InvItem::query()->where('id', $itemId);
|
||||
|
||||
// 如果不允许负数,并且是出库场景(变动为负)
|
||||
if (! $allowNegative && $changeQty < 0) {
|
||||
// 要保证:当前 stock_qty >= 出库数量
|
||||
$query->where('stock_qty', '>=', -$changeQty);
|
||||
}
|
||||
|
||||
// 使用 increment 实现:UPDATE inv_item SET stock_qty = stock_qty + ?, updated_at = ? WHERE ...
|
||||
$affected = $query->increment('stock_qty', $changeQty, [
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
if ($affected === 0) {
|
||||
// 更新不到行:
|
||||
// 1) 物品不存在
|
||||
// 2) 或者库存不足(不允许负数时)
|
||||
$exists = InvItem::query()->where('id', $itemId)->exists();
|
||||
if (! $exists) {
|
||||
throw new BusinessException('物品不存在');
|
||||
}
|
||||
throw new BusinessException('库存不足,无法完成库存变动');
|
||||
}
|
||||
|
||||
// 再查一次最新数据返回(在外层事务中也会保持一致)
|
||||
/** @var InvItem $item */
|
||||
$item = InvItem::query()->find($itemId);
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库(减库存,默认不允许负数)
|
||||
*/
|
||||
public function stockOut(int $itemId, int $qty, bool $allowNegative = false): InvItem
|
||||
{
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('出库数量必须大于 0');
|
||||
}
|
||||
return $this->changeStock($itemId, -$qty, $allowNegative);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入库(加库存)
|
||||
*/
|
||||
public function stockIn(int $itemId, int $qty): InvItem
|
||||
{
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('入库数量必须大于 0');
|
||||
}
|
||||
return $this->changeStock($itemId, $qty, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车型list
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->logic->getList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionApproveValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionApprove;
|
||||
|
||||
class InvRequisitionApproveService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionApprove::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionApproveValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionAttachmentValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionAttachment;
|
||||
|
||||
class InvRequisitionAttachmentService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionAttachment::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionAttachmentValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionItemReturnAttachmentValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionItemReturnAttachment;
|
||||
|
||||
class InvRequisitionItemReturnAttachmentService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItemReturnAttachment::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionItemReturnAttachmentValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvRequisition;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use app\common\model\InvRequisitionReturn;
|
||||
use app\common\validate\InvRequisitionItemReturnValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionItemReturn;
|
||||
use support\Request;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
use app\common\service\UserService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use app\common\model\InvRequisitionItemReturnAttachment;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
class InvRequisitionItemReturnService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItemReturn::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionItemReturnValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将二维数组按指定 key 重组为关联数组,该 key 的值作为新数组的键,
|
||||
* 其余元素组成子数组作为值。
|
||||
*
|
||||
* @param array $array 原始二维数组
|
||||
* @param string $keyField 用作新键的字段名(如 'id')
|
||||
* @return array 重组后的数组
|
||||
*/
|
||||
private function groupArrayByKey($array, $keyField = 'id')
|
||||
{
|
||||
$result = [];
|
||||
foreach ($array as $item) {
|
||||
if (!isset($item[$keyField])) {
|
||||
continue; // 跳过没有该 key 的项
|
||||
}
|
||||
$key = $item[$keyField];
|
||||
// 移除用作键的字段
|
||||
unset($item[$keyField]);
|
||||
$result[$key] = $item;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建物品归还申请前置处理
|
||||
*
|
||||
* @param array $data 归还申请数据
|
||||
* @param Request $request 请求对象
|
||||
* @return array 处理后的数据
|
||||
*/
|
||||
public function beforeServiceStore($data, Request $request): array
|
||||
{
|
||||
try {
|
||||
// // 模拟数据
|
||||
// $data = [
|
||||
// [
|
||||
// 'requisition_item_id' => 9,
|
||||
// 'return_qty' => 1,
|
||||
// 'apply_remark' => '申请备注',
|
||||
// 'attachment' => [
|
||||
// [
|
||||
// 'original_name' => '附件1.jpg',
|
||||
// 'file_path' => '/uploads/2023/07/01/attachment1.jpg'
|
||||
// ],
|
||||
// [
|
||||
// 'original_name' => '附件2.jpg',
|
||||
// 'file_path' => '/uploads/2023/07/01/attachment2.jpg'
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ];
|
||||
|
||||
// 获取所有申请项ID
|
||||
$reqItemIds = array_column($data['items'], 'requisition_item_id');
|
||||
|
||||
// 参数校验
|
||||
if (empty($reqItemIds)) {
|
||||
throw new BusinessException('未提供有效的申请项ID');
|
||||
}
|
||||
|
||||
// 将申请项按 ID 重组为关联数组
|
||||
$newReqItems = $this->groupArrayByKey($data['items'], 'requisition_item_id');
|
||||
|
||||
$data['return_at'] = is_int($data['return_at']) ? $data['return_at']: strtotime($data['return_at']);
|
||||
|
||||
// 获取申请项明细
|
||||
$reqItemsArr = InvRequisitionItem::whereIn('id', $reqItemIds)->get()->toArray();
|
||||
|
||||
// 检查是否获取到有效数据
|
||||
if (empty($reqItemsArr)) {
|
||||
throw new BusinessException('未找到对应的申请项');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
$userService = new UserService();
|
||||
$user = $userService->getUserInfo($request->user->uid, '');
|
||||
$data['applicant_id'] = $user['user']['id'];
|
||||
$data['applicant_name'] = $user['user']['name'];
|
||||
|
||||
$reqItemId = $data['requisition_id'];
|
||||
$arr = [];
|
||||
foreach ($reqItemsArr as $reqItem) {
|
||||
|
||||
// 检查是否有对应的新申请项数据
|
||||
if (!isset($newReqItems[$reqItemId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$arr[] = [
|
||||
'requisition_id' => $data['requisition_id'],
|
||||
'requisition_item_id' => $reqItem['id'],
|
||||
'item_id' => $reqItem['item_id'],
|
||||
'apply_user_id' => $user['user']['id'],
|
||||
'apply_user_name' => $user['user']['name'],
|
||||
'return_qty' => $newReqItems[$reqItemId]['return_qty'] ?: '1',
|
||||
'apply_remark' => $newReqItems[$reqItemId]['apply_remark'] ?? '',
|
||||
//'attachment' => $newReqItems[$reqItemId]['attachment'],
|
||||
//'return_at' => time(),
|
||||
'return_at' => $data['return_at'],
|
||||
];
|
||||
}
|
||||
$data['items'] =$arr;
|
||||
|
||||
return $data;
|
||||
} catch (BusinessException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $th) {
|
||||
throw new BusinessException('创建物品归还申请失败:' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function serviceStore(Request $request, array $data, string $scene = '')
|
||||
{
|
||||
try {
|
||||
// 关联表增加事务处理
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$data = $this->preProcess('Store', $data, $request);
|
||||
//$dl = $this->getDataLimitFromRequest($request);
|
||||
|
||||
$items = $data['items'];
|
||||
$attachment = $data['attachment'];
|
||||
unset($data['items']);
|
||||
unset($data['attachment']);
|
||||
$data['order_no'] = $this->generateOrderNo();
|
||||
$res = InvRequisitionReturn::create($data);
|
||||
$created = [];
|
||||
foreach ($items as $item) {
|
||||
//$created = $this->logicCreate($item, $dl);
|
||||
$created = $this->logicCreate($item);
|
||||
}
|
||||
$this->processRequisitionItemReturnAttachment($res, $attachment);
|
||||
return $this->postProcess('Store', $created);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("创建物品申请失败:" . $e->getMessage().$e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新物品归还申请前置处理
|
||||
*
|
||||
* @param array $data 归还申请数据
|
||||
* @param Request $request 请求对象
|
||||
* @return array 处理后的数据
|
||||
*/
|
||||
public function beforeServiceUpdate($data, Request $request): array
|
||||
{
|
||||
// // 模拟数据
|
||||
// $data = [
|
||||
// 'id' => 1,
|
||||
// 'requisition_item_id' => 9,
|
||||
// 'return_qty' => 1,
|
||||
// 'apply_remark' => '申请备注22',
|
||||
// 'attachment' => [
|
||||
// [
|
||||
// 'original_name' => '附件1.jpg',
|
||||
// 'file_path' => '/uploads/2023/07/01/attachment1.jpg'
|
||||
// ],
|
||||
// [
|
||||
// 'original_name' => '附件2.jpg',
|
||||
// 'file_path' => '/uploads/2023/07/01/attachment2.jpg'
|
||||
// ],
|
||||
// [
|
||||
// 'original_name' => '附件3.jpg',
|
||||
// 'file_path' => '/uploads/2023/07/01/attachment3.jpg'
|
||||
// ]
|
||||
// ]
|
||||
// ];
|
||||
|
||||
$data['return_at'] = time();
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新物品归还申请
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function serviceUpdate(Request $request, array $data, string $scene = '')
|
||||
{
|
||||
try {
|
||||
// 关联表增加事务处理
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$data = $this->preProcess('Update', $data, $request);
|
||||
$config = $this->buildQueryConfig($request, 'update');
|
||||
|
||||
$attachment = $data['attachment'];
|
||||
unset($data['attachment']);
|
||||
|
||||
$this->logicUpdateBy($config, $data);
|
||||
// 获取更新后的完整记录
|
||||
$updated = $this->logicShow($config);
|
||||
// 处理附件更新
|
||||
$this->processRequisitionItemReturnAttachment($updated, $attachment, true);
|
||||
return $this->postProcess('Update', $updated);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("更新物品归还申请失败:" . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请附件
|
||||
*
|
||||
* @param Model $created
|
||||
* @param array $items
|
||||
* @param bool $isUpdate
|
||||
* @return void
|
||||
*/
|
||||
private function processRequisitionItemReturnAttachment(Model $created, array $attachment, bool $isUpdate = false): void
|
||||
{
|
||||
if ($isUpdate) {
|
||||
$this->clearRequisitionItemReturnAttachment($created->id);
|
||||
}
|
||||
|
||||
foreach ($attachment as $file) {
|
||||
$attachmentData = [
|
||||
'return_id' => $created->id,
|
||||
'file_name' => $file['original_name'],
|
||||
'file_path' => $file['file_path']
|
||||
];
|
||||
InvRequisitionItemReturnAttachment::create($attachmentData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空物品申请附件
|
||||
*
|
||||
* @param int $returnId
|
||||
* @return void
|
||||
*/
|
||||
private function clearRequisitionItemReturnAttachment(int $returnId): void
|
||||
{
|
||||
InvRequisitionItemReturnAttachment::where('return_id', $returnId)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 物品领用归还确认
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @param string $scene
|
||||
* @return array|bool
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function serviceReturnConfirm(Request $request, array $data, string $scene = 'returnConfirm')
|
||||
{
|
||||
try {
|
||||
// 归还确认是业务顶层方法,这里统一使用事务
|
||||
return Db::transaction(function () use ($request, $data, $scene) {
|
||||
|
||||
// 1. 当前操作人信息
|
||||
$userInfo = (new UserService())->getUserInfo($request->user->uid, '');
|
||||
if (empty($userInfo['user']['id']) || empty($userInfo['user']['name'])) {
|
||||
throw new BusinessException('无法获取当前用户信息');
|
||||
}
|
||||
|
||||
$handlerId = (int)$userInfo['user']['id'];
|
||||
$handlerName = (string)$userInfo['user']['name'];
|
||||
|
||||
$data['handler_id'] = $handlerId;
|
||||
$data['handler_name'] = $handlerName;
|
||||
|
||||
// 确认状态:1=通过,2=拒绝 (按你现在用法)
|
||||
$status = isset($data['status']) ? (int)$data['status'] : null;
|
||||
if (!in_array($status, [1, 2], true)) {
|
||||
throw new BusinessException('非法的归还处理状态');
|
||||
}
|
||||
|
||||
// 2. 构建查询配置,定位到这条归还记录
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
|
||||
// 更新时不允许前端覆盖主键
|
||||
unset($data['requisition_item_return_id']);
|
||||
|
||||
// 3. 获取现有记录
|
||||
/** @var \app\model\InvRequisitionItemReturn|null $existingRecord */
|
||||
$existingRecord = $this->logicShow($config);
|
||||
|
||||
if (!$existingRecord) {
|
||||
throw new BusinessException('未找到对应的归还申请记录');
|
||||
}
|
||||
|
||||
// 已处理校验:只要归还记录的 status 已经是「通过或拒绝」,就不能再处理
|
||||
$existingStatus = (int)($existingRecord->status ?? 0);
|
||||
if (in_array($existingStatus, [1, 2], true) || !empty($existingRecord->handler_id)) {
|
||||
throw new BusinessException('该归还申请已经被处理过了');
|
||||
}
|
||||
|
||||
// 4. 更新归还记录(状态 + 审核人 + 备注等)
|
||||
$updateResult = $this->logicUpdateBy($config, $data);
|
||||
if ($updateResult === 0) {
|
||||
throw new BusinessException('更新归还申请失败,可能记录不存在或已被处理');
|
||||
}
|
||||
|
||||
// 5. 若是拒绝,直接返回(不改库存、不改已归还数量)
|
||||
if ($status === 2) {
|
||||
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. 审核通过:继续处理领用明细 & 库存 & 领用单状态
|
||||
// 注意:requisition_item_id、item_id、return_qty 等字段在本次确认流程中不会被修改,
|
||||
// 所以可以直接使用 $existingRecord 中的值(都是只读字段)
|
||||
$requisitionItemId = (int)$existingRecord->requisition_item_id;
|
||||
$itemId = (int)$existingRecord->item_id;
|
||||
$returnQty = (int)$existingRecord->return_qty;
|
||||
$requisitionId = (int)$existingRecord->requisition_id;
|
||||
|
||||
if ($requisitionItemId <= 0 || $itemId <= 0 || $returnQty <= 0 || $requisitionId <= 0) {
|
||||
throw new BusinessException('归还申请记录缺少必要字段,无法处理');
|
||||
}
|
||||
|
||||
// 6.1 处理领用明细
|
||||
/** @var \app\model\InvRequisitionItem|null $item */
|
||||
$item = InvRequisitionItem::find($requisitionItemId);
|
||||
if (!$item) {
|
||||
throw new BusinessException('未找到对应的领用明细记录');
|
||||
}
|
||||
|
||||
$approvedQty = (int)($item->approved_qty ?? 0);
|
||||
$returnedQty = (int)($item->returned_qty ?? 0);
|
||||
|
||||
if ($approvedQty <= 0) {
|
||||
throw new BusinessException('该物品尚未审批出库,无法归还');
|
||||
}
|
||||
|
||||
if ($returnedQty + $returnQty > $approvedQty) {
|
||||
throw new BusinessException('归还数量超过已借数量,无法确认');
|
||||
}
|
||||
|
||||
$item->returned_qty = $returnedQty + $returnQty;
|
||||
if ($item->returned_qty >= $approvedQty) {
|
||||
$item->is_finished = 1;
|
||||
}
|
||||
$item->updated_at = time();
|
||||
|
||||
if ($item->save() === false) {
|
||||
throw new BusinessException('更新领用明细失败');
|
||||
}
|
||||
|
||||
// 6.2 处理库存(入库)
|
||||
(new InvItemService())->stockIn($itemId, $returnQty);
|
||||
|
||||
// 6.3 检查整张单是否已全部归还(只看非耗材)
|
||||
$notFinishedCount = InvRequisitionItem::query()
|
||||
->where('requisition_id', $requisitionId)
|
||||
->where('is_consumable_snap', 0)
|
||||
->where('is_finished', 0)
|
||||
->count();
|
||||
|
||||
if ($notFinishedCount === 0) {
|
||||
// 全部非耗材都已归还
|
||||
$affected = InvRequisition::where('id', $requisitionId)->update([
|
||||
'is_returned' => 1,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
// Eloquent::update 返回的是影响行数,0 表示未更新到任何行
|
||||
if ($affected === 0) {
|
||||
throw new BusinessException('更新领用单状态失败,可能记录不存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 返回处理结果(保持你原来的 postProcess 习惯)
|
||||
return $this->postProcess(ucfirst($scene), $existingRecord);
|
||||
});
|
||||
|
||||
} catch (BusinessException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("确认归还物品失败:" . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $scene
|
||||
* @return array
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'returnConfirm':
|
||||
$config['where']['id'] = $p['requisition_item_return_id'];
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成申请单号
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateOrderNo(): string
|
||||
{
|
||||
return 'RR' . date('Ymd') . strtoupper(substr(uniqid(), -6));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionItemValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
|
||||
class InvRequisitionItemService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionItem::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionItemValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionReturnValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionReturn;
|
||||
|
||||
class InvRequisitionReturnService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionReturn::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionReturnValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvItem;
|
||||
use app\common\validate\InvRequisitionValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisition;
|
||||
use app\common\model\InvRequisitionAttachment;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use support\Request;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class InvRequisitionService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisition::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 CrudUtil 查询配置
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['applicant_id'] = $request->user->uid;
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建物品申请 前置
|
||||
*
|
||||
* @param array $data
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function beforeServiceStore($data, Request $request): array
|
||||
{
|
||||
$user = (new UserService())->getUserInfo($request->user->uid, '');
|
||||
|
||||
if (empty($user['user']['id']) || empty($user['user']['name'])) {
|
||||
throw new BusinessException('无法获取当前用户信息');
|
||||
}
|
||||
|
||||
$data['order_no'] = $this->generateOrderNo();
|
||||
$data['applicant_id'] = (int)$user['user']['id'];
|
||||
$data['applicant_name'] = (string)$user['user']['name'];
|
||||
$data['leader_id'] = $user['user']['report_to'];
|
||||
$data['status'] = InvRequisition::STATUS_LEADER;
|
||||
|
||||
//查询物品管理人是否是一个
|
||||
if(count($data['items'])>=2){
|
||||
$manage_user = [];
|
||||
foreach ($data['items'] as $item) {
|
||||
$manage_user[] = InvItem::where('id',$item['item_id'])->value('manage_user_id');
|
||||
}
|
||||
$manage_user = array_unique($manage_user);
|
||||
if(count($manage_user)>1){
|
||||
throw new BusinessException("审批实例未找到");
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function serviceStore(Request $request, array $data, string $scene = '')
|
||||
{
|
||||
try {
|
||||
// 关联表增加事务处理
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$data = $this->preProcess('Store', $data, $request);
|
||||
$dl = $this->getDataLimitFromRequest($request);
|
||||
$created = $this->logicCreate($data, $dl);
|
||||
// 处理物品申请项
|
||||
$this->processRequisitionItems($created, $data['items']);
|
||||
// 处理物品申请附件
|
||||
$this->processRequisitionAttachmentItems($created, $data['attachment']);
|
||||
$res = $this->postProcess('Store', $created);
|
||||
|
||||
$manage_user_id = InvItem::where('id',$data['items'][0]['item_id'])->value('manage_user_id');
|
||||
$approval = [
|
||||
'id' => $res['id'], //申请单id
|
||||
'applicant_id' => $res['applicant_id'], //申请人id
|
||||
'manage_user_id' => $manage_user_id,
|
||||
];
|
||||
$this->startApproval($approval);
|
||||
|
||||
return $res;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("创建物品申请失败:" . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动审批
|
||||
*/
|
||||
public function startApproval($requisition)
|
||||
{
|
||||
// 启动审批
|
||||
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
|
||||
$formData = [
|
||||
'manage_user_id' => $requisition['manage_user_id'],
|
||||
];
|
||||
$appro->start('inv_requisition', 'inv_requisition', $requisition['id'], $formData, $requisition['applicant_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新物品申请
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function serviceUpdate(Request $request, array $data, string $scene = '')
|
||||
{
|
||||
try {
|
||||
// 关联表增加事务处理
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$data = $this->preProcess('Update', $data, $request);
|
||||
$config = $this->buildQueryConfig($request, 'update');
|
||||
|
||||
// 获取申请项/附件
|
||||
$items = $data['items'];
|
||||
$attachment = $data['attachment'];
|
||||
unset($data['items']);
|
||||
unset($data['attachment']);
|
||||
|
||||
// 执行更新操作
|
||||
$this->logicUpdateBy($config, $data);
|
||||
|
||||
// 获取更新后的完整记录
|
||||
$updated = $this->logicShow($config);
|
||||
|
||||
// 重新创建新的申请项
|
||||
$this->processRequisitionItems((object)$updated, $items, true);
|
||||
|
||||
// 重新创建新的附件
|
||||
$this->processRequisitionAttachmentItems((object)$updated, $attachment, true);
|
||||
|
||||
return $this->postProcess('Update', $updated);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("更新物品申请失败:" . $e->getMessage()."|".$e->getLine()."|".$e->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请项
|
||||
*
|
||||
* @param Model $created
|
||||
* @param array $items
|
||||
* @return void
|
||||
*/
|
||||
private function processRequisitionItems(Model $created, array $items, bool $isUpdate = false): void
|
||||
{
|
||||
if ($isUpdate) {
|
||||
$this->clearRequisitionItems($created->id);
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemData = [
|
||||
'requisition_id' => $created->id,
|
||||
'item_id' => $item['item_id'],
|
||||
'item_name_snap' => $item['item_name_snap'], // 修改:使用 snap 字段
|
||||
'spec_model_snap' => $item['spec_model_snap']??'', // 修改:使用 snap 字段
|
||||
'unit_snap' => $item['unit_snap']??'', // 修改:使用 snap 字段
|
||||
'is_consumable_snap' => $item['is_consumable_snap']?? 0, // 修改:使用 snap 字段
|
||||
'request_qty' => (int) $item['request_qty'] ?? 1, // 动态获取数量,转为整型
|
||||
'approved_qty' => (int) $item['request_qty'] ?? 1, // 初始审批数量等于申请数量
|
||||
'remark' => $item['remark'] ?? ''
|
||||
];
|
||||
InvRequisitionItem::create($itemData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请项
|
||||
*
|
||||
* @param Model $created
|
||||
* @param array $items
|
||||
* @return void
|
||||
*/
|
||||
private function processRequisitionAttachmentItems(Model $created, array $attachment, bool $isUpdate = false): void
|
||||
{
|
||||
if ($isUpdate) {
|
||||
$this->clearRequisitionAttachmentItems($created->id);
|
||||
}
|
||||
|
||||
foreach ($attachment as $file) {
|
||||
$attachmentData = [
|
||||
'requisition_id' => $created->id,
|
||||
'file_name' => $file['original_name'],
|
||||
'file_path' => $file['file_path']
|
||||
];
|
||||
InvRequisitionAttachment::create($attachmentData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除申请项
|
||||
*
|
||||
* @param int $requisitionId
|
||||
* @return void
|
||||
*/
|
||||
public function clearRequisitionItems(int $requisitionId): void
|
||||
{
|
||||
InvRequisitionItem::where('requisition_id', $requisitionId)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除申请项
|
||||
*
|
||||
* @param int $requisitionId
|
||||
* @return void
|
||||
*/
|
||||
public function clearRequisitionAttachmentItems(int $requisitionId): void
|
||||
{
|
||||
InvRequisitionAttachment::where('requisition_id', $requisitionId)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成申请单号
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateOrderNo(): string
|
||||
{
|
||||
return 'RQ' . date('Ymd') . strtoupper(substr(uniqid(), -6));
|
||||
}
|
||||
}
|
||||
@@ -91,11 +91,11 @@ class Feishu
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
public function authorize($certificate, $url = 'authen/v1/authorize')
|
||||
public function authorize($certificate,$mobile, $url = 'authen/v1/authorize')
|
||||
{
|
||||
$arr = [
|
||||
'app_id' => $certificate['app_id'],
|
||||
'redirect_uri' => $certificate['r_url'],
|
||||
'redirect_uri' => $mobile == 1 ? $certificate['m_url'] : $certificate['r_url'],
|
||||
'scope' => $certificate['scope'],
|
||||
'state' => $certificate['state']
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait ApprovalDiscussionTrait
|
||||
{
|
||||
/** 是否该实例参与者 */
|
||||
private function isParticipantOfInstance(int $instanceId, int $userId): bool
|
||||
{
|
||||
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
|
||||
if (!$inst) return false;
|
||||
if ((int)$inst->initiator_id === $userId) return true;
|
||||
|
||||
$inRecords = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)->where('approver_id', $userId)->exists();
|
||||
if ($inRecords) return true;
|
||||
|
||||
$inCc = Db::table('approval_ccs')
|
||||
->where('instance_id', $instanceId)->where('user_id', $userId)->exists();
|
||||
return $inCc;
|
||||
}
|
||||
|
||||
/** 是否该实例任一步的审批人 */
|
||||
private function isApproverOfInstance(int $instanceId, int $userId): bool
|
||||
{
|
||||
return Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('approver_id', $userId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/** 获取讨论受众 */
|
||||
private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array
|
||||
{
|
||||
$uids = [];
|
||||
if (!$approversOnly) {
|
||||
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
|
||||
if ($inst) $uids[] = (int)$inst->initiator_id;
|
||||
$ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray();
|
||||
foreach ($ccs as $u) $uids[] = (int)$u;
|
||||
}
|
||||
$approvers = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->distinct()->pluck('approver_id')->toArray();
|
||||
foreach ($approvers as $u) $uids[] = (int)$u;
|
||||
|
||||
$uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId)));
|
||||
return $uids;
|
||||
}
|
||||
|
||||
/** 发表讨论 */
|
||||
public function postDiscussion(
|
||||
int $instanceId,
|
||||
int $userId,
|
||||
string $content,
|
||||
bool $isInternal = false,
|
||||
array $attachments = [],
|
||||
?string $userNameColumn = null
|
||||
): array {
|
||||
$content = trim($content);
|
||||
if ($content === '') throw new BusinessException('讨论内容不能为空');
|
||||
if (!$this->isParticipantOfInstance($instanceId, $userId)) {
|
||||
throw new BusinessException('无权参与该实例的讨论');
|
||||
}
|
||||
if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) {
|
||||
throw new BusinessException('仅审批人可发布内部讨论');
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$id = Db::table('approval_discussions')->insertGetId([
|
||||
'instance_id' => $instanceId,
|
||||
'user_id' => $userId,
|
||||
'is_internal' => $isInternal ? 1 : 0,
|
||||
'content' => $content,
|
||||
'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 通知其他人
|
||||
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
|
||||
if ($inst) {
|
||||
$audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId);
|
||||
// 发言人姓名(可选)
|
||||
$speaker = (string)$userId;
|
||||
if ($userNameColumn) {
|
||||
$name = Db::table('user')->where('id', $userId)->value($userNameColumn);
|
||||
if ($name) $speaker = $name;
|
||||
}
|
||||
$snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content;
|
||||
$extra = ['note' => "【讨论】{$speaker}:{$snippet}"];
|
||||
|
||||
$this->sendNotification(
|
||||
$audience,
|
||||
(object)[
|
||||
'id' => (int)$inst->id,
|
||||
'flow_type' => (string)$inst->flow_type,
|
||||
'initiator_id' => (int)$inst->initiator_id,
|
||||
'form_data' => $inst->form_data,
|
||||
'biz_type' => (string)$inst->biz_type,
|
||||
'biz_id' => (string)$inst->biz_id,
|
||||
],
|
||||
'pending',
|
||||
false,
|
||||
$extra
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)$id,
|
||||
'instance_id' => $instanceId,
|
||||
'user_id' => $userId,
|
||||
'is_internal' => (bool)$isInternal,
|
||||
'content' => $content,
|
||||
'attachments' => $attachments,
|
||||
'created_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
/** 获取讨论列表 */
|
||||
public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array
|
||||
{
|
||||
$q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId);
|
||||
if ($afterId) $q->where('d.id', '>', $afterId);
|
||||
$q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit)));
|
||||
|
||||
if ($userNameColumn) {
|
||||
$rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id')
|
||||
->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name"))
|
||||
->get()->toArray();
|
||||
} else {
|
||||
$rows = $q->select('d.*')->get()->toArray();
|
||||
}
|
||||
|
||||
return array_map(function($r){
|
||||
return [
|
||||
'id' => (int)$r->id,
|
||||
'instance_id' => (int)$r->instance_id,
|
||||
'user_id' => (int)$r->user_id,
|
||||
'is_internal' => (bool)$r->is_internal,
|
||||
'content' => $r->content,
|
||||
'attachments' => $r->attachments ? json_decode($r->attachments, true) : [],
|
||||
'created_at' => $r->created_at,
|
||||
'updated_at' => $r->updated_at,
|
||||
'user_name' => property_exists($r, 'user_name') ? $r->user_name : null,
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\Log;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait ApprovalEngineTrait
|
||||
{
|
||||
/** 启动审批流程 */
|
||||
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
|
||||
{
|
||||
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}");
|
||||
|
||||
$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];
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$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,
|
||||
'round' => 1,
|
||||
'deadline_at' => $deadlineAt,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$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',
|
||||
'flow_type' => $flowType,
|
||||
'biz_type' => $bizType,
|
||||
'biz_id' => $bizId,
|
||||
'round' => 1,
|
||||
];
|
||||
|
||||
// 解析首步审批人(支持 role_id 或 roles.name)
|
||||
$approverIds = $this->resolveApprovers($firstStep, $instance, true);
|
||||
foreach ($approverIds as $uid) {
|
||||
$this->insertApprovalRecord($instance, $firstIndex, $uid);
|
||||
}
|
||||
|
||||
$extra = [];
|
||||
$submitNote = $formData['submit_note'] ?? '';
|
||||
if (is_string($submitNote) && $submitNote !== '') {
|
||||
$extra['note'] = $submitNote;
|
||||
}
|
||||
$this->sendNotification($approverIds, $instance, 'pending', false, $extra);
|
||||
|
||||
// $this->notifyCCs($firstStep, $instance, 'pending');
|
||||
|
||||
$this->pushTodoChanged($approverIds);
|
||||
|
||||
return ['instance_id' => $instanceId];
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行审批(幂等友好) */
|
||||
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') {
|
||||
return ['status' => 'noop', 'msg' => '实例未处理'];
|
||||
}
|
||||
|
||||
$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;
|
||||
$round = (int)$instance->round;
|
||||
$step = $steps[$idx] ?? null;
|
||||
if (!$step) throw new BusinessException("当前步骤配置错误");
|
||||
|
||||
// 找到该用户在本步的 pending 记录(加锁)
|
||||
$pendingRecord = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', $round)
|
||||
->where('approver_id', $approverId)
|
||||
->where('step_index', $idx)
|
||||
->where('status', 'pending')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$pendingRecord) {
|
||||
// 已处理过?
|
||||
$anyRecord = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', $round)
|
||||
->where('approver_id', $approverId)
|
||||
->where('step_index', $idx)
|
||||
->first();
|
||||
if ($anyRecord && $anyRecord->status !== 'pending') {
|
||||
return ['status' => 'noop', 'msg' => '已处理'];
|
||||
}
|
||||
// 步骤已推进
|
||||
if ((int)$instance->current_step !== $idx) {
|
||||
return ['status' => 'noop', 'msg' => '非当前审批'];
|
||||
}
|
||||
return ['status' => 'noop', 'msg' => '无待处理记录'];
|
||||
}
|
||||
|
||||
$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,
|
||||
]);
|
||||
|
||||
// 异步审批回调 9.23 add
|
||||
$approvalcallback = [
|
||||
'biz_id' => $instance->biz_id,
|
||||
'flow_type' => $instance->flow_type,
|
||||
'instance_id' => $instance->id,
|
||||
'initiator_id' => $instance->initiator_id,
|
||||
'status' => $action
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback);
|
||||
|
||||
// 2) 聚合本步结论(any / all)
|
||||
$typeRaw = strtolower($step['type'] ?? 'all');
|
||||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||||
$outcome = $this->getStepOutcome($instanceId, $round, $idx, $type); // 'pending' | 'approve' | 'reject'
|
||||
|
||||
if ($outcome === 'pending') {
|
||||
$this->pushTodoChanged([$approverId]);
|
||||
return ['status' => 'step_pending', 'msg' => '审批待定'];
|
||||
}
|
||||
|
||||
$otherPendingUids = $this->getPendingApproverIds($instanceId, $round, $idx);
|
||||
|
||||
// 3) 有结论:关闭本步其余 pending
|
||||
$this->closeOtherPending($instanceId, $round, $idx, [$approverId]);
|
||||
|
||||
// 这些人 pending→skipped,待办数减少
|
||||
$this->pushTodoChanged($otherPendingUids);
|
||||
|
||||
// 4) 拒绝 => 结束
|
||||
if ($outcome === 'reject') {
|
||||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||||
'status' => 'rejected',
|
||||
'finished_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->onFlowFinalized($instance, 'rejected');
|
||||
$this->sendNotification([$instance->initiator_id], $instance, 'reject', false);
|
||||
$this->pushTodoChanged([$approverId]);
|
||||
return ['status' => 'completed', 'msg' => '拒绝'];
|
||||
}
|
||||
|
||||
// 5) 通过 => 进入下一步或结束
|
||||
$formData = json_decode($instance->form_data, true) ?: [];
|
||||
$nextIndex = $this->getNextStepIndex($steps, $formData, $idx);
|
||||
|
||||
if ($nextIndex === null) {
|
||||
$finalStatus = $this->normalizeFinalStatus((string)$instance->flow_type, 'approved');
|
||||
$notifyAction = ($finalStatus === 'voided') ? 'voided' : 'approve';
|
||||
|
||||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||||
'status' => $finalStatus,
|
||||
'finished_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->onFlowFinalized($instance, $finalStatus);
|
||||
|
||||
// 新增:在流程最终完成时才发本步骤的抄送
|
||||
if (!empty($step['cc'])) {
|
||||
$this->notifyCCs($step, $instance, $notifyAction);
|
||||
}
|
||||
|
||||
$this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false);
|
||||
$this->pushTodoChanged([$approverId]);
|
||||
return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')];
|
||||
}
|
||||
|
||||
// 乐观推进 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', 'msg' => '并发进展'];
|
||||
}
|
||||
|
||||
// 生成下一步待办 + 通知
|
||||
$nextStep = $steps[$nextIndex];
|
||||
$nextApprovers = $this->resolveApprovers($nextStep, $instance, true);
|
||||
foreach ($nextApprovers as $uid) {
|
||||
$this->insertApprovalRecord($instance, $nextIndex, $uid);
|
||||
}
|
||||
$this->sendNotification($nextApprovers, $instance, 'pending', false);
|
||||
// $this->notifyCCs($nextStep, $instance, 'pending');
|
||||
$this->pushTodoChanged(array_merge([$approverId], $nextApprovers));
|
||||
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' => 'revoked',
|
||||
'finished_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$this->onFlowFinalized($instance, 'revoked');
|
||||
|
||||
$pendingUids = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('status', 'pending')
|
||||
->pluck('approver_id')->toArray();
|
||||
|
||||
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);
|
||||
$this->pushTodoChanged($pendingUids);
|
||||
return ['status' => 'completed', 'result' => 'revoked'];
|
||||
});
|
||||
}
|
||||
|
||||
/** 定时任务:处理超时(实例级) */
|
||||
public function handleTimeouts()
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$expired = Db::table('approval_instances')
|
||||
->where('status', 'processing')
|
||||
->whereNotNull('deadline_at')
|
||||
->where('deadline_at', '<=', $now)
|
||||
->get();
|
||||
|
||||
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';
|
||||
$final = $this->normalizeFinalStatus((string)$instance->flow_type, $final);
|
||||
$finalAction = ($final === 'voided') ? 'voided' : (($final === 'approved') ? 'approve' : 'reject');
|
||||
|
||||
Db::table('approval_instances')->where('id', $instance->id)->update([
|
||||
'status' => $final,
|
||||
'finished_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$this->onFlowFinalized((object)$instance, $final);
|
||||
|
||||
$pendingUids = Db::table('approval_records')
|
||||
->where('instance_id', $instance->id)
|
||||
->where('round', (int)$instance->round)
|
||||
->where('step_index', $instance->current_step)
|
||||
->where('status', 'pending')
|
||||
->pluck('approver_id')->toArray();
|
||||
|
||||
Db::table('approval_records')
|
||||
->where('instance_id', $instance->id)
|
||||
->where('round', (int)$instance->round)
|
||||
->where('step_index', $instance->current_step)
|
||||
->where('status', 'pending')
|
||||
->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]);
|
||||
|
||||
$this->notifyInitiator($instance, $finalAction);
|
||||
|
||||
$this->pushTodoChanged($pendingUids);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 重提交(仅对 status=rejected 的实例;支持申诉说明) */
|
||||
public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array
|
||||
{
|
||||
return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) {
|
||||
$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 !== 'rejected') 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) ?: [];
|
||||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
|
||||
// 合并改动 & 打上重提标记
|
||||
$formData = array_merge($formData, $patchData);
|
||||
$formData['resubmitted'] = 1;
|
||||
|
||||
$appealText = trim((string)$appealText);
|
||||
if ($appealText !== '') {
|
||||
$formData['appeal_last'] = $appealText;
|
||||
}
|
||||
|
||||
// 计算重提后首个命中的步骤
|
||||
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
|
||||
if ($firstIndex === null) 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);
|
||||
}
|
||||
|
||||
$oldPendingUids = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('status', 'pending')
|
||||
->pluck('approver_id')->toArray();
|
||||
|
||||
// 清理可能遗留的 pending
|
||||
Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('status', 'pending')
|
||||
->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]);
|
||||
|
||||
// 轮次 +1
|
||||
$newRound = ((int)$instance->round) + 1;
|
||||
|
||||
// 重新开启实例
|
||||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||||
'status' => 'processing',
|
||||
'current_step' => $firstIndex,
|
||||
'round' => $newRound,
|
||||
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'finished_at' => null,
|
||||
'deadline_at' => $deadlineAt,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 创建待办 + 通知
|
||||
$firstStep = $steps[$firstIndex];
|
||||
$instanceLite = (object)[
|
||||
'id' => (int)$instanceId,
|
||||
'flow_type' => (string)$instance->flow_type,
|
||||
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'initiator_id' => (int)$instance->initiator_id,
|
||||
'biz_type' => (string)$instance->biz_type,
|
||||
'biz_id' => (string)$instance->biz_id,
|
||||
'round' => $newRound,
|
||||
];
|
||||
$approvers = $this->resolveApprovers($firstStep, $instanceLite, true);
|
||||
if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人');
|
||||
|
||||
foreach ($approvers as $uid) {
|
||||
$this->insertApprovalRecord($instanceLite, $firstIndex, (int)$uid);
|
||||
}
|
||||
|
||||
$extra = $appealText !== '' ? ['appeal' => $appealText] : [];
|
||||
$this->sendNotification($approvers, $instanceLite, 'pending', false, $extra);
|
||||
$this->notifyCCs($firstStep, $instanceLite, 'pending');
|
||||
$this->pushTodoChanged(array_merge($oldPendingUids, $approvers));
|
||||
|
||||
return [
|
||||
'status' => 'resubmitted',
|
||||
'instance_id' => (int)$instanceId,
|
||||
'current_step' => $firstIndex,
|
||||
'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增:发起“作废审批” */
|
||||
public function startNullify(
|
||||
string $voidFlowType,
|
||||
string $bizType,
|
||||
string $bizId,
|
||||
array $formData,
|
||||
int $initiatorId
|
||||
): array {
|
||||
return Db::transaction(function () use ($voidFlowType, $bizType, $bizId, $formData, $initiatorId) {
|
||||
// 1) 流程定义校验
|
||||
$flow = Db::table('approval_flows')->where('type', $voidFlowType)->where('is_active', 1)->first();
|
||||
if (!$flow) throw new BusinessException("作废流程未找到或已停用: {$voidFlowType}");
|
||||
$steps = json_decode($flow->steps, true);
|
||||
if (!is_array($steps) || empty($steps)) throw new BusinessException("作废流程步骤配置错误");
|
||||
|
||||
// 2) 必须存在“已通过”的最新有效实例(非作废流程)
|
||||
$latestApproved = Db::table('approval_instances')
|
||||
->where('biz_type', $bizType)
|
||||
->where('biz_id', $bizId)
|
||||
->where('flow_type', '!=', $voidFlowType)
|
||||
->where('status', 'approved')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (!$latestApproved) {
|
||||
throw new BusinessException('仅已通过的单据可发起作废');
|
||||
}
|
||||
|
||||
// 3) 不允许并行的作废流程
|
||||
$voidProcessing = Db::table('approval_instances')
|
||||
->where('flow_type', $voidFlowType)
|
||||
->where('biz_id', $bizId)
|
||||
->where('status', 'processing')
|
||||
->exists();
|
||||
if ($voidProcessing) {
|
||||
throw new BusinessException('已存在进行中的作废审批');
|
||||
}
|
||||
|
||||
// A. 基于历史“作废流程已通过”的实例拦截
|
||||
$voidApproved = Db::table('approval_instances')
|
||||
->where('flow_type', $voidFlowType)
|
||||
->where('biz_id', $bizId)
|
||||
->whereIn('status', ['approved','voided'])
|
||||
->exists();
|
||||
if ($voidApproved) {
|
||||
throw new BusinessException('该单据已作废,无需重复作废');
|
||||
}
|
||||
|
||||
// B.(可选)直接查业务表状态拦截(若你用 approval_status=4 表示已作废)
|
||||
/*
|
||||
$orderVoided = \app\common\model\StockOrders::where('id', $bizId)
|
||||
->where('approval_status', 4)
|
||||
->exists();
|
||||
if ($orderVoided) {
|
||||
throw new BusinessException('该单据已作废,无需重复作废');
|
||||
}
|
||||
*/
|
||||
|
||||
// 4) 计算首步
|
||||
// 注:作废流程可以有独立条件;若不需要条件,直接第一步
|
||||
$formData = array_merge([
|
||||
'_op' => 'nullify',
|
||||
'_nullify_reason' => $formData['nullify_reason'] ?? ($formData['reason'] ?? ''),
|
||||
'_nullify_of_instance_id' => (int)$latestApproved->id,
|
||||
], $formData);
|
||||
|
||||
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
|
||||
if ($firstIndex === null) throw new BusinessException("作废流程未命中任何审批步骤");
|
||||
$firstStep = $steps[$firstIndex];
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
// 5) 新建作废实例(round 固定从 1 开始)
|
||||
$instanceId = Db::table('approval_instances')->insertGetId([
|
||||
'flow_type' => $voidFlowType,
|
||||
'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,
|
||||
'round' => 1, //
|
||||
'deadline_at' => $deadlineAt,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$instance = (object)[
|
||||
'id' => $instanceId,
|
||||
'flow_type' => $voidFlowType,
|
||||
'biz_type' => $bizType,
|
||||
'biz_id' => $bizId,
|
||||
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'initiator_id' => $initiatorId,
|
||||
'current_step' => $firstIndex,
|
||||
'status' => 'processing',
|
||||
'round' => 1, //
|
||||
];
|
||||
|
||||
// 6) 生成首步待办 + 通知(带上作废原因)
|
||||
$approverIds = $this->resolveApprovers($firstStep, $instance, true);
|
||||
foreach ($approverIds as $uid) {
|
||||
$this->insertApprovalRecord($instance, $firstIndex, $uid);
|
||||
}
|
||||
$extra = [];
|
||||
if (!empty($formData['_nullify_reason'])) {
|
||||
$extra['note'] = '【作废原因】'.$formData['_nullify_reason'];
|
||||
}
|
||||
$this->sendNotification($approverIds, $instance, 'pending', false, $extra);
|
||||
$this->notifyCCs($firstStep, $instance, 'pending');
|
||||
$this->pushTodoChanged($approverIds);
|
||||
return ['instance_id' => $instanceId];
|
||||
});
|
||||
}
|
||||
|
||||
// 流程完结后的业务回调(approved|rejected|revoked|timeout?)
|
||||
private function onFlowFinalized(object $instance, string $final): void
|
||||
{
|
||||
try {
|
||||
switch ($instance->flow_type) {
|
||||
case 'stock_orders': {
|
||||
$order = \app\common\model\StockOrders::find($instance->biz_id);
|
||||
if (!$order) {
|
||||
throw new BusinessException('订单不存在');
|
||||
}
|
||||
if ($final === 'approved') {
|
||||
$order->approval_status = 2;
|
||||
$order->save();
|
||||
} elseif ($final === 'rejected') {
|
||||
$order->approval_status = 3;
|
||||
$order->save();
|
||||
} elseif ($final === 'revoked') {
|
||||
$order->approval_status = 0;
|
||||
$order->save();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'stock_orders_nullify':
|
||||
if ($final === 'approved') {
|
||||
$final = 'voided';
|
||||
Db::table('approval_instances')->where('id', $instance->id)->update([
|
||||
'status' => 'voided',
|
||||
]);
|
||||
}
|
||||
if (in_array($final, ['voided','approved'], true)) {
|
||||
$order = \app\common\model\StockOrders::find($instance->biz_id);
|
||||
if ($order && $order->approval_status !== 4) {
|
||||
$order->approval_status = 4;
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 非作废流程,无需处理
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('onFlowFinalized error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 是否作废流程
|
||||
private function isNullifyFlow(string $flowType): bool
|
||||
{
|
||||
$t = strtolower(trim($flowType));
|
||||
return str_ends_with($t, '_nullify') || in_array($t, [
|
||||
// 所有“作废”类流程的类型
|
||||
'stock_orders_nullify',
|
||||
], true);
|
||||
}
|
||||
|
||||
// 归一化最终状态
|
||||
private function normalizeFinalStatus(string $flowType, string $final): string
|
||||
{
|
||||
if ($this->isNullifyFlow($flowType) && $final === 'approved') {
|
||||
return 'voided';
|
||||
}
|
||||
return $final;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\Log;
|
||||
|
||||
trait ApprovalNotificationTrait
|
||||
{
|
||||
/** 发送通知 */
|
||||
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC, array $extra = []): void
|
||||
{
|
||||
$map = [
|
||||
'pending' => '待审批',
|
||||
'approve' => '已通过',
|
||||
'reject' => '已拒绝',
|
||||
'revoked' => '已撤回',
|
||||
'voided' => '已作废',
|
||||
];
|
||||
$prefix = $isCC ? '【抄送】' : '【审批】';
|
||||
$text = $map[$action] ?? $action;
|
||||
|
||||
// 业务标题/内容
|
||||
$approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type'));
|
||||
|
||||
$flowTxt = $approvalType[$instance->flow_type] ?? (string)$instance->flow_type;
|
||||
$title = $prefix.'审批通知';
|
||||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务标识 {$instance->biz_id}";
|
||||
|
||||
switch ($instance->flow_type) {
|
||||
case 'stock_orders':
|
||||
// 备货单
|
||||
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
|
||||
if ($stockOrder) {
|
||||
$title = $prefix."备货单:{$stockOrder->order_no}";
|
||||
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
|
||||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}";
|
||||
}
|
||||
break;
|
||||
case 'stock_orders_nullify':
|
||||
// 备货单作废
|
||||
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
|
||||
if ($stockOrder) {
|
||||
$title = $prefix."备货单作废:{$stockOrder->order_no}";
|
||||
$flowTxt = $approvalType[$instance->flow_type] ?? '备货单作废';
|
||||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}";
|
||||
// 把作废原因附带展示更清晰(也可依赖 $extra['note'])
|
||||
$fd = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
if (!empty($fd['_nullify_reason'])) {
|
||||
$content .= ';原因:'.(function_exists('mb_strimwidth') ? mb_strimwidth($fd['_nullify_reason'], 0, 120, '...', 'UTF-8') : $fd['_nullify_reason']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'purchase_orders':
|
||||
// 采购单
|
||||
$purchaseOrder = \app\common\model\PurchaseOrders::find($instance->biz_id);
|
||||
if ($purchaseOrder) {
|
||||
$title = $prefix."采购单:{$purchaseOrder->order_no}";
|
||||
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
|
||||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$purchaseOrder->order_no}";
|
||||
}
|
||||
break;
|
||||
case 'purchase_order':
|
||||
// 采购单
|
||||
break;
|
||||
case 'inv_requisition':
|
||||
// 备货单
|
||||
$invRequisition = \app\common\model\InvRequisition::find($instance->biz_id);
|
||||
if ($invRequisition) {
|
||||
$title = $prefix."物品借用:{$invRequisition->order_no}";
|
||||
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
|
||||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$invRequisition->order_no}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 附加说明(提交说明/申诉/讨论摘要)
|
||||
$append = $extra['appeal'] ?? ($extra['note'] ?? null);
|
||||
if ($append) {
|
||||
if (function_exists('mb_strimwidth')) {
|
||||
$append = mb_strimwidth($append, 0, 120, '...', 'UTF-8');
|
||||
}
|
||||
$content .= ";说明:{$append}";
|
||||
}
|
||||
|
||||
noticePush([
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'recipient' => $userIds,
|
||||
'email' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function notifyInitiator(object $instance, string $result): void
|
||||
{
|
||||
$this->sendNotification([(int)$instance->initiator_id], $instance, $result, false);
|
||||
}
|
||||
|
||||
/** WebSocket:把一组用户的“待办数量”推送给前端(旧版) */
|
||||
private function pushTodoChanged22(array $userIds): void
|
||||
{
|
||||
$userIds = array_values(array_unique(array_map('intval', $userIds)));
|
||||
if (empty($userIds)) return;
|
||||
|
||||
// 统计这些用户当前的待办数量(以 approval_records.status = 'pending' 为准)
|
||||
$rows = Db::table('approval_records')
|
||||
->whereIn('approver_id', $userIds)
|
||||
->where('status', 'pending')
|
||||
->groupBy('approver_id')
|
||||
->select('approver_id', Db::raw('COUNT(*) as cnt'))
|
||||
->get();
|
||||
|
||||
// 默认都置 0,再填充真实值
|
||||
$payload = array_fill_keys($userIds, 0);
|
||||
foreach ($rows as $r) {
|
||||
$payload[(int)$r->approver_id] = (int)$r->cnt;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($payload as $uid => $cnt) {
|
||||
$data = [
|
||||
'event' => 'approval.todo.counts',
|
||||
'user_id' => $uid,
|
||||
'num' => $cnt,
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send('ws-msg', $data);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('ws todo push error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** WebSocket:把一组用户的“待办数量”推送给前端(统一口径) */
|
||||
private function pushTodoChanged(array $userIds): void
|
||||
{
|
||||
$userIds = array_values(array_unique(array_map('intval', $userIds)));
|
||||
if (empty($userIds)) return;
|
||||
|
||||
// 只统计:实例进行中 + 当前轮次 + 当前步骤 的 pending
|
||||
$rows = Db::table('approval_records as ar')
|
||||
->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id')
|
||||
->whereIn('ar.approver_id', $userIds)
|
||||
->where('ar.status', 'pending')
|
||||
->where('ai.status', 'processing')
|
||||
->whereRaw('ar.round = ai.round')
|
||||
->whereRaw('ar.step_index = ai.current_step')
|
||||
->groupBy('ar.approver_id')
|
||||
->select('ar.approver_id', Db::raw('COUNT(*) as cnt'))
|
||||
->get();
|
||||
|
||||
// 默认置 0,再填充真实值
|
||||
$payload = array_fill_keys($userIds, 0);
|
||||
foreach ($rows as $r) {
|
||||
$payload[(int)$r->approver_id] = (int)$r->cnt;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($payload as $uid => $cnt) {
|
||||
(new \app\common\utils\WsEvents())->todoCounts($uid, $cnt);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('ws todo push error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait ApprovalQueryTrait
|
||||
{
|
||||
/** 流程图数据 */
|
||||
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)
|
||||
->where('round', (int)$instance->round)
|
||||
->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc')
|
||||
->get()
|
||||
->groupBy('step_index');
|
||||
|
||||
$formData = json_decode($instance->form_data, true) ?: [];
|
||||
$data = [];
|
||||
foreach ($steps as $i => $step) {
|
||||
$actions = ($records->get($i) ?? collect())->toArray();
|
||||
|
||||
if (!empty($actions)) {
|
||||
$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';
|
||||
}
|
||||
}
|
||||
|
||||
$data[] = [
|
||||
'index' => $i,
|
||||
'name' => $step['name'] ?? ('Step '.$i),
|
||||
'type' => $step['type'] ?? 'all',
|
||||
'status' => $status,
|
||||
'actions' => $actions,
|
||||
];
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** 查询某个审批实例的审批记录 */
|
||||
public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array
|
||||
{
|
||||
// 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,
|
||||
];
|
||||
}
|
||||
|
||||
/** 简单时间线 */
|
||||
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||||
{
|
||||
// 基础数据
|
||||
$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) ?: [];
|
||||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
$curStep = (int)$instance->current_step;
|
||||
$round = (int)$instance->round;
|
||||
|
||||
// 申请人
|
||||
$initiatorId = (int)$instance->initiator_id;
|
||||
$initiatorName = null;
|
||||
if ($userNameColumn) {
|
||||
$initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn);
|
||||
}
|
||||
|
||||
// 审批记录(仅当前轮次)
|
||||
$records = Db::table('approval_records as ar')
|
||||
->where('ar.instance_id', $instanceId)
|
||||
->where('ar.round', $round)
|
||||
->orderBy('ar.step_index')
|
||||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||||
->select('ar.*')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
// 一次性拉取姓名,避免 N+1
|
||||
$nameMap = [];
|
||||
if ($userNameColumn && !empty($records)) {
|
||||
$uids = [];
|
||||
foreach ($records as $r) $uids[] = (int)$r->approver_id;
|
||||
$uids = array_values(array_unique($uids));
|
||||
if (!empty($uids)) {
|
||||
$pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray();
|
||||
foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录分组
|
||||
$byStep = [];
|
||||
foreach ($records as $r) {
|
||||
$idx = (int)$r->step_index;
|
||||
$byStep[$idx][] = $r;
|
||||
}
|
||||
|
||||
$timeline = [];
|
||||
|
||||
// 可选:发起申请块
|
||||
if ($initiatorPlacement === 'prepend') {
|
||||
$appliedAt = $instance->created_at ?: null;
|
||||
$timeline[] = [
|
||||
'index' => -1,
|
||||
'name' => '发起申请',
|
||||
'type' => '-',
|
||||
'status' => 'submitted',
|
||||
'started_at' => $appliedAt,
|
||||
'finished_at' => $appliedAt,
|
||||
'duration_seconds' => 0,
|
||||
'initiator_id' => $initiatorId,
|
||||
'initiator_name' => $initiatorName,
|
||||
];
|
||||
}
|
||||
|
||||
// 每一步
|
||||
foreach ($steps as $i => $meta) {
|
||||
$name = $meta['name'] ?? ('Step '.$i);
|
||||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||||
$condStr = $meta['condition'] ?? null;
|
||||
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
|
||||
|
||||
$recs = $byStep[$i] ?? [];
|
||||
$hasRecs = !empty($recs);
|
||||
|
||||
$status = 'pending';
|
||||
$startedAt = null;
|
||||
$finishedAt = null;
|
||||
$durationSec = null;
|
||||
|
||||
// 每一步的“审批意见”(只展示有内容的 comment)
|
||||
$opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...]
|
||||
|
||||
if ($hasRecs) {
|
||||
// 统计时间
|
||||
$minCreated = null;
|
||||
$minApproveAt = null; $maxApproveAt = null;
|
||||
$minRejectAt = null;
|
||||
$maxTimeoutAt = null;
|
||||
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
|
||||
|
||||
foreach ($recs as $rr) {
|
||||
// 收集“审批意见”(非空 comment 都算)
|
||||
$cmt = trim((string)($rr->comment ?? ''));
|
||||
if ($cmt !== '') {
|
||||
$opinions[] = [
|
||||
'approver_id' => (int)$rr->approver_id,
|
||||
'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null,
|
||||
'action' => $rr->action, // approve | reject | null
|
||||
'status' => $rr->status, // approved|rejected|pending|skipped|timeout
|
||||
'comment' => $cmt,
|
||||
'time' => $rr->approved_at ?: $rr->created_at,
|
||||
];
|
||||
}
|
||||
|
||||
if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at;
|
||||
if ($rr->approved_at) {
|
||||
if ($rr->status === 'approved') {
|
||||
if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at;
|
||||
if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at;
|
||||
} elseif ($rr->status === 'rejected') {
|
||||
if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at;
|
||||
} elseif ($rr->status === 'timeout') {
|
||||
if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at;
|
||||
}
|
||||
}
|
||||
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
|
||||
}
|
||||
|
||||
// 时间升序排序 opinions
|
||||
if (!empty($opinions)) {
|
||||
usort($opinions, function($a, $b) {
|
||||
return strcmp($a['time'] ?? '', $b['time'] ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
$startedAt = $minCreated;
|
||||
|
||||
// 总结果(注意:这里按你现在的签名调用,含 round)
|
||||
$outcome = $this->getStepOutcome($instanceId, $round, $i, $type); // 'pending'|'approve'|'reject'
|
||||
if ($outcome === 'reject') {
|
||||
$status = 'rejected';
|
||||
$finishedAt = $minRejectAt;
|
||||
} elseif ($outcome === 'approve') {
|
||||
$status = 'approved';
|
||||
$finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt;
|
||||
} else {
|
||||
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
|
||||
$status = 'timeout';
|
||||
$finishedAt = $maxTimeoutAt;
|
||||
} else {
|
||||
$status = 'processing';
|
||||
}
|
||||
}
|
||||
|
||||
if ($startedAt && $finishedAt) {
|
||||
$durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt));
|
||||
}
|
||||
} else {
|
||||
// 无记录:根据位置与条件展示
|
||||
if ($i < $curStep) {
|
||||
$status = $condOk ? 'approved' : 'skipped';
|
||||
} elseif ($i === $curStep && $instance->status === 'processing') {
|
||||
$status = 'processing';
|
||||
} else {
|
||||
$status = $condOk ? 'pending' : 'skipped';
|
||||
}
|
||||
}
|
||||
|
||||
$row = [
|
||||
'index' => (int)$i,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'status' => $status,
|
||||
'started_at' => $startedAt,
|
||||
'finished_at' => $finishedAt,
|
||||
'duration_seconds' => $durationSec,
|
||||
'opinions' => $opinions, // 这就是每个审批意见
|
||||
];
|
||||
|
||||
// 把申请人信息并到第一步(可选)
|
||||
if ($initiatorPlacement === 'attach' && empty($timeline)) {
|
||||
$row['initiator_id'] = $initiatorId;
|
||||
$row['initiator_name'] = $initiatorName;
|
||||
}
|
||||
|
||||
$timeline[] = $row;
|
||||
}
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
/** 按业务ID获取“最新进展”的极简状态 */
|
||||
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;
|
||||
$round = (int)$instance->round;
|
||||
$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.round', $round)
|
||||
->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('round', $round)
|
||||
->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('round', $round)
|
||||
->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;
|
||||
}
|
||||
|
||||
/** 登录/首屏初始化:获取当前用户的待办总数 */
|
||||
public function getInitTodoCount(int $userId): int
|
||||
{
|
||||
$userId = (int)$userId;
|
||||
if ($userId <= 0) {
|
||||
throw new BusinessException('无效的用户ID');
|
||||
}
|
||||
|
||||
$row = Db::table('approval_records as ar')
|
||||
->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id')
|
||||
->where('ar.approver_id', $userId)
|
||||
->where('ar.status', 'pending')
|
||||
->where('ai.status', 'processing')
|
||||
// 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰
|
||||
->whereRaw('ar.round = ai.round')
|
||||
->whereRaw('ar.step_index = ai.current_step')
|
||||
->selectRaw('COUNT(*) as c')
|
||||
->first();
|
||||
|
||||
return (int)($row->c ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait ApprovalResolverTrait
|
||||
{
|
||||
/** 解析审批人(role/user/initiator_leader/field_user/dept_head/...) */
|
||||
private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array
|
||||
{
|
||||
$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;
|
||||
|
||||
case 'group_members': {
|
||||
// $gid = (int)($cfg['group_id'] ?? 0);
|
||||
$gid = (int)($cfg['value'] ?? 0);
|
||||
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
|
||||
$ids = $this->getGroupMemberIds($gid); // A 方案
|
||||
break;
|
||||
}
|
||||
|
||||
case 'scenario_leader_of_initiator': {
|
||||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
// $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null);
|
||||
$scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null);
|
||||
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
|
||||
|
||||
$mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true;
|
||||
$preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true;
|
||||
|
||||
$ids = $this->getLeadersByScenarioForInitiator(
|
||||
(int)$instance->initiator_id,
|
||||
(string)$scenario,
|
||||
$mustBelong,
|
||||
$preferPrimary
|
||||
); // B 方案(按场景)
|
||||
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));
|
||||
$resolved = array_values($resolved);
|
||||
}
|
||||
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;
|
||||
case 'group_members': {
|
||||
$gid = (int)($val ?? 0);
|
||||
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
|
||||
$uids = $this->getGroupMemberIds($gid); // A 方案
|
||||
break;
|
||||
}
|
||||
case 'scenario_leader_of_initiator': {
|
||||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
$scenario = $val ?? ($formData[$cc['scenario_field']] ?? null);
|
||||
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
|
||||
|
||||
$mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true;
|
||||
$preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true;
|
||||
|
||||
$uids = $this->getLeadersByScenarioForInitiator(
|
||||
(int)$instance->initiator_id,
|
||||
(string)$scenario,
|
||||
$mustBelong,
|
||||
$preferPrimary
|
||||
); // B 方案(按场景)
|
||||
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}"),
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取下一个有效步骤索引 */
|
||||
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;
|
||||
}
|
||||
|
||||
/** 聚合本步骤结论 */
|
||||
private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string
|
||||
{
|
||||
$records = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', $round) //
|
||||
->where('step_index', $stepIndex)
|
||||
->get();
|
||||
|
||||
$pending = $approved = $rejected = $timeout = $skipped = 0;
|
||||
foreach ($records as $r) {
|
||||
switch ($r->status) {
|
||||
case 'pending': $pending++; break;
|
||||
case 'approved': $approved++; break;
|
||||
case 'rejected': $rejected++; break;
|
||||
case 'timeout': $timeout++; break;
|
||||
case 'skipped': $skipped++; break;
|
||||
}
|
||||
}
|
||||
|
||||
$type = strtolower($type);
|
||||
if ($type === 'any') {
|
||||
if ($rejected > 0) return 'reject';
|
||||
if ($approved > 0) return 'approve';
|
||||
// 全为 skipped/timeout 且无 pending/rejected ⇒ 通过
|
||||
if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve';
|
||||
return 'pending';
|
||||
}
|
||||
if ($rejected > 0) return 'reject';
|
||||
if ($pending > 0) return 'pending';
|
||||
return 'approve';
|
||||
}
|
||||
|
||||
/** 关闭本步其余待办为 skipped */
|
||||
private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$q = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', $round) //
|
||||
->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(object $instance, int $stepIndex, int $approverId): void
|
||||
{
|
||||
$round = (int)($instance->round ?? 1);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$exists = Db::table('approval_records')
|
||||
->where('instance_id', (int)$instance->id)
|
||||
->where('round', $round) // 限当前轮
|
||||
->where('step_index', $stepIndex)
|
||||
->where('approver_id', $approverId)
|
||||
->where('status', 'pending')
|
||||
->exists();
|
||||
|
||||
if ($exists) return;
|
||||
|
||||
Db::table('approval_records')->insert([
|
||||
'instance_id' => (int)$instance->id,
|
||||
'approver_id' => $approverId,
|
||||
'step_index' => $stepIndex,
|
||||
'round' => $round, //
|
||||
'action' => null,
|
||||
'status' => 'pending',
|
||||
'comment' => null,
|
||||
'approved_at' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/** 取“某实例、某轮次、某步骤”里当前 pending 的审批人 */
|
||||
private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array
|
||||
{
|
||||
$ids = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', $round)
|
||||
->where('step_index', $stepIndex)
|
||||
->where('status', 'pending')
|
||||
->pluck('approver_id')->toArray();
|
||||
|
||||
return array_values(array_unique(array_map('intval', $ids)));
|
||||
}
|
||||
|
||||
/** 从角色取用户ID */
|
||||
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
|
||||
{
|
||||
$id = Db::table('user')->where('id', $userId)->value('report_to');
|
||||
return $id ? (int)$id : null;
|
||||
}
|
||||
|
||||
private function getDeptHeadId(int $deptId): ?int
|
||||
{
|
||||
$id = Db::table('departments')->where('id', $deptId)->value('head_user_id');
|
||||
return $id ? (int)$id : null;
|
||||
}
|
||||
|
||||
/** A) 取组内全部成员 */
|
||||
private function getGroupMemberIds(int $groupId): array
|
||||
{
|
||||
$rows = Db::table('user_group_members')
|
||||
->where('group_id', $groupId)
|
||||
->where('status', 1)
|
||||
->pluck('user_id')->toArray();
|
||||
|
||||
return array_values(array_unique(array_map('intval', $rows)));
|
||||
}
|
||||
|
||||
/** B) 按“场景”在发起人所在的组里找负责人 */
|
||||
private function getLeadersByScenarioForInitiator(
|
||||
int $initiatorId,
|
||||
string $scenarioCode,
|
||||
bool $mustBelong = true,
|
||||
bool $preferPrimary = true
|
||||
): array {
|
||||
// 找出“该场景下发起人所在的所有组”
|
||||
$groups = Db::table('user_group as g')
|
||||
->join('user_group_members as m', 'm.group_id', '=', 'g.id')
|
||||
->where('g.scenario_code', $scenarioCode)
|
||||
->where('g.status', 1)
|
||||
->where('m.user_id', $initiatorId)
|
||||
->where('m.status', 1)
|
||||
->orderByDesc('m.is_primary') // 主归属优先
|
||||
->orderByDesc('g.sort') // 其次按组排序
|
||||
->select('g.id', 'm.is_primary')
|
||||
->get()->toArray();
|
||||
|
||||
if (empty($groups)) {
|
||||
if ($mustBelong) {
|
||||
throw new BusinessException('发起人在该业务场景下未加入任何分组');
|
||||
}
|
||||
// 不强制属于:允许只按“该场景下任一组”的负责人(不推荐)
|
||||
$groups = Db::table('user_group as g')
|
||||
->where('g.scenario_code', $scenarioCode)
|
||||
->where('g.status', 1)
|
||||
->orderByDesc('g.sort')
|
||||
->select('g.id', Db::raw('0 as is_primary'))
|
||||
->get()->toArray();
|
||||
if (empty($groups)) {
|
||||
throw new BusinessException('该业务场景下未配置任何分组');
|
||||
}
|
||||
}
|
||||
|
||||
// 选择一个“目标组”
|
||||
$targetGroupId = null;
|
||||
if ($preferPrimary) {
|
||||
foreach ($groups as $g) {
|
||||
if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; }
|
||||
}
|
||||
}
|
||||
if (!$targetGroupId) {
|
||||
$targetGroupId = (int)$groups[0]->id; // 用排序第一的组
|
||||
}
|
||||
|
||||
// 取负责人(可支持多负责人)
|
||||
$leaders = Db::table('user_group_members')
|
||||
->where('group_id', $targetGroupId)
|
||||
->where('is_leader', 1)
|
||||
->where('status', 1)
|
||||
->pluck('user_id')->toArray();
|
||||
|
||||
$ids = array_values(array_unique(array_map('intval', $leaders)));
|
||||
if (empty($ids)) throw new BusinessException('目标分组未配置负责人');
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
namespace app\common\utils\approval\traits;
|
||||
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait ApprovalTransferTrait
|
||||
{
|
||||
/** 查看某个实例的转交流水(按时间升序) */
|
||||
public function listTransfers(int $instanceId): array
|
||||
{
|
||||
$rows = Db::table('approval_transfers')
|
||||
->where('instance_id', $instanceId)
|
||||
->orderBy('id', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return array_map(function($r){
|
||||
return [
|
||||
'id' => (int)$r->id,
|
||||
'instance_id' => (int)$r->instance_id,
|
||||
'step_index' => (int)$r->step_index,
|
||||
'record_id' => (int)$r->record_id,
|
||||
'from_user_id' => (int)$r->from_user_id,
|
||||
'to_user_id' => (int)$r->to_user_id,
|
||||
'reason' => $r->reason,
|
||||
'created_at' => $r->created_at,
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/** 转交当前待办给他人 */
|
||||
public function transferApproval(
|
||||
int $instanceId,
|
||||
int $stepIndex,
|
||||
int $fromUserId,
|
||||
int $toUserId,
|
||||
string $reason = '',
|
||||
?string $userNameColumn = null
|
||||
): array {
|
||||
if ($fromUserId === $toUserId) {
|
||||
return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交'];
|
||||
}
|
||||
|
||||
return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) {
|
||||
// 锁实例,避免并发推进
|
||||
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
|
||||
if (!$instance) throw new BusinessException('审批实例未找到');
|
||||
if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交');
|
||||
|
||||
// 锁定来源人的 pending 记录
|
||||
$record = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', (int)$instance->round)
|
||||
->where('step_index', $stepIndex)
|
||||
->where('approver_id', $fromUserId)
|
||||
->where('status', 'pending')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
if (!$record) throw new BusinessException('未找到可转交的待办或已处理');
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 若目标已有 pending:合并(本记录置为 skipped)
|
||||
$target = Db::table('approval_records')
|
||||
->where('instance_id', $instanceId)
|
||||
->where('round', (int)$instance->round)
|
||||
->where('step_index', $stepIndex)
|
||||
->where('approver_id', $toUserId)
|
||||
->where('status', 'pending')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($target) {
|
||||
Db::table('approval_records')->where('id', $record->id)->update([
|
||||
'status' => 'skipped',
|
||||
'approved_at' => $now,
|
||||
'updated_at' => $now,
|
||||
'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"),
|
||||
]);
|
||||
$effectiveRecordId = (int)$target->id;
|
||||
} else {
|
||||
// 原地替换 approver_id
|
||||
Db::table('approval_records')->where('id', $record->id)->update([
|
||||
'approver_id' => $toUserId,
|
||||
'updated_at' => $now,
|
||||
'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"),
|
||||
]);
|
||||
$effectiveRecordId = (int)$record->id;
|
||||
}
|
||||
|
||||
// 组织“转交来源/原因”文案(优先姓名)
|
||||
$fromLabel = (string)$fromUserId;
|
||||
$toLabel = (string)$toUserId;
|
||||
if ($userNameColumn) {
|
||||
$fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn);
|
||||
$toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn);
|
||||
if ($fromName) $fromLabel = $fromName;
|
||||
if ($toName) $toLabel = $toName;
|
||||
}
|
||||
$note = "【转交】{$fromLabel} → {$toLabel}";
|
||||
$r = trim($reason);
|
||||
if ($r !== '') $note .= ",原因:{$r}";
|
||||
|
||||
// 通知被转交人(在内容里追加 note)
|
||||
$this->sendNotification([$toUserId], (object)[
|
||||
'id' => (int)$instance->id,
|
||||
'flow_type' => (string)$instance->flow_type,
|
||||
'biz_type' => (string)$instance->biz_type,
|
||||
'biz_id' => (string)$instance->biz_id,
|
||||
'initiator_id' => (int)$instance->initiator_id,
|
||||
'form_data' => $instance->form_data,
|
||||
], 'pending', false, ['note' => $note]);
|
||||
|
||||
// 记转交流水
|
||||
Db::table('approval_transfers')->insert([
|
||||
'instance_id' => (int)$instanceId,
|
||||
'step_index' => (int)$stepIndex,
|
||||
'record_id' => $effectiveRecordId,
|
||||
'from_user_id' => (int)$fromUserId,
|
||||
'to_user_id' => (int)$toUserId,
|
||||
'reason' => $reason,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
|
||||
$this->pushTodoChanged([$fromUserId, $toUserId]);
|
||||
return ['status' => 'ok', 'record_id' => $effectiveRecordId];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvItemValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionApproveValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionAttachmentValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemReturnAttachmentValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemReturnValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionReturnValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
'applicant_id' => 'number',
|
||||
'borrow_reason' => 'require|max:255',
|
||||
'use_start_date' => 'require|date',
|
||||
'use_end_date' => 'date',
|
||||
'items' => 'require|array',
|
||||
'items.*.item_id' => 'require|number',
|
||||
'items.*.request_qty' => 'require|number|gt:0',
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
'applicant_id.number' => '申请人格式错误',
|
||||
'borrow_reason.require' => '领用事由不能为空',
|
||||
'borrow_reason.max' => '领用事由最多不能超过255个字符',
|
||||
'use_start_date.require' => '使用开始日期不能为空',
|
||||
'use_start_date.date' => '使用开始日期格式错误',
|
||||
'use_end_date.date' => '预计归还日期格式错误',
|
||||
'items.require' => '申请物品明细不能为空',
|
||||
'items.array' => '申请物品明细格式错误',
|
||||
'items.*.item_id.require' => '物品ID不能为空',
|
||||
'items.*.item_id.number' => '物品ID格式错误',
|
||||
'items.*.request_qty.require' => '申请数量不能为空',
|
||||
'items.*.request_qty.number' => '申请数量格式错误',
|
||||
'items.*.request_qty.gt' => '申请数量必须大于0',
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
],
|
||||
|
||||
'apply' => [
|
||||
'applicant_id',
|
||||
'borrow_reason',
|
||||
'use_start_date',
|
||||
'use_end_date',
|
||||
'items'
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -73,26 +73,26 @@ class IndexController
|
||||
{
|
||||
$options = [
|
||||
'controller' => [
|
||||
'module' => 'common',
|
||||
'name' => 'usedcar_crawl_results_images'
|
||||
'module' => 'mobile',
|
||||
'name' => 'inv_requisition_return'
|
||||
],
|
||||
'model' => [
|
||||
'module' => 'common',
|
||||
'name' => 'usedcar_crawl_results_images'
|
||||
'name' => 'inv_requisition_return'
|
||||
],
|
||||
'service' => [
|
||||
'module' => 'common',
|
||||
'name' => 'usedcar_crawl_results_images'
|
||||
'name' => 'inv_requisition_return'
|
||||
],
|
||||
'logic' => [
|
||||
'module' => 'common',
|
||||
'name' => 'usedcar_crawl_results_images'
|
||||
'name' => 'inv_requisition_return'
|
||||
],
|
||||
'validate' => [
|
||||
'module' => 'common',
|
||||
'name' => 'usedcar_crawl_results_images'
|
||||
'name' => 'inv_requisition_return'
|
||||
],
|
||||
'table' => 'usedcar_crawl_results_images'
|
||||
'table' => 'inv_requisition_return'
|
||||
];
|
||||
|
||||
$result = (new Generate($options))->doGenerate();
|
||||
|
||||
@@ -25,6 +25,8 @@ class StockOrdersService extends \app\common\service\StockOrdersService
|
||||
// 业务id
|
||||
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
|
||||
$config['where']['salesperson_id'] = $p['salesperson_id'];
|
||||
} else {
|
||||
$config['where']['salesperson_id'] = $request->user->uid;
|
||||
}
|
||||
// 初、终版
|
||||
$config['where']['is_final'] = $p['is_final'] ?? 0;
|
||||
@@ -40,14 +42,16 @@ class StockOrdersService extends \app\common\service\StockOrdersService
|
||||
|
||||
$user_role_id = UserRoles::where('user_id', $request->user->uid)->pluck('role_id')->toArray();
|
||||
//$roles_id = array_intersect([1,6], $request->user->role_ids);
|
||||
$roles_id = array_intersect([1,6], $user_role_id);
|
||||
if (count($roles_id) == 0){
|
||||
$roles_id = array_intersect([1,5,6], $user_role_id);
|
||||
if (count($roles_id) != 0){
|
||||
// if(in_array(4, $request->user->role_ids)){ //业务主管
|
||||
// $report = User::where('report_to',$request->user->uid)->pluck('id')->toArray();
|
||||
// $report = array_merge($report, [$request->user->uid]);
|
||||
// $config['where']['salesperson_id'] = ['in',$report];
|
||||
// } else
|
||||
if (in_array(5, $request->user->role_ids)){ //内勤
|
||||
|
||||
//内勤分组的人可以看到自己分组的备货单
|
||||
if (in_array(5,$roles_id)){ //内勤
|
||||
$userGroupMember = UserGroupMembers::where('user_id',$request->user->uid)
|
||||
->where('is_leader',1)
|
||||
->first();
|
||||
@@ -55,12 +59,13 @@ class StockOrdersService extends \app\common\service\StockOrdersService
|
||||
$report = UserGroupMembers::where('group_id',$userGroupMember->group_id)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
$report = array_merge($report, [$request->user->uid]);
|
||||
$config['where']['salesperson_id'] = ['in',$report];
|
||||
}else{ //内勤没有负责的组时
|
||||
$config['where']['salesperson_id'] = $request->user->uid;
|
||||
}
|
||||
} else {
|
||||
} elseif (in_array(1,$roles_id)) { //超管可以看所有的
|
||||
unset($config['where']['salesperson_id']);
|
||||
}else{
|
||||
$config['where']['salesperson_id'] = $request->user->uid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace app\mobile\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\User;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvItemService;
|
||||
use support\Request;
|
||||
|
||||
class InvItemController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvItemService();
|
||||
}
|
||||
|
||||
public function preset(Request $request){
|
||||
try {
|
||||
$res = $this->service->getList();
|
||||
$result = [];
|
||||
foreach ($res as $value) {
|
||||
$value['company_name'] = Corp::where('id', $value['company_id'])->value('name');
|
||||
$value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name');
|
||||
$result[] = $value;
|
||||
}
|
||||
return $this->success($result, '获取物品列表');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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']);
|
||||
if (isset($data['company_id']))
|
||||
$data['company_name'] = Corp::where('id', $data['company_id'])->value('name');
|
||||
if (isset($data['manage_user_id']))
|
||||
$data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name');
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
namespace app\mobile\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\InvItem;
|
||||
use app\common\model\InvRequisitionAttachment;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use app\common\service\OssService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionService;
|
||||
use support\Request;
|
||||
|
||||
class InvRequisitionController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交物品申请
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function apply(Request $request)
|
||||
{
|
||||
try {
|
||||
// 验证申请数据
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'apply');
|
||||
|
||||
// 处理申请逻辑
|
||||
$result = $this->service->serviceStore($request, $data, 'apply');
|
||||
|
||||
return $this->success($result, '申请提交成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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']);
|
||||
if (isset($data['id'])){
|
||||
$data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get();
|
||||
foreach ($data['item'] as $key => $item){
|
||||
$corpId = InvItem::where('id', $item->item_id)->value('company_id');
|
||||
$item['company_name'] = Corp::where('id', $corpId)->value('name');
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function showFormat($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']);
|
||||
if (isset($data['id'])){
|
||||
$data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get();
|
||||
$data['attachment'] = InvRequisitionAttachment::where('requisition_id', $data['id'])->get();
|
||||
$oss = new OssService();
|
||||
foreach ($data['attachment'] as $key => $value) {
|
||||
$data['attachment'][$key]['file_path_pull'] = $oss->generatePrivateUrl($value['file_path']);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace app\mobile\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionItemReturnService;
|
||||
use support\Request;
|
||||
|
||||
class InvRequisitionItemReturnController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionItemReturnService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* 物品兑换确认
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function returnConfirm(Request $request)
|
||||
{
|
||||
try {
|
||||
// 验证申请数据
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm');
|
||||
|
||||
// 处理申请逻辑
|
||||
$result = $this->service->serviceReturnConfirm($request, $data, 'returnConfirm');
|
||||
|
||||
return $this->success($result, '申请提交成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\mobile\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionReturnService;
|
||||
|
||||
class InvRequisitionReturnController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionReturnService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -12,16 +12,6 @@ class CheckWeb
|
||||
|
||||
public function onWorkerStart()
|
||||
{
|
||||
// if (getenv('ENVIRONMENT') === 'master') {
|
||||
// new Crontab('0 */10 * * * *', function(){
|
||||
// $result = $this->tocurl('https://magicbridgeauto.com');
|
||||
// if (!preg_match('/magicbridgeauto/', $result['body'])) {
|
||||
// flg("【magicbridgeauto】疑似被篡改");
|
||||
// }
|
||||
// return true;
|
||||
// });
|
||||
// }
|
||||
|
||||
// 每秒执行:队列二手车数据处理
|
||||
new Crontab('* * * * * *', function(){
|
||||
$service = new \app\common\utils\crawl\service\UsedCarCrawl();
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\model\UserGroup;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\inquiry\service\FbFileImportService;
|
||||
use app\inquiry\service\FbLeadSyncService;
|
||||
use app\inquiry\service\InquiryAssignService;
|
||||
@@ -55,11 +57,18 @@ class InquiryAutoProcess
|
||||
protected function runForChannel(string $channelKey, array $cfg)
|
||||
{
|
||||
$watchDir = public_path($cfg['watch_dir'] ?? 'excel');
|
||||
$enterUserId = (int)($cfg['enter_user_id'] ?? 0);
|
||||
// $enterUserId = (int)($cfg['enter_user_id'] ?? 0);
|
||||
$channelId = (int)($cfg['channel_id'] ?? 0);
|
||||
$salesUserIds = $cfg['sales_user_ids'] ?? [];
|
||||
// $salesUserIds = $cfg['sales_user_ids'] ?? [];
|
||||
$roundRobinKey = $cfg['round_robin_key'] ?? $channelKey;
|
||||
|
||||
// 查分组
|
||||
$group = UserGroup::where('scenario_code', $cfg['group_key'])->where('status', 1)->first();
|
||||
// 查组长
|
||||
$enterUserId = UserGroupMembers::where('group_id', $group->id)->where('is_leader', 1)->where('status', 1)->value('user_id');
|
||||
// 查分组成员
|
||||
$salesUserIds = UserGroupMembers::where('group_id', $group->id)->where('is_leader', 0)->where('status', 1)->pluck('user_id')->toArray();
|
||||
|
||||
if ($enterUserId <= 0 || $channelId <= 0) {
|
||||
Log::error("InquiryAutoProcess: 渠道 {$channelKey} 配置缺少正确的 enter_user_id 或 channel_id,已跳过");
|
||||
return;
|
||||
|
||||
@@ -12,19 +12,7 @@ return [
|
||||
|
||||
// 参与轮询分配的业务员 ID 列表
|
||||
'sales_user_ids' => [
|
||||
63,
|
||||
67,
|
||||
93,
|
||||
108,
|
||||
132,
|
||||
143,
|
||||
145,
|
||||
157,
|
||||
160,
|
||||
168,
|
||||
172,
|
||||
191,
|
||||
194
|
||||
63, 67, 93, 108, 132, 143, 145, 157, 160, 168, 172, 191, 194
|
||||
],
|
||||
|
||||
// 轮询用的分组 key(Redis 记指针用)
|
||||
@@ -39,6 +27,8 @@ return [
|
||||
'round_robin_key' => 'fb_paika_shemei_gh',
|
||||
]
|
||||
],
|
||||
|
||||
'group_key' => 'paika_inquiry'
|
||||
],
|
||||
|
||||
// 合塑社媒
|
||||
@@ -59,5 +49,7 @@ return [
|
||||
'round_robin_key' => 'fb_hesu_shemei_gh',
|
||||
]
|
||||
],
|
||||
],
|
||||
|
||||
'group_key' => 'hesu_inquiry'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -516,6 +516,15 @@ Route::group('/admin', function () {
|
||||
Route::any('/destroy', [app\admin\controller\UserDashboardRuleController::class, 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
// 物品领用
|
||||
Route::group('/inv', function () {
|
||||
Route::any('/item/list', [app\admin\controller\InvItemController::class, 'index']);
|
||||
Route::any('/item/store', [app\admin\controller\InvItemController::class, 'store']);
|
||||
Route::any('/item/update', [app\admin\controller\InvItemController::class, 'update']);
|
||||
Route::any('/item/show', [app\admin\controller\InvItemController::class, 'show']);
|
||||
Route::any('/item/destroy', [app\admin\controller\InvItemController::class, 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -635,4 +644,26 @@ Route::group('/crm', function () {
|
||||
Route::any('/update', [app\common\controller\VehModificationController::class, 'update']);
|
||||
Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 移动端
|
||||
Route::group('/mobile', function () {
|
||||
// 物品领用
|
||||
Route::group('/inv', function () {
|
||||
//物品下拉
|
||||
Route::any('/item/preset', [app\mobile\controller\InvItemController::class, 'preset']);
|
||||
|
||||
// 物品领用申请
|
||||
Route::any('/requisition/list', [app\mobile\controller\InvRequisitionController::class, 'index']);
|
||||
Route::any('/requisition/show', [app\mobile\controller\InvRequisitionController::class, 'show']);
|
||||
Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']);
|
||||
Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']);
|
||||
|
||||
// 物品归还
|
||||
Route::any('/requisition/item/return', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']);
|
||||
Route::any('/requisition/item/return/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']);
|
||||
Route::any('/requisition/item/return/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
CREATE TABLE `approval_flows` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL COMMENT '流程名称',
|
||||
`type` varchar(100) NOT NULL COMMENT '业务类型标识,如 leave, reimbursement',
|
||||
`steps` json NOT NULL COMMENT '审批步骤配置',
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
|
||||
`timeout_hours` int(11) NOT NULL DEFAULT '0' COMMENT '实例级超时时间(小时),0为不超时',
|
||||
`timeout_action` varchar(20) NOT NULL DEFAULT 'auto_reject' COMMENT 'auto_approve | auto_reject',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flow_type` (`type`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='审批流程定义';
|
||||
|
||||
CREATE TABLE `approval_instances` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`flow_type` varchar(100) NOT NULL COMMENT '关联 approval_flows.type',
|
||||
`biz_id` varchar(255) NOT NULL COMMENT '业务ID',
|
||||
`biz_type` varchar(100) NOT NULL COMMENT '业务标识/表名',
|
||||
`form_data` json NOT NULL COMMENT '提交表单快照',
|
||||
`initiator_id` bigint(20) unsigned NOT NULL COMMENT '发起人',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'processing' COMMENT 'processing|approved|rejected|revoked|timed_out',
|
||||
`current_step` int(11) NOT NULL DEFAULT '0' COMMENT '当前步骤索引',
|
||||
`round` int(11) NOT NULL DEFAULT '1' COMMENT '当前轮次:1=首次,重提+1',
|
||||
`finished_at` timestamp NULL DEFAULT NULL,
|
||||
`deadline_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flow_type` (`flow_type`),
|
||||
KEY `idx_biz` (`biz_type`,`biz_id`),
|
||||
KEY `idx_initiator` (`initiator_id`),
|
||||
KEY `idx_status_updated` (`status`,`updated_at`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb4 COMMENT='审批实例';
|
||||
|
||||
CREATE TABLE `approval_records` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL COMMENT 'approval_instances.id',
|
||||
`approver_id` bigint(20) unsigned NOT NULL COMMENT '审批人',
|
||||
`step_index` int(11) NOT NULL COMMENT '步骤索引',
|
||||
`round` int(11) NOT NULL DEFAULT '1' COMMENT '所属轮次',
|
||||
`action` varchar(20) DEFAULT NULL COMMENT 'approve|reject,待办时为NULL',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|approved|rejected|skipped|timeout',
|
||||
`comment` text COMMENT '审批意见',
|
||||
`approved_at` timestamp NULL DEFAULT NULL COMMENT '审批时间/关闭时间',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_instance_step_approver` (`instance_id`,`step_index`,`approver_id`),
|
||||
KEY `idx_instance_step_status` (`instance_id`,`step_index`,`status`),
|
||||
KEY `idx_approver` (`approver_id`),
|
||||
KEY `idx_instance_step_round` (`instance_id`,`step_index`,`round`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COMMENT='审批记录';
|
||||
|
||||
CREATE TABLE `approval_transfers` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`step_index` int(11) NOT NULL,
|
||||
`record_id` bigint(20) unsigned NOT NULL COMMENT '转交后仍然有效的记录ID(若合并则为目标记录ID)',
|
||||
`from_user_id` bigint(20) unsigned NOT NULL,
|
||||
`to_user_id` bigint(20) unsigned NOT NULL,
|
||||
`reason` text,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_record` (`record_id`),
|
||||
KEY `idx_instance_step` (`instance_id`,`step_index`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批转交日志';
|
||||
|
||||
CREATE TABLE `approval_ccs` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`notified_at` timestamp NULL DEFAULT NULL,
|
||||
`read_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_instance_user` (`instance_id`,`user_id`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_instance` (`instance_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='审批抄送';
|
||||
|
||||
CREATE TABLE `approval_discussions` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`is_internal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=公开,1=仅审批人可见',
|
||||
`content` text NOT NULL,
|
||||
`attachments` json DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_instance_id` (`instance_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='审批讨论';
|
||||
@@ -0,0 +1,174 @@
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 1. 物品主数据表:增加 company_id
|
||||
DROP TABLE IF EXISTS inv_item;
|
||||
CREATE TABLE inv_item (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品所属公司ID',
|
||||
item_code VARCHAR(64) NOT NULL COMMENT '物品编号(公司内部编码)',
|
||||
item_name VARCHAR(128) NOT NULL COMMENT '物品名称',
|
||||
category_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品分类ID(可选,指向分类表)',
|
||||
spec_model VARCHAR(128) DEFAULT NULL COMMENT '规格型号',
|
||||
unit VARCHAR(32) DEFAULT '件' COMMENT '计量单位,如 件/盒/包',
|
||||
is_consumable TINYINT(1) DEFAULT 1 COMMENT '是否耗材:1耗材(不需归还),0非耗材(需归还)',
|
||||
stock_qty INT(11) DEFAULT 0 COMMENT '当前库存数量',
|
||||
manage_user_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID',
|
||||
location VARCHAR(255) DEFAULT NULL COMMENT '存放位置说明',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_item_code (item_code),
|
||||
KEY idx_company (company_id),
|
||||
KEY idx_category (category_id),
|
||||
KEY idx_manage_user (manage_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品主数据表';
|
||||
|
||||
|
||||
-- 2. 物品领用/借用申请单(主表)
|
||||
DROP TABLE IF EXISTS inv_requisition;
|
||||
CREATE TABLE inv_requisition (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
order_no VARCHAR(64) NOT NULL COMMENT '申请单号',
|
||||
|
||||
applicant_company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人所在公司ID(可选)',
|
||||
dept_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人部门ID',
|
||||
applicant_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请人用户ID',
|
||||
applicant_name VARCHAR(64) DEFAULT NULL COMMENT '申请人姓名快照',
|
||||
leader_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '直属上级用户ID',
|
||||
manager_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID(主负责人)',
|
||||
|
||||
borrow_reason VARCHAR(255) NOT NULL COMMENT '领用/借用事由',
|
||||
use_start_date DATE NOT NULL COMMENT '借用/预计使用开始日期',
|
||||
use_end_date DATE DEFAULT NULL COMMENT '预计归还日期(耗材可为空)',
|
||||
|
||||
-- 0草稿 10待直属上级审批 20直属上级通过待物品管理员审批
|
||||
-- 30全部通过 40已驳回 50已取消
|
||||
status TINYINT(3) NOT NULL DEFAULT 10 COMMENT '审批状态',
|
||||
|
||||
is_returned TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已全部归还(针对需要归还的物品):0否 1是',
|
||||
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_order_no (order_no),
|
||||
KEY idx_applicant (applicant_id),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用/借用申请单';
|
||||
|
||||
|
||||
-- 3. 申请单明细表:支持部分、多次归还
|
||||
DROP TABLE IF EXISTS inv_requisition_item;
|
||||
CREATE TABLE inv_requisition_item (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
|
||||
|
||||
item_name_snap VARCHAR(128) DEFAULT NULL COMMENT '物品名称快照',
|
||||
spec_model_snap VARCHAR(128) DEFAULT NULL COMMENT '规格型号快照',
|
||||
unit_snap VARCHAR(32) DEFAULT NULL COMMENT '单位快照',
|
||||
is_consumable_snap TINYINT(1) DEFAULT NULL COMMENT '是否耗材快照:1耗材 0非耗材',
|
||||
|
||||
request_qty INT(11) NOT NULL DEFAULT 1 COMMENT '申请数量',
|
||||
approved_qty INT(11) DEFAULT NULL COMMENT '实批数量(可与申请数量不同)',
|
||||
returned_qty INT(11) NOT NULL DEFAULT 0 COMMENT '已归还数量(累加)',
|
||||
|
||||
-- 对于非耗材:returned_qty >= approved_qty 时置为1
|
||||
is_finished TINYINT(1) NOT NULL DEFAULT 0 COMMENT '该物品是否已完成归还:0否 1是',
|
||||
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '行备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_item (item_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请明细表';
|
||||
|
||||
|
||||
-- 4. 审批记录表(同上一版,略微无改动)
|
||||
DROP TABLE IF EXISTS inv_requisition_approve;
|
||||
CREATE TABLE inv_requisition_approve (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
node_type VARCHAR(32) NOT NULL COMMENT '审批节点类型:leader=直属上级,manager=物品管理员,other=其他',
|
||||
approver_id BIGINT(20) UNSIGNED NOT NULL COMMENT '审批人用户ID',
|
||||
approver_name VARCHAR(64) DEFAULT NULL COMMENT '审批人姓名快照',
|
||||
action TINYINT(3) NOT NULL COMMENT '审批动作:1通过 2驳回',
|
||||
comment VARCHAR(255) DEFAULT NULL COMMENT '审批意见',
|
||||
action_at INT(11) NOT NULL DEFAULT 0 COMMENT '审批时间时间戳',
|
||||
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_node (node_type),
|
||||
KEY idx_approver (approver_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请审批记录表';
|
||||
|
||||
|
||||
-- 5. 申请单附件表(申请时上传)
|
||||
DROP TABLE IF EXISTS inv_requisition_attachment;
|
||||
CREATE TABLE inv_requisition_attachment (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
|
||||
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL',
|
||||
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请附件表';
|
||||
|
||||
|
||||
/* ================= 归 还 相 关 表 ================= */
|
||||
|
||||
-- 6. 归还记录表:一次归还动作(可归还一个明细的一部分)
|
||||
DROP TABLE IF EXISTS inv_requisition_item_return;
|
||||
CREATE TABLE inv_requisition_item_return (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
requisition_item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单明细ID',
|
||||
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
|
||||
|
||||
return_qty INT(11) NOT NULL DEFAULT 0 COMMENT '本次归还数量',
|
||||
handler_id BIGINT(20) UNSIGNED NOT NULL COMMENT '办理归还的人(通常是物品管理员)',
|
||||
handler_name VARCHAR(64) DEFAULT NULL COMMENT '办理人姓名快照',
|
||||
|
||||
return_status TINYINT(3) NOT NULL DEFAULT 1 COMMENT '归还状态:1正常归还 2损坏 3其他',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '归还备注',
|
||||
|
||||
return_at INT(11) NOT NULL DEFAULT 0 COMMENT '归还时间时间戳',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_req_item (requisition_item_id),
|
||||
KEY idx_item (item_id),
|
||||
KEY idx_handler (handler_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录表(支持多次、部分归还)';
|
||||
|
||||
|
||||
-- 7. 归还附件表:归还时拍照留底
|
||||
DROP TABLE IF EXISTS inv_requisition_item_return_attachment;
|
||||
CREATE TABLE inv_requisition_item_return_attachment (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
return_id BIGINT(20) UNSIGNED NOT NULL COMMENT '归还记录ID',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
|
||||
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL(通常为照片)',
|
||||
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_return (return_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录附件表(归还拍照留底)';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
Reference in New Issue
Block a user