Compare commits
456 Commits
develop_bm
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ea23307d | |||
| 929bae6f13 | |||
| a72b30a699 | |||
| 4ec72f7772 | |||
| 75a59a7197 | |||
| 51a553e1a4 | |||
| 46666d1c2f | |||
| 5a94a74590 | |||
| 88338b60fa | |||
| fda46cd187 | |||
| f16feca4f4 | |||
| 6e458e3751 | |||
| f2451a48bb | |||
| c88d2f20bc | |||
| a7bc73f0d3 | |||
| 4787e21491 | |||
| d5a1b11b35 | |||
| 530a8123ba | |||
| acdabea76e | |||
| f33f969166 | |||
| 499611c700 | |||
| 114b1fbf92 | |||
| a1c2b36d92 | |||
| f7fa768da1 | |||
| 51344ac1fc | |||
| 7ac5bcd516 | |||
| 67069576b7 | |||
| 1951e2333c | |||
| 19eab96bdc | |||
| ae9dd498af | |||
| 766b717a5a | |||
| f2524dec22 | |||
| 89a824f1b8 | |||
| 5a3a1cf0de | |||
| 9210dcb312 | |||
| 0e88fdf1d1 | |||
| 6c9adce32a | |||
| 73ce99edfb | |||
| 3168885701 | |||
| 30e04b3d4c | |||
| ddbec81d04 | |||
| 63605accc0 | |||
| 9f78530185 | |||
| 1c26828569 | |||
| ef2bd1df55 | |||
| 1d88ef932a | |||
| 5120f4bf4f | |||
| 1251f78c6b | |||
| f44c8611d1 | |||
| e829e406e1 | |||
| f28da4c4b9 | |||
| 407e573c0b | |||
| c2e2a20e35 | |||
| 8d83309daf | |||
| a8f1af9995 | |||
| fb8f87dd5f | |||
| d59c390674 | |||
| 144fa72075 | |||
| b4445abc3a | |||
| cf7d8a9f5b | |||
| 976945378e | |||
| d02555df0a | |||
| a9aabfa454 | |||
| 0067a7508f | |||
| cd692594e4 | |||
| 6028ad3b6a | |||
| 7098f30af2 | |||
| 119e725b17 | |||
| 28287e7a7c | |||
| 3e16776d65 | |||
| 244b874751 | |||
| e4fb1fda2c | |||
| 9b55bcce79 | |||
| 466519378e | |||
| b721860d4a | |||
| 6a0fbd01fd | |||
| d3d55df4c9 | |||
| 39e673ea1a | |||
| 41d968b369 | |||
| 01f49f35b2 | |||
| 20857453d2 | |||
| f57b0ca1f0 | |||
| 452182f0b2 | |||
| 90a431015b | |||
| 6469e05176 | |||
| 698f532ced | |||
| 3da11b4b37 | |||
| af90824edd | |||
| f6c8271729 | |||
| ad5c053a5e | |||
| 40bc3a894d | |||
| c72b128872 | |||
| fde98ea5b9 | |||
| 5f605f5a63 | |||
| 4894b25722 | |||
| 92776b39d4 | |||
| 537b22c6fb | |||
| 1dfc183cac | |||
| 25b79d6f63 | |||
| 359a8f403c | |||
| 3d33a502d9 | |||
| eeab4934cd | |||
| 74ca1a5d8c | |||
| 488d510ad0 | |||
| d719ee5ea2 | |||
| 04a3c85bc1 | |||
| d3ed9c05c6 | |||
| 8105200154 | |||
| 070cb87d17 | |||
| 032b19a4ce | |||
| 320211cd21 | |||
| a2218dc517 | |||
| 7b1837ed20 | |||
| 828c8f1d60 | |||
| b831775490 | |||
| d58c54da76 | |||
| cdb889137f | |||
| e3b38ee117 | |||
| 4464c86788 | |||
| 750c2fc642 | |||
| a521cfcaf4 | |||
| 385f380914 | |||
| e84aba0a49 | |||
| f67f9ddef9 | |||
| a39c18b714 | |||
| 44dc6c06ee | |||
| 757689573b | |||
| be44d1dda2 | |||
| 28e5afe62a | |||
| e922e78ae2 | |||
| 7bf2249ce6 | |||
| f0a92cafda | |||
| 9686e52484 | |||
| c896bef9a6 | |||
| a8cab8c51c | |||
| e4f0d107ec | |||
| 30f3e211af | |||
| eee09316df | |||
| a16429ff3a | |||
| 59750d52f6 | |||
| 2da3804953 | |||
| eae699cd27 | |||
| bce47e81f8 | |||
| 4ea4204ff4 | |||
| 6496a40679 | |||
| ab3bffe7cb | |||
| a8d1309349 | |||
| 80d0196c3d | |||
| 67a39917f0 | |||
| 278dda4f7a | |||
| b6851cdef0 | |||
| 733996f346 | |||
| 607de6c70a | |||
| e7c5e17592 | |||
| 2ec89a880a | |||
| dffaac5b07 | |||
| 907be759a0 | |||
| f7b94da39f | |||
| c6fada47fc | |||
| b09e695ca7 | |||
| de39108efa | |||
| 7dceb4b828 | |||
| d056ba5f6f | |||
| 99551bba7f | |||
| e408f7e20e | |||
| 7cafb61797 | |||
| 5a7a6a45d4 | |||
| 5841b342de | |||
| 63011a0351 | |||
| 2b529a27c8 | |||
| cab15c4fab | |||
| 139acd9e70 | |||
| c38a53f053 | |||
| 07a40bf688 | |||
| 5e06b1737c | |||
| b43335c0d9 | |||
| c2e402722d | |||
| de9484f570 | |||
| ca757e67dd | |||
| ac6bfcf16c | |||
| 9f74934f37 | |||
| 9776df8289 | |||
| 6178084612 | |||
| d58c33e758 | |||
| 24e45f93db | |||
| 530d72071f | |||
| 8382a47a8c | |||
| f83e578bed | |||
| 2c00a35b6a | |||
| cd597b9605 | |||
| 259a7d2ae6 | |||
| 10d3701ab0 | |||
| 279139e18f | |||
| 4d957e8f43 | |||
| 57251718b4 | |||
| 0bcd9bf029 | |||
| 927f1d9c28 | |||
| 05096bc5ed | |||
| ece685fbe5 | |||
| 8072388da2 | |||
| f2b7d40b17 | |||
| e514129ff6 | |||
| a951fc39b3 | |||
| 7c12ab54e1 | |||
| d81fdae406 | |||
| 1e70bbe0ad | |||
| 861b8b216f | |||
| 5b2590bff8 | |||
| 7d1ea71e94 | |||
| d171ad58c7 | |||
| 31a610664f | |||
| 8f60c79b7c | |||
| 5aadcac7e3 | |||
| ca3455b219 | |||
| 56f8657886 | |||
| c29c4941fc | |||
| e960dd9b36 | |||
| cd622addec | |||
| da3794db50 | |||
| 8fab10b972 | |||
| eea13a2a58 | |||
| 1d0b51d598 | |||
| f0af3a0cdb | |||
| 45a77ba12e | |||
| 85a79193a6 | |||
| 38deea3b0d | |||
| b37cdc6834 | |||
| b60df7f5cf | |||
| 628429af66 | |||
| 2a6d517a74 | |||
| f58b0ee986 | |||
| 1207352e5a | |||
| 14967f28c2 | |||
| 725710abcb | |||
| 4ff2c2493b | |||
| b391608d87 | |||
| 0ace4a7b0b | |||
| 7258d9890e | |||
| 8c3fbd4d9a | |||
| a33b109ddb | |||
| 4bcc6c53f7 | |||
| 32aa7170a5 | |||
| 59994d1f5a | |||
| a0a71161d3 | |||
| 399eaa2b5c | |||
| 06a264ff21 | |||
| 82673cdcfa | |||
| b4b4f6cd5f | |||
| 143dde08ce | |||
| 2ffd053723 | |||
| d238b47aa8 | |||
| 5c3f8679ce | |||
| 3520f10572 | |||
| 1858d3e586 | |||
| 253616b2bb | |||
| c0a5904c12 | |||
| e2f79598b2 | |||
| 75a333b756 | |||
| beabfd96fb | |||
| 0d979934be | |||
| 0153f40127 | |||
| cc93979e9a | |||
| fb5b523264 | |||
| 54c23a26b7 | |||
| f336d1c4af | |||
| 35836edbfa | |||
| 32a7752f8b | |||
| f182684378 | |||
| bbb3f2f70e | |||
| d53f18181a | |||
| 75e31a7e9f | |||
| 707fd275c1 | |||
| 80d458a8b0 | |||
| c4a2b39edd | |||
| fe78500ef0 | |||
| 45169572b5 | |||
| db7c8a650b | |||
| bf74d9b917 | |||
| 631d433b7e | |||
| 05e39d2d65 | |||
| 9fbe094a30 | |||
| af8577064d | |||
| 1ba1d5763f | |||
| 22ba33125a | |||
| 5086344742 | |||
| 6997947717 | |||
| 63cca12a8d | |||
| 1e37560301 | |||
| eb4813b77b | |||
| f664653eb9 | |||
| 8a94de610d | |||
| 00bd5ada53 | |||
| 6bdcc73ee8 | |||
| 2c295cd631 | |||
| 134505be8c | |||
| 79d19a53e6 | |||
| fa22cfe132 | |||
| 6096f7ee01 | |||
| cb1b208190 | |||
| 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 |
+4
-1
@@ -10,4 +10,7 @@
|
||||
/webman
|
||||
/start.php
|
||||
/windows.php
|
||||
/bootstrap.php
|
||||
/bootstrap.php
|
||||
/composer.lock
|
||||
.env_copy
|
||||
/public/excel
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InquiryTransmitConfigService;
|
||||
|
||||
class InquiryTransmitConfigController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InquiryTransmitConfigService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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,280 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\InquiryTransmit;
|
||||
use app\common\model\InquiryTransmitConfig;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InquiryTransmitService;
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
|
||||
class InquiryTransmitController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $noNeedLogin = ['form'];
|
||||
|
||||
protected $noNeedHeaderData = ['form'];
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InquiryTransmitService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
public function form(Request $request)
|
||||
{
|
||||
try {
|
||||
$params = $request->all();
|
||||
$url = $request->header('referer');
|
||||
$domain = parse_url($url, PHP_URL_HOST);
|
||||
$transmitConfig = InquiryTransmitConfig::where('domain', $domain)->first();
|
||||
if ($transmitConfig) {
|
||||
$mailto = $transmitConfig->mailto;
|
||||
$fromDomain = $domain;
|
||||
} else {
|
||||
$mailto = '1696136552@qq.com';
|
||||
$fromDomain = 'referer Failed to obtain';
|
||||
}
|
||||
|
||||
if ($request->method() == 'GET') {
|
||||
$config = [
|
||||
'iLang' => 'en',
|
||||
'Act' => 'Inquiry',
|
||||
'post_url' => getenv('MAIN_DOMAIN').'/inquiry/transmit/form'.'?m=Data&a=save',
|
||||
'mailto' => $mailto,
|
||||
'fromDomain' => $fromDomain,
|
||||
'fromPath' => parse_url($params['fromlink'], PHP_URL_PATH),
|
||||
'm' => $params['m'],
|
||||
'a' => $params['a'],
|
||||
'tid' => $params['tid'],
|
||||
'cid' => $params['cid'],
|
||||
'version' => $params['version'],
|
||||
'fromlink' => $params['fromlink']
|
||||
];
|
||||
|
||||
$html = match ($params['tid']) {
|
||||
'en-5-LP-pnx' => $this->formHmtl($config),
|
||||
'en-3-P-vdx' => $this->formHmtl2($config)
|
||||
};
|
||||
|
||||
return $this->success(['html' => $html], 'success');
|
||||
// return json([
|
||||
// 'code' => 200,
|
||||
// 'data' => [
|
||||
// 'html' => $html
|
||||
// ],
|
||||
// 'message' => 'success'
|
||||
// ]);
|
||||
}
|
||||
|
||||
if ($request->method() == 'POST') {
|
||||
$ip = $request->getRealIp($safe_mode=true);
|
||||
$edata = [
|
||||
'mailto' => $params['mailto'],
|
||||
|
||||
'Date' => date('Y-m-d H:i:s', time()),
|
||||
'Email' => $params['userEMail'],
|
||||
'Name' => $params['userName'],
|
||||
'Tel' => $params['userTel'],
|
||||
'Company' => '',
|
||||
'Country' => $params['uCountry'],
|
||||
'Form Url' => $params['fromlink'],
|
||||
'Message' => $params['Message'],
|
||||
'Ip' => $ip,
|
||||
'IpLocation' => ip2region($ip).'(仅供参考)',
|
||||
|
||||
'iLang' => $params['iLang'],
|
||||
'mailto' => $params['mailto'],
|
||||
'navigator_language' => $params['navigator_language'],
|
||||
'navigator_platform' => $params['navigator_platform'],
|
||||
'screen_width' => $params['screen_width'],
|
||||
'screen_height' => $params['screen_height'],
|
||||
'uCountry' => $params['uCountry']
|
||||
];
|
||||
|
||||
$this->service->serviceStore($request, $edata, $domain);
|
||||
|
||||
$this->emailHtml($edata);
|
||||
|
||||
return $this->success([], 'Thank you for your message! We will contact with you soon.');
|
||||
// return json([
|
||||
// 'code' => 200,
|
||||
// 'data' => [],
|
||||
// 'message' => 'Thank you for your message! We will contact with you soon.'
|
||||
// ]);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Log::info('Data:'.date('Y-m-d H:i:s').':'.$th->getMessage().'|'.$th->getLine().'|'.$th->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
public function formHmtl($config)
|
||||
{
|
||||
// https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-5-LP-pnx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/contact-us/
|
||||
$html = '<form class="xz-form-x xz-form-pnx" action="'.$config['post_url'].'"> <style> .xz-form-pnx { --c-theme: #BB0010; --c-border: #f0f0f0; --c-bg: #fff; --c-label: #333; --c-font: #666; --c-green: #00B160; --size: 1.6rem; --gap: 1.25em; --h: 3em; --lh: 1.8em; --pd: .4em .625em; --br: 4px; --btn-w: 18.75em; --btn-jc: center; --lines: 4; } .xz-form-pnx { font-size: var(--size); } .xz-form-pnx .xz-form-infos { display: grid; grid-template-columns: 1fr 1fr; grid-gap: var(--gap); margin-bottom: var(--gap); } .xz-form-pnx .xz-form-info, .xz-form-pnx .xz-form-msg { border: solid 1px var(--c-border); background: var(--c-bg); padding: var(--pd); display: flex; align-items: center; border-radius: var(--br); box-sizing: border-box; } .xz-form-pnx .xz-form-info { height: var(--h); } .xz-form-pnx .xz-form-msg { align-items: flex-start; } .xz-form-pnx .xz-form-input, .xz-form-pnx .xz-form-textarea { width: 100%; border: none; font-size: var(--size); line-height: var(--lh); outline: 0; padding: 0; margin-left: 1em; color: var(--c-font); } .xz-form-pnx .xz-form-textarea { height: calc(var(--lh) * var(--lines)); min-height: var(--h); font-family: inherit; resize: none; } .xz-form-pnx .xz-form-label { display: block; color: var(--c-label); line-height: var(--lh); white-space: nowrap; } .xz-form-pnx .xz-form-label.required:after { content: "*"; margin-left: .625em; color: var(--c-theme); } .xz-form-pnx .xz-form-btns { margin-top: var(--gap); display: flex; justify-content: var(--btn-jc); } .xz-form-pnx .xz-form-button { font-size: var(--size); border-radius: var(--br); display: inline-flex; justify-content: center; align-items: center; width: var(--btn-w); height: var(--h); background: var(--c-theme); color: #fff; border: none; transition: all .3s; display: flex; justify-content: center; align-items: center; gap: 1em; } .xz-form-pnx .xz-form-button path { fill: #fff; } .xz-form-pnx .xz-form-button:hover { filter: brightness(1.1); } .xz-form-pnx .xz-form-button:active { transform: scale(.98); } @media screen and (max-width: 500px){ .xz-form-pnx { --gap: .8em; } .xz-form-pnx .xz-form-infos { grid-template-columns: 1fr; } .xz-form-pnx .xz-form-button { width: 100%; } } .xzform_o { position: relative; } .xz-form-pnx .xz-form-message.success { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 9; max-width: 75%; display: flex; text-align: center; align-items: center; padding: 4em; background: rgba(255,255,255,.8); color: var(--c-green); font-size: 1.125em; line-height: 1.8em; font-weight: 500; box-shadow: 0 .5rem 3rem -.5rem rgba(0,0,0,.3); } </style> <input type="hidden" name="iLang" value="'.$config['iLang'].'"> <input type="hidden" name="Act" value="'.$config['Act'].'"> <input type="hidden" name="mailto" value="'.$config['mailto'].'"> <input type="hidden" name="customerId" value="'.$config['cid'].'"> <input type="hidden" name="fromDomain" value="'.$config['fromDomain'].'"> <input type="hidden" name="fromlink" value="'.$config['fromlink'].'"> <input type="hidden" name="fromPath" value="'.$config['fromPath'].'"> <input type="hidden" name="navigator.language"> <input type="hidden" name="navigator.platform"> <input type="hidden" name="screen.width"> <input type="hidden" name="screen.height"> <div class="xz-form-infos"> <div class="xz-form-info"> <label class="xz-form-label" for="userName-pnx">Name</label> <input type="text" name="userName" id="userName-pnx" maxlength="80" class="xz-form-input" required> </div> <div class="xz-form-info"> <label class="xz-form-label" for="uCountry-pnx">Country/Region</label> <input type="text" name="uCountry" id="uCountry-pnx" maxlength="80" class="xz-form-input" required> </div> <div class="xz-form-info"> <label class="xz-form-label" for="userEMail-pnx">Email</label> <input type="email" name="userEMail" id="userEMail-pnx" maxlength="80" class="xz-form-input"> </div> <div class="xz-form-info"> <label class="xz-form-label" for="userTel-pnx">Mobile</label> <input type="tel" name="userTel" id="userTel-pnx" maxlength="80" class="xz-form-input" required placeholder="Or Whatsapp/Skype"> </div> </div> <div class="xz-form-msg"> <label class="xz-form-label" for="Message-pnx">Message</label> <textarea name="Message" id="Message-pnx" class="xz-form-textarea" required></textarea> </div> <div class="xz-form-btns"> <button class="xz-form-button"> Submit <svg t="1664258923484" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3462" width="20" height="20"> <path d="M871.04 89.770667L120.064 380.16a51.2 51.2 0 0 0-1.792 94.762667l303.36 130.56 131.072 303.957333a51.2 51.2 0 0 0 94.805333-1.877333l289.792-751.573334a51.2 51.2 0 0 0-66.261333-66.133333z m-41.130667 107.392l-231.978666 601.642666-97.962667-227.114666-3.584-7.338667a85.333333 85.333333 0 0 0-41.045333-37.248l-226.56-97.536 601.173333-232.405333z" p-id="3463"></path> </svg> </button> </div></form>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function formHmtl2($config)
|
||||
{
|
||||
// https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-3-P-vdx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/news/
|
||||
$html = '<form class="xz-form-x xz-form-vdx" action="'.$config['post_url'].'"> <style> .xz-form-vdx { --c-theme: #2C9F1C; --c-border: #f8f8f8; --c-bg: #f8f8f8; --c-font: #333; --c-green: #2C9F1C; --size: 1.6rem; --gap: .625em; --h: 3.5em; --lh: 1.25em; --pd: 1em; --br: .625em; --btn-w: 100%; --lines: 1; font-size: var(--size); } .xz-form-vdx .xz-form-infos { display: grid; grid-template-columns: 1fr; grid-gap: var(--gap); margin-bottom: var(--gap); } .xz-form-vdx .xz-form-info, .xz-form-vdx .xz-form-msg { border: solid 1px var(--c-border); background: var(--c-bg); padding: var(--pd); display: flex; align-items: center; border-radius: var(--br); box-sizing: border-box; } .xz-form-vdx .xz-form-info { height: var(--h); } .xz-form-vdx .xz-form-input, .xz-form-vdx .xz-form-textarea { width: 100%; border: none; font-size: var(--size); line-height: var(--lh); outline: 0; padding: 0; color: var(--c-font); background: transparent; } .xz-form-vdx .xz-form-textarea { height: calc(var(--lh) * var(--lines)); font-family: inherit; resize: none; } .xz-form-vdx .xz-form-btns { margin-top: var(--gap); display: flex; justify-content: var(--btn-jc); } .xz-form-vdx .xz-form-button { font-size: var(--size); border-radius: var(--br); display: inline-flex; justify-content: center; align-items: center; width: var(--btn-w); height: var(--h); background: var(--c-theme); color: #fff; border: none; transition: all .3s; display: flex; justify-content: center; align-items: center; } .xz-form-vdx .xz-form-button:hover { filter: brightness(1.1); } .xz-form-vdx .xz-form-button:active { transform: scale(.98); } .xzform_o { position: relative; } .xz-form-vdx .xz-form-message.success { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9; max-width: 90%; display: flex; text-align: center; align-items: center; padding: 2em; background: rgba(255, 255, 255, .8); color: var(--c-green); font-size: 1em; line-height: 1.5em; box-shadow: 0 .5rem 3rem -.5rem rgba(0, 0, 0, .3); } </style> <input type="hidden" name="iLang" value="'.$config['iLang'].'"> <input type="hidden" name="Act" value="'.$config['Act'].'"> <input type="hidden" name="mailto" value="'.$config['mailto'].'"> <input type="hidden" name="customerId" value="'.$config['cid'].'"> <input type="hidden" name="fromDomain" value="'.$config['fromDomain'].'"> <input type="hidden" name="fromlink" value="'.$config['fromlink'].'"> <input type="hidden" name="fromPath" value="'.$config['fromPath'].'"> <input type="hidden" name="navigator.language"> <input type="hidden" name="navigator.platform"> <input type="hidden" name="screen.width"> <input type="hidden" name="screen.height"> <div class="xz-form-infos"> <div class="xz-form-info"> <input type="text" name="userName" id="userName-vdx" maxlength="80" class="xz-form-input" placeholder="Name"> </div> <div class="xz-form-info"> <input type="email" name="userEMail" id="userEMail-vdx" maxlength="80" class="xz-form-input" placeholder="Email"> </div> </div> <div class="xz-form-msg"> <textarea name="Message" id="Message-vdx" class="xz-form-textarea" placeholder="What can we do for you? *" required></textarea> </div> <div class="xz-form-btns"> <button class="xz-form-button"> SUBMIT <svg t="1690535811615" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5056" width="1em" height="1em" fill="#fff"> <path d="M851.2 166.4c-12.8-6.4-25.6-6.4-32 0l-640 320C166.4 486.4 160 499.2 160 512s6.4 25.6 12.8 25.6l153.6 96c12.8 6.4 25.6 6.4 38.4-6.4L704 320l12.8 6.4-307.2 326.4c-6.4 6.4-6.4 12.8-6.4 19.2l0 140.8c0 12.8 6.4 25.6 19.2 32 12.8 6.4 25.6 0 32-6.4l76.8-76.8 153.6 102.4c6.4 6.4 12.8 6.4 19.2 6.4 6.4 0 6.4 0 12.8 0 12.8-6.4 19.2-12.8 19.2-25.6l128-640C864 185.6 864 172.8 851.2 166.4z" p-id="5057"></path> </svg> </button> </div></form>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
public function emailHtml($data)
|
||||
{
|
||||
$html = '<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Lead Preview</title>
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f6f7f9;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background:#f6f7f9;">
|
||||
<tr>
|
||||
<td align="center" style="padding:24px 12px;">
|
||||
<!-- container -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="width:600px; max-width:100%; background:#ffffff; border:1px solid #e5e7eb; border-radius:8px;">
|
||||
<!-- header -->
|
||||
<tr>
|
||||
<td style="padding:16px 20px; border-bottom:1px solid #e5e7eb; font:700 18px/1.4 Arial, Helvetica, sans-serif; color:#111;">
|
||||
Preview
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Preview section -->
|
||||
<tr>
|
||||
<td style="padding:18px 20px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<!-- 一行:左 label / 右 value -->
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Date:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Date'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Email:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">
|
||||
<a href="mailto:muhseen2222@yaho.com" style="color:#2563eb; text-decoration:none;">'.$data['Email'].'</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Name:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Name'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Tel:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Tel'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Company:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#666;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Country:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Country'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Form Url:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222; word-break:break-word;">
|
||||
<a href="'.$data['Form Url'].'" style="color:#2563eb; text-decoration:none;">'.$data['Form Url'].'</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Message:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Message'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Ip:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Ip'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">IpLocation:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['IpLocation'].'</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- divider + More -->
|
||||
<tr>
|
||||
<td style="padding:0 20px;">
|
||||
<div style="height:1px; background:#e5e7eb; line-height:1px; font-size:0;"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:14px 20px 0; font:700 18px Arial, Helvetica, sans-serif; color:#111;">More</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 20px 18px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">iLang:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['iLang'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">mailto:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">
|
||||
<a href="mailto:info@borderlesscar.com" style="color:#2563eb; text-decoration:none;">'.$data['mailto'].'</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">navigator_language:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['navigator_language'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">navigator_platform:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['navigator_platform'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">screen_width:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['screen_width'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">screen_height:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['screen_height'].'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">uCountry:</td>
|
||||
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['uCountry'].'</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- /container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
// $email = new Email();
|
||||
//return $email->send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html);
|
||||
return \app\common\utils\email\MailSend::send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\User;
|
||||
use app\common\service\OssService;
|
||||
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');
|
||||
if (!empty($data['qr_code']))
|
||||
$data['qr_code_pull'] = (new OssService)->generatePrivateUrl($data['qr_code']);;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成物品二维码
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function generateQrCode(Request $request)
|
||||
{
|
||||
try {
|
||||
$id = $request->post('id');
|
||||
$num = $request->post('num');
|
||||
if (!$id) {
|
||||
return $this->error('物品ID不能为空');
|
||||
}
|
||||
|
||||
$item = \app\common\model\InvItem::find($id);
|
||||
if (!$item) {
|
||||
return $this->error('物品不存在');
|
||||
}
|
||||
|
||||
// 获取应用访问地址
|
||||
$appUrl = 'https://'.$request->host();
|
||||
|
||||
$qrCodeService = new \app\common\service\QrCodeService();
|
||||
$content = $qrCodeService->generateItemQrCode($id, $appUrl);
|
||||
$fileName = 'qr_item_' . $id . '_' . time() . '.png';
|
||||
|
||||
$result = $qrCodeService->generateAndUploadQrCode($id, $content, $fileName);
|
||||
|
||||
// 更新物品记录
|
||||
$item->qr_code = $result['path'];
|
||||
//$item->qr_code_num = !empty($num)?$num:0;
|
||||
$item->qr_code_url = $result['url'];
|
||||
$item->save();
|
||||
|
||||
return $this->success([
|
||||
'qr_code_url' => $result['url'],
|
||||
'content' => $content,
|
||||
], '二维码生成成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除物品二维码
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteQrCode(Request $request)
|
||||
{
|
||||
try {
|
||||
$id = $request->post('id');
|
||||
if (!$id) {
|
||||
return $this->error('物品ID不能为空');
|
||||
}
|
||||
|
||||
$item = \app\common\model\InvItem::find($id);
|
||||
if (!$item) {
|
||||
return $this->error('物品不存在');
|
||||
}
|
||||
|
||||
// 删除 OSS 文件
|
||||
if ($item->qr_code) {
|
||||
$qrCodeService = new \app\common\service\QrCodeService();
|
||||
$qrCodeService->deleteQrCode($item->qr_code);
|
||||
}
|
||||
|
||||
// 清空物品记录中的二维码字段
|
||||
$item->qr_code = '';
|
||||
//$item->qr_code_num = 0;
|
||||
$item->qr_code_url = '';
|
||||
$item->save();
|
||||
|
||||
return $this->success([], '二维码删除成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\InvRequisitionApprove;
|
||||
use app\common\model\InvRequisitionAttachment;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use app\common\service\OssService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\admin\service\InvRequisitionService;
|
||||
use support\Request;
|
||||
|
||||
class InvRequisitionController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvRequisitionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function approve(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->service->serviceApprove($request);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回分页数据及总数
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$applicant_id = $request->get('applicant_id');
|
||||
$uid = $request->user->uid;
|
||||
$raw = $this->service->serviceList($request);
|
||||
$data = $this->formatindex($raw,$applicant_id,$uid);
|
||||
// $data = $this->transformData($raw, method_exists($this, 'indexFormat')
|
||||
// ? [$this, 'indexFormat']
|
||||
// : null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function formatindex($p,$applicant_id,$uid){
|
||||
$records = $p->getCollection()
|
||||
->map(fn($item) => $this->indexFormat($item, $applicant_id,$uid))
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'records' => $records,
|
||||
'total' => $p->total(),
|
||||
'current' => $p->currentPage(),
|
||||
'size' => $p->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function indexFormat($data, $applicant_id, $uid)
|
||||
{
|
||||
if (isset($data['id'])){
|
||||
//$data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->get();
|
||||
$data = $data->toArray();
|
||||
}
|
||||
if (isset($data['created_at'])){
|
||||
$data['created_at'] = is_int($data['created_at'])?$data['created_at']:strtotime($data['created_at']);
|
||||
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
}
|
||||
if (isset($data['updated_at'])){
|
||||
$data['updated_at'] = is_int($data['updated_at'])?$data['updated_at']:strtotime($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();
|
||||
}
|
||||
|
||||
if($applicant_id != 'self' && isset($data['id'])){ //不是查询自己数据时查询是否已审核
|
||||
$approve = InvRequisitionApprove::where('requisition_id', $data['id'])
|
||||
->where('approver_id', $uid)
|
||||
->first();
|
||||
$data['action'] = $approve['action']??3;//查询人审核状态:0待审核 1通过 2驳回 3非审核人
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function showFormat($data)
|
||||
{
|
||||
if (isset($data['id'])){
|
||||
$data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->orderBy('id')->get()->toArray();
|
||||
|
||||
$approve_id = 0;
|
||||
foreach ($data['approve'] as $key => $value) {
|
||||
if (isset($value['created_at'])){
|
||||
$value['created_at'] = is_int($value['created_at'])?$value['created_at']:strtotime($value['created_at']);
|
||||
$data['approve'][$key]['created_at'] = date('Y-m-d H:i:s', $value['created_at']);
|
||||
}
|
||||
if (isset($value['updated_at'])){
|
||||
$value['updated_at'] = is_int($value['updated_at'])?$value['updated_at']:strtotime($value['updated_at']);
|
||||
$data['approve'][$key]['updated_at'] = date('Y-m-d H:i:s', $value['updated_at']);
|
||||
}
|
||||
$data['approve'][$key]['action_at'] = date('Y-m-d H:i:s', $data['approve'][$key]['action_at']);
|
||||
$approve_id = $value['id'];
|
||||
}
|
||||
$data['approve_id'] = $approve_id;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员代领
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function proxyClaim(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->service->proxyClaim($request, $request->post());
|
||||
return $this->success($data, '代领成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码/代领记录查询
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function qrClaims(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->service->qrClaimsList($request);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\InvRequisitionReturnService;
|
||||
use support\Request;
|
||||
|
||||
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;
|
||||
// }
|
||||
|
||||
public function returnConfirm(Request $request){
|
||||
try {
|
||||
// 处理申请逻辑
|
||||
$result = $this->service->serviceReturnConfirm($request);
|
||||
|
||||
return $this->success($result, '申请提交成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\service\InvStockLogService;
|
||||
use support\Request;
|
||||
|
||||
class InvStockLogController extends BaseController
|
||||
{
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InvStockLogService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存变化记录列表
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->service->getList($request->get());
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\Resource;
|
||||
use app\common\model\Role;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserGroup;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\UserDashboardRuleService;
|
||||
use Couchbase\Group;
|
||||
|
||||
class UserDashboardRuleController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new UserDashboardRuleService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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['resources_id']))
|
||||
$data['resources'] = Resource::where(['id' => $data['resources_id']])->first();
|
||||
if (isset($data['rule_type']) && $data['rule_type'] == 'role'){
|
||||
$data['related_rt'] = Role::where(['id' => $data['related']])->value('name');
|
||||
}elseif (isset($data['rule_type']) && $data['rule_type'] == 'user'){
|
||||
$data['related_rt'] = User::where(['id' => $data['related']])->value('name');
|
||||
}elseif (isset($data['rule_type']) && $data['rule_type'] == 'group'){
|
||||
$data['related_rt'] = UserGroup::where(['id' => $data['related']])->value('title');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -328,20 +328,22 @@ class BrandSeriesService
|
||||
// 执行抓取
|
||||
$crawlService = new CrawlService();
|
||||
|
||||
//懂车帝模糊搜索返回车系地址
|
||||
//懂车帝模糊搜索返回车系地址|单个车系id抓取
|
||||
$href = $crawlService->modelSearch($keyword);
|
||||
$seriesId = $this->extractSeriesId($href); //懂车帝车系id
|
||||
|
||||
if (empty($seriesId)) { //没有抓取到懂车帝系列id时,按关键词为品牌名抓取所有的系列名
|
||||
$crawlSeries = $crawlService->brandSeries($keyword);
|
||||
}
|
||||
// if (empty($seriesId)) { //没有抓取到懂车帝系列id时,按关键词为品牌名抓取所有的系列名
|
||||
// $crawlSeries = $crawlService->brandSeries($keyword);
|
||||
// }
|
||||
//按关键词为品牌名抓取所有的系列名
|
||||
$crawlSeries = $crawlService->brandSeries($keyword);
|
||||
|
||||
if (empty($seriesId) && empty($crawlSeries)) {
|
||||
throw new \Exception('抓取失败,未找到该系列车型');
|
||||
}
|
||||
|
||||
$brandSerieses = [];
|
||||
if(empty($seriesId) && !empty($crawlSeries)){ //关键词为品牌名时抓取所有车系的懂车帝id
|
||||
if(empty($seriesId) && !empty($crawlSeries) || count($crawlSeries)>1){ //关键词为品牌名时抓取所有车系的懂车帝id
|
||||
$href = $crawlService->modelSearch($crawlSeries[0]);
|
||||
$seriesId = $this->extractSeriesId($href); //懂车帝车系id
|
||||
//抓取品牌与系列
|
||||
@@ -396,21 +398,39 @@ class BrandSeriesService
|
||||
*/
|
||||
public function crawlKeywordCreate(Request $request)
|
||||
{
|
||||
$series_id = $request->input('series_id');
|
||||
$brand_id = $request->input('brand_id');
|
||||
$brand_name = $request->input('brand_name');
|
||||
$brand_code = $request->input('brand_code');
|
||||
$series_id = $request->input('series_id');
|
||||
$series_name = $request->input('series_name');
|
||||
$models = $request->input('models');
|
||||
|
||||
if(empty($brand_id)){
|
||||
if(empty($brand_id) && empty($brand_name)){
|
||||
throw new \Exception("请选择品牌");
|
||||
}
|
||||
if(empty($series_id) && empty($series_name)){
|
||||
throw new \Exception("请选择车系");
|
||||
}
|
||||
|
||||
// 获取品牌信息
|
||||
if (empty($brand_id)) {
|
||||
$brand = Brand::where('id', $brand_id)->where('brand_name', $brand_name)->first();
|
||||
if (empty($brand)) { //如果没有该车系则创建
|
||||
$brandService = new \app\admin\service\BrandService();
|
||||
$brandData = [
|
||||
'brand_code' => $brandService->generateUniqueCode(),
|
||||
'brand_name' => $brand_name,
|
||||
];
|
||||
$brand = CrudUtil::create(Brand::class, $brandData);
|
||||
}
|
||||
$brand_id = $brand->id;
|
||||
}else{
|
||||
$brand = Brand::where('id', $brand_id)->first();
|
||||
}
|
||||
|
||||
if (empty($series_id)) {
|
||||
$series = BrandSeries::where('brand_id', $brand_id)->where('series_name', $series_name)->first();
|
||||
if (empty($brandseries)) { //如果没有该车系则创建
|
||||
if (empty($series)) { //如果没有该车系则创建
|
||||
$seriesService = new \app\admin\service\BrandSeriesService();
|
||||
$seriesData = [
|
||||
'brand_id' => $brand_id,
|
||||
@@ -423,8 +443,6 @@ class BrandSeriesService
|
||||
}else{
|
||||
$series = BrandSeries::where('id', $series_id)->first();
|
||||
}
|
||||
// 获取品牌信息
|
||||
$brand = Brand::where('id', $series->brand_id)->first();
|
||||
|
||||
// 保存车型数据
|
||||
$createdCount = 0;
|
||||
@@ -479,6 +497,91 @@ class BrandSeriesService
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取
|
||||
*/
|
||||
public function crawl(Request $request): array
|
||||
{
|
||||
$keyword = $request->get('keyword');
|
||||
if(filter_var($keyword, FILTER_VALIDATE_URL)){
|
||||
return $this->crawlUrl($request);
|
||||
}else{
|
||||
return $this->crawlKeyword($request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取车型数据(通过接口)
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function crawlUrl(Request $request): array
|
||||
{
|
||||
//$url = $request->get('url');
|
||||
$url = $request->get('keyword');
|
||||
|
||||
// 执行抓取
|
||||
$crawlService = new CrawlService();
|
||||
|
||||
//懂车帝提取车系id
|
||||
$seriesId = $this->get_series_from_url($url); //懂车帝车系id
|
||||
|
||||
if (empty($seriesId)) {
|
||||
throw new \Exception('抓取失败,未找到该系列车型');
|
||||
}
|
||||
|
||||
$brandSerieses = [];
|
||||
//抓取品牌与系列
|
||||
$brandSeries = $crawlService->crawlBrandSeries($seriesId);
|
||||
$brandSerieses[] = ['brand' => $brandSeries['brand'], 'series' => $brandSeries['series']];
|
||||
|
||||
$result = [];
|
||||
try {
|
||||
foreach ($brandSerieses as $brandSeries) {
|
||||
$item = [];
|
||||
// 验证品牌是否存在
|
||||
$brand = Brand::where('brand_name', $brandSeries['brand'])->first();
|
||||
// 获取品牌信息
|
||||
$series = BrandSeries::where('series_name', $brandSeries['series'])->first();
|
||||
$item['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand ? 1 : 0];
|
||||
if ($brand) {
|
||||
$item['brand']['id'] = $brand['id'];
|
||||
}
|
||||
$item['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series ? 1 : 0];
|
||||
if ($series) {
|
||||
$item['series']['id'] = $series['id'];
|
||||
if (empty($item['brand']['id'])) {
|
||||
$item['brand']['id'] = $series['brand_id'];
|
||||
}
|
||||
}
|
||||
|
||||
//获取车型数据
|
||||
$item['model'] = [];
|
||||
if (count($brandSerieses)<2) {
|
||||
$item['model'] = $this->crawlSeriesModel($seriesId);
|
||||
}
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
return ['is_series' => (count($brandSerieses)<2)?1:0, 'data' => $result];
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function get_series_from_url($url) {
|
||||
// 使用正则表达式匹配 "https://www.dongchedi.com/auto/series/{数字}"
|
||||
$pattern = '/^https:\/\/www\.dongchedi\.com\/auto\/series\/(\d+)$/';
|
||||
if (preg_match($pattern, $url, $matches)) {
|
||||
// 提取数字部分作为 series ID
|
||||
$series_id = $matches[1];
|
||||
return $series_id; // 返回系列ID
|
||||
} else {
|
||||
throw new \Exception("URL格式不匹配");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车系下车型数据
|
||||
* @return array
|
||||
@@ -516,7 +619,11 @@ class BrandSeriesService
|
||||
}
|
||||
$official_price = 0;
|
||||
if(!isset($model["info"]['official_price'])){
|
||||
$official_price = (int)(str_replace("万", "", $model["info"]['official_price_str']) * 10000) ?? 0;
|
||||
if(str_contains($model["info"]['official_price_str'],'万')){
|
||||
$official_price = (int)(str_replace("万", "", $model["info"]['official_price_str']) * 10000) ?? 0;
|
||||
}else{
|
||||
$official_price = 0;
|
||||
}
|
||||
}else{
|
||||
$official_price = $model["info"]['official_price'] * 10000;
|
||||
}
|
||||
|
||||
@@ -151,4 +151,37 @@ class BrandService
|
||||
$msg .= (!empty($fail))?',失败' . $fail . '条':'';
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的7位颜色编码
|
||||
* 格式:C + (O/I) + 5位字母数字组合
|
||||
*
|
||||
* @param int $type 类型
|
||||
* @return string 7位唯一编码
|
||||
*/
|
||||
public function generateUniqueCode()
|
||||
{
|
||||
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$maxAttempts = 10; // 防止无限循环
|
||||
|
||||
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
|
||||
// 生成后5位随机字符
|
||||
$suffix = '';
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$suffix .= $chars[rand(0, strlen($chars) - 1)];
|
||||
}
|
||||
|
||||
$code = $suffix;
|
||||
|
||||
// 检查是否已存在
|
||||
$existing = Brand::where('brand_code', $code)->first();
|
||||
if (!$existing) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果1000次尝试后仍然重复,使用时间戳确保唯一性
|
||||
$timestamp = substr(time(), -6);
|
||||
return $suffix . $timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use app\common\validate\InvRequisitionValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisition;
|
||||
use support\Request;
|
||||
|
||||
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']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
if(isset($p['status']) && $p['status']){
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
if (isset($p['applicant_id']) && $p['applicant_id'] == 'self') { //自己的领用申请
|
||||
$config['where']['applicant_id'] = $request->user->uid;
|
||||
} elseif (isset($p['applicant_id']) && $p['applicant_id']) { //指定人员的申请
|
||||
$config['where']['applicant_id'] = $p['applicant_id'];
|
||||
} else { //审批人对应的记录
|
||||
$config['where']['approval.approver_id'] = $request->user->uid;
|
||||
$config['where']['approval.action'] = 0;
|
||||
}
|
||||
if(isset($p['phase']) && $p['phase'] == 'approve'){//借用审批中
|
||||
$config['where']['status'] = ['in',[10,11,13]];
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'repaid'){//已全部归还
|
||||
$config['where']['status'] = 17;
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'unreturned'){ //未归还
|
||||
$config['where']['status'] = ['in',[12,14,15,16,18]];
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批
|
||||
* @param Request $request
|
||||
* @return mixed|string[]|null
|
||||
*/
|
||||
public function serviceApprove(Request $request)
|
||||
{
|
||||
$param = $request->all();
|
||||
$attachment = $param['attachment']??[];
|
||||
//$appro = new \app\common\utils\approval\service\ApprovalFlowService();
|
||||
$appro = new \app\common\service\InvRequisitionApproveService();
|
||||
//requisition_id:申请id
|
||||
//approve_id:审核步骤id
|
||||
//action:审批动作:1通过 2驳回
|
||||
//comment:审批意见
|
||||
return $appro->approve($param['requisition_id'], $param['approve_id'], $request->user->uid, $param['action'], $param['comment'], $attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员代领
|
||||
* 调用 common 服务层方法
|
||||
*/
|
||||
public function proxyClaim(Request $request, array $data): array
|
||||
{
|
||||
return (new \app\common\service\InvRequisitionService())->proxyClaim($request, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码/代领记录查询
|
||||
* 调用 common 服务层方法
|
||||
*/
|
||||
public function qrClaimsList(Request $request): array
|
||||
{
|
||||
return (new \app\common\service\InvRequisitionService())->qrClaimsList($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use support\Redis;
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Redis as RedisQueueRedis;
|
||||
|
||||
/**
|
||||
* 轮询分配 inquiry_log 里的询盘给业务员,并推送到 CRM 队列
|
||||
*/
|
||||
class InquiryAssignInquiryCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'iq-assign';
|
||||
protected static $defaultDescription = '轮询分配询盘给业务员,并推送到 CRM 队列';
|
||||
|
||||
/**
|
||||
* 参与轮询的业务员 ID 列表(请按你想要的顺序填写)
|
||||
* 比如:销售1=186,销售2=233...
|
||||
*/
|
||||
protected const SALES_USER_IDS = [
|
||||
63,
|
||||
64,
|
||||
67,
|
||||
93,
|
||||
108,
|
||||
132,
|
||||
143,
|
||||
145,
|
||||
157,
|
||||
160,
|
||||
168,
|
||||
172,
|
||||
173,
|
||||
191,
|
||||
193,
|
||||
194,
|
||||
195,
|
||||
196,
|
||||
197
|
||||
];
|
||||
|
||||
/**
|
||||
* 轮询分配的分组 key(同一分组共用一个轮询指针)
|
||||
*/
|
||||
protected const ROUND_ROBIN_GROUP_KEY = 'fb_inquiry';
|
||||
|
||||
/**
|
||||
* 每次脚本最多分配多少条
|
||||
*/
|
||||
protected const BATCH_LIMIT = 300;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// 业务员列表检查
|
||||
$userIds = array_values(array_filter(
|
||||
array_map('intval', static::SALES_USER_IDS),
|
||||
fn($v) => $v > 0
|
||||
));
|
||||
|
||||
if (empty($userIds)) {
|
||||
throw new BusinessException('SALES_USER_IDS 业务员列表不能为空,请先在命令中配置.');
|
||||
}
|
||||
|
||||
// 取出“待分配”的询盘(你可以根据自己系统调整条件)
|
||||
// 当前策略:status = 0 且 recipient = 0 的视为未分配
|
||||
$inquiries = Db::table('inquiry_log')
|
||||
->where('enter_user_id', 186)
|
||||
->where('channel', 10)
|
||||
->where('status', 0)
|
||||
->where('recipient', 0)
|
||||
->where('id', '>', 34847)
|
||||
->orderBy('id')
|
||||
->limit(static::BATCH_LIMIT)
|
||||
->get();
|
||||
|
||||
if ($inquiries->isEmpty()) {
|
||||
$output->writeln('<comment>没有需要分配的询盘(status=0 且 recipient=0)</comment>');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$count = 0;
|
||||
|
||||
foreach ($inquiries as $row) {
|
||||
$inquiryId = (int)$row->id;
|
||||
sleep(1);
|
||||
// 轮询取出一个业务员 ID
|
||||
$recipientUserId = $this->nextRecipientId($userIds, static::ROUND_ROBIN_GROUP_KEY);
|
||||
|
||||
// 对每一条记录单独事务,避免单条异常拖垮全部
|
||||
Db::transaction(function () use ($inquiryId, $recipientUserId, $now, $output, &$count) {
|
||||
// 1)更新 inquiry_log
|
||||
$affected = Db::table('inquiry_log')
|
||||
->where('id', $inquiryId)
|
||||
->update([
|
||||
'recipient' => $recipientUserId,
|
||||
'status' => 2,
|
||||
'r_time' => $now,
|
||||
't_time' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
if ($affected === 0) {
|
||||
// 被其他进程抢先更新也有可能,这里只打印提示不中断整个批次
|
||||
$output->writeln("<comment>询盘 {$inquiryId} 未更新(可能已被处理)</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2)构造队列 payload(后续 CRM 消费时至少知道 ID)
|
||||
$res = [
|
||||
'id' => $inquiryId,
|
||||
'recipient' => $recipientUserId,
|
||||
'dispatched_at' => $now,
|
||||
'source' => 'fb_leads_inquiry',
|
||||
];
|
||||
|
||||
// 3)推送到 CRM 队列
|
||||
RedisQueueRedis::send('inquiry-to-crm-new', $res);
|
||||
|
||||
$count++;
|
||||
$output->writeln("<info>已分配询盘 {$inquiryId} 给业务员 {$recipientUserId},并推送到队列</info>");
|
||||
});
|
||||
}
|
||||
|
||||
$output->writeln("<info>本次共分配成功 {$count} 条询盘</info>");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询获取“下一位业务员 ID”
|
||||
*
|
||||
* @param int[] $userIds
|
||||
* @param string $groupKey
|
||||
* @return int
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function nextRecipientId(array $userIds, string $groupKey = 'default'): int
|
||||
{
|
||||
$userIds = array_values(array_filter(array_map('intval', $userIds), fn($v) => $v > 0));
|
||||
if (empty($userIds)) {
|
||||
throw new BusinessException('轮询分配失败:业务员列表为空');
|
||||
}
|
||||
|
||||
$count = count($userIds);
|
||||
$key = 'inquiry:assign:round_robin:' . $groupKey;
|
||||
|
||||
// Redis INCR 是原子操作,适合并发下使用
|
||||
$val = Redis::incr($key); // 第一次会从 0 变成 1
|
||||
|
||||
// 换算为从 0 开始的下标
|
||||
$index = ($val - 1) % $count;
|
||||
|
||||
return $userIds[$index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\inquiry\utils\PhoneCountryHelper;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 将 fb_leads 数据清洗后导入 inquiry_log
|
||||
*/
|
||||
class InquiryFbLeadCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'iq-fb-to';
|
||||
protected static $defaultDescription = '将 fb_leads 数据清洗后导入 inquiry_log';
|
||||
|
||||
// 这些常量根据你实际业务改
|
||||
protected const DEFAULT_ENTER_USER_ID = 186;
|
||||
protected const DEFAULT_CHANNEL = 10;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// 只取未同步的记录
|
||||
$leads = Db::table('fb_leads')
|
||||
->where('sync_status', 0)
|
||||
->get();
|
||||
|
||||
if ($leads->isEmpty()) {
|
||||
$output->writeln('<comment>没有需要同步的 fb_leads(sync_status = 0)</comment>');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
Db::transaction(function () use ($leads, $now, $output) {
|
||||
$insert = [];
|
||||
$ids = [];
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$leadArr = (array)$lead;
|
||||
$insert[] = $this->mapLeadToInquiry($leadArr, $now);
|
||||
// 假设 fb_leads 的主键字段名为 id
|
||||
$ids[] = $leadArr['id'];
|
||||
}
|
||||
|
||||
if ($insert) {
|
||||
// 1) 写入 inquiry_log
|
||||
Db::table('inquiry_log')->insert($insert);
|
||||
|
||||
// 2) 回写 fb_leads 的同步状态
|
||||
Db::table('fb_leads')
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'sync_status' => 1,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$output->writeln('<info>已导入 ' . count($insert) . ' 条数据到 inquiry_log,并更新 fb_leads.sync_status = 1</info>');
|
||||
}
|
||||
});
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一条 fb_leads 记录映射为 inquiry_log 记录
|
||||
*/
|
||||
protected function mapLeadToInquiry(array $lead, int $now): array
|
||||
{
|
||||
// 联系方式
|
||||
$email = (string)($lead['email'] ?? '');
|
||||
$tel = (string)($lead['phone'] ?? '');
|
||||
$wa = $tel; // 先用电话号码当 WhatsApp
|
||||
|
||||
// 根据电话或 WhatsApp 区号识别国家
|
||||
$countryId = PhoneCountryHelper::detectCountryId($tel ?: $wa);
|
||||
|
||||
// 所有默认值按你的要求设置
|
||||
$data = [
|
||||
'order_no' => $this->orderNum(),
|
||||
'enter_user_id' => static::DEFAULT_ENTER_USER_ID,
|
||||
'recipient' => 0,
|
||||
'quality' => 2,
|
||||
'channel' => static::DEFAULT_CHANNEL,
|
||||
|
||||
'email' => $email,
|
||||
'tel' => $tel,
|
||||
'whatsapp' => $wa,
|
||||
'country' => $countryId ?: 0,
|
||||
'contacts' => (string)($lead['full_name'] ?? ''),
|
||||
|
||||
'content' => $this->buildContentHtml($lead),
|
||||
|
||||
'status' => 0,
|
||||
'r_time' => 0,
|
||||
't_time' => 0,
|
||||
'sync_crm' => 0,
|
||||
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
protected function orderNum(): string
|
||||
{
|
||||
return 'SN' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 fb_leads 的字段拼出富文本内容
|
||||
*/
|
||||
protected function buildContentHtml(array $lead): string
|
||||
{
|
||||
$e = function ($v) {
|
||||
return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
};
|
||||
|
||||
$lines = [];
|
||||
|
||||
if (!empty($lead['car_model'])) {
|
||||
$lines[] = '<p><strong>意向车型:</strong>' . $e($lead['car_model']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) {
|
||||
$qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? '';
|
||||
$lines[] = '<p><strong>意向数量:</strong>' . $e($qty) . '</p>';
|
||||
}
|
||||
if (!empty($lead['favorite_color'])) {
|
||||
$lines[] = '<p><strong>意向颜色:</strong>' . $e($lead['favorite_color']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['usage_type'])) {
|
||||
$lines[] = '<p><strong>用途:</strong>' . $e($lead['usage_type']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['seat_preference'])) {
|
||||
$lines[] = '<p><strong>座位数偏好:</strong>' . $e($lead['seat_preference']) . '</p>';
|
||||
}
|
||||
|
||||
$lines[] = '<hr />';
|
||||
$lines[] = '<p><strong>广告与表单信息:</strong></p>';
|
||||
$lines[] = '<p>平台:' . $e($lead['platform'] ?? '') . '</p>';
|
||||
$lines[] = '<p>Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')</p>';
|
||||
|
||||
$lines[] = '<hr />';
|
||||
$lines[] = '<p><strong>Meta 信息:</strong></p>';
|
||||
$lines[] = '<p>Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '</p>';
|
||||
$lines[] = '<p>FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '</p>';
|
||||
|
||||
if (!empty($lead['inbox_url'])) {
|
||||
$url = $e($lead['inbox_url']);
|
||||
$lines[] = '<p>Inbox 链接:<a href="' . $url . '" target="_blank">' . $url . '</a></p>';
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use app\inquiry\utils\FbLeadHeaderMapper;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 从Excel文件导入fb_leads
|
||||
*/
|
||||
class InquiryImportFbCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'iq-fb';
|
||||
protected static $defaultDescription = '导入 Facebook Leads Excel 数据';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'file',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'要导入的单个 Excel 文件名(相对 public/excel 的路径,或绝对路径)'
|
||||
)
|
||||
->addOption(
|
||||
'dir',
|
||||
null,
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Excel 目录(相对 public/ 的路径)',
|
||||
'excel' // 默认就是 public/excel
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$fileOption = $input->getOption('file');
|
||||
$dirOption = $input->getOption('dir') ?? 'excel';
|
||||
|
||||
// 基准目录:public/{$dirOption}
|
||||
$baseDir = rtrim(public_path($dirOption), DIRECTORY_SEPARATOR);
|
||||
|
||||
if ($fileOption) {
|
||||
// 如果传的是绝对路径,直接用;否则认为是相对 baseDir 的文件名
|
||||
if ($this->isAbsolutePath($fileOption)) {
|
||||
$filePath = $fileOption;
|
||||
} else {
|
||||
$filePath = $baseDir . DIRECTORY_SEPARATOR . $fileOption;
|
||||
}
|
||||
|
||||
$output->writeln("<info>开始导入单个文件:</info> {$filePath}");
|
||||
|
||||
$this->importOneFile($filePath, $output);
|
||||
} else {
|
||||
// 不指定 file 时,默认导入目录下所有 .xlsx
|
||||
if (!is_dir($baseDir)) {
|
||||
throw new BusinessException("目录不存在:{$baseDir}");
|
||||
}
|
||||
|
||||
$pattern = $baseDir . DIRECTORY_SEPARATOR . '*.xlsx';
|
||||
$files = glob($pattern);
|
||||
|
||||
if (empty($files)) {
|
||||
$output->writeln("<comment>目录 {$baseDir} 下没有找到 .xlsx 文件</comment>");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln("<info>开始批量导入目录:</info> {$baseDir}");
|
||||
foreach ($files as $filePath) {
|
||||
$output->writeln(" -> 文件:{$filePath}");
|
||||
try {
|
||||
$this->importOneFile($filePath, $output);
|
||||
$output->writeln(" <info>导入成功</info>");
|
||||
} catch (\Throwable $e) {
|
||||
// 某个文件失败,不影响后面的文件
|
||||
$output->writeln(" <error>导入失败:{$e->getMessage()}</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入单个 Excel 文件
|
||||
*/
|
||||
protected function importOneFile(string $filePath, OutputInterface $output): void
|
||||
{
|
||||
if (!is_file($filePath)) {
|
||||
throw new BusinessException("文件不存在:{$filePath}");
|
||||
}
|
||||
|
||||
$spreadsheet = IOFactory::load($filePath);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$rows = $sheet->toArray(null, true, true, true);
|
||||
|
||||
if (empty($rows)) {
|
||||
throw new BusinessException('Excel 内容为空:' . $filePath);
|
||||
}
|
||||
|
||||
// 取第一行表头
|
||||
$headerRow = array_shift($rows);
|
||||
|
||||
// 表头映射器
|
||||
$mapper = new FbLeadHeaderMapper();
|
||||
|
||||
// 如果有未知表头,这里会直接抛异常中断当前文件
|
||||
$colToField = $mapper->buildColumnFieldMap($headerRow);
|
||||
|
||||
$insertData = [];
|
||||
$now = time();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$item = [];
|
||||
|
||||
foreach ($colToField as $colIndex => $field) {
|
||||
$raw = $row[$colIndex] ?? null;
|
||||
|
||||
switch ($field) {
|
||||
// 1) is_organic: Excel 里是 true/false,这里转成 0/1
|
||||
case 'is_organic':
|
||||
$item['is_organic'] = $this->normalizeIsOrganic($raw);
|
||||
break;
|
||||
|
||||
// 2) fb_created_time: 统一转成 Y-m-d H:i:s
|
||||
case 'fb_created_time':
|
||||
$item['fb_created_time'] = $this->normalizeFbCreatedTime($raw);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (is_string($raw)) {
|
||||
$raw = trim($raw);
|
||||
}
|
||||
$item[$field] = $raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 补充公共字段
|
||||
$item['source_file'] = basename($filePath);
|
||||
$item['created_at'] = $now;
|
||||
$item['updated_at'] = $now;
|
||||
|
||||
// 整行都空就跳过
|
||||
$allNull = true;
|
||||
foreach ($item as $v) {
|
||||
if ($v !== null && $v !== '') {
|
||||
$allNull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($allNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertData[] = $item;
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
Db::table('fb_leads')->insert($insertData);
|
||||
} else {
|
||||
$output->writeln(" <comment>文件 {$filePath} 没有有效数据行</comment>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Excel 里的 is_organic 值转换为 0/1
|
||||
*/
|
||||
protected function normalizeIsOrganic($value): int
|
||||
{
|
||||
if ($value === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$v = strtolower(trim((string)$value));
|
||||
|
||||
// 认为这些是 “true”
|
||||
$trueValues = ['1', 'true', 'yes', 'y'];
|
||||
|
||||
return in_array($v, $trueValues, true) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一处理 fb_created_time
|
||||
* Excel 目前是类似 '12/8/25',这里尽量转成 'Y-m-d H:i:s'
|
||||
* 解析失败时返回 null
|
||||
*/
|
||||
protected function normalizeFbCreatedTime($value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$v = trim((string)$value);
|
||||
|
||||
$formats = [
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d',
|
||||
'n/j/y', // 12/8/25
|
||||
'n/j/Y',
|
||||
'd/m/Y',
|
||||
'd.m.Y',
|
||||
];
|
||||
|
||||
foreach ($formats as $fmt) {
|
||||
$dt = \DateTime::createFromFormat($fmt, $v);
|
||||
if ($dt !== false) {
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
|
||||
// 纯数字当时间戳试一下
|
||||
if (is_numeric($v) && strlen($v) >= 9 && strlen($v) <= 11) {
|
||||
$ts = (int)$v;
|
||||
if ($ts > 0) {
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为绝对路径(简单兼容 Windows / Linux)
|
||||
*/
|
||||
protected function isAbsolutePath(string $path): bool
|
||||
{
|
||||
// Windows: X:\ 或 \\server\share
|
||||
if (preg_match('/^[a-zA-Z]:\\\\/', $path) || str_starts_with($path, '\\\\')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// *nix: / 开头
|
||||
return str_starts_with($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
php webman tb-db
|
||||
# 等价于 php webman tb-db --dir=excel
|
||||
# 会遍历 public/excel/*.xlsx 逐个导入
|
||||
|
||||
# 相对路径(相对 public/excel)
|
||||
php webman tb-db --file="BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
|
||||
|
||||
# 或给绝对路径
|
||||
php webman tb-db --file="/www/wwwroot/xxx/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use Webman\RedisQueue\Redis as RedisQueueRedis;
|
||||
|
||||
/**
|
||||
* 询盘补发
|
||||
*/
|
||||
class InquiryReissueCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'iq-bf';
|
||||
protected static $defaultDescription = '';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$inquiries = Db::table('inquiry_log')
|
||||
->where('sync_crm', 0)
|
||||
->where('id', '>', 33785)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
foreach ($inquiries as $row) {
|
||||
$inquiryId = (int)$row->id;
|
||||
|
||||
var_dump('补发ID:'.$inquiryId);
|
||||
|
||||
sleep(1);
|
||||
|
||||
$res = [
|
||||
'id' => $inquiryId
|
||||
];
|
||||
RedisQueueRedis::send('inquiry-to-crm-new', $res);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserSn;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use app\common\utils\email\Emails;
|
||||
|
||||
/**
|
||||
* 批量处理用户编号(也用于调试)
|
||||
*/
|
||||
class VehMoneyCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'veh-money';
|
||||
protected static $defaultDescription = '';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace app\common\controller;
|
||||
|
||||
use app\common\service\AuthService;
|
||||
use app\common\validate\UserValidate;
|
||||
use Exception;
|
||||
use support\Request;
|
||||
|
||||
class AuthController extends BaseController
|
||||
@@ -31,7 +32,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());
|
||||
@@ -112,4 +114,19 @@ class AuthController extends BaseController
|
||||
// (new AuthService())->blockToken($token, $systemCode);
|
||||
// return $this->success([], '已强制停用该token');
|
||||
// }
|
||||
|
||||
|
||||
public function getJsApi(Request $request)
|
||||
{
|
||||
try {
|
||||
$url = $request->get('url');
|
||||
if(empty($url)){
|
||||
throw new Exception("url不能为空");
|
||||
}
|
||||
$result = (new AuthService())->getJsApiConfig($request->corp_abbr, $request->system_code,$url);
|
||||
return $this->success($result);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,42 @@ class BrandSeriesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL抓取品牌、车系、车型
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function crawlUrl(Request $request){
|
||||
try {
|
||||
$result = $this->service->crawlUrl($request);
|
||||
if($result){
|
||||
return $this->success($result,'抓取成功');
|
||||
}else{
|
||||
return $this->error('抓取任务失败');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取品牌、车系、车型
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function crawl(Request $request){
|
||||
try {
|
||||
$result = $this->service->crawl($request);
|
||||
if($result){
|
||||
return $this->success($result,'抓取成功');
|
||||
}else{
|
||||
return $this->error('抓取任务失败');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存抓取的系列车型
|
||||
* @param Request $request
|
||||
|
||||
@@ -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,181 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\InvItem;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use app\common\model\InvRequisitionItemReturn;
|
||||
use app\common\model\InvRequisitionItemReturnAttachment;
|
||||
use app\common\service\OssService;
|
||||
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 Request $request 请求对象
|
||||
* @return mixed 返回分页数据及总数
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
//$request->setGet('$request');
|
||||
$raw = $this->service->serviceList($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'indexFormat')
|
||||
? [$this, 'indexFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function indexFormat($data)
|
||||
{
|
||||
if(isset($data['id'])){
|
||||
$data['return_item'] = InvRequisitionItemReturn::where('return_id', $data['id'])->get();
|
||||
foreach ($data['return_item'] as $key => $value) {
|
||||
$data['return_item'][$key]['item'] = InvItem::where('id', $value['item_id'])->get();
|
||||
}
|
||||
}
|
||||
if (isset($data['return_at']))
|
||||
$data['return_at'] = date('Y-m-d H:i:s', $data['return_at']);
|
||||
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 请求对象
|
||||
* @param int|string $id 资源 ID
|
||||
* @return mixed 返回资源详情数据
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
try {
|
||||
$raw = $this->service->serviceShow($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'showFormat')
|
||||
? [$this, 'showFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function showFormat($data)
|
||||
{
|
||||
foreach ($data['item_return'] as $key => $item){
|
||||
$data['item_return'][$key]['requisition_item'] = InvRequisitionItem::where('id', $item['requisition_item_id'])->first();
|
||||
}
|
||||
if (isset($data['return_at']))
|
||||
$data['return_at'] = date('Y-m-d H:i:s', $data['return_at']);
|
||||
if (isset($data['id'])){
|
||||
//归还附件
|
||||
$data['return_attachment'] = InvRequisitionItemReturnAttachment::where('return_id', $data['id'])->get();
|
||||
$ossservice = new OssService();
|
||||
foreach ($data['return_attachment'] as $item){
|
||||
$item['file_path_pull'] = $ossservice->generatePrivateUrl($item['file_path']);
|
||||
|
||||
}
|
||||
|
||||
//归还审核附件
|
||||
$data['return_approve_attachment'] = InvRequisitionItemReturnAttachment::where('return_id', $data['id'])->get();
|
||||
foreach ($data['return_approve_attachment'] as $item){
|
||||
$item['file_path_pull'] = $ossservice->generatePrivateUrl($item['file_path']);
|
||||
}
|
||||
}
|
||||
/*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 update(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'update');
|
||||
$res = $this->service->serviceUpdate($request, $data);
|
||||
return $this->success([], '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物品归还确认
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function returnItemConfirm(Request $request)
|
||||
{
|
||||
try {
|
||||
// 验证申请数据
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm');
|
||||
// 处理申请逻辑
|
||||
$result = $this->service->serviceReturnItemConfirm($request, $data, 'returnConfirm');
|
||||
|
||||
return $this->success($result, '申请提交成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 物品归还确认
|
||||
*
|
||||
* @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\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;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\traits\ResponseTrait;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use app\common\utils\SystemAuth;
|
||||
use app\common\utils\Jwt;
|
||||
use app\common\model\User;
|
||||
use app\common\model\Systems;
|
||||
use support\Redis;
|
||||
|
||||
class SystemController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $noNeedLogin = ['auth'];
|
||||
|
||||
protected $noNeedHeaderData = ['auth'];
|
||||
/**
|
||||
* 系统间认证接口
|
||||
* POST /api/system/auth
|
||||
*/
|
||||
public function auth(Request $request): Response
|
||||
{
|
||||
$userId = $request->post('user_id');
|
||||
$timestamp = (int)$request->post('timestamp');
|
||||
$signature = $request->post('signature');
|
||||
$systemCode = $request->post('system_code');
|
||||
|
||||
// 参数验证
|
||||
if (empty($userId) || empty($timestamp) || empty($signature)) {
|
||||
return $this->error('参数不完整', 400);
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
$secret = config('system_auth.shared_secret');
|
||||
$expire = config('system_auth.signature_expire', 600);
|
||||
|
||||
// 验证时间戳(10分钟有效期)
|
||||
if (!SystemAuth::verifyTimestamp($timestamp, $expire)) {
|
||||
return $this->error('请求已过期', 401);
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if (!SystemAuth::verifySignature($userId, $timestamp, $signature, $secret)) {
|
||||
return $this->error('签名验证失败', 401);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
return $this->error('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 验证系统(如果提供了 system_code)
|
||||
$system = null;
|
||||
if ($systemCode) {
|
||||
$system = Systems::where('code', $systemCode)->first();
|
||||
if (!$system) {
|
||||
return $this->error('系统不存在', 404);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 corp_abbr(优先从请求参数获取,否则通过用户 corp_id 查询)
|
||||
$corpAbbr = $request->post('corp_abbr');
|
||||
if (!$corpAbbr && $user->corp_id) {
|
||||
$corp = \app\common\model\Corp::find($user->corp_id);
|
||||
$corpAbbr = $corp ? $corp->abbr : '';
|
||||
}
|
||||
|
||||
// 构建 JWT payload
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
|
||||
// 生成 access_token
|
||||
$jwt = new Jwt();
|
||||
$token = $jwt->generateToken($payload);
|
||||
|
||||
// 生成 refresh_token
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $systemCode ?? '');
|
||||
|
||||
// 获取 home_url
|
||||
$homeUrl = $system ? $this->getHome($system->id, $payload) : '';
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $systemCode ?? '');
|
||||
|
||||
// 构建用户信息(去除敏感信息)
|
||||
$userInfo = [
|
||||
'id' => $user->id,
|
||||
'sn' => $user->sn ?? '',
|
||||
'name' => $user->name ?? '',
|
||||
'avatar' => $user->avatar ?? '',
|
||||
'phone' => $user->phone ?? '',
|
||||
'email' => $user->email ?? '',
|
||||
'sex' => $user->sex ?? '',
|
||||
'position' => $user->position ?? '',
|
||||
'status' => $user->status ?? 1,
|
||||
'corp_id' => $user->corp_id,
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'system_code' => $systemCode ?? '',
|
||||
'corp_abbr' => $corpAbbr ?? '',
|
||||
'login_type' => 'system',
|
||||
'home_url' => $homeUrl,
|
||||
'user_info' => $userInfo,
|
||||
], '认证成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 JWT payload
|
||||
*/
|
||||
private function buildJwtPayload(User $user, ?Systems $system): array
|
||||
{
|
||||
$now = time();
|
||||
$exp = $now + config('jwt.jwt_expiration');
|
||||
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
|
||||
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
|
||||
$payload = [
|
||||
'sub' => 'user_' . $user->id,
|
||||
'aud' => $system->code,
|
||||
'iat' => $now,
|
||||
'exp' => $exp,
|
||||
'nbf' => $now,
|
||||
'typ' => 'access',
|
||||
'uid' => $user->id,
|
||||
'uname' => $user->name,
|
||||
'role_ids' => $role_ids,
|
||||
'system_ids' => $system_ids,
|
||||
// 'user_id' => $user->id,
|
||||
// 'sn' => $user->sn ?? '',
|
||||
// 'name' => $user->name ?? '',
|
||||
];
|
||||
|
||||
if ($system) {
|
||||
$payload['system_code'] = $system->code;
|
||||
$payload['system_id'] = $system->id;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 refresh_token
|
||||
*/
|
||||
private function generateRefreshToken(int $userId, string $systemCode): string
|
||||
{
|
||||
$refreshToken = bin2hex(random_bytes(32));
|
||||
|
||||
// 存储到 Redis
|
||||
$key = "refresh_token:{$refreshToken}";
|
||||
$expire = config('jwt.jwt_refresh_token_expiration');
|
||||
Redis::setex($key, $expire, json_encode([
|
||||
'user_id' => $userId,
|
||||
'system_code' => $systemCode,
|
||||
]));
|
||||
|
||||
return $refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户首页地址
|
||||
*/
|
||||
private function getHome(int $systemId, array $payload): string
|
||||
{
|
||||
// 简单实现,可以根据实际需求调整
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户token到 Redis 和 MySQL
|
||||
*/
|
||||
private function recordUserToken(int $userId, string $token, string $systemCode): void
|
||||
{
|
||||
$request = request();
|
||||
$ip = $request ? $request->getRealIp() : '';
|
||||
$ua = $request ? $request->header('user-agent', '') : '';
|
||||
$now = time();
|
||||
$expireAt = $now + config('jwt.jwt_expiration');
|
||||
|
||||
// Redis Set 用户token集合
|
||||
Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token);
|
||||
Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration'));
|
||||
|
||||
// Redis Hash token元信息
|
||||
$meta = [
|
||||
'system_code' => $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'login_time' => $now,
|
||||
'expired_at' => $expireAt,
|
||||
];
|
||||
Redis::hMSet("user_token_meta:{$token}", $meta);
|
||||
Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
|
||||
|
||||
// MySQL 持久化
|
||||
\app\common\model\UserTokenLog::create([
|
||||
'user_id' => $userId,
|
||||
'token' => $token,
|
||||
'system_code' => $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'expired_at' => $expireAt,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class UploadController
|
||||
|
||||
$result = Storage::adapter('oss')
|
||||
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
|
||||
->size(1024*1024*5)
|
||||
->size(1024*1024*50)
|
||||
->extYes($mimes)
|
||||
->upload($file);
|
||||
|
||||
@@ -133,7 +133,7 @@ class UploadController
|
||||
|
||||
$result = Storage::adapter('oss')
|
||||
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
|
||||
->size(1024*1024*5)
|
||||
->size(1024*1024*50)
|
||||
->extYes(['image/jpeg','image/png'])
|
||||
->extNo(['image/gif'])
|
||||
->upload($file);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\UsedcarCrawlResultsService;
|
||||
|
||||
class UsedcarCrawlResultsController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new UsedcarCrawlResultsService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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\UsedcarCrawlResultsImagesService;
|
||||
|
||||
class UsedcarCrawlResultsImagesController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new UsedcarCrawlResultsImagesService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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,
|
||||
@@ -539,23 +548,30 @@ class AuthLogic
|
||||
|
||||
public function getHome($systemId, $data)
|
||||
{
|
||||
// 2025.10.30 临时处理 ====== start
|
||||
if ($systemId == 1) {
|
||||
if (in_array(1, $data['role_ids'])) {
|
||||
return '/dashboard/console';
|
||||
}
|
||||
if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) {
|
||||
return '/dashboard/console/backoffice';
|
||||
}
|
||||
if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) {
|
||||
return '/dashboard/console/finance';
|
||||
}
|
||||
// // 2025.10.30 临时处理 ====== start
|
||||
// if ($systemId == 1) {
|
||||
// if (in_array(1, $data['role_ids'])) {
|
||||
// return '/dashboard/console';
|
||||
// }
|
||||
// if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) {
|
||||
// return '/dashboard/console/backoffice';
|
||||
// }
|
||||
// if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) {
|
||||
// return '/dashboard/console/finance';
|
||||
// }
|
||||
// }
|
||||
// // 2025.10.30 临时处理 ====== end
|
||||
//
|
||||
// $info = Resource::where('system_id', $systemId)
|
||||
// ->where('is_home', 1)
|
||||
// ->first();
|
||||
|
||||
$resources_id = (new UserDashboardRuleLogic)->getUserDashboard($data['uid']);
|
||||
if(!empty($resources_id)){
|
||||
$info = Resource::where('system_id', $systemId)
|
||||
->where('id', $resources_id)
|
||||
->first();
|
||||
}
|
||||
// 2025.10.30 临时处理 ====== end
|
||||
|
||||
$info = Resource::where('system_id', $systemId)
|
||||
->where('is_home', 1)
|
||||
->first();
|
||||
|
||||
if (!$info) {
|
||||
return '/dashboard/console';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InquiryTransmitConfig;
|
||||
|
||||
class InquiryTransmitConfigLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryTransmitConfig::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InquiryTransmit;
|
||||
|
||||
class InquiryTransmitLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryTransmit::class;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -23,6 +23,13 @@ class PermLogic
|
||||
}
|
||||
|
||||
$all = collect();
|
||||
//获取仪表盘
|
||||
$resources_id = (new UserDashboardRuleLogic)->getUserDashboard($userId);
|
||||
if(!empty($resources_id)){
|
||||
$resources = Resource::where('id', $resources_id)->get();
|
||||
$all = $all->merge($resources);
|
||||
}
|
||||
|
||||
// 遍历用户的所有角色,合并每个角色所拥有的资源
|
||||
foreach ($user->roles as $role) {
|
||||
$all = $all->merge(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
|
||||
class UsedcarCrawlResultsImagesLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UsedcarCrawlResultsImages::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\UsedcarCrawlResults;
|
||||
|
||||
class UsedcarCrawlResultsLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UsedcarCrawlResults::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserGroup;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\UserDashboardRule;
|
||||
|
||||
class UserDashboardRuleLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UserDashboardRule::class;
|
||||
|
||||
public function getUserDashboard($userId)
|
||||
{
|
||||
$user = User::with('reportToInfo')->find($userId);;
|
||||
//获取用户所在分组
|
||||
$ugm = UserGroupMembers::where('user_id', $userId)->where('status', 1)->get();
|
||||
$group_id = isset($ugm->group_id)?$ugm->group_id:0;
|
||||
|
||||
$roleRule = UserDashboardRule::orWhere(function ($query) use ($user) {
|
||||
$query->whereIn('related', $user->roles->pluck('id'))
|
||||
->where('rule_type', 'role');
|
||||
})
|
||||
->orWhere(function ($query) use ($userId) {
|
||||
$query->where('related', $userId)
|
||||
->where('rule_type', 'user');
|
||||
})
|
||||
->orWhere(function ($query) use ($group_id) {
|
||||
$query->where('related', $group_id)
|
||||
->where('rule_type', 'group');
|
||||
})
|
||||
->orderBy('weight','desc')
|
||||
->orderBy('rule_type','desc')
|
||||
->first();
|
||||
|
||||
$resources_id = $roleRule?$roleRule->resources_id:null;
|
||||
return $resources_id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class EmailMailbox extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_mailbox';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'channel_id', 'corp_id', 'email', 'imap_host', 'imap_port', 'imap_flags', 'mailbox', 'username', 'password', 'status', 'last_uid', 'last_fetch_at', 'remark'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class EmailPull extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_pull';
|
||||
|
||||
/**
|
||||
* 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', 'email', 'uid', 'subject', 'from', 'date', 'body', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class EmailPullParsed extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_pull_parsed';
|
||||
|
||||
/**
|
||||
* 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', 'email_id', 'date', 'email', 'name', 'tel', 'company', 'country', 'form_url', 'message', 'ip', 'iplocation', 'ilang', 'mailto', 'navigator_language', 'navigator_platform', 'screen_width', 'screen_height', 'ucountry', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class EmailPullUids extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_pull_uids';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['uid', 'fetched_at', 'status', 'processed_at', 'email'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InquiryTransmit extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inquiry_transmit';
|
||||
|
||||
/**
|
||||
* 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', 'date', 'email', 'name', 'tel', 'company', 'country', 'domain', 'form_url', 'message', 'ip', 'ip_location', 'ilang', 'mailto', 'navigator_language', 'navigator_platform', 'screen_width', 'screen_height', 'uCountry', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InquiryTransmitConfig extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inquiry_transmit_config';
|
||||
|
||||
/**
|
||||
* 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', 'domain', 'mailto', 'lang', 'status', 'remark', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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', 'allow_qr_claim', 'qr_code', 'qr_code_num', 'qr_code_url',
|
||||
'stock_qty', 'manage_user_id', 'location', 'remark',
|
||||
'sort', 'status',
|
||||
'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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', 'claim_type', 'qr_claim_time', 'proxy_claim_user_id', 'proxy_claim_user_name', 'is_returned', 'remark', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
// 领取方式常量
|
||||
const CLAIM_TYPE_NORMAL = 1; // 正常申请
|
||||
const CLAIM_TYPE_QR = 2; // 扫码领取
|
||||
const CLAIM_TYPE_PROXY = 3; // 管理员代领
|
||||
|
||||
/**
|
||||
* 领取方式文本
|
||||
*/
|
||||
public static function getClaimTypeText($type)
|
||||
{
|
||||
return [
|
||||
self::CLAIM_TYPE_NORMAL => '正常申请',
|
||||
self::CLAIM_TYPE_QR => '扫码领取',
|
||||
self::CLAIM_TYPE_PROXY => '管理员代领',
|
||||
][$type] ?? '未知';
|
||||
}
|
||||
|
||||
public function approval()
|
||||
{
|
||||
return $this->hasMany('app\common\model\InvRequisitionApprove',
|
||||
'requisition_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 领用明细关联
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany('app\common\model\InvRequisitionItem',
|
||||
'requisition_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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 = [];
|
||||
|
||||
public function requisition()
|
||||
{
|
||||
return $this->belongsTo('app\common\model\InvRequisition',
|
||||
'requisition_id','id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionApproveAttachment 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_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', 'approve_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 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,57 @@
|
||||
<?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 = [];
|
||||
|
||||
/**
|
||||
* 关联领用申请主表
|
||||
*/
|
||||
public function requisition()
|
||||
{
|
||||
return $this->belongsTo('app\common\model\InvRequisition', 'requisition_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -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', 'return_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 = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InvRequisitionReturnApproveAttachment 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_approve_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 = [];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* 物品库存变化记录模型
|
||||
*/
|
||||
class InvStockLog extends Model
|
||||
{
|
||||
protected $table = 'inv_stock_log';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'item_id',
|
||||
'item_name_snap',
|
||||
'change_type',
|
||||
'change_qty',
|
||||
'before_qty',
|
||||
'after_qty',
|
||||
'biz_type',
|
||||
'biz_no',
|
||||
'operator_id',
|
||||
'operator_name',
|
||||
'remark',
|
||||
'updated_at',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
public const CHANGE_TYPE_IN = 1; // 入库
|
||||
public const CHANGE_TYPE_OUT_CLAIM = 2; // 出库(领用)
|
||||
public const CHANGE_TYPE_OUT_LOSS = 3; // 出库(损耗)
|
||||
public const CHANGE_TYPE_IN_RETURN = 4; // 入库(归还)
|
||||
|
||||
public const BIZ_TYPE_PURCHASE = 1; // 采购入库
|
||||
public const BIZ_TYPE_QR_CLAIM = 2; // 扫码领取
|
||||
public const BIZ_TYPE_PROXY_CLAIM = 3; // 管理员代领
|
||||
public const BIZ_TYPE_NORMAL_CLAIM = 4; // 正常领用(审批后)
|
||||
public const BIZ_TYPE_RETURN = 5; // 归还入库
|
||||
public const BIZ_TYPE_LOSS = 6; // 损耗出库
|
||||
|
||||
/**
|
||||
* 变动类型文本
|
||||
*/
|
||||
public static function getChangeTypeText($type)
|
||||
{
|
||||
return [
|
||||
self::CHANGE_TYPE_IN => '入库',
|
||||
self::CHANGE_TYPE_OUT_CLAIM => '出库(领用)',
|
||||
self::CHANGE_TYPE_OUT_LOSS => '出库(损耗)',
|
||||
self::CHANGE_TYPE_IN_RETURN => '入库(归还)',
|
||||
][$type] ?? '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务类型文本
|
||||
*/
|
||||
public static function getBizTypeText($type)
|
||||
{
|
||||
return [
|
||||
self::BIZ_TYPE_PURCHASE => '采购入库',
|
||||
self::BIZ_TYPE_QR_CLAIM => '扫码领取',
|
||||
self::BIZ_TYPE_PROXY_CLAIM => '管理员代领',
|
||||
self::BIZ_TYPE_NORMAL_CLAIM => '正常领用',
|
||||
self::BIZ_TYPE_RETURN => '归还入库',
|
||||
self::BIZ_TYPE_LOSS => '损耗出库',
|
||||
][$type] ?? '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联物品
|
||||
*/
|
||||
public function item()
|
||||
{
|
||||
return $this->belongsTo(InvItem::class, 'item_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UsedcarCrawlResults extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'usedcar_crawl_results';
|
||||
|
||||
/**
|
||||
* 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', 'used_car_id', 'car_id', 'url', 'car_id_url', 'fuel_form', 'classify_energy', 'brand_name', 'series_name', 'car_name', 'year', 'title', 'price', 'new_price', 'price_difference', 'usage_mileage', 'condition_desc', 'images', 'used_detail', 'config_detail', 'format_shop_info', 'format_car_info', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UsedcarCrawlResultsImages extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'usedcar_crawl_results_images';
|
||||
|
||||
/**
|
||||
* 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', 'uc_id', 'used_car_id', 'original_name', 'file_path', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UserDashboardRule extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'user_dashboard_rule';
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\common\validate\ApprovalRecordsValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\ApprovalRecords;
|
||||
@@ -56,16 +58,62 @@ class ApprovalRecordsService
|
||||
case 'todo':
|
||||
|
||||
if ($p['scene'] === 'todo') {
|
||||
if (!in_array(1, $request->user->role_ids)) {
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
// if (!in_array(1, $request->user->role_ids)) {
|
||||
// $config['where']['approver_id'] = $request->user->uid;
|
||||
// }
|
||||
$roles_id = array_intersect([1,6], $request->user->role_ids);
|
||||
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']['approver_id'] = ['in',$report];
|
||||
} elseif (in_array(5, $request->user->role_ids)){ //内勤
|
||||
$userGroupMember = UserGroupMembers::where('user_id',$request->user->uid)
|
||||
->where('is_leader',1)
|
||||
->first();
|
||||
if($userGroupMember){
|
||||
$report = UserGroupMembers::where('group_id',$userGroupMember->group_id)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
$report = array_merge($report, [$request->user->uid]);
|
||||
$config['where']['approver_id'] = ['in',$report];
|
||||
}else{ //内勤没有负责的组时
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
}
|
||||
} else {
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
}
|
||||
}
|
||||
$config['where']['status'] = 'pending';
|
||||
$config['where']['instance.status'] = 'processing';
|
||||
}
|
||||
|
||||
if ($p['scene'] === 'history') {
|
||||
if (!in_array(1, $request->user->role_ids)) {
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
// if (!in_array(1, $request->user->role_ids) && !in_array(5, $request->user->role_ids)) {
|
||||
// $config['where']['approver_id'] = $request->user->uid;
|
||||
// }
|
||||
$roles_id = array_intersect([1,6], $request->user->role_ids);
|
||||
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']['approver_id'] = ['in',$report];
|
||||
} elseif (in_array(5, $request->user->role_ids)){ //内勤
|
||||
$userGroupMember = UserGroupMembers::where('user_id',$request->user->uid)
|
||||
->where('is_leader',1)
|
||||
->first();
|
||||
if($userGroupMember){
|
||||
$report = UserGroupMembers::where('group_id',$userGroupMember->group_id)
|
||||
->pluck('user_id')
|
||||
->toArray();
|
||||
$report = array_merge($report, [$request->user->uid]);
|
||||
$config['where']['approver_id'] = ['in',$report];
|
||||
}else{ //内勤没有负责的组时
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
}
|
||||
} else {
|
||||
$config['where']['approver_id'] = $request->user->uid;
|
||||
}
|
||||
}
|
||||
$config['where']['status'] = ['!=', 'pending'];
|
||||
}
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,4 +86,20 @@ class AuthService
|
||||
{
|
||||
(new AuthLogic())->blockToken($token, $systemCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取jsapi配置
|
||||
* @param string $corpAbbr
|
||||
* @param string $systemCcode
|
||||
* @param string $url 当前页面完整 URL(不包含 # 后面的内容)
|
||||
* @return array
|
||||
*/
|
||||
public function getJsApiConfig(string $corpAbbr, string $systemCcode, string $url)
|
||||
{
|
||||
$logic = new AuthLogic();
|
||||
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
|
||||
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
|
||||
$jsApi = $feishu->getJsApiConfig($url);
|
||||
return $jsApi;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ class CrawlService
|
||||
{
|
||||
public function brandSeries($brand)
|
||||
{
|
||||
$url = "https://www.dongchedi.com/search?currTab=6&keyword=".$brand;
|
||||
$url = "https://www.dongchedi.com/search?currTab=6&keyword=".$brand.'&from=search_tab_er&api_param={"en_qc":+0}';
|
||||
$ql = QueryList::get($url );
|
||||
$series = [];
|
||||
$ql->find('div.card-name')->texts()->map(function($item) use(&$series) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InquiryTransmitConfigValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InquiryTransmitConfig;
|
||||
use support\Request;
|
||||
|
||||
class InquiryTransmitConfigService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryTransmitConfig::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InquiryTransmitConfigValidate::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']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InquiryTransmitValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InquiryTransmit;
|
||||
use support\Request;
|
||||
|
||||
class InquiryTransmitService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryTransmit::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InquiryTransmitValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/** 创建 */
|
||||
public function serviceStore(Request $request, array $data = [], string $domain = '')
|
||||
{
|
||||
$data['email'] = $data['Email'];
|
||||
$data['name'] = $data['Name'];
|
||||
$data['tel'] = $data['Tel'];
|
||||
$data['company'] = $data['Company'];
|
||||
$data['country'] = $data['Country'];
|
||||
$data['form_Url'] = $data['Form Url'];
|
||||
$data['message'] = $data['Message'];
|
||||
$data['ip_location'] = $data['IpLocation'];
|
||||
$data['ip'] = $data['Ip'];
|
||||
$data['domain'] = $domain;
|
||||
|
||||
return InquiryTransmit::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 CrudUtil 查询配置
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
// 域名
|
||||
if (isset($p['domain']) && !empty($p['domain'])) {
|
||||
$config['where']['domain'] = $p['domain'];
|
||||
}
|
||||
// 关键字
|
||||
if (isset($p['keyword']) && $p['keyword']) {
|
||||
$k = '%' . $p['keyword'] . '%';
|
||||
foreach (['email', 'name', 'tel', 'country', 'message', 'form_url'] as $col) {
|
||||
$config['orWhere'][] = [$col, 'like', $k];
|
||||
}
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\InvItemLogic;
|
||||
use app\common\model\User;
|
||||
use app\common\validate\InvItemValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvItem;
|
||||
use support\Redis;
|
||||
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['stock_qty'] = $data['stock_qty']??1;
|
||||
$data['location'] = '';
|
||||
|
||||
Redis::set('inv_store_u_stock', $request->user->uid, 'EX', 30);
|
||||
|
||||
return $data;
|
||||
}
|
||||
public function afterServiceStore($res)
|
||||
{
|
||||
$uid = redis::get('inv_store_u_stock');
|
||||
$name = $uid ? User::where('id', $uid)->value('name') : '';
|
||||
Redis::del('inv_store_u_stock');
|
||||
// 记录库存变化日志
|
||||
$this->recordStockLog([
|
||||
'item_id' => $res['id'],
|
||||
'item_name_snap' => $res['item_name'],
|
||||
'change_qty' => $res['stock_qty'],
|
||||
'before_qty' => 0,
|
||||
'after_qty' => $res['stock_qty'],
|
||||
'biz_type' => 1,
|
||||
'biz_no' => '',
|
||||
'operator_id' => $uid,
|
||||
'operator_name' => $name,
|
||||
'remark' => '初始数量',
|
||||
], $res['stock_qty']);
|
||||
}
|
||||
|
||||
public function beforeServiceUpdate($data, Request $request)
|
||||
{
|
||||
$uid = $request->user->uid;
|
||||
$name = $uid ? User::where('id', $uid)->value('name') : '';
|
||||
$item = InvItem::where('id', $data['id'])->first();
|
||||
$change_qty = $data['stock_qty'] - $item['stock_qty'];
|
||||
// 记录库存变化日志
|
||||
$this->recordStockLog([
|
||||
'item_id' => $data['id'],
|
||||
'item_name_snap' => $data['item_name'],
|
||||
'change_qty' => $change_qty,
|
||||
'before_qty' => $item['stock_qty'],
|
||||
'after_qty' => $data['stock_qty'],
|
||||
'biz_type' => 1,
|
||||
'biz_no' => '',
|
||||
'operator_id' => $uid,
|
||||
'operator_name' => $name,
|
||||
'remark' => '管理员修改数量',
|
||||
], $change_qty);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变动物品库存数量
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param int $changeQty 变动数量:>0 入库,<0 出库
|
||||
* @param bool $allowNegative 是否允许库存变成负数,默认不允许
|
||||
* @param array $options 额外选项(用于记录日志)
|
||||
* - change_type: 变动类型
|
||||
* - biz_type: 业务类型
|
||||
* - biz_no: 单据号
|
||||
* - operator_id: 操作人ID
|
||||
* - operator_name: 操作人姓名
|
||||
* - remark: 备注
|
||||
* @return InvItem 最新的物品记录
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false, array $options = []): InvItem
|
||||
{
|
||||
if ($changeQty === 0) {
|
||||
throw new BusinessException('库存变动数量不能为 0');
|
||||
}
|
||||
|
||||
// 获取变动前的库存(用于日志)
|
||||
$itemBefore = InvItem::query()->find($itemId);
|
||||
if (!$itemBefore) {
|
||||
throw new BusinessException('物品不存在');
|
||||
}
|
||||
$beforeQty = $itemBefore->stock_qty;
|
||||
|
||||
$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) 或者库存不足(不允许负数时)
|
||||
throw new BusinessException('库存不足,无法完成库存变动');
|
||||
}
|
||||
|
||||
// 再查一次最新数据返回(在外层事务中也会保持一致)
|
||||
/** @var InvItem $item */
|
||||
$item = InvItem::query()->find($itemId);
|
||||
|
||||
// 记录库存变化日志
|
||||
$this->recordStockLog([
|
||||
'item_id' => $itemId,
|
||||
'item_name_snap' => $item->item_name,
|
||||
'change_qty' => $changeQty,
|
||||
'before_qty' => $beforeQty,
|
||||
'after_qty' => $item->stock_qty,
|
||||
'change_type' => $options['change_type'] ?? null,
|
||||
'biz_type' => $options['biz_type'] ?? null,
|
||||
'biz_no' => $options['biz_no'] ?? null,
|
||||
'operator_id' => $options['operator_id'] ?? null,
|
||||
'operator_name' => $options['operator_name'] ?? null,
|
||||
'remark' => $options['remark'] ?? null,
|
||||
], $changeQty);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库(减库存,默认不允许负数)
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param int $qty 出库数量
|
||||
* @param bool $allowNegative 是否允许负数
|
||||
* @param array $options 日志选项(同 changeStock)
|
||||
* @return InvItem
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function stockOut(int $itemId, int $qty, bool $allowNegative = false, array $options = []): InvItem
|
||||
{
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('出库数量必须大于 0');
|
||||
}
|
||||
// 确定变动类型:2-出库(领用)或 3-出库(损耗)
|
||||
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
|
||||
|
||||
return $this->changeStock($itemId, -$qty, $allowNegative, array_merge($options, [
|
||||
'change_type' => $changeType,
|
||||
'remark' => $options['remark'] ?? '领用出库',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 入库(加库存)
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param int $qty 入库数量
|
||||
* @param array $options 日志选项(同 changeStock)
|
||||
* @return InvItem
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function stockIn(int $itemId, int $qty, array $options = []): InvItem
|
||||
{
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('入库数量必须大于 0');
|
||||
}
|
||||
// 确定变动类型:1-入库 或 4-入库(归还)
|
||||
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_IN;
|
||||
|
||||
return $this->changeStock($itemId, $qty, true, array_merge($options, [
|
||||
'change_type' => $changeType,
|
||||
'remark' => $options['remark'] ?? '入库',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录库存变化日志
|
||||
*
|
||||
* @param array $logData
|
||||
* @param int $changeQty 变动数量
|
||||
* @return void
|
||||
*/
|
||||
private function recordStockLog(array $logData, int $changeQty): void
|
||||
{
|
||||
try {
|
||||
// 根据变动数量确定变动类型(如果未指定)
|
||||
if (!isset($logData['change_type'])) {
|
||||
if ($changeQty > 0) {
|
||||
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_IN;
|
||||
} else {
|
||||
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
|
||||
}
|
||||
}
|
||||
|
||||
(new InvStockLogService())->record($logData);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志记录失败不影响主业务,仅记录错误
|
||||
error_log('库存日志记录失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车型list
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->logic->getList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvItem;
|
||||
use app\common\model\User;
|
||||
use app\common\validate\InvRequisitionApproveValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionApprove;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class InvRequisitionApproveService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionApprove::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionApproveValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $flowType
|
||||
* @param string $bizType
|
||||
* @param string $bizId
|
||||
* @param array $formData ['manage_user_id'=>23]//物品管理员
|
||||
* @param int $initiatorId //申请人id
|
||||
* @return array
|
||||
*/
|
||||
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');
|
||||
$now = time();
|
||||
|
||||
|
||||
$service = new \app\common\utils\approval\service\ApprovalFlowService();
|
||||
|
||||
// 解析首步审批人(支持 role_id 或 roles.name)
|
||||
$instance = (object)[
|
||||
'initiator_id' => $initiatorId,
|
||||
'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
];
|
||||
$approverIds = $service->StetApprovers($firstStep, $instance, true);
|
||||
|
||||
if(empty($approverIds)){
|
||||
throw new BusinessException('未解析到任何审批人');
|
||||
}
|
||||
$uid = array_shift($approverIds);
|
||||
$approver = User::where('id', $uid)->first();
|
||||
|
||||
$approveId = Db::table('inv_requisition_approve')->insertGetId([
|
||||
'requisition_id' => $bizId, //申请单ID
|
||||
'node_type' => $bizType,
|
||||
'approver_id' => $approver->id,
|
||||
'approver_name' => $approver->name,
|
||||
'action' => 0,
|
||||
'comment' => '',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// foreach ($approverIds as $uid) {
|
||||
// $this->insertApprovalRecord($instance, $firstIndex, $uid);
|
||||
// }
|
||||
|
||||
return ['instance_id' => $approveId];
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行审批 */
|
||||
public function approve(int $requisitionId, int $approverId, int $uid, string $action, string $comment = '', array $attachment = [])
|
||||
{
|
||||
if (!in_array($action, ['1','2'], true)) { //审批动作:1通过 2驳回
|
||||
throw new BusinessException("无效的审批操作");
|
||||
}
|
||||
|
||||
return Db::transaction(function () use ($requisitionId, $approverId, $uid, $action, $comment, $attachment) {
|
||||
// $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
|
||||
$requisition = Db::table('inv_requisition')->where('id', $requisitionId)->lockForUpdate()->first();
|
||||
if (!$requisition) throw new BusinessException("审批实例未找到");
|
||||
if ($requisition->status !== 10 && $requisition->status !== 11) {
|
||||
return ['status' => 'noop', 'msg' => '实例未处理'];
|
||||
}
|
||||
|
||||
//审批流程
|
||||
$flow = Db::table('approval_flows')->where('type', 'inv_requisition')->first();
|
||||
if (!$flow) throw new BusinessException("审批流程定义未找到");
|
||||
|
||||
$steps = json_decode($flow->steps, true);
|
||||
//$idx = (int)$requisition->current_step;
|
||||
//$step = $steps[$idx] ?? null;
|
||||
//if (!$step) throw new BusinessException("当前步骤配置错误");
|
||||
|
||||
// 找到该用户在本步的 pending 记录(加锁)
|
||||
$pendingRecord = Db::table('inv_requisition_approve')
|
||||
->where('requisition_id', $requisitionId)
|
||||
->where('id', $approverId)
|
||||
->where('action', 0)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$pendingRecord) {
|
||||
// 已处理过?
|
||||
$anyRecord = Db::table('inv_requisition_approve')
|
||||
->where('requisition_id', $requisitionId)
|
||||
->where('id', $approverId)
|
||||
->first();
|
||||
if ($anyRecord && $anyRecord->action > 0) {
|
||||
return ['status' => 'noop', 'msg' => '已处理'];
|
||||
}
|
||||
// 步骤已推进
|
||||
// if ((int)$requisition->current_step !== $idx) {
|
||||
// return ['status' => 'noop', 'msg' => '非当前审批'];
|
||||
// }
|
||||
return ['status' => 'noop', 'msg' => '无待处理记录'];
|
||||
} else if ($pendingRecord->approver_id != $uid) { //检查当前用户是否为审核人
|
||||
return ['status' => 'noop', 'msg' => '无审核权限'];
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
// 1) 更新当前人的审批记录
|
||||
Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([
|
||||
'action' => $action,
|
||||
'comment' => $comment,
|
||||
'action_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
foreach ($attachment as $attac) {
|
||||
Db::table('inv_requisition_approve_attachment')->insertGetId([
|
||||
'requisition_id' => $requisitionId,
|
||||
'approve_id' => $approverId,
|
||||
'file_name' => $attac['original_name'],
|
||||
'file_path' => $attac['file_path'],
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// 2) 已通过步骤与总步数相等则结束审核流程
|
||||
$pass = Db::table('inv_requisition_approve')
|
||||
->where('requisition_id', $requisitionId)
|
||||
->where('action', 1)
|
||||
->count();
|
||||
if($action == 1){
|
||||
if(count($steps) == $pass){
|
||||
//判断是否都是消耗品,是否需要归还
|
||||
$requisitionItem = Db::table('inv_requisition_item')
|
||||
->where('requisition_id', $requisition->id)
|
||||
->where('is_consumable_snap', 0)
|
||||
->get();
|
||||
if($requisitionItem){
|
||||
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]);
|
||||
}else{
|
||||
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>19]);
|
||||
}
|
||||
|
||||
//更改库存
|
||||
$requisitionItem = Db::table('inv_requisition_item')
|
||||
->where('requisition_id', $requisition->id)
|
||||
->get();
|
||||
foreach ($requisitionItem as $item) {
|
||||
//$invItem = InvItem::where('id', $item->item_id)->first();
|
||||
(new InvItemService())->stockOut($item->item_id,$item->approved_qty);
|
||||
}
|
||||
|
||||
// $requisition->status = 12;
|
||||
// $requisition->save();
|
||||
return ['status' => 'complete', 'msg' => '已通过'];
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 拒绝 => 结束
|
||||
if($action == 2){
|
||||
Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([
|
||||
'action' => $action,
|
||||
'comment' => $comment,
|
||||
'approved_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
//if(count($steps) == $pass){
|
||||
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>13]);
|
||||
return ['status' => 'complete', 'msg' => '拒绝'];
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
// 4) 通过 => 进入下一步
|
||||
Db::table('inv_requisition')->where('id', $requisitionId)->update(['status'=>11]);
|
||||
$requisitionItem = Db::table('inv_requisition_item')->where('requisition_id', $requisitionId)->first();
|
||||
$item_id = Db::table('inv_item')->where('id', $requisitionItem->item_id)->first();
|
||||
$formData = ['manage_user_id'=>$item_id->manage_user_id];
|
||||
|
||||
$nextIndex = $pass;
|
||||
$nextStep = $steps[$nextIndex];
|
||||
$service = new \app\common\utils\approval\service\ApprovalFlowService();
|
||||
|
||||
$instance = (object)[
|
||||
'initiator_id' => $requisition->applicant_id, //申请人id
|
||||
'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
];
|
||||
$approverIds = $service->StetApprovers($nextStep, $instance, true);
|
||||
// if($approverIds == "manage_user_id"){ //物品管理人审核步骤查询管理人
|
||||
// $requisitionItem = Db::table('inv_requisition_item')->where('id', $requisitionId)->first();
|
||||
// $invItem = Db::table('inv_requisition_item')->where('id', $requisitionItem->item_id)->first();
|
||||
//
|
||||
// if(empty($invItem)){
|
||||
// return ['status' => 'noop', 'msg' => '未查询到物品管理人'];
|
||||
// }
|
||||
// $approverIds = [$invItem->manage_user_id];
|
||||
// }
|
||||
|
||||
$approver = User::where('id', array_pop($approverIds))->first();
|
||||
Db::table('inv_requisition_approve')->insertGetId([
|
||||
'requisition_id' => $requisitionId, //申请单ID
|
||||
'node_type' => $nextStep['approvers'][0]['value'],
|
||||
'approver_id' => $approver->id,
|
||||
'approver_name' => $approver->name,
|
||||
'action' => 0,
|
||||
'comment' => '',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);;
|
||||
return ['status' => 'complete', 'msg' => '已通过'];
|
||||
});
|
||||
}
|
||||
|
||||
// public function insertApprovalRecord()
|
||||
// {
|
||||
// }
|
||||
|
||||
|
||||
/** 获取下一个有效步骤索引 */
|
||||
private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int
|
||||
{
|
||||
for ($i = $currentStepIndex + 1; $i < count($steps); $i++) {
|
||||
$s = $steps[$i];
|
||||
if (empty($s['condition'])) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,789 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvItem;
|
||||
use app\common\model\InvRequisition;
|
||||
use app\common\model\InvRequisitionItem;
|
||||
use app\common\model\InvRequisitionReturn;
|
||||
use app\common\model\InvStockLog;
|
||||
use app\common\model\User;
|
||||
use app\common\service\UserService;
|
||||
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 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) {
|
||||
$reqItemId = $reqItem['id'];
|
||||
// 检查是否有对应的新申请项数据
|
||||
if (!isset($newReqItems[$reqItemId])) {
|
||||
continue;
|
||||
}
|
||||
$item = InvItem::where('id', $reqItem['item_id'])->first();
|
||||
$manage = User::where('id',$item['manage_user_id'])->first();
|
||||
|
||||
$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' => $data['apply_remark'] ?? '',
|
||||
'handler_id' => $item['manage_user_id'] ?? 0,
|
||||
'handler_name' => $manage['name'] ?? '',
|
||||
//'attachment' => $newReqItems[$reqItemId]['attachment'],
|
||||
//'return_at' => time(),
|
||||
'return_at' => $data['return_at'],
|
||||
];
|
||||
$data['handler_id'] = $item['manage_user_id'] ?? 0;
|
||||
$data['handler_name'] = $manage['name'] ?? '';
|
||||
}
|
||||
$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);
|
||||
$item['return_id'] = $res->id;
|
||||
$item['requisition_id'] = $data['requisition_id'];
|
||||
$item['return_at'] = $data['return_at'];
|
||||
$item['apply_remark'] = $data['apply_remark'];
|
||||
$created = $this->logicCreate($item);
|
||||
}
|
||||
$this->processRequisitionItemReturnAttachment($res, $attachment);
|
||||
InvRequisition::where('id',$request->post('requisition_id'))->update(
|
||||
['status' => 14] //审批状态:10默认、11审批中、12已审批(待归还)、13拒绝、14申请归还、15归还审核中、16部分归还、17已全部归还、18已拒绝、19已审批(无需归还)
|
||||
);
|
||||
$this->postProcess('Store', $created);
|
||||
return $res;
|
||||
});
|
||||
} 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'
|
||||
// ]
|
||||
// ]
|
||||
// ];
|
||||
|
||||
try {
|
||||
$approved = InvRequisitionItemReturn::where('return_id',$data['id'])
|
||||
->where('status','>',0)
|
||||
->count();
|
||||
if ($approved > 0) {
|
||||
throw new BusinessException('已审核禁止编辑');
|
||||
}
|
||||
|
||||
// 获取所有申请项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) {
|
||||
$reqItemId = $reqItem['id'];
|
||||
// 检查是否有对应的新申请项数据
|
||||
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' => $data['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 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']);
|
||||
$items = $data['items'];
|
||||
unset($data['items']);
|
||||
|
||||
|
||||
$config = [];
|
||||
$config['model'] = InvRequisitionReturn::class;
|
||||
$config['where']['id'] = $request->post('id');
|
||||
$returnService = new InvRequisitionReturnService();
|
||||
$returnService->logicUpdateBy($config, $data);
|
||||
|
||||
InvRequisitionItemReturn::where('return_id',$data['id'])->delete();
|
||||
foreach ($items as $item) {
|
||||
$item['return_id'] = $data['id'];
|
||||
$item['requisition_id'] = $data['requisition_id'];
|
||||
$item['return_at'] = $data['return_at'];
|
||||
$item['apply_remark'] = $data['apply_remark'];
|
||||
$created = $this->logicCreate($item);
|
||||
//$this->logicUpdateBy($config, $data);
|
||||
}
|
||||
// 获取更新后的完整记录
|
||||
$updated = $returnService->logicShow($config);
|
||||
// 处理附件更新
|
||||
$this->processRequisitionItemReturnAttachment($updated, $attachment, true);
|
||||
return $this->postProcess('Update', $updated);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("更新物品归还申请失败:" . $e->getMessage().$e->getLine().$e->getFile().$e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理物品申请附件
|
||||
*
|
||||
* @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 serviceReturnItemConfirm(Request $request, array $data, string $scene = 'returnConfirm')
|
||||
{
|
||||
try {
|
||||
$attachment = $request->post('attachment') ?? [];
|
||||
unset($data['attachment']);
|
||||
// 归还确认是业务顶层方法,这里统一使用事务
|
||||
return Db::transaction(function () use ($request, $data, $scene, $attachment) {
|
||||
|
||||
// 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);
|
||||
$resultItem = $this->logicShow($config);
|
||||
InvRequisitionReturn::where('id',$resultItem['return_id'])->update(['status' => $data['status']]);
|
||||
foreach ($attachment as $attac) {
|
||||
Db::table('inv_requisition_return_approve_attachment')->insertGetId([
|
||||
'requisition_id' => $resultItem['requisition_id'],
|
||||
'return_id' => $resultItem['return_id'],
|
||||
'file_name' => $attac['original_name'],
|
||||
'file_path' => $attac['file_path'],
|
||||
'created_at' => time(),
|
||||
]);
|
||||
}
|
||||
if ($updateResult === 0) {
|
||||
throw new BusinessException('更新归还申请失败,可能记录不存在或已被处理');
|
||||
}
|
||||
|
||||
//InvRequisitionReturn::where('return_id', $data['id'])->update(['status' => $status]);
|
||||
|
||||
// 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,
|
||||
'status' => 17,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
// Eloquent::update 返回的是影响行数,0 表示未更新到任何行
|
||||
if ($affected === 0) {
|
||||
throw new BusinessException('更新领用单状态失败,可能记录不存在');
|
||||
}
|
||||
}else{
|
||||
// 非耗材未全部归还
|
||||
$affected = InvRequisition::where('id', $requisitionId)->update([
|
||||
'status' => 16,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 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 array $data
|
||||
* @param string $scene
|
||||
* @return array|bool
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function serviceReturnConfirm(Request $request, array $data, string $scene = 'returnConfirm')
|
||||
{
|
||||
try {
|
||||
if(empty($data['return_id'])) {
|
||||
$data['return_id'] = $data['requisition_item_return_id'];
|
||||
}
|
||||
$attachment = $request->post('attachment') ?? [];
|
||||
unset($data['attachment']);
|
||||
// 归还确认是业务顶层方法,这里统一使用事务
|
||||
return Db::transaction(function () use ($request, $data, $scene, $attachment) {
|
||||
|
||||
// 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. 构建查询配置,定位到这条归还记录
|
||||
$returnService = new InvRequisitionReturnService();
|
||||
$return = InvRequisitionReturn::where('id', $data['return_id'])->first();
|
||||
if (!$return) {
|
||||
throw new BusinessException('未找到对应的领用归还记录');
|
||||
}
|
||||
$requisitionId = (int)$return->requisition_id;
|
||||
$existingStatus = (int)($return->status ?? 0);
|
||||
if (in_array($existingStatus, [1, 2], true)) {
|
||||
throw new BusinessException('该归还申请已经被处理过了');
|
||||
}
|
||||
$return->status = $status;
|
||||
$return->save();
|
||||
if($status == 1){
|
||||
//更改库存
|
||||
$returnItem = Db::table('inv_requisition_item_return')
|
||||
->where('return_id', $return->id)
|
||||
->get();
|
||||
foreach ($returnItem as $item) {
|
||||
//$invItem = InvItem::where('id', $item->item_id)->first();
|
||||
(new InvItemService())->stockIn($item->item_id,$item->return_qty,['change_type'=>InvStockLog::CHANGE_TYPE_IN_RETURN,
|
||||
'biz_type'=>InvStockLog::BIZ_TYPE_RETURN,]);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存归还审核附件
|
||||
foreach ($attachment as $attac) {
|
||||
Db::table('inv_requisition_return_approve_attachment')->insertGetId([
|
||||
'requisition_id' => $requisitionId,
|
||||
'return_id' => $data['return_id'],
|
||||
'file_name' => $attac['original_name'],
|
||||
'file_path' => $attac['file_path'],
|
||||
'created_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
//查询归还明细
|
||||
$itemReturns = InvRequisitionItemReturn::where('return_id', $data['return_id'])->get();
|
||||
foreach ($itemReturns as $itemReturn) {
|
||||
$itemReturn->status = $status;
|
||||
$itemReturn->handle_remark = $data['handle_remark'];
|
||||
$itemReturn->save();
|
||||
|
||||
// 5. 若是拒绝,直接跳出(不改库存、不改已归还数量)
|
||||
if ($status === 2) {
|
||||
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
|
||||
continue;
|
||||
}
|
||||
|
||||
// 6. 审核通过:继续处理领用明细 & 库存 & 领用单状态
|
||||
// 注意:requisition_item_id、item_id、return_qty 等字段在本次确认流程中不会被修改,
|
||||
// 所以可以直接使用 $existingRecord 中的值(都是只读字段)
|
||||
$requisitionItemId = (int)$itemReturn->requisition_item_id;
|
||||
$itemId = (int)$itemReturn->item_id;
|
||||
$returnQty = (int)$itemReturn->return_qty;
|
||||
|
||||
if ($requisitionItemId <= 0 || $itemId <= 0 || $returnQty <= 0 || $requisitionId <= 0) {
|
||||
throw new BusinessException('归还申请记录缺少必要字段,无法处理');
|
||||
}
|
||||
|
||||
// 6.2 处理领用明细
|
||||
/** @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.3 处理库存(入库)
|
||||
(new InvItemService())->stockIn($itemId, $returnQty);
|
||||
}
|
||||
// 5. 若是拒绝,直接返回(不修改领用申请状态)
|
||||
if ($status === 2) {
|
||||
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
|
||||
return $return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
'status' => 17,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
// Eloquent::update 返回的是影响行数,0 表示未更新到任何行
|
||||
if ($affected === 0) {
|
||||
throw new BusinessException('更新领用单状态失败,可能记录不存在');
|
||||
}
|
||||
}else{
|
||||
// 非耗材未全部归还
|
||||
$affected = InvRequisition::where('id', $requisitionId)->update([
|
||||
'status' => 16,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 7. 返回处理结果(保持你原来的 postProcess 习惯)
|
||||
//return $this->postProcess(ucfirst($scene), $existingRecord);
|
||||
return $return;
|
||||
});
|
||||
|
||||
} 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':
|
||||
// if(isset($p['status']) && $p['status']!=''){
|
||||
// $config['where']['status'] = $p['status'];
|
||||
// }
|
||||
// if(isset($p['handler_id']) && $p['handler_id']=='self'){
|
||||
// $config['where']['applicant_id'] = $request->user->uid;
|
||||
// }else{
|
||||
// $config['where']['handler_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;
|
||||
}
|
||||
|
||||
/** 分页列表 */
|
||||
public function serviceList(Request $request, string $scene = 'index')
|
||||
{
|
||||
$InvRequisitionReturn = new InvRequisitionReturnService();
|
||||
return $InvRequisitionReturn->serviceList($request, $scene);
|
||||
}
|
||||
|
||||
/** 查看单条 */
|
||||
public function serviceShow(Request $request, string $scene = 'show')
|
||||
{
|
||||
$id = $request->get('id');
|
||||
$return = InvRequisitionReturn::where('id', $id)->first();
|
||||
$return['item_return'] = InvRequisitionItemReturn::where('return_id', $return['id'])->orderBy('id', 'desc')->get();
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成申请单号
|
||||
*
|
||||
* @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,93 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\InvRequisitionReturnValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InvRequisitionReturn;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
|
||||
class InvRequisitionReturnService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InvRequisitionReturn::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InvRequisitionReturnValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
public function serviceReturnConfirm(Request$request)
|
||||
{
|
||||
$status = $request->post('status');
|
||||
if(in_array($status,[1,2])){
|
||||
throw new BusinessException("状态不正确");
|
||||
}
|
||||
$requisitionReturn = InvRequisitionReturn::where(['id' => $request['id']])->first();
|
||||
if (!$requisitionReturn && $requisitionReturn->handler_id == $request->user->uid) {
|
||||
$requisitionReturn->status = $status;
|
||||
return $requisitionReturn->save();
|
||||
}else{
|
||||
throw new BusinessException("审核示例不正确或非物品管理员");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
*
|
||||
* @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':
|
||||
if(isset($p['status']) && $p['status']!=''){
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
if (isset($p['handler_id']) && $p['handler_id'] == 'self') {
|
||||
$config['where']['applicant_id'] = $request->user->uid;
|
||||
} else {
|
||||
$config['where']['handler_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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvItem;
|
||||
use app\common\model\InvStockLog;
|
||||
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':
|
||||
if(isset($p['status']) && $p['status']){
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
if (isset($p['applicant_id']) && $p['applicant_id'] == 'self') { //自己的领用申请
|
||||
$config['where']['applicant_id'] = $request->user->uid;
|
||||
} elseif (isset($p['applicant_id']) && $p['applicant_id']) { //指定人员的申请
|
||||
$config['where']['applicant_id'] = $p['applicant_id'];
|
||||
}
|
||||
/* else { //审批人对应的记录
|
||||
$config['with'] = ['approval'];
|
||||
$config['where']['approval.approver_id'] = $request->user->uid;
|
||||
//$config['where']['approval.action'] = 0;
|
||||
}*/
|
||||
if(isset($p['phase']) && $p['phase'] == 'approve'){//借用审批中
|
||||
$config['where']['status'] = ['in',[10,11]];
|
||||
if(isset($p['scene']) && $p['scene'] == 'approval'){
|
||||
$config['with'] = ['approval'];
|
||||
$config['where']['approval.action'] = 0;
|
||||
}
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'repaid'){//已全部归还
|
||||
$config['where']['status'] = 17;
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'unreturned'){ //未归还
|
||||
$config['where']['status'] = ['in',[11,12,13,14,15,16,18,19]];
|
||||
}
|
||||
//审核列表
|
||||
elseif(isset($p['phase']) && $p['phase'] == 'pending'){ //待审核
|
||||
$config['where']['status'] = ['in',[10,11]];
|
||||
$config['with'] = ['approval'];
|
||||
$config['where']['approval.approver_id'] = $request->user->uid;
|
||||
$config['where']['approval.action'] = 0;
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'approved'){ //已通过
|
||||
$config['where']['status'] = ['in',[11,12,14,15,16,17,18,19]];
|
||||
$config['with'] = ['approval'];
|
||||
$config['where']['approval.approver_id'] = $request->user->uid;
|
||||
$config['where']['approval.action'] = [">",0];
|
||||
}elseif(isset($p['phase']) && $p['phase'] == 'refuse'){ //拒绝,未通过
|
||||
$config['where']['status'] = ['in',[13]];
|
||||
}
|
||||
$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'] = 10;
|
||||
|
||||
//查询物品管理人是否是一个
|
||||
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();
|
||||
$appro = new \app\common\service\InvRequisitionApproveService();
|
||||
$formData = [
|
||||
'manage_user_id' => $requisition['manage_user_id'],
|
||||
];
|
||||
return $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) {
|
||||
$invitem = InvItem::where('id',$item)->first();
|
||||
$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']?? ($invitem['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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码领取物品
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function qrClaim(Request $request, array $data): array
|
||||
{
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$itemId = (int)($data['item_id'] ?? 0);
|
||||
$qty = (int)($data['qty'] ?? 1);
|
||||
|
||||
if ($itemId <= 0) {
|
||||
throw new BusinessException('物品ID无效');
|
||||
}
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('领取数量必须大于0');
|
||||
}
|
||||
|
||||
// 获取物品信息
|
||||
$item = InvItem::find($itemId);
|
||||
if (!$item) {
|
||||
throw new BusinessException('物品不存在');
|
||||
}
|
||||
|
||||
// 检查是否允许扫码领取
|
||||
if (!$item->allow_qr_claim) {
|
||||
throw new BusinessException('该物品不允许扫码领取');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
$user = (new UserService())->getUserInfo($request->user->uid, '');
|
||||
$applicantId = (int)$user['user']['id'];
|
||||
$applicantName = (string)$user['user']['name'];
|
||||
|
||||
// 检查扫码领取数量限制
|
||||
if ($item->qr_code_num > 0) {
|
||||
// 查询该用户已扫码领取该物品的数量
|
||||
$claimedQty = InvRequisitionItem::whereHas('requisition', function ($q) use ($applicantId) {
|
||||
$q->where('applicant_id', $applicantId)
|
||||
->where('claim_type', InvRequisition::CLAIM_TYPE_QR);
|
||||
})->where('item_id', $itemId)
|
||||
->sum('approved_qty');
|
||||
|
||||
if (($claimedQty + $qty) > $item->qr_code_num) {
|
||||
throw new BusinessException("扫码领取数量超限,每人最多可领取 {$item->qr_code_num} {$item->unit},已领取 {$claimedQty} {$item->unit}");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if ($item->stock_qty < $qty) {
|
||||
throw new BusinessException('库存不足,当前库存: ' . $item->stock_qty);
|
||||
}
|
||||
|
||||
// 创建领用申请记录(直接完成)
|
||||
$requisition = InvRequisition::create([
|
||||
'order_no' => $this->generateOrderNo(),
|
||||
'applicant_company_id' => $user['user']['company_id'] ?? 0,
|
||||
'dept_id' => $user['user']['dept_id'] ?? 0,
|
||||
'applicant_id' => $applicantId,
|
||||
'applicant_name' => $applicantName,
|
||||
'leader_id' => $user['user']['report_to'] ?? 0,
|
||||
'manager_id' => $item->manage_user_id,
|
||||
'borrow_reason' => '扫码领取',
|
||||
'use_start_date' => date('Y-m-d', time()),
|
||||
'use_end_date' => date('Y-m-d', time()),
|
||||
'status' => 19, // 已审批(无需归还)
|
||||
'claim_type' => InvRequisition::CLAIM_TYPE_QR,
|
||||
'qr_claim_time' => time(),
|
||||
'remark' => $data['remark'] ?? '扫码领取',
|
||||
]);
|
||||
|
||||
// 创建领用明细
|
||||
InvRequisitionItem::create([
|
||||
'requisition_id' => $requisition->id,
|
||||
'item_id' => $itemId,
|
||||
'item_name_snap' => $item->item_name,
|
||||
'spec_model_snap' => $item->spec_model,
|
||||
'unit_snap' => $item->unit,
|
||||
'is_consumable_snap' => $item->is_consumable,
|
||||
'request_qty' => $qty,
|
||||
'approved_qty' => $qty,
|
||||
'remark' => '扫码领取',
|
||||
]);
|
||||
|
||||
// 扣减库存
|
||||
(new InvItemService())->stockOut($itemId, $qty, false, [
|
||||
'biz_type' => InvStockLog::BIZ_TYPE_QR_CLAIM,
|
||||
'biz_no' => $requisition->order_no,
|
||||
'operator_id' => $applicantId,
|
||||
'operator_name' => $applicantName,
|
||||
'remark' => '扫码领取',
|
||||
]);
|
||||
|
||||
return $requisition->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员代领(支持多人)
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function proxyClaim(Request $request, array $data): array
|
||||
{
|
||||
return Db::transaction(function () use ($request, $data) {
|
||||
$itemId = (int)($data['item_id'] ?? 0);
|
||||
$qty = (int)($data['qty'] ?? 1);
|
||||
$targetUserIds = $data['target_user_id'];
|
||||
|
||||
if ($itemId <= 0) {
|
||||
throw new BusinessException('物品ID无效');
|
||||
}
|
||||
if ($qty <= 0) {
|
||||
throw new BusinessException('领取数量必须大于0');
|
||||
}
|
||||
if (empty($targetUserIds)) {
|
||||
throw new BusinessException('请选择代领用户');
|
||||
}
|
||||
|
||||
// 获取物品信息
|
||||
$item = InvItem::find($itemId);
|
||||
if (!$item) {
|
||||
throw new BusinessException('物品不存在');
|
||||
}
|
||||
|
||||
$targetUserIds = explode(',', $targetUserIds);
|
||||
|
||||
// 获取操作人(管理员)信息
|
||||
$proxyUser = (new UserService())->getUserInfo($request->user->uid, '');
|
||||
$proxyUserId = (int)$proxyUser['user']['id'];
|
||||
$proxyUserName = (string)$proxyUser['user']['name'];
|
||||
|
||||
// 验证操作人是否是物品管理员
|
||||
if ($item->manage_user_id != $proxyUserId) {
|
||||
throw new BusinessException('您不是该物品的管理员,无权代领');
|
||||
}
|
||||
|
||||
// 计算总需求量
|
||||
$totalQty = $qty * count($targetUserIds);
|
||||
|
||||
// 检查库存
|
||||
if ($item->stock_qty < $totalQty) {
|
||||
throw new BusinessException('库存不足,当前库存: ' . $item->stock_qty . ',需要: ' . $totalQty);
|
||||
}
|
||||
|
||||
$createdRequisitions = [];
|
||||
$remark = $data['remark'] ?? '管理员代领';
|
||||
|
||||
// 为每个目标用户创建领用记录
|
||||
foreach ($targetUserIds as $targetUserId) {
|
||||
$targetUserId = (int)$targetUserId;
|
||||
if ($targetUserId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取目标用户信息
|
||||
$targetUser = (new UserService())->getUserInfo($targetUserId, '');
|
||||
if (!$targetUser['user']) {
|
||||
throw new BusinessException('用户ID ' . $targetUserId . ' 不存在');
|
||||
}
|
||||
|
||||
// 创建领用申请记录(直接完成)
|
||||
$requisition = InvRequisition::create([
|
||||
'order_no' => $this->generateOrderNo(),
|
||||
'applicant_company_id' => $targetUser['user']['company_id'] ?? 0,
|
||||
'dept_id' => $targetUser['user']['dept_id'] ?? 0,
|
||||
'applicant_id' => $targetUserId,
|
||||
'applicant_name' => $targetUser['user']['name'],
|
||||
'leader_id' => $targetUser['user']['report_to'] ?? 0,
|
||||
'manager_id' => $item->manage_user_id,
|
||||
'borrow_reason' => $data['borrow_reason'] ?? '管理员代领',
|
||||
'use_start_date' => date('Y-m-d', time()),
|
||||
'use_end_date' => date('Y-m-d', time()),
|
||||
'status' => 19, // 已审批(无需归还)
|
||||
'claim_type' => InvRequisition::CLAIM_TYPE_PROXY,
|
||||
'proxy_claim_user_id' => $proxyUserId,
|
||||
'proxy_claim_user_name' => $proxyUserName,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
|
||||
// 创建领用明细
|
||||
InvRequisitionItem::create([
|
||||
'requisition_id' => $requisition->id,
|
||||
'item_id' => $itemId,
|
||||
'item_name_snap' => $item->item_name,
|
||||
'spec_model_snap' => $item->spec_model,
|
||||
'unit_snap' => $item->unit,
|
||||
'is_consumable_snap' => $item->is_consumable,
|
||||
'request_qty' => $qty,
|
||||
'approved_qty' => $qty,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
|
||||
$createdRequisitions[] = $requisition->toArray();
|
||||
}
|
||||
|
||||
// 一次性扣减总库存
|
||||
(new InvItemService())->stockOut($itemId, $totalQty, false, [
|
||||
'biz_type' => InvStockLog::BIZ_TYPE_PROXY_CLAIM,
|
||||
'biz_no' => 'PROXY_' . time(),
|
||||
'operator_id' => $proxyUserId,
|
||||
'operator_name' => $proxyUserName,
|
||||
'remark' => '管理员代领(' . count($targetUserIds) . '人×' . $qty . ')',
|
||||
]);
|
||||
|
||||
return [
|
||||
'created_count' => count($createdRequisitions),
|
||||
'requisitions' => $createdRequisitions,
|
||||
'total_qty' => $totalQty,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询扫码/代领记录列表
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function qrClaimsList(Request $request): array
|
||||
{
|
||||
$query = InvRequisition::query()->with(['items']);
|
||||
|
||||
// 只查询扫码领取和代领记录
|
||||
$query->whereIn('claim_type', [
|
||||
InvRequisition::CLAIM_TYPE_QR,
|
||||
InvRequisition::CLAIM_TYPE_PROXY
|
||||
]);
|
||||
|
||||
// 按申请人筛选(移动端"我的记录"功能)
|
||||
if ($request->get('applicant_id') === 'self') {
|
||||
$query->where('applicant_id', $request->user->uid);
|
||||
} elseif (!empty($request->get('applicant_id'))) {
|
||||
$query->where('applicant_id', $request->get('applicant_id'));
|
||||
}
|
||||
|
||||
// 按领取方式筛选
|
||||
if (!empty($request->get('claim_type'))) {
|
||||
$query->where('claim_type', $request->get('claim_type'));
|
||||
}
|
||||
|
||||
// 按代领操作人筛选
|
||||
if (!empty($request->get('proxy_user_id'))) {
|
||||
$query->where('proxy_claim_user_id', $request->get('proxy_user_id'));
|
||||
}
|
||||
|
||||
// 按物品ID筛选
|
||||
if (!empty($request->get('item_id'))) {
|
||||
$query->whereHas('items', function ($q) use ($request) {
|
||||
$q->where('item_id', $request->get('item_id'));
|
||||
});
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (!empty($request->get('start_time'))) {
|
||||
$query->where('created_at', '>=', strtotime($request->get('start_time')));
|
||||
}
|
||||
if (!empty($request->get('end_time'))) {
|
||||
$query->where('created_at', '<=', strtotime($request->get('end_time')));
|
||||
}
|
||||
|
||||
// 排序
|
||||
$query->orderBy('id', 'desc');
|
||||
|
||||
// 分页
|
||||
$perPage = $request->get('size', 20);
|
||||
$page = $request->get('current', 1);
|
||||
|
||||
$result = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
return [
|
||||
'records' => $result->getCollection()->map(function ($req) {
|
||||
return [
|
||||
'id' => $req->id,
|
||||
'order_no' => $req->order_no,
|
||||
'applicant_id' => $req->applicant_id,
|
||||
'applicant_name' => $req->applicant_name,
|
||||
'claim_type' => $req->claim_type,
|
||||
'claim_type_text' => InvRequisition::getClaimTypeText($req->claim_type),
|
||||
'qr_claim_time' => $req->qr_claim_time ? date('Y-m-d H:i:s', $req->qr_claim_time) : null,
|
||||
'proxy_claim_user_id' => $req->proxy_claim_user_id,
|
||||
'proxy_claim_user_name' => $req->proxy_claim_user_name,
|
||||
'status' => $req->status,
|
||||
'borrow_reason' => $req->borrow_reason,
|
||||
'remark' => $req->remark,
|
||||
'created_at' => $req->created_at ? $req->created_at->format('Y-m-d H:i:s') : null,
|
||||
'items' => $req->items->map(function ($item) {
|
||||
return [
|
||||
'item_id' => $item->item_id,
|
||||
'item_name_snap' => $item->item_name_snap,
|
||||
'approved_qty' => $item->approved_qty,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray(),
|
||||
'total' => $result->total(),
|
||||
'current' => $result->currentPage(),
|
||||
'size' => $result->perPage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\InvStockLog;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 物品库存变化记录服务
|
||||
*/
|
||||
class InvStockLogService
|
||||
{
|
||||
/**
|
||||
* 记录库存变化
|
||||
*
|
||||
* @param array $data 日志数据
|
||||
* @return InvStockLog
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function record(array $data): InvStockLog
|
||||
{
|
||||
$required = ['item_id', 'change_type', 'change_qty', 'before_qty', 'after_qty'];
|
||||
foreach ($required as $field) {
|
||||
if (!isset($data[$field])) {
|
||||
throw new BusinessException("库存日志缺少必需字段: {$field}");
|
||||
}
|
||||
}
|
||||
|
||||
return InvStockLog::create([
|
||||
'company_id' => $data['company_id'] ?? 0,
|
||||
'item_id' => $data['item_id'],
|
||||
'item_name_snap' => $data['item_name_snap'] ?? null,
|
||||
'change_type' => $data['change_type'],
|
||||
'change_qty' => $data['change_qty'],
|
||||
'before_qty' => $data['before_qty'],
|
||||
'after_qty' => $data['after_qty'],
|
||||
'biz_type' => $data['biz_type'] ?? null,
|
||||
'biz_no' => $data['biz_no'] ?? null,
|
||||
'operator_id' => $data['operator_id'] ?? null,
|
||||
'operator_name' => $data['operator_name'] ?? null,
|
||||
'remark' => $data['remark'] ?? null,
|
||||
'updated_at' => time(),
|
||||
'created_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询库存变化记录列表
|
||||
*
|
||||
* @param array $params 查询参数
|
||||
* @return array
|
||||
*/
|
||||
public function getList(array $params): array
|
||||
{
|
||||
$query = InvStockLog::query()->with(['item']);
|
||||
|
||||
// 物品ID筛选
|
||||
if (!empty($params['item_id'])) {
|
||||
$query->where('item_id', $params['item_id']);
|
||||
}
|
||||
|
||||
// 变动类型筛选
|
||||
if (!empty($params['change_type'])) {
|
||||
$query->where('change_type', $params['change_type']);
|
||||
}
|
||||
|
||||
// 业务类型筛选
|
||||
if (!empty($params['biz_type'])) {
|
||||
$query->where('biz_type', $params['biz_type']);
|
||||
}
|
||||
|
||||
// 单据号筛选
|
||||
if (!empty($params['biz_no'])) {
|
||||
$query->where('biz_no', 'like', '%' . $params['biz_no'] . '%');
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (!empty($params['start_time'])) {
|
||||
$query->where('created_at', '>=', strtotime($params['start_time']));
|
||||
}
|
||||
if (!empty($params['end_time'])) {
|
||||
$query->where('created_at', '<=', strtotime($params['end_time']));
|
||||
}
|
||||
|
||||
// 排序
|
||||
$query->orderBy('id', 'desc');
|
||||
|
||||
// 分页
|
||||
$perPage = $params['size'] ?? 20;
|
||||
$page = $params['current'] ?? 1;
|
||||
|
||||
$result = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
return [
|
||||
'records' => $result->getCollection()->map(function ($log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'item_id' => $log->item_id,
|
||||
'item_name' => $log->item_name_snap,
|
||||
'change_type' => $log->change_type,
|
||||
'change_type_text' => InvStockLog::getChangeTypeText($log->change_type),
|
||||
'change_qty' => $log->change_qty,
|
||||
'before_qty' => $log->before_qty,
|
||||
'after_qty' => $log->after_qty,
|
||||
'biz_type' => $log->biz_type,
|
||||
'biz_type_text' => $log->biz_type ? InvStockLog::getBizTypeText($log->biz_type) : null,
|
||||
'biz_no' => $log->biz_no,
|
||||
'operator_id' => $log->operator_id,
|
||||
'operator_name' => $log->operator_name,
|
||||
'remark' => $log->remark,
|
||||
'created_at' => date('Y-m-d H:i:s', is_int($log->created_at)?$log->created_at:strtotime($log->created_at)),
|
||||
];
|
||||
})->toArray(),
|
||||
'total' => $result->total(),
|
||||
'current' => $result->currentPage(),
|
||||
'size' => $result->perPage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 二维码生成服务
|
||||
*/
|
||||
class QrCodeService
|
||||
{
|
||||
/**
|
||||
* 生成物品二维码
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param string $appUrl 应用访问地址
|
||||
* @return string 二维码内容URL
|
||||
*/
|
||||
public function generateItemQrCode(int $itemId, string $appUrl): string
|
||||
{
|
||||
//return rtrim($appUrl, '/') . '/mobile/inv/claim/' . $itemId;
|
||||
return "https://mweb-fdesk.hnxinyouhe.com/pages/scan/external-scan?id=" . $itemId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码图片并上传到 OSS
|
||||
*
|
||||
* @param int $itemId 物品ID
|
||||
* @param string $content 二维码内容
|
||||
* @param string $fileName 文件名
|
||||
* @return array 返回OSS路径和完整URL
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function generateAndUploadQrCode(int $itemId, string $content, string $fileName): array
|
||||
{
|
||||
// 使用 endroid/qr-code 库生成二维码
|
||||
// 如果未安装,使用 API 方式生成
|
||||
|
||||
$tempPath = $this->generateQrCodeImage($content, $fileName);
|
||||
|
||||
if (!$tempPath) {
|
||||
throw new BusinessException('二维码生成失败');
|
||||
}
|
||||
|
||||
try {
|
||||
// 上传到 OSS
|
||||
$ossService = new OssService();
|
||||
$ossPath = 'qr/' . $fileName;
|
||||
$ossUrl = $ossService->uploadImage($tempPath, $ossPath);
|
||||
|
||||
// 删除临时文件
|
||||
if (file_exists($tempPath)) {
|
||||
unlink($tempPath);
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $ossPath,
|
||||
'url' => $ossUrl,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
// 删除临时文件
|
||||
if (file_exists($tempPath)) {
|
||||
unlink($tempPath);
|
||||
}
|
||||
throw new BusinessException('二维码上传失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码图片(使用 QR Server API 或本地库)
|
||||
*
|
||||
* @param string $content 二维码内容
|
||||
* @param string $fileName 文件名
|
||||
* @return string 临时文件路径,失败返回 false
|
||||
*/
|
||||
private function generateQrCodeImage(string $content, string $fileName)
|
||||
{
|
||||
$tempDir = runtime_path() . '/qr_temp/';
|
||||
if (!is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
}
|
||||
|
||||
$tempPath = $tempDir . $fileName;
|
||||
|
||||
// 方式1: 使用 endroid/qr-code 库(推荐)
|
||||
if (class_exists('\Endroid\QrCode\QrCode')) {
|
||||
return $this->generateWithEndroid($content, $tempPath);
|
||||
}
|
||||
|
||||
// 方式2: 使用在线 API(备用方案)
|
||||
return $this->generateWithApi($content, $tempPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 endroid/qr-code 生成二维码
|
||||
*/
|
||||
private function generateWithEndroid(string $content, string $tempPath): string
|
||||
{
|
||||
try {
|
||||
// 5.x 版本使用静态工厂方法
|
||||
if (method_exists(\Endroid\QrCode\QrCode::class, 'create')) {
|
||||
$qrCode = \Endroid\QrCode\QrCode::create($content)
|
||||
->setSize(300)
|
||||
->setMargin(10);
|
||||
} else {
|
||||
// 4.x 及以下版本
|
||||
$qrCode = new \Endroid\QrCode\QrCode($content);
|
||||
if (method_exists($qrCode, 'setSize')) {
|
||||
$qrCode->setSize(300);
|
||||
}
|
||||
if (method_exists($qrCode, 'setMargin')) {
|
||||
$qrCode->setMargin(10);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Writer
|
||||
if (class_exists('\Endroid\QrCode\Writer\PngWriter')) {
|
||||
$writer = new \Endroid\QrCode\Writer\PngWriter();
|
||||
} else {
|
||||
// 旧版本可能使用不同的命名空间
|
||||
$writer = new \Endroid\QrCode\Writer();
|
||||
}
|
||||
|
||||
$result = $writer->write($qrCode);
|
||||
|
||||
// 保存文件
|
||||
if (method_exists($result, 'getString')) {
|
||||
file_put_contents($tempPath, $result->getString());
|
||||
} else {
|
||||
// 更旧版本可能直接返回字符串
|
||||
file_put_contents($tempPath, $result);
|
||||
}
|
||||
|
||||
return $tempPath;
|
||||
} catch (\Throwable $e) {
|
||||
// 如果 endroid 库使用失败,回退到在线 API
|
||||
error_log('endroid/qr-code 生成失败: ' . $e->getMessage() . ',使用在线 API 备用方案');
|
||||
return $this->generateWithApi($content, $tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用在线 API 生成二维码(备用方案)
|
||||
*/
|
||||
private function generateWithApi(string $content, string $tempPath): string
|
||||
{
|
||||
$apiUrl = 'https://api.qrserver.com/v1/create-qr-code/?';
|
||||
$params = [
|
||||
'size' => '300x300',
|
||||
'data' => $content,
|
||||
'margin' => '10',
|
||||
];
|
||||
|
||||
$url = $apiUrl . http_build_query($params);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
$imageData = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !$imageData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file_put_contents($tempPath, $imageData);
|
||||
return $tempPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 OSS 上的二维码文件
|
||||
*
|
||||
* @param string $ossPath OSS路径
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteQrCode(string $ossPath): bool
|
||||
{
|
||||
try {
|
||||
$ossService = new OssService();
|
||||
return $ossService->deleteFile($ossPath);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,9 @@ class ResourceService
|
||||
$config['order']['id'] = 'asc';
|
||||
break;
|
||||
case 'menuList':
|
||||
if(isset($p['is_home']) && $p['is_home']==1){
|
||||
$config['where']['is_home'] = 1;
|
||||
}
|
||||
$config['where']['status'] = 1;
|
||||
$config['where']['type'] = ['!=', 'permission'];
|
||||
$config['order']['sort'] = 'asc';
|
||||
|
||||
@@ -227,6 +227,8 @@ class StockOrdersService
|
||||
if (!$order->save()) {
|
||||
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
|
||||
}
|
||||
|
||||
StockOrders::where('object_id', $res->object_id)->where('is_final', 1)->where('approval_status', 3)->delete();
|
||||
}
|
||||
// 启动审批
|
||||
if ($res->approval_status == 1) {
|
||||
@@ -353,6 +355,10 @@ class StockOrdersService
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, 'approval');
|
||||
$item = $this->logicShow($config);
|
||||
|
||||
if (!$item || $item->approval_status != 0) {
|
||||
throw new BusinessException('该单据已提交审批或单子不存在');
|
||||
}
|
||||
|
||||
// 检查备货单是否包含必需的文件
|
||||
$this->validateRequiredFilesForApproval($item);
|
||||
@@ -454,6 +460,12 @@ class StockOrdersService
|
||||
// 检查必填文件是否已上传
|
||||
// $this->validateRequiredFiles($data);
|
||||
|
||||
$param = $request->all();
|
||||
$info = StockOrders::find($param['id']);
|
||||
if (!$info || $info->approval_status != 0) {
|
||||
throw new BusinessException('该单不存在或状态异常,请关闭或刷新页面后重试~');
|
||||
}
|
||||
|
||||
$data['biz_date'] = strtotime($data['biz_date']);
|
||||
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
|
||||
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\UsedcarCrawlResultsImagesValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
|
||||
class UsedcarCrawlResultsImagesService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UsedcarCrawlResultsImages::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = UsedcarCrawlResultsImagesValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\UsedcarCrawlResultsValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UsedcarCrawlResults;
|
||||
|
||||
class UsedcarCrawlResultsService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UsedcarCrawlResults::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = UsedcarCrawlResultsValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\UserDashboardRuleValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UserDashboardRule;
|
||||
use support\Request;
|
||||
|
||||
class UserDashboardRuleService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UserDashboardRule::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = UserDashboardRuleValidate::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']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加前置处理
|
||||
*/
|
||||
public function beforeServiceStore($data, Request $request)
|
||||
{
|
||||
if(!isset($data['weight']) || ($data['weight'] == null || $data['weight'] == '')){
|
||||
throw new \Exception("权重不能为空");
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,27 @@ class UserService
|
||||
// $menuTree = $permService->getMenuTree($uid, $systemCode);
|
||||
// $permissionCodes = $permService->getPermissionCodes($uid, $systemCode);
|
||||
$corp_name = Corp::where('abbr', $user->corp_id)->value('name');
|
||||
|
||||
// 查找直属上级 10.13 add
|
||||
$reportToInfo = [];
|
||||
if ($user->reportToInfo) {
|
||||
$reportToInfo = [
|
||||
'id' => $user->reportToInfo->id,
|
||||
'name' => $user->reportToInfo->name
|
||||
];
|
||||
}
|
||||
|
||||
// 查找绑定内勤 10.13 add
|
||||
$officeClerkInfo = [];
|
||||
$ugm = UserGroupMembers::where('user_id', $uid)->where('status', 1)->first();
|
||||
if ($ugm) {
|
||||
$lugm = UserGroupMembers::where('group_id', $ugm->group_id)->where('is_leader', 1)->first();
|
||||
if ($lugm) {
|
||||
$officeClerkInfo['id'] = $lugm->user_id;
|
||||
$officeClerkInfo['name'] = User::where('id', $lugm->user_id)->value('name');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
@@ -50,6 +71,7 @@ class UserService
|
||||
'email' => $user->email,
|
||||
'status' => $user->status,
|
||||
'roles' => $user->roles->pluck('name'),
|
||||
'roles_id' => $user->roles->pluck('id'),
|
||||
'sex' => $user->sex,
|
||||
'sn' => $user->sn,
|
||||
'corp_id' => $user->corp_id,
|
||||
@@ -57,6 +79,8 @@ class UserService
|
||||
'report_to' => $user->report_to,
|
||||
'sign_img' => $user->sign_img,
|
||||
'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'',
|
||||
'report_to_info' => $reportToInfo,
|
||||
'office_clerk_info' => $officeClerkInfo
|
||||
],
|
||||
// 'menu' => $menuTree,
|
||||
// 'permissions' => $permissionCodes
|
||||
|
||||
@@ -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']
|
||||
];
|
||||
@@ -225,4 +225,101 @@ class Feishu
|
||||
// ];
|
||||
// return $this->requestResult($url);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获取 JSAPI 配置(签名)
|
||||
* @param string $url 当前页面完整 URL(不包含 # 后面的内容)
|
||||
* @return array
|
||||
*/
|
||||
public function getJsApiConfig($url)
|
||||
{
|
||||
// 1. 获取 tenant_access_token
|
||||
//$accessToken = $this->getTenantAccessToken();
|
||||
$accessToken = $this->tenantAccessToken();
|
||||
|
||||
// 2. 获取 jsapi_ticket
|
||||
$ticket = $this->getJsapiTicket($accessToken);
|
||||
|
||||
// 3. 生成签名参数
|
||||
$timestamp = (int) round(microtime(true) * 1000);
|
||||
$nonceStr = $this->generateNonceStr();
|
||||
|
||||
// 4. 生成签名
|
||||
$signature = $this->generateSignature($ticket, $nonceStr, $timestamp, $url);
|
||||
|
||||
return [
|
||||
'appId' => $this->appId,
|
||||
'timestamp' => $timestamp,
|
||||
'nonceStr' => $nonceStr,
|
||||
'signature' => $signature,
|
||||
'url' => $url
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 jsapi_ticket
|
||||
* @param string $accessToken
|
||||
* @return string
|
||||
*/
|
||||
private function getJsapiTicket($accessToken)
|
||||
{
|
||||
// 检查缓存
|
||||
$cachedTicket = Cache::get($this->abbr.'-feishu:jsapi_ticket');
|
||||
// $cachedTicket = $this->getCachedData('feishu:jsapi_ticket');
|
||||
if ($cachedTicket) {
|
||||
return $cachedTicket;
|
||||
}
|
||||
|
||||
// 使用 requestResult 方法发起请求,而不是 httpGet
|
||||
$this->options["headers"] = [
|
||||
"Authorization" => "Bearer " . $accessToken,
|
||||
"Content-Type" => "application/json; charset=utf-8"
|
||||
];
|
||||
|
||||
$result = $this->requestResult('jssdk/ticket/get', 'GET');
|
||||
|
||||
if (!$result) {
|
||||
throw new BusinessException('获取 jsapi_ticket 失败: ' . $this->error());
|
||||
}
|
||||
|
||||
$ticket = $result['data']['ticket'];
|
||||
$expire = $result['data']['expire_in'] - 300; // 提前5分钟过期
|
||||
|
||||
// 缓存 ticket
|
||||
Cache::set($this->abbr.'-feishu:jsapi_ticket', $ticket, $expire);
|
||||
// $this->setCachedData('feishu:jsapi_ticket', $ticket, $expire);
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* @param string $ticket
|
||||
* @param string $nonceStr
|
||||
* @param int $timestamp
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
private function generateSignature($ticket, $nonceStr, $timestamp, $url)
|
||||
{
|
||||
// 拼接待签名字符串(注意顺序)
|
||||
$signStr = "jsapi_ticket={$ticket}&noncestr={$nonceStr}×tamp={$timestamp}&url={$url}";
|
||||
|
||||
return sha1($signStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
private function generateNonceStr($length = 16)
|
||||
{
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$str = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
class SystemAuth
|
||||
{
|
||||
/**
|
||||
* 生成签名
|
||||
* @param string $userId 用户ID
|
||||
* @param int $timestamp Unix时间戳
|
||||
* @param string $secret 共享密钥
|
||||
* @return string
|
||||
*/
|
||||
public static function generateSignature(string $userId, int $timestamp, string $secret): string
|
||||
{
|
||||
$data = $userId . $timestamp;
|
||||
return hash_hmac('md5', $data, $secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名
|
||||
* @param string $userId 用户ID
|
||||
* @param int $timestamp Unix时间戳
|
||||
* @param string $receivedSignature 接收到的签名
|
||||
* @param string $secret 共享密钥
|
||||
* @return bool
|
||||
*/
|
||||
public static function verifySignature(string $userId, int $timestamp, string $receivedSignature, string $secret): bool
|
||||
{
|
||||
$expectedSignature = self::generateSignature($userId, $timestamp, $secret);
|
||||
return hash_equals($expectedSignature, $receivedSignature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证时间戳是否在有效期内
|
||||
* @param int $timestamp Unix时间戳
|
||||
* @param int $maxSeconds 有效期(秒)
|
||||
* @return bool
|
||||
*/
|
||||
public static function verifyTimestamp(int $timestamp, int $maxSeconds = 600): bool
|
||||
{
|
||||
$now = time();
|
||||
$diff = abs($now - $timestamp);
|
||||
return $diff <= $maxSeconds;
|
||||
}
|
||||
}
|
||||
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,410 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
public function StetApprovers(array $step, object $instance, bool $avoidSelf = true){
|
||||
return$this->resolveApprovers($step, $instance, $avoidSelf);
|
||||
}
|
||||
}
|
||||
@@ -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,857 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
use app\common\service\UploadService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use QL\QueryList;
|
||||
use support\Redis;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
use Throwable;
|
||||
|
||||
class UsedCarCrawl
|
||||
{
|
||||
// 默认请求头
|
||||
private const DEFAULT_HEADERS = [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/',
|
||||
];
|
||||
|
||||
// Redis队列键名
|
||||
private const REDIS_QUEUE_KEY = 'usedcar:urls';
|
||||
|
||||
// 能源类型常量
|
||||
public const ENERGY_TYPE_RENEWABLE = 'RE';
|
||||
public const ENERGY_TYPE_CONVENTIONAL = 'CE';
|
||||
public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* 插入队列url
|
||||
*
|
||||
* @param string $url URL地址
|
||||
* @param string $scene 场景:usedcar(二手信息) config(配置信息)
|
||||
* @param int $id 记录ID
|
||||
* @return int
|
||||
*/
|
||||
public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int
|
||||
{
|
||||
$data = [
|
||||
'scene' => $scene,
|
||||
'url' => $url,
|
||||
'id' => $id,
|
||||
];
|
||||
|
||||
return Redis::lpush(
|
||||
self::REDIS_QUEUE_KEY,
|
||||
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列url
|
||||
*
|
||||
* @return array|null
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function popUrl(): ?array
|
||||
{
|
||||
$data = Redis::rPop(self::REDIS_QUEUE_KEY);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$task = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($task)) {
|
||||
throw new BusinessException('任务数据解析失败:' . json_last_error_msg());
|
||||
}
|
||||
|
||||
// 基本字段校验
|
||||
if (empty($task['url']) || empty($task['scene'])) {
|
||||
throw new BusinessException('队列任务数据缺少必要字段:url 或 scene');
|
||||
}
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行抓取任务
|
||||
*
|
||||
* @return bool
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
try {
|
||||
$task = $this->popUrl();
|
||||
|
||||
if (!$task) {
|
||||
// 队列为空,任务结束
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = (string)$task['url'];
|
||||
$scene = (string)$task['scene'];
|
||||
$id = (int)($task['id'] ?? 0);
|
||||
|
||||
$html = $this->fetchHtml($url);
|
||||
if ($html === '') {
|
||||
throw new BusinessException("页面内容为空: {$url}");
|
||||
}
|
||||
|
||||
switch ($scene) {
|
||||
case 'usedcar':
|
||||
$info = $this->parseUsedCarDetail($html, $url);
|
||||
$insertId = $this->saveUsedCarBasicInfo($info);
|
||||
|
||||
// 下载图片 + 上传 + 入库(图片失败不影响主记录)
|
||||
$imagesArray = json_decode($info['images'] ?? '[]', true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$imagesArray = [];
|
||||
}
|
||||
$saveDir = public_path('uploads/usedcar/' . $insertId);
|
||||
$this->uploadAndRemove($imagesArray, $saveDir, $info['used_car_id'], $insertId);
|
||||
|
||||
// 继续派发配置页任务
|
||||
if (!empty($info['car_id_url'])) {
|
||||
$this->pushUrl($info['car_id_url'], 'config', $insertId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
if ($id <= 0) {
|
||||
throw new BusinessException("配置任务缺少主记录ID");
|
||||
}
|
||||
|
||||
$data = $this->parseConfigDetail($html);
|
||||
$this->saveUsedCarConfigInfo($data, $id);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BusinessException("未知的任务场景: " . $scene);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (GuzzleException $e) {
|
||||
throw new BusinessException("网络请求失败: " . $e->getMessage());
|
||||
} catch (BusinessException $e) {
|
||||
// 业务异常原样抛出,方便上层知道是逻辑问题
|
||||
throw $e;
|
||||
} catch (Throwable $th) {
|
||||
// 兜底异常,避免 worker 直接挂掉
|
||||
throw new BusinessException(
|
||||
"执行任务出错: " . $th->getMessage()
|
||||
. ' | 行号: ' . $th->getLine()
|
||||
. ' | 文件: ' . $th->getFile()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网页HTML内容
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function fetchHtml(string $url): string
|
||||
{
|
||||
$client = new Client();
|
||||
$response = $client->request('GET', $url, [
|
||||
'headers' => self::DEFAULT_HEADERS,
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二手车详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @param string $url
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function parseUsedCarDetail(string $html = '', string $url = ''): array
|
||||
{
|
||||
try {
|
||||
$ql = QueryList::html($html);
|
||||
} catch (Throwable $e) {
|
||||
throw new BusinessException('解析 HTML 失败(详情页): ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$title = trim($ql->find('h1')->text());
|
||||
$price = trim($ql->find('.head-info_price-wrap__Y4bxi > p')->text());
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
if ($jsonText === '') {
|
||||
throw new BusinessException('详情页未找到 __NEXT_DATA__ JSON');
|
||||
}
|
||||
|
||||
$jsonArray = $this->decodeJson($jsonText, '详情页 __NEXT_DATA__');
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID / 年份
|
||||
$carInfo = $jsonArray['props']['pageProps']['skuDetail']['car_info'] ?? [];
|
||||
$carId = $carInfo['car_id'] ?? 0;
|
||||
$year = (int)($carInfo['year'] ?? 0);
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型(两个字段择一)
|
||||
$overview = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview'] ?? [];
|
||||
$fuelForm = $overview['power']['fuel_form'] ?? '';
|
||||
$newFuelForm = $overview['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = $importantText ? explode(' | ', $importantText) : [];
|
||||
$usageMileage = '';
|
||||
if (isset($importantTextArray[1])) {
|
||||
$usageMileage = str_replace('万公里', '', $importantTextArray[1]);
|
||||
}
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $carInfo['brand_name'] ?? '';
|
||||
$seriesName = $carInfo['series_name'] ?? '';
|
||||
$carName = $carInfo['car_name'] ?? '';
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? '',
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
$displacement = $this->getParamValue($otherParams, '排量');
|
||||
$gearbox = $this->getParamValue($otherParams, '变速箱');
|
||||
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox,
|
||||
];
|
||||
|
||||
// 提取新车指导价 / 比新车省金额
|
||||
$newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u');
|
||||
$priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText && preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
|
||||
return [
|
||||
'used_car_id' => $this->extractIdFromUrl($url),
|
||||
'car_id' => $carId,
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
'price' => str_replace('万', '', $this->decodePrice($price)),
|
||||
'new_price' => str_replace('万', '', $newPrice),
|
||||
'price_difference' => str_replace('万', '', $priceDifference),
|
||||
'condition_desc' => $conditionDesc,
|
||||
'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'car_id_url' => $carIdUrl,
|
||||
'used_detail' => $jsonText,
|
||||
'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'fuel_form' => $resultFuelForm,
|
||||
'classify_energy' => $this->detectEnergyType($resultFuelForm),
|
||||
'usage_mileage' => $usageMileage,
|
||||
'brand_name' => $brandName,
|
||||
'series_name' => $seriesName,
|
||||
'car_name' => $carName,
|
||||
'year' => $year,
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function parseConfigDetail(string $html = ''): array
|
||||
{
|
||||
try {
|
||||
$ql = QueryList::html($html);
|
||||
} catch (Throwable $e) {
|
||||
throw new BusinessException('解析 HTML 失败(配置页): ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
if ($jsonText === '') {
|
||||
throw new BusinessException('配置页未找到 __NEXT_DATA__ JSON');
|
||||
}
|
||||
|
||||
$raw = $this->decodeJson($jsonText, '配置页 __NEXT_DATA__');
|
||||
|
||||
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
|
||||
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
|
||||
|
||||
$valuesMap = $this->buildValuesMap($carInfo);
|
||||
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
|
||||
|
||||
return [
|
||||
'format_car_info' => [
|
||||
'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间
|
||||
'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式
|
||||
'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数
|
||||
'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机
|
||||
'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率
|
||||
'electric_description' => $this->getFieldValue($groups, 'electric_description'), // 电动机
|
||||
'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), // 电池容量
|
||||
'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型
|
||||
'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage'), // 续航里程
|
||||
],
|
||||
'groups' => $groups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置中获取能源类型
|
||||
*
|
||||
* @param array $data JSON数据
|
||||
* @return string|null 能源类型
|
||||
*/
|
||||
public function getFuelFormByConfig(array $data): ?string
|
||||
{
|
||||
if (empty($data[0]['items']) || !is_array($data[0]['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($data[0]['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === 'fuel_form') {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据中根据字段key获取值
|
||||
*
|
||||
* @param mixed $data 数据
|
||||
* @param string $field 字段名
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFieldValue($data, string $field): ?string
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($data as $group) {
|
||||
if (isset($group['items']) && is_array($group['items'])) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if (($item['key'] ?? null) === $field) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
|
||||
if (!empty($item['children']) && is_array($item['children'])) {
|
||||
foreach ($item['children'] as $child) {
|
||||
if (($child['key'] ?? null) === $field) {
|
||||
return $child['value'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数列表中获取指定名称的值
|
||||
*/
|
||||
public function getParamValue(array $params, string $name): ?string
|
||||
{
|
||||
foreach ($params as $item) {
|
||||
if (($item['name'] ?? null) === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断车辆能源类型
|
||||
*/
|
||||
public function detectEnergyType(string $text): string
|
||||
{
|
||||
if (function_exists('mb_internal_encoding')) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
// 明确定义的新能源关键字
|
||||
$newEnergyKeywords = [
|
||||
'纯电', '纯电动', '电动车', 'BEV',
|
||||
'插电混动', '插电式混合', '插电式', '插电', 'PHEV',
|
||||
'增程式', '增程', 'REEV',
|
||||
'氢能', '氢燃料', '燃料电池', 'FCEV',
|
||||
'太阳能',
|
||||
];
|
||||
|
||||
// 传统能源关键字
|
||||
$traditionalKeywords = [
|
||||
'汽油',
|
||||
'柴油',
|
||||
'天然气', 'LNG', 'CNG',
|
||||
];
|
||||
|
||||
// 普通混动关键字(归为传统能源)
|
||||
$hybridKeywords = [
|
||||
'油电混合', '油电混动', '油电',
|
||||
'混动',
|
||||
'双擎',
|
||||
'HEV',
|
||||
];
|
||||
|
||||
$contains = function (string $haystack, string $needle): bool {
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('mb_stripos')) {
|
||||
return mb_stripos($haystack, $needle) !== false;
|
||||
}
|
||||
return stripos($haystack, $needle) !== false;
|
||||
};
|
||||
|
||||
foreach ($newEnergyKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_RENEWABLE;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($hybridKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($traditionalKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
return self::ENERGY_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存二手车基本信息到数据库
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return int 插入记录ID
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function saveUsedCarBasicInfo(array $data): int
|
||||
{
|
||||
try {
|
||||
$time = time();
|
||||
$data['created_at'] = $data['created_at'] ?? $time;
|
||||
$data['updated_at'] = $time;
|
||||
|
||||
return (int)Db::table('usedcar_crawl_results')->insertGetId($data);
|
||||
} catch (Throwable $e) {
|
||||
throw new BusinessException('保存二手车基础信息失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置信息到数据库
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
* @param int $id 记录ID
|
||||
* @return bool|int
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function saveUsedCarConfigInfo(array $data, int $id)
|
||||
{
|
||||
try {
|
||||
$record = Db::table('usedcar_crawl_results')->find($id);
|
||||
|
||||
if (!$record) {
|
||||
// 找不到记录就算了,不抛致命错误,视业务情况也可以抛异常
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $data['groups'] ?? [];
|
||||
$oldFormat = [];
|
||||
|
||||
if (!empty($record->format_car_info)) {
|
||||
$decoded = json_decode($record->format_car_info, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$oldFormat = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$formatCarInfo = array_merge(
|
||||
$oldFormat,
|
||||
$data['format_car_info'] ?? []
|
||||
);
|
||||
|
||||
return Db::table('usedcar_crawl_results')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
throw new BusinessException('保存车辆配置信息失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建值映射表
|
||||
*/
|
||||
public function buildValuesMap(array $carInfo): array
|
||||
{
|
||||
$valuesMap = [];
|
||||
|
||||
// 顶层字段
|
||||
foreach ($carInfo as $k => $v) {
|
||||
if ($k === 'info' || is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
$valuesMap[$k] = ['value' => $v];
|
||||
}
|
||||
|
||||
// info中的配置项
|
||||
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
|
||||
foreach ($carInfo['info'] as $k => $v) {
|
||||
if (is_array($v) && array_key_exists('value', $v)) {
|
||||
$valuesMap[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建车辆规格分组
|
||||
*/
|
||||
public function buildCarSpecGroups(array $properties, array $valuesMap): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentKey = null;
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$type = (int)($item['type'] ?? 1);
|
||||
$key = $item['key'] ?? '';
|
||||
$text = $item['text'] ?? '';
|
||||
$subList = $item['sub_list'] ?? null;
|
||||
|
||||
// 分类字段:type = 0 / 2
|
||||
if (in_array($type, [0, 2], true)) {
|
||||
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
|
||||
|
||||
if (!isset($groups[$currentKey])) {
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => $currentKey,
|
||||
'group_text' => $text,
|
||||
'group_type' => $type,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通字段:挂到最近的分类下
|
||||
if ($currentKey === null) {
|
||||
$currentKey = '_default';
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => '_default',
|
||||
'group_text' => '其他',
|
||||
'group_type' => -1,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$value = '';
|
||||
if ($key && isset($valuesMap[$key])) {
|
||||
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$node = [
|
||||
'key' => $key,
|
||||
'text' => $text,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
if (is_array($subList) && !empty($subList)) {
|
||||
$children = [];
|
||||
foreach ($subList as $sub) {
|
||||
$subKey = $sub['key'] ?? '';
|
||||
$subText = $sub['text'] ?? '';
|
||||
$subValue = '';
|
||||
|
||||
if ($subKey && isset($valuesMap[$subKey])) {
|
||||
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$children[] = [
|
||||
'key' => $subKey,
|
||||
'text' => $subText,
|
||||
'value' => $subValue,
|
||||
];
|
||||
}
|
||||
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
$groups[$currentKey]['items'][] = $node;
|
||||
}
|
||||
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码懂车帝价格
|
||||
*/
|
||||
protected function decodePrice(string $str): string
|
||||
{
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取ID
|
||||
*/
|
||||
protected function extractIdFromUrl(string $url)
|
||||
{
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return (int)$matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取符合正则表达式的值(只保留数字和小数点)
|
||||
*/
|
||||
protected function extractPatternValue(string $text, string $pattern): string
|
||||
{
|
||||
$value = '';
|
||||
if ($text && preg_match($pattern, $text, $m)) {
|
||||
$value = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*/
|
||||
public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!@mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
// echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$ext = pathinfo($path ?? '', PATHINFO_EXTENSION);
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg';
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
$defaultHeaders = [
|
||||
'User-Agent: ' . self::DEFAULT_HEADERS['User-Agent'],
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer: ' . self::DEFAULT_HEADERS['Referer'],
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0 || $code !== 200 || $data === false || $data === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bytes = @file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片URL数组
|
||||
* @param string $saveDir 保存目录
|
||||
* @return array [原始URL => 保存路径或null]
|
||||
*/
|
||||
public function downloadImageAlbum(array $urls, string $saveDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim((string)$url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->downloadImage($url, $saveDir);
|
||||
} catch (Throwable $e) {
|
||||
$path = null;
|
||||
}
|
||||
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 -> 上传 -> 删除本地
|
||||
*
|
||||
* @param array $images 原始相册URL列表
|
||||
* @param string $path 本地保存目录
|
||||
* @param int $usedCarId 懂车帝 used_car_id
|
||||
* @param int $id 本地 crawl 记录id
|
||||
* @return bool
|
||||
*/
|
||||
public function uploadAndRemove(array $images, string $path, $usedCarId, $id): bool
|
||||
{
|
||||
if (empty($images)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
$filePatch = $this->downloadImageAlbum($images, $path);
|
||||
if (empty($filePatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uploadService = new UploadService();
|
||||
|
||||
foreach ($filePatch as $url => $filePath) {
|
||||
if ($filePath === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 上传云端
|
||||
$param = [
|
||||
'path' => $filePath,
|
||||
'scene' => 'usedcar_crawl',
|
||||
'category' => 'usedcar',
|
||||
];
|
||||
|
||||
$result = $uploadService->uploadLocalFile($param);
|
||||
if (empty($result['file_name'])) {
|
||||
// 上传失败则跳过,但不抛异常影响主流程
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存图片信息
|
||||
UsedcarCrawlResultsImages::create([
|
||||
'uc_id' => $id,
|
||||
'used_car_id' => $usedCarId,
|
||||
'original_name'=> $result['original_name'] ?? '',
|
||||
'file_path' => $result['file_name'],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
// 单张图失败不影响其他
|
||||
// 可以按需写日志
|
||||
} finally {
|
||||
// 删除本地文件
|
||||
if (is_file($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除目录
|
||||
rmdir($path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解 JSON
|
||||
*
|
||||
* @param string $json
|
||||
* @param string $context
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function decodeJson(string $json, string $context = 'JSON'): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||
throw new BusinessException($context . ' 解析失败:' . json_last_error_msg());
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use QL\QueryList;
|
||||
use support\Redis;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class UsedCarCrawl_bak
|
||||
{
|
||||
|
||||
/**
|
||||
* 插入队列url
|
||||
*
|
||||
* $scene 二手信息 usedcar 配置信息 config
|
||||
*/
|
||||
public function iUrl(string $url = '', string $scene = 'usedcar', int $id = 0)
|
||||
{
|
||||
return Redis::lpush('usedcar:urls', json_encode([
|
||||
'scene' => $scene,
|
||||
'url' => $url,
|
||||
'id' => $id
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列url
|
||||
*
|
||||
* $scene 二手信息 usedcar 配置信息 config
|
||||
*/
|
||||
public function rUrl()
|
||||
{
|
||||
return json_decode(Redis::rPop('usedcar:urls'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行抓取
|
||||
*/
|
||||
public function doData()
|
||||
{
|
||||
try {
|
||||
$raw = $this->rUrl();
|
||||
|
||||
if (!$raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$html = $this->getHtml($raw['url']);
|
||||
|
||||
switch ($raw['scene']) {
|
||||
case 'usedcar':
|
||||
$info = $this->getUsedDetail($html, $raw['url']);
|
||||
$id = $this->saveUsedcarBasicToDb($info);
|
||||
$this->iUrl($info['car_id_url'], 'config', $id);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
$data = $this->getConfigDetail($html);
|
||||
$this->saveUsedcarConfigToDb($data, $raw['id']);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
throw new BusinessException($th->getMessage().'|'.$th->getLine().'|'.$th->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取html
|
||||
*/
|
||||
public function getHtml($url)
|
||||
{
|
||||
$client = new Client();
|
||||
$res = $client->request('GET', $url, [
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||
. 'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
. 'Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/'
|
||||
]
|
||||
]);
|
||||
return (string)$res->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二手信息
|
||||
*/
|
||||
public function getUsedDetail(string $html = '', string $url = '')
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$title = $ql->find('h1')->text();
|
||||
$price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text();
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
// 新车指导价
|
||||
$newPrice = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/新车指导价[::]\s*([^\r\n万]*万)/u', $pageText, $m)) {
|
||||
$newPrice = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 比新车省
|
||||
$priceDifference = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/比新车省[::]\s*([^\r\n万]*万)/u', $pageText, $m)) {
|
||||
$priceDifference = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$jsonArray = json_decode($jsonText, true);
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID
|
||||
$carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0;
|
||||
|
||||
// 车辆年份
|
||||
$year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0;
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型
|
||||
$fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? '';
|
||||
$newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = explode(' | ', $importantText);
|
||||
$usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : '');
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? '';
|
||||
$seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? '';
|
||||
$carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? '';
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? ''
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
// 排量
|
||||
$displacement = $this->getOtherParamValue($otherParams, '排量');
|
||||
// 变速箱
|
||||
$gearbox = $this->getOtherParamValue($otherParams, '变速箱');
|
||||
// 格式化车辆信息
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox
|
||||
];
|
||||
|
||||
// 整理车辆信息
|
||||
$info['used_car_id'] = $this->getLastIdFromUrl($url);
|
||||
$info['car_id'] = $carId;
|
||||
$info['url'] = $url;
|
||||
$info['title'] = $title;
|
||||
$info['price'] = str_replace('万', '', $this->decodeDongchediPrice($price));
|
||||
$info['new_price'] = str_replace('万', '', $newPrice);
|
||||
$info['price_difference'] = str_replace('万', '', $priceDifference);
|
||||
$info['condition_desc'] = $conditionDesc;
|
||||
$info['images'] = json_encode($images);
|
||||
$info['car_id_url'] = $carIdUrl;
|
||||
$info['used_detail'] = $jsonText;
|
||||
$info['format_shop_info'] = json_encode($formatShopInfo);
|
||||
$info['fuel_form'] = $resultFuelForm;
|
||||
$info['classify_energy'] = $this->detectEnergyType($resultFuelForm);
|
||||
$info['usage_mileage'] = $usageMileage;
|
||||
$info['brand_name'] = $brandName;
|
||||
$info['series_name'] = $seriesName;
|
||||
$info['car_name'] = $carName;
|
||||
$info['year'] = $year;
|
||||
$info['format_car_info'] = json_encode($formatCarInfo);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
public function getConfigDetail(string $html = '')
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
$raw = json_decode($jsonText, true);
|
||||
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
|
||||
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
|
||||
|
||||
$valuesMap = $this->buildValuesMapFromCarInfo($carInfo);
|
||||
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
|
||||
|
||||
$data['format_car_info'] = [
|
||||
'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间
|
||||
'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式
|
||||
'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数
|
||||
'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机
|
||||
'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率
|
||||
'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机
|
||||
'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量
|
||||
'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型
|
||||
'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程
|
||||
];
|
||||
$data['groups'] = $groups;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置 JSON 数据中获取能源类型
|
||||
*
|
||||
* @param array $data JSON 数据
|
||||
* @return string|null 能源类型,如 '油电混合'、'纯电动'、
|
||||
*/
|
||||
public function getFuelFormByConfigInfo(array $data)
|
||||
{
|
||||
$fuelForm = null;
|
||||
foreach ($data[0]['items'] as $item) {
|
||||
if ($item['key'] === 'fuel_form') {
|
||||
$fuelForm = $item['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fuelForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置 JSON 数据中根据字段 key 获取对应的 value
|
||||
*
|
||||
* @param string|array $data JSON 字符串 或 已解析的数组
|
||||
* @param string $field 要查找的字段 key(如 'fuel_form')
|
||||
* @return string|null 找到则返回 value,否则返回 null
|
||||
*/
|
||||
public function getFieldValue($data, string $field)
|
||||
{
|
||||
// 如果传入的是 JSON 字符串,先解码
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null; // JSON 格式错误
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历第一层(假设结构是 [ { "items": [...] } ])
|
||||
foreach ($data as $group) {
|
||||
if (isset($group['items']) && is_array($group['items'])) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === $field) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // 未找到
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 other_params 数组中根据 name 获取对应的 value
|
||||
*
|
||||
* @param array $otherParams 格式为 [ ['name' => 'xxx', 'value' => 'yyy'], ... ]
|
||||
* @param string $name 要查找的字段名称(如 '上牌地')
|
||||
* @return string|null 找到则返回 value,否则返回 null
|
||||
*/
|
||||
public function getOtherParamValue(array $otherParams, string $name): ?string
|
||||
{
|
||||
foreach ($otherParams as $item) {
|
||||
if (isset($item['name']) && $item['name'] === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据给定字符串判断车辆能源类型
|
||||
*
|
||||
* 返回:
|
||||
* - RE 新能源(纯电、插电混动、增程、氢燃料等)
|
||||
* - CE 传统能源(汽油、柴油、天然气、非插电混动等)
|
||||
* - UNKNOWN 无法判断
|
||||
*
|
||||
* @param string $text 例如:'2022款 某某牌 2.0L 油电混合 双擎'
|
||||
* @return string
|
||||
*/
|
||||
public function detectEnergyType(string $text): string
|
||||
{
|
||||
if (function_exists('mb_internal_encoding')) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
// 明确定义的新能源关键字(纯电 / 插电 / 增程 / 氢 / 太阳能 等)
|
||||
$newEnergyKeywords = [
|
||||
// 纯电
|
||||
'纯电', '纯电动', '电动车', 'BEV',
|
||||
// 插电式混合
|
||||
'插电混动', '插电式混合', '插电式', '插电', 'PHEV',
|
||||
// 增程式
|
||||
'增程式', '增程', 'REEV',
|
||||
// 氢能 / 燃料电池
|
||||
'氢能', '氢燃料', '燃料电池', 'FCEV',
|
||||
// 其它
|
||||
'太阳能',
|
||||
];
|
||||
|
||||
// 传统能源关键字(包括燃油/燃气)
|
||||
$traditionalKeywords = [
|
||||
'汽油',
|
||||
'柴油',
|
||||
'天然气', 'LNG', 'CNG',
|
||||
];
|
||||
|
||||
// 普通混动关键字(按你的要求,统一归为传统能源)
|
||||
$hybridKeywords = [
|
||||
'油电混合', '油电混动', '油电',
|
||||
'混动', // 不带“插电”的混动
|
||||
'双擎', // 丰田常见表达
|
||||
'HEV',
|
||||
];
|
||||
|
||||
// 辅助:不区分大小写的包含判断,优先 mb_stripos
|
||||
$contains = function (string $haystack, string $needle): bool {
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('mb_stripos')) {
|
||||
return mb_stripos($haystack, $needle) !== false;
|
||||
}
|
||||
return stripos($haystack, $needle) !== false;
|
||||
};
|
||||
|
||||
// 1. 先判断“明确新能源”关键词(插电、增程、氢、纯电等)
|
||||
foreach ($newEnergyKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'RE';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再判断“普通油电混动”——按照你的业务,一律归为传统能源
|
||||
foreach ($hybridKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'CE';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 再判断传统能源(纯燃油/气)
|
||||
foreach ($traditionalKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'CE';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 都没匹配上
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* 二手信息入库
|
||||
*/
|
||||
public function saveUsedcarBasicToDb(array $data = [])
|
||||
{
|
||||
$data['created_at'] = time();
|
||||
$data['updated_at'] = time();
|
||||
return Db::table('usedcar_crawl_results')->insertGetId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新信息入库
|
||||
*/
|
||||
public function saveUsedcarConfigToDb(array $data = [], int $id = 0)
|
||||
{
|
||||
$info = Db::table('usedcar_crawl_results')->find($id);
|
||||
|
||||
if (!$info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $data['groups'];
|
||||
$formatCarInfo = array_merge($data['format_car_info'], json_decode($info->format_car_info, true));
|
||||
|
||||
return Db::table('usedcar_crawl_results')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE),
|
||||
'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 car_info 扁平成一个 [key => ['value' => xxx]] 的 map
|
||||
*
|
||||
* @param array $carInfo rawData['car_info'][0]
|
||||
* @return array
|
||||
*/
|
||||
public function buildValuesMapFromCarInfo(array $carInfo): array
|
||||
{
|
||||
$valuesMap = [];
|
||||
|
||||
// 1) car_info 顶层的一些字段(official_price、series_name 等)
|
||||
foreach ($carInfo as $k => $v) {
|
||||
if ($k === 'info' || is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
$valuesMap[$k] = ['value' => $v];
|
||||
}
|
||||
|
||||
// 2) info 里的配置项
|
||||
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
|
||||
foreach ($carInfo['info'] as $k => $v) {
|
||||
if (is_array($v) && array_key_exists('value', $v)) {
|
||||
$valuesMap[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 type 重新分组:
|
||||
* - type=0,2 为分组(左侧分类)
|
||||
* - 其它 type 都视为该分组下的字段
|
||||
*
|
||||
* @param array $properties rawData['properties']
|
||||
* @param array $valuesMap 来自 buildValuesMapFromCarInfo()
|
||||
* @return array
|
||||
*/
|
||||
public function buildCarSpecGroups(array $properties, array $valuesMap): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentKey = null; // 当前分组 key
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$type = (int)($item['type'] ?? 1);
|
||||
$key = $item['key'] ?? '';
|
||||
$text = $item['text'] ?? '';
|
||||
$subList = $item['sub_list'] ?? null;
|
||||
|
||||
// ---------- 1) 分类字段:type = 0 / 2 ----------
|
||||
if (in_array($type, [0, 2], true)) {
|
||||
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
|
||||
|
||||
if (!isset($groups[$currentKey])) {
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => $currentKey,
|
||||
'group_text' => $text,
|
||||
'group_type' => $type,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- 2) 普通字段:挂到最近的分类下 ----------
|
||||
if ($currentKey === null) {
|
||||
// 如果一开始就没有遇到分类,给一个默认分组
|
||||
$currentKey = '_default';
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => '_default',
|
||||
'group_text' => '其他',
|
||||
'group_type' => -1,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// 取 value
|
||||
$value = '';
|
||||
if ($key && isset($valuesMap[$key])) {
|
||||
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$node = [
|
||||
'key' => $key,
|
||||
'text' => $text,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
// ---------- 3) sub_list 子项(有就当成 children) ----------
|
||||
if (is_array($subList) && !empty($subList)) {
|
||||
$children = [];
|
||||
foreach ($subList as $sub) {
|
||||
$subKey = $sub['key'] ?? '';
|
||||
$subText = $sub['text'] ?? '';
|
||||
$subValue = '';
|
||||
|
||||
if ($subKey && isset($valuesMap[$subKey])) {
|
||||
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$children[] = [
|
||||
'key' => $subKey,
|
||||
'text' => $subText,
|
||||
'value' => $subValue,
|
||||
];
|
||||
}
|
||||
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
$groups[$currentKey]['items'][] = $node;
|
||||
}
|
||||
|
||||
// 你可以按需返回关联数组或索引数组,这里转成索引数组方便前端遍历
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义价格
|
||||
*/
|
||||
protected function decodeDongchediPrice(string $str): string
|
||||
{
|
||||
// 映射表:PUA 字符 => 数字 / 万
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL最后ID
|
||||
*/
|
||||
protected function getLastIdFromUrl($url)
|
||||
{
|
||||
// 方法1:使用正则表达式提取最后的数字ID
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*
|
||||
* @param string $url 图片完整 URL
|
||||
* @param string $saveDir 保存目录(如 /www/usedcar_images/2025-11-20)
|
||||
* @param string|null $filename 指定文件名(可选,不含路径),为空则自动生成
|
||||
* @param array $headers 额外 HTTP 头(可选)
|
||||
*
|
||||
* @return string|null 成功返回保存路径,失败返回 null
|
||||
*/
|
||||
protected function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
// 1. 目录检查 & 创建
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 自动生成文件名(基于 URL md5 + 原扩展名)
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH); // /xxx/xxx.jpg
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION); // jpg 或 webp 之类
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg'; // 没有扩展名时兜底
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
// 3. 组装 HTTP 头
|
||||
$defaultHeaders = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||
. 'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
. 'Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer: https://www.dongchedi.com/', // 很多图床需要 Referer
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
// 4. 使用 cURL 下载
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$errMsg = curl_error($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($code !== 200 || $data === false || $data === '') {
|
||||
echo "[Image] 下载失败(HTTP {$code}):{$url}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 写入本地文件
|
||||
$bytes = file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
echo "[Image] 保存失败:{$savePath}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
echo "[Image] 下载成功:{$url} => {$savePath}\n";
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片 URL 数组
|
||||
* @param string $saveDir 保存目录
|
||||
*
|
||||
* @return array [原始 URL => 保存路径或 null]
|
||||
*/
|
||||
protected function downloadImageAlbum(array $urls, string $saveDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果存在 // 开头的相对协议,补全
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
$path = $this->downloadImage($url, $saveDir);
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use QL\QueryList;
|
||||
use support\Redis;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
use Throwable;
|
||||
|
||||
class UsedCarCrawl_bak2
|
||||
{
|
||||
// 默认请求头
|
||||
private const DEFAULT_HEADERS = [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/'
|
||||
];
|
||||
|
||||
// Redis队列键名
|
||||
private const REDIS_QUEUE_KEY = 'usedcar:urls';
|
||||
|
||||
// 能源类型常量
|
||||
public const ENERGY_TYPE_RENEWABLE = 'RE';
|
||||
public const ENERGY_TYPE_CONVENTIONAL = 'CE';
|
||||
public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* 插入队列url
|
||||
*
|
||||
* @param string $url URL地址
|
||||
* @param string $scene 场景:usedcar(二手信息) config(配置信息)
|
||||
* @param int $id 记录ID
|
||||
* @return int
|
||||
*/
|
||||
public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int
|
||||
{
|
||||
$data = [
|
||||
'scene' => $scene,
|
||||
'url' => $url,
|
||||
'id' => $id
|
||||
];
|
||||
|
||||
return Redis::lpush(self::REDIS_QUEUE_KEY, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列url
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function popUrl(): ?array
|
||||
{
|
||||
$data = Redis::rPop(self::REDIS_QUEUE_KEY);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行抓取任务
|
||||
*
|
||||
* @return bool
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
try {
|
||||
$task = $this->popUrl();
|
||||
|
||||
if (!$task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$html = $this->fetchHtml($task['url']);
|
||||
|
||||
switch ($task['scene']) {
|
||||
case 'usedcar':
|
||||
$info = $this->parseUsedCarDetail($html, $task['url']);
|
||||
$id = $this->saveUsedCarBasicInfo($info);
|
||||
$this->uploadAndRemove(json_decode($info['images'], true), public_path('uploads/usedcar/' . $id), $info['used_car_id'], $id);
|
||||
$this->pushUrl($info['car_id_url'], 'config', $id);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
$data = $this->parseConfigDetail($html);
|
||||
$this->saveUsedCarConfigInfo($data, $task['id']);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BusinessException("未知的任务场景: " . $task['scene']);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (GuzzleException $e) {
|
||||
throw new BusinessException("网络请求失败: " . $e->getMessage());
|
||||
} catch (Throwable $th) {
|
||||
throw new BusinessException("执行任务出错: " . $th->getMessage() . ' | 行号: ' . $th->getLine() . ' | 文件: ' . $th->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网页HTML内容
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function fetchHtml(string $url): string
|
||||
{
|
||||
$client = new Client();
|
||||
$response = $client->request('GET', $url, [
|
||||
'headers' => self::DEFAULT_HEADERS
|
||||
]);
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二手车详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function parseUsedCarDetail(string $html = '', string $url = ''): array
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$title = $ql->find('h1')->text();
|
||||
$price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text();
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
// 解析JSON数据
|
||||
$jsonArray = json_decode($jsonText, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new BusinessException("JSON解析失败: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID
|
||||
$carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0;
|
||||
|
||||
// 车辆年份
|
||||
$year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0;
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型
|
||||
$fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? '';
|
||||
$newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = explode(' | ', $importantText);
|
||||
$usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : '');
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? '';
|
||||
$seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? '';
|
||||
$carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? '';
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? ''
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
// 排量
|
||||
$displacement = $this->getParamValue($otherParams, '排量');
|
||||
// 变速箱
|
||||
$gearbox = $this->getParamValue($otherParams, '变速箱');
|
||||
// 格式化车辆信息
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox
|
||||
];
|
||||
|
||||
// 提取新车指导价
|
||||
$newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取比新车省金额
|
||||
$priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 整理车辆信息
|
||||
return [
|
||||
'used_car_id' => $this->extractIdFromUrl($url),
|
||||
'car_id' => $carId,
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
'price' => str_replace('万', '', $this->decodePrice($price)),
|
||||
'new_price' => str_replace('万', '', $newPrice),
|
||||
'price_difference' => str_replace('万', '', $priceDifference),
|
||||
'condition_desc' => $conditionDesc,
|
||||
'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'car_id_url' => $carIdUrl,
|
||||
'used_detail' => $jsonText,
|
||||
'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'fuel_form' => $resultFuelForm,
|
||||
'classify_energy' => $this->detectEnergyType($resultFuelForm),
|
||||
'usage_mileage' => $usageMileage,
|
||||
'brand_name' => $brandName,
|
||||
'series_name' => $seriesName,
|
||||
'car_name' => $carName,
|
||||
'year' => $year,
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @return array
|
||||
*/
|
||||
public function parseConfigDetail(string $html = ''): array
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
$raw = json_decode($jsonText, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new BusinessException("配置页面JSON解析失败: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
|
||||
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
|
||||
|
||||
$valuesMap = $this->buildValuesMap($carInfo);
|
||||
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
|
||||
|
||||
return [
|
||||
'format_car_info' => [
|
||||
'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间
|
||||
'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式
|
||||
'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数
|
||||
'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机
|
||||
'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率
|
||||
'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机
|
||||
'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量
|
||||
'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型
|
||||
'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程
|
||||
],
|
||||
'groups' => $groups
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置中获取能源类型
|
||||
*
|
||||
* @param array $data JSON数据
|
||||
* @return string|null 能源类型
|
||||
*/
|
||||
public function getFuelFormByConfig(array $data): ?string
|
||||
{
|
||||
if (empty($data[0]['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($data[0]['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === 'fuel_form') {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据中根据字段key获取值
|
||||
*
|
||||
* @param mixed $data 数据
|
||||
* @param string $field 字段名
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFieldValue($data, string $field): ?string
|
||||
{
|
||||
// 如果传入的是 JSON 字符串,先解码
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是数组,直接返回null
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 遍历寻找对应字段
|
||||
foreach ($data as $group) {
|
||||
if (isset($group['items']) && is_array($group['items'])) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === $field) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
|
||||
// 检查子元素
|
||||
if (isset($item['children']) && is_array($item['children'])) {
|
||||
foreach ($item['children'] as $child) {
|
||||
if (isset($child['key']) && $child['key'] === $field) {
|
||||
return $child['value'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数列表中获取指定名称的值
|
||||
*
|
||||
* @param array $params 参数列表
|
||||
* @param string $name 名称
|
||||
* @return string|null
|
||||
*/
|
||||
public function getParamValue(array $params, string $name): ?string
|
||||
{
|
||||
foreach ($params as $item) {
|
||||
if (isset($item['name']) && $item['name'] === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断车辆能源类型
|
||||
*
|
||||
* @param string $text 描述文本
|
||||
* @return string 能源类型标识
|
||||
*/
|
||||
public function detectEnergyType(string $text): string
|
||||
{
|
||||
if (function_exists('mb_internal_encoding')) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
// 明确定义的新能源关键字
|
||||
$newEnergyKeywords = [
|
||||
// 纯电
|
||||
'纯电', '纯电动', '电动车', 'BEV',
|
||||
// 插电式混合
|
||||
'插电混动', '插电式混合', '插电式', '插电', 'PHEV',
|
||||
// 增程式
|
||||
'增程式', '增程', 'REEV',
|
||||
// 氢能 / 燃料电池
|
||||
'氢能', '氢燃料', '燃料电池', 'FCEV',
|
||||
// 其它
|
||||
'太阳能',
|
||||
];
|
||||
|
||||
// 传统能源关键字
|
||||
$traditionalKeywords = [
|
||||
'汽油',
|
||||
'柴油',
|
||||
'天然气', 'LNG', 'CNG',
|
||||
];
|
||||
|
||||
// 普通混动关键字(归为传统能源)
|
||||
$hybridKeywords = [
|
||||
'油电混合', '油电混动', '油电',
|
||||
'混动', // 不带"插电"的混动
|
||||
'双擎', // 丰田常见表达
|
||||
'HEV',
|
||||
];
|
||||
|
||||
// 辅助函数:不区分大小写的包含判断
|
||||
$contains = function (string $haystack, string $needle): bool {
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('mb_stripos')) {
|
||||
return mb_stripos($haystack, $needle) !== false;
|
||||
}
|
||||
return stripos($haystack, $needle) !== false;
|
||||
};
|
||||
|
||||
// 1. 先判断"明确新能源"关键词
|
||||
foreach ($newEnergyKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_RENEWABLE;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再判断"普通油电混动"
|
||||
foreach ($hybridKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 再判断传统能源
|
||||
foreach ($traditionalKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 都没匹配上
|
||||
return self::ENERGY_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存二手车基本信息到数据库
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return int 插入记录ID
|
||||
*/
|
||||
public function saveUsedCarBasicInfo(array $data): int
|
||||
{
|
||||
$data['created_at'] = time();
|
||||
$data['updated_at'] = time();
|
||||
return Db::table('usedcar_crawl_results')->insertGetId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置信息到数据库
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
* @param int $id 记录ID
|
||||
* @return bool|int
|
||||
*/
|
||||
public function saveUsedCarConfigInfo(array $data, int $id)
|
||||
{
|
||||
$record = Db::table('usedcar_crawl_results')->find($id);
|
||||
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $data['groups'];
|
||||
$formatCarInfo = array_merge(
|
||||
$data['format_car_info'],
|
||||
json_decode($record->format_car_info, true)
|
||||
);
|
||||
|
||||
return Db::table('usedcar_crawl_results')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'updated_at' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建值映射表
|
||||
*
|
||||
* @param array $carInfo 车辆信息
|
||||
* @return array
|
||||
*/
|
||||
public function buildValuesMap(array $carInfo): array
|
||||
{
|
||||
$valuesMap = [];
|
||||
|
||||
// 处理顶层字段
|
||||
foreach ($carInfo as $k => $v) {
|
||||
if ($k === 'info' || is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
$valuesMap[$k] = ['value' => $v];
|
||||
}
|
||||
|
||||
// 处理info中的配置项
|
||||
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
|
||||
foreach ($carInfo['info'] as $k => $v) {
|
||||
if (is_array($v) && array_key_exists('value', $v)) {
|
||||
$valuesMap[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建车辆规格分组
|
||||
*
|
||||
* @param array $properties 属性列表
|
||||
* @param array $valuesMap 值映射表
|
||||
* @return array
|
||||
*/
|
||||
public function buildCarSpecGroups(array $properties, array $valuesMap): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentKey = null;
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$type = (int)($item['type'] ?? 1);
|
||||
$key = $item['key'] ?? '';
|
||||
$text = $item['text'] ?? '';
|
||||
$subList = $item['sub_list'] ?? null;
|
||||
|
||||
// 分类字段:type = 0 / 2
|
||||
if (in_array($type, [0, 2], true)) {
|
||||
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
|
||||
|
||||
if (!isset($groups[$currentKey])) {
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => $currentKey,
|
||||
'group_text' => $text,
|
||||
'group_type' => $type,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通字段:挂到最近的分类下
|
||||
if ($currentKey === null) {
|
||||
// 如果一开始就没有遇到分类,给一个默认分组
|
||||
$currentKey = '_default';
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => '_default',
|
||||
'group_text' => '其他',
|
||||
'group_type' => -1,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// 取 value
|
||||
$value = '';
|
||||
if ($key && isset($valuesMap[$key])) {
|
||||
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$node = [
|
||||
'key' => $key,
|
||||
'text' => $text,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
// 处理子项
|
||||
if (is_array($subList) && !empty($subList)) {
|
||||
$children = [];
|
||||
foreach ($subList as $sub) {
|
||||
$subKey = $sub['key'] ?? '';
|
||||
$subText = $sub['text'] ?? '';
|
||||
$subValue = '';
|
||||
|
||||
if ($subKey && isset($valuesMap[$subKey])) {
|
||||
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$children[] = [
|
||||
'key' => $subKey,
|
||||
'text' => $subText,
|
||||
'value' => $subValue,
|
||||
];
|
||||
}
|
||||
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
$groups[$currentKey]['items'][] = $node;
|
||||
}
|
||||
|
||||
// 转成索引数组方便前端遍历
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码懂车帝价格
|
||||
*
|
||||
* @param string $str 加密字符串
|
||||
* @return string 解码后的字符串
|
||||
*/
|
||||
protected function decodePrice(string $str): string
|
||||
{
|
||||
// 映射表:PUA 字符 => 数字 / 万
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取ID
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return int|false
|
||||
*/
|
||||
protected function extractIdFromUrl(string $url)
|
||||
{
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return (int)$matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取符合正则表达式的值
|
||||
*
|
||||
* @param string $text 文本
|
||||
* @param string $pattern 正则表达式
|
||||
* @return string
|
||||
*/
|
||||
protected function extractPatternValue(string $text, string $pattern): string
|
||||
{
|
||||
$value = '';
|
||||
if ($text) {
|
||||
if (preg_match($pattern, $text, $m)) {
|
||||
$value = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*
|
||||
* @param string $url 图片完整URL
|
||||
* @param string $saveDir 保存目录
|
||||
* @param string|null $filename 指定文件名
|
||||
* @param array $headers 额外HTTP头
|
||||
* @return string|null 成功返回保存路径,失败返回null
|
||||
*/
|
||||
public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
// 目录检查 & 创建
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成文件名
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg';
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
// 组装HTTP头
|
||||
$defaultHeaders = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer: https://www.dongchedi.com/',
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
// 使用cURL下载
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$errMsg = curl_error($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
// echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($code !== 200 || $data === false || $data === '') {
|
||||
// echo "[Image] 下载失败(HTTP {$code}):{$url}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// 写入本地文件
|
||||
$bytes = file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
// echo "[Image] 保存失败:{$savePath}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// echo "[Image] 下载成功:{$url} => {$savePath}\n";
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片URL数组
|
||||
* @param string $saveDir 保存目录
|
||||
* @return array [原始URL => 保存路径或null]
|
||||
*/
|
||||
public function downloadImageAlbum(array $urls, string $saveDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 补全协议
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
$path = $this->downloadImage($url, $saveDir);
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function uploadAndRemove($images, $path, $usedCarId, $id)
|
||||
{
|
||||
// 下载图片
|
||||
$filePatch = $this->downloadImageAlbum($images, $path);
|
||||
|
||||
// 检查图片
|
||||
if (empty($filePatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
foreach ($filePatch as $url => $path) {
|
||||
if ($path === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 上传云端
|
||||
$param = [
|
||||
'path' => $path,
|
||||
'scene' => 'usedcar_crawl',
|
||||
'category' => 'usedcar'
|
||||
];
|
||||
$up = new \app\common\service\UploadService();
|
||||
$result = $up->uploadLocalFile($param);
|
||||
|
||||
// 保存图片信息
|
||||
UsedcarCrawlResultsImages::create([
|
||||
'uc_id' => $id,
|
||||
'used_car_id' => $usedCarId,
|
||||
'original_name' => $result['original_name'],
|
||||
'file_path' => $result['file_name'],
|
||||
]);
|
||||
|
||||
// 删除本地文件
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,9 +434,15 @@ class Emails
|
||||
$channel = 12;
|
||||
$channelInfo = InquiryChannel::where('id', $channel)->first();
|
||||
|
||||
// 从库实时获取
|
||||
$users = \app\common\model\User::where('corp_id', 3)->where('status', 1)->orderBy('id')->pluck('id')->toArray();
|
||||
|
||||
// 移除不参与分配人员
|
||||
$users = array_values(array_diff($users, [190]));
|
||||
|
||||
// 初始化 UID 轮询器
|
||||
$uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804');
|
||||
$uidRotator->setUids([64, 63, 93, 108, 132, 145, 150, 143, 156, 157, 158], false);
|
||||
$uidRotator->setUids($users, false);
|
||||
|
||||
EmailParsedResult::where('status', 0)
|
||||
->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) {
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\email;
|
||||
|
||||
use app\common\model\Country;
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\model\EmailMailbox;
|
||||
use app\common\model\EmailPull;
|
||||
use app\common\model\EmailPullParsed;
|
||||
use app\common\model\EmailPullUids;
|
||||
use app\common\model\InquiryChannel;
|
||||
use app\common\model\InquiryLog;
|
||||
use app\common\model\User;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class Mailbox
|
||||
{
|
||||
/**
|
||||
* 多邮箱处理:拉取uid
|
||||
*/
|
||||
function mailbox()
|
||||
{
|
||||
$mailboxModel = EmailMailbox::where('last_fetch_at', '<=', time())
|
||||
->where('status',1)->first();
|
||||
//$mailboxModel['last_fetch_at'] = time();
|
||||
//$mailboxModel->save();
|
||||
|
||||
$mailbox = $this->mailboxSplice($mailboxModel);
|
||||
// $INBOX= $mailboxModel['imap_host'].':'.$mailboxModel['imap_port'].$mailboxModel['imap_flags'].$mailboxModel['mailbox'];
|
||||
// $mailbox = '{'.$INBOX.'}'.'INBOX';
|
||||
//$username = $mailbox['username'];
|
||||
//$password = $mailbox['password'];
|
||||
|
||||
$this->pullUid($mailboxModel, $mailbox);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接INBOX
|
||||
* @param $mailboxModel
|
||||
* @return string
|
||||
*/
|
||||
function mailboxSplice($mailboxModel){
|
||||
$INBOX= $mailboxModel['imap_host'].':'.$mailboxModel['imap_port'].$mailboxModel['imap_flags'];
|
||||
$mailbox = '{'.$INBOX.'}'.'INBOX';
|
||||
return $mailbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取邮件uid
|
||||
*/
|
||||
function pullUid($mailboxModel, $mailbox)
|
||||
{
|
||||
try {
|
||||
$username = $mailboxModel['username'];
|
||||
$password = $mailboxModel['password'];
|
||||
$lastUid = $mailboxModel['last_uid'];
|
||||
|
||||
$retryTimes = 3;
|
||||
$retryDelay = 10;
|
||||
$inbox = false;
|
||||
$attempt = 0;
|
||||
|
||||
while ($attempt < $retryTimes) {
|
||||
$attempt++;
|
||||
flg("尝试第 {$attempt} 次连接 IMAP...", false,'mailbox');
|
||||
|
||||
$inbox = @imap_open($mailbox, $username, $password);
|
||||
|
||||
if ($inbox) {
|
||||
break;
|
||||
}
|
||||
|
||||
$error = imap_last_error();
|
||||
flg("<comment>{$username}第 {$attempt} 次失败:{$error}</comment>", false,'mailbox');
|
||||
|
||||
// 若出现 too many login failures 立即暂停一段时间
|
||||
if (stripos($error, 'Too many login failures') !== false) {
|
||||
flg("<error>检测到登录失败次数过多,暂停 60 秒避免封禁</error>", false,'mailbox');
|
||||
sleep(60);
|
||||
} else {
|
||||
sleep($retryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$inbox) {
|
||||
flg("{$username}<error>最终仍然连接失败:" . imap_last_error() . "</error>", false,'mailbox');
|
||||
return false;
|
||||
}
|
||||
// 拉取 UID 并入库
|
||||
flg("开始拉取 UID 并入库 …", false,'mailbox');
|
||||
$uids = imap_search($inbox, 'ALL', SE_UID) ?: [];
|
||||
|
||||
$uids = array_filter($uids, function($uid) use ($lastUid) {
|
||||
return $uid > $lastUid;
|
||||
});
|
||||
|
||||
sort($uids);
|
||||
$now = time();
|
||||
$count = 0;
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
try {
|
||||
Db::table('email_pull_uids')->insert([
|
||||
'uid' => $uid,
|
||||
'fetched_at' => $now,
|
||||
'email' => $username
|
||||
]);
|
||||
$count++;
|
||||
flg("{$username} UID:".$uid." 已写入数据库", false,'mailbox');
|
||||
} catch (\Throwable $e) {
|
||||
flg("{$username} <comment>{$e->getMessage()}</comment>", false,'mailbox');
|
||||
}
|
||||
}
|
||||
|
||||
imap_close($inbox);
|
||||
flg("✔ {$username}获取到新询盘邮件 {$count} 条 ...", false,'mailbox');
|
||||
|
||||
|
||||
if(!empty($uids)){
|
||||
$mailboxModel->last_uid = max($uids);
|
||||
}
|
||||
$mailboxModel->last_fetch_at = time();
|
||||
$mailboxModel->save();
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
flg(dpr($th), false,'mailbox');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多邮箱处理:拉取详情
|
||||
*/
|
||||
function pullDetails()
|
||||
{
|
||||
$emails = EmailPullUids::where('status', 0)
|
||||
->select('email')
|
||||
->distinct()
|
||||
->get();
|
||||
foreach ($emails as $email) {
|
||||
$mailboxModel = EmailMailbox::where('username', $email->email)->first();
|
||||
$mailbox = $this->mailboxSplice($mailboxModel);
|
||||
$this->details($mailboxModel, $mailbox);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件详情
|
||||
*/
|
||||
public function details($mailboxModel, $mailbox)
|
||||
{
|
||||
try {
|
||||
|
||||
$username = $mailboxModel['username'];
|
||||
$password = $mailboxModel['password'];
|
||||
$inbox = @imap_open($mailbox, $username, $password);
|
||||
if (!$inbox) {
|
||||
flg("<error>{$username} IMAP 连接失败:". imap_last_error() ."</error>", false,'mailbox');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 依次拉取详情并写入 emails 表
|
||||
flg("依次拉取详情并写入 email_pull 表 …", false,'mailbox');
|
||||
while (true) {
|
||||
$pending = Db::table('email_pull_uids')
|
||||
->where('email', $username)
|
||||
->where('status', 0)
|
||||
->orderBy('uid', 'desc')
|
||||
->limit(2)
|
||||
->get();
|
||||
|
||||
if (count($pending) === 0) {
|
||||
flg("邮件UID的详情,已拉取完毕。", false,'mailbox');
|
||||
break;
|
||||
}
|
||||
|
||||
flg("处理下一批 " . count($pending) . " 条 …", false,'mailbox');
|
||||
|
||||
foreach ($pending as $row) {
|
||||
$uid = $row->uid;
|
||||
|
||||
// 包裹整行处理在 try/catch 中,任何异常都不会中断循环
|
||||
try {
|
||||
flg("---- 开始处理 UID={$uid} ----", false,'mailbox');
|
||||
|
||||
// 1. fetch overview
|
||||
$ov = @imap_fetch_overview($inbox, $uid, FT_UID)[0] ?? null;
|
||||
if (!$ov) {
|
||||
$err = imap_last_error() ?: '未知错误';
|
||||
flg("UID={$uid} overview 失败:{$err}", false,'mailbox');
|
||||
continue;
|
||||
}
|
||||
$subject = mb_convert_encoding($ov->subject ?? '', 'UTF-8', 'GBK,UTF-8');
|
||||
$from = $ov->from ?? '';
|
||||
$date = $ov->date ?? '';
|
||||
|
||||
flg("UID={$uid} overview 成功,subject={$subject}", false,'mailbox');
|
||||
|
||||
// 2. fetch body
|
||||
$body = $this->getMailBody($inbox, $uid, true);
|
||||
flg("UID={$uid} body 长度:" . strlen($body), false,'mailbox');
|
||||
|
||||
// 3. sanitize & insert
|
||||
$body = $this->sanitizeUtf8($body);
|
||||
$now = time();
|
||||
|
||||
try {
|
||||
Db::table('email_pull')->insert([
|
||||
'email' => $username,
|
||||
'uid' => $uid,
|
||||
'subject' => $subject,
|
||||
'from' => $from,
|
||||
'date' => $date,
|
||||
'body' => $body,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
flg($username." UID={$uid} 写入 emails 成功", false,'mailbox');
|
||||
} catch (\Throwable $e) {
|
||||
flg($username." UID={$uid} 写入失败:{$e->getMessage()}", false,'mailbox');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 标记已处理
|
||||
try {
|
||||
Db::table('email_pull_uids')
|
||||
->where('uid', $uid)
|
||||
->update([
|
||||
'status' => 1,
|
||||
'processed_at' => $now,
|
||||
]);
|
||||
flg($username." UID={$uid} 标记为已处理", false,'mailbox');
|
||||
} catch (\Throwable $e) {
|
||||
flg($username." UID={$uid} 标记状态失败:{$e->getMessage()}", false,'mailbox');
|
||||
// 即使更新 status 失败,也继续下一条
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 捕获任何非预期错误
|
||||
flg($username." UID={$uid} 处理异常:{$e->getMessage()}", false,'mailbox');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 批次结束后稍作休眠,防止服务器过载
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
imap_close($inbox);
|
||||
flg($username." ✔ 新邮件详情拉取完成 ...", false,'mailbox');
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
flg(dpr($th), false,'mailbox');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将邮件按编码解码并保留 "\r\n"
|
||||
*/
|
||||
protected function getMailBody($inbox, int $id, bool $isUid = false): string
|
||||
{
|
||||
$flags = $isUid ? FT_UID : 0;
|
||||
$structure = imap_fetchstructure($inbox, $id, $flags);
|
||||
|
||||
if (!empty($structure->parts)) {
|
||||
// HTML 部分
|
||||
foreach ($structure->parts as $i => $part) {
|
||||
if (isset($part->subtype) && strtolower($part->subtype) === 'html') {
|
||||
$data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
|
||||
return $this->decodeBody($data, $part->encoding);
|
||||
}
|
||||
}
|
||||
// 文本部分
|
||||
foreach ($structure->parts as $i => $part) {
|
||||
if (isset($part->subtype) && strtolower($part->subtype) === 'plain') {
|
||||
$data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
|
||||
return $this->decodeBody($data, $part->encoding);
|
||||
}
|
||||
}
|
||||
// 原始 body
|
||||
return imap_body($inbox, $id, $flags);
|
||||
}
|
||||
|
||||
return imap_body($inbox, $id, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意字符串清洗为有效 UTF-8,移除或替换无效字节
|
||||
*/
|
||||
protected function sanitizeUtf8(string $text): string
|
||||
{
|
||||
return iconv('UTF-8', 'UTF-8//IGNORE', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按编码解码
|
||||
*/
|
||||
protected function decodeBody(string $body, int $encoding): string
|
||||
{
|
||||
switch ($encoding) {
|
||||
case 3: return imap_base64($body);
|
||||
case 4: return imap_qprint($body);
|
||||
default: return $body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多邮箱处理:清洗数据
|
||||
*/
|
||||
function processMailbox()
|
||||
{
|
||||
$emails = EmailPull::where('status', 0)
|
||||
->select('email')
|
||||
->distinct()
|
||||
->get();
|
||||
foreach ($emails as $email) {
|
||||
$mailboxModel = EmailMailbox::where('username', $email->email)->first();
|
||||
$this->process($mailboxModel);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function process($mailboxModel)
|
||||
{
|
||||
switch ($mailboxModel['username']) {
|
||||
case 'paika@paikacar.com':
|
||||
$this->paika($mailboxModel);
|
||||
break;
|
||||
case 'hesu@hesucar.com':
|
||||
$this->hesu($mailboxModel);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗询盘邮件
|
||||
* @param $mailbox
|
||||
* @return void
|
||||
*/
|
||||
function paika($mailbox)
|
||||
{
|
||||
$batchSize = 100; // 每次处理 100 条
|
||||
$page = 0;
|
||||
|
||||
// 明确定义 parsed_contacts 表里要插入的所有列(顺序无所谓,但要全部包含)
|
||||
$fields = [
|
||||
'email_id',
|
||||
'date',
|
||||
'email',
|
||||
'name',
|
||||
'tel',
|
||||
'company',
|
||||
'country',
|
||||
'form_url',
|
||||
'message',
|
||||
'ip',
|
||||
'iplocation',
|
||||
'ilang',
|
||||
'mailto',
|
||||
'navigator_language',
|
||||
'navigator_platform',
|
||||
'screen_width',
|
||||
'screen_height',
|
||||
'ucountry',
|
||||
];
|
||||
|
||||
while (true) {
|
||||
$rows = EmailPull::where('email', $mailbox['username'])
|
||||
->where('status', 0)
|
||||
->orderBy('id', 'asc')
|
||||
->limit($batchSize)
|
||||
->offset($page * $batchSize)
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
foreach ($rows as $row) {
|
||||
// $parsed = $this->parseHtml($row->body);
|
||||
$parsed = $this->parsePaikaInquiry($row->body, $row->date);
|
||||
if ($parsed === null) {
|
||||
$row->status = 2; //其他邮件
|
||||
$row->save();
|
||||
flg("<error>{$mailbox['username']} {$row->id} skip: not a contact-form HTML</error>", false,'mailbox');
|
||||
continue;
|
||||
}
|
||||
|
||||
$row->status = 1; //已处理
|
||||
$row->save();
|
||||
|
||||
// 严格按 $fields 顺序填充每条记录,缺失的字段补 null
|
||||
$record = [];
|
||||
foreach ($fields as $col) {
|
||||
if ($col === 'email_id') {
|
||||
$record[$col] = $row->id;
|
||||
} else {
|
||||
$record[$col] = $parsed[$col] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$toInsert[] = $record;
|
||||
}
|
||||
if (!empty($toInsert)) {
|
||||
$emailId = array_column($toInsert, 'email_id');
|
||||
$mailto = array_column($toInsert, 'mailto')[0];
|
||||
$email = Db::table('email_pull_parsed')->whereIn('email_id', $emailId)
|
||||
// ->where('mailto', $mailto)
|
||||
->first();
|
||||
if(!empty($email)){
|
||||
continue;
|
||||
}
|
||||
Db::table('email_pull_parsed')->insert($toInsert);
|
||||
}
|
||||
|
||||
$page++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析https://paikacar.com/wp-admin询盘邮件
|
||||
* DOM 解析为数组
|
||||
*/
|
||||
public function parsePaikaInquiry(string $html, $date)
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$dom = new DOMDocument();
|
||||
|
||||
//$dom->loadHTML($html);
|
||||
$dom->loadHTML('<?xml encoding="utf-8"?>' . $html);
|
||||
libxml_clear_errors();
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
if (
|
||||
!str_contains($html, 'Product Inquiry') ||
|
||||
!str_contains($html, 'Product Name') ||
|
||||
!str_contains($html, 'Your Name')
|
||||
) {
|
||||
// 认为是询盘
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
// 找到所有 tr
|
||||
$rows = $xpath->query('//table//tr');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$th = $xpath->query('.//th//p', $row)->item(0);
|
||||
$td = $xpath->query('.//td//p', $row)->item(0);
|
||||
|
||||
if (!$th || !$td) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = trim(str_replace(':', '', $th->textContent));
|
||||
$value = trim($td->textContent);
|
||||
|
||||
// 特殊字段:Product Name 是链接
|
||||
if ($key === 'Product Name') {
|
||||
$a = $xpath->query('.//a', $td)->item(0);
|
||||
if ($a) {
|
||||
$data['product_name'] = trim($a->textContent);
|
||||
$data['product_url'] = $a->getAttribute('href');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$data[$key] = $value;
|
||||
}
|
||||
$data['date'] = $date;
|
||||
|
||||
return $this->mapInquiryFields($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* paika.@paikacar.com字段处理
|
||||
*/
|
||||
function mapInquiryFields(array $raw): array
|
||||
{
|
||||
$result = [
|
||||
'product_name' => $raw['product_name'] ?? '',
|
||||
'form_url' => $raw['product_url'] ?? '',
|
||||
'quantity' => $this->normalizeValue($raw['Quantity'] ?? ''), //数量
|
||||
'name' => $raw['Your Name'] ?? '',
|
||||
'user_type' => $raw['User type'] ?? '',
|
||||
'email' => $raw['E-mail'] ?? '',
|
||||
'tel' => $this->normalizePhone($raw['Phone'] ?? ''),
|
||||
'whatsapp' => $raw['WhatsApp'] ?? '',
|
||||
// 'skype' => $raw['Skype'] ?? '',
|
||||
'company' => $raw['Company'] ?? '',
|
||||
'country' => $this->normalizeCountry($raw['Country'] ?? ''),
|
||||
'ip' => $this->extractIp($raw['IP'] ?? ''),
|
||||
'content' => $raw['Content'] ?? '',
|
||||
'date' => date('Y-m-d H:i:s',strtotime($raw['date'])),
|
||||
];
|
||||
//只有ipv4分析ip所属地区
|
||||
if (filter_var($result['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$result['iplocation'] = empty($result['ip'])?ip2region($result['ip']).'(仅供参考)':'';
|
||||
}
|
||||
// $message = trim(
|
||||
// (!empty($result['product_name']) ? "意向车型:{$result['product_name']}" : '') .
|
||||
// (!empty($result['quantity']) ? (!empty($result['product_name']) ? ', ' : '') . "数量:{$result['quantity']}" : '')
|
||||
// ) ?: '';
|
||||
$message[] = !empty($result['product_name']) ? "意向车型:{$result['product_name']}" : '';
|
||||
$message[] = !empty($result['quantity']) ? "数量:{$result['quantity']}" : '';
|
||||
$message[] = !empty($result['user_type']) ? "用户类型:{$result['user_type']}" : '';
|
||||
$message[] = "<p>询盘来源:" . "paikacar.com</p>";
|
||||
$message[] = "<p>FB 创建时间原始值:".$raw['date']."</p>";
|
||||
|
||||
$result['message'] = implode(',', $message);
|
||||
$result['ilang'] = 'en';
|
||||
$result['mailto'] = 'paika.@paikacar.com';
|
||||
unset($result['product_name']);
|
||||
unset($result['quantity']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
function normalizeValue(string $value): string
|
||||
{
|
||||
if (str_contains($value, '[your-')) {
|
||||
return '';
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
function normalizePhone(string $phone): string
|
||||
{
|
||||
return preg_replace('/[^\d+]/', '', $phone);
|
||||
}
|
||||
|
||||
function extractIp(string $text): string
|
||||
{
|
||||
if (preg_match('/\b\d{1,3}(\.\d{1,3}){3}\b/', $text, $m)) {
|
||||
return $m[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeCountry(string $country): string
|
||||
{
|
||||
if (str_contains($country, 'Please select')) {
|
||||
return '';
|
||||
}
|
||||
return $country;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗合塑询盘邮件
|
||||
* @param $mailbox
|
||||
* @return void
|
||||
*/
|
||||
function hesu($mailbox)
|
||||
{
|
||||
$batchSize = 100; // 每次处理 100 条
|
||||
$page = 0;
|
||||
|
||||
// 明确定义 parsed_contacts 表里要插入的所有列(顺序无所谓,但要全部包含)
|
||||
$fields = [
|
||||
'email_id',
|
||||
'date',
|
||||
'email',
|
||||
'name',
|
||||
'tel',
|
||||
'company',
|
||||
'country',
|
||||
'form_url',
|
||||
'message',
|
||||
'ip',
|
||||
'iplocation',
|
||||
'ilang',
|
||||
'mailto',
|
||||
'navigator_language',
|
||||
'navigator_platform',
|
||||
'screen_width',
|
||||
'screen_height',
|
||||
'ucountry',
|
||||
];
|
||||
|
||||
while (true) {
|
||||
$rows = EmailPull::where('email', $mailbox['username'])
|
||||
->where('status', 0)
|
||||
->orderBy('id', 'asc')
|
||||
->limit($batchSize)
|
||||
->offset($page * $batchSize)
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$toInsert = [];
|
||||
foreach ($rows as $row) {
|
||||
// $parsed = $this->parseHtml($row->body);
|
||||
$parsed = $this->parseHesucarInquiry($row->body, $row->date);
|
||||
if ($parsed === null) {
|
||||
$row->status = 2; //其他邮件
|
||||
$row->save();
|
||||
flg($mailbox['username']." <error>{$mailbox['username']} {$row->id} skip: not a contact-form HTML</error>", false,'mailbox');
|
||||
continue;
|
||||
}
|
||||
|
||||
$row->status = 1; //已处理
|
||||
$row->save();
|
||||
|
||||
// 严格按 $fields 顺序填充每条记录,缺失的字段补 null
|
||||
$record = [];
|
||||
foreach ($fields as $col) {
|
||||
if ($col === 'email_id') {
|
||||
$record[$col] = $row->id;
|
||||
} else {
|
||||
$record[$col] = $parsed[$col] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$toInsert[] = $record;
|
||||
}
|
||||
if (!empty($toInsert)) {
|
||||
|
||||
$emailId = array_column($toInsert, 'email_id');
|
||||
$mailto = array_column($toInsert, 'mailto')[0];
|
||||
$email = Db::table('email_pull_parsed')->whereIn('email_id', $emailId)
|
||||
//->where('mailto', $mailto)
|
||||
->first();
|
||||
if(!empty($email)){
|
||||
continue;
|
||||
}
|
||||
Db::table('email_pull_parsed')->insert($toInsert);
|
||||
}
|
||||
|
||||
$page++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析https://hesucar.com询盘邮件
|
||||
* DOM 解析为数组
|
||||
*/
|
||||
public function parseHesucarInquiry(string $html, $date)
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$dom = new DOMDocument();
|
||||
|
||||
//$dom->loadHTML($html);
|
||||
$dom->loadHTML('<?xml encoding="utf-8"?>' . $html);
|
||||
libxml_clear_errors();
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
if (
|
||||
!str_contains($html, 'HESUCAR')
|
||||
|| !str_contains($html, 'Tel/Whatsapp')
|
||||
|| !str_contains($html, 'Sent from')
|
||||
) {
|
||||
// 认为是询盘
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'product_url' => '',
|
||||
'name' => '',
|
||||
'phone' => '',
|
||||
'message' => '',
|
||||
'source' => 'hesucar_inquiry',
|
||||
'date' => $date,
|
||||
];
|
||||
|
||||
/**
|
||||
* 1️⃣ 产品链接(正文最前面的 a)
|
||||
*/
|
||||
$productLink = $xpath->query(
|
||||
'//td[contains(@class,"mcnTextContent")]//a[starts-with(@href,"http")]'
|
||||
)->item(0);
|
||||
|
||||
if ($productLink) {
|
||||
$data['product_url'] = trim($productLink->getAttribute('href'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2️⃣ Name / Tel / Message
|
||||
* 结构特点:
|
||||
* <strong>Label</strong>
|
||||
* 下一行 td 就是 value
|
||||
*/
|
||||
$labels = $xpath->query('//strong');
|
||||
|
||||
foreach ($labels as $labelNode) {
|
||||
$label = trim($labelNode->textContent);
|
||||
|
||||
// 当前 strong 所在 td
|
||||
$td = $labelNode->parentNode;
|
||||
|
||||
// 下一个 tr 的 td 才是值
|
||||
$valueTd = $td->parentNode->nextSibling;
|
||||
while ($valueTd && $valueTd->nodeName !== 'tr') {
|
||||
$valueTd = $valueTd->nextSibling;
|
||||
}
|
||||
|
||||
if (!$valueTd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim($valueTd->textContent);
|
||||
|
||||
switch ($label) {
|
||||
case 'Name':
|
||||
$data['name'] = $value;
|
||||
break;
|
||||
|
||||
case 'Tel/Whatsapp':
|
||||
$data['phone'] = $this->normalizePhone($value);
|
||||
break;
|
||||
|
||||
case 'Message':
|
||||
$data['message'] = $this->cleanMessage($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// return $data;
|
||||
return $this->hesucarInquiryFields($data);
|
||||
}
|
||||
/**
|
||||
* hesucar.@hesucar.com字段处理
|
||||
*/
|
||||
function hesucarInquiryFields(array $raw): array
|
||||
{
|
||||
$result = [
|
||||
'form_url' => $raw['product_url'] ?? '',
|
||||
'name' => $raw['name'] ?? '',
|
||||
'tel' => $this->normalizePhone($raw['phone'] ?? ''),
|
||||
'whatsapp' => $raw['phone'] ?? '',
|
||||
'phone' => $raw['phone'] ?? '',
|
||||
'country' => '未知',
|
||||
'message' => $raw['message'] ?? '',
|
||||
'date' => date('Y-m-d H:i:s',strtotime($raw['date'])),
|
||||
];
|
||||
|
||||
$message = [];
|
||||
$message[] = "<p>询盘来源:" . $raw['product_url']."</p>";
|
||||
$message[] = "<p>Tel/Whatsapp:" . $raw['phone']."</p>";
|
||||
$message[] = "<p>FB 创建时间原始值:".$raw['date']."</p>";
|
||||
|
||||
$result['message'] = $result['message'].implode(',', $message);
|
||||
$result['ilang'] = 'en';
|
||||
$result['mailto'] = 'hesu.@hesucar.com';
|
||||
return $result;
|
||||
}
|
||||
|
||||
function cleanMessage(string $message): string
|
||||
{
|
||||
return trim(preg_replace("/\s+/", " ", $message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 多邮箱处理:分配客户
|
||||
*/
|
||||
function fenpaiMailbox()
|
||||
{
|
||||
$emails = EmailPullParsed::where('status', 0)
|
||||
->select('mailto')
|
||||
->distinct()
|
||||
->get();
|
||||
foreach ($emails as $email) {
|
||||
$mailboxModel = EmailMailbox::where('username', $email['mailto'])->first();
|
||||
$this->fenpai($mailboxModel);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配清洗好的数据
|
||||
* @param $mailbox
|
||||
* @return false|void
|
||||
*/
|
||||
public function fenpai($mailbox)
|
||||
{
|
||||
try {
|
||||
// 配置参数
|
||||
$batchSize = 100; // 每批处理量
|
||||
$delaySeconds = 2; // 每封邮件间隔秒数
|
||||
$throttleAfter = 50; // 每发送多少封后增加休息
|
||||
$throttleSeconds = 5; // 批量休息秒数
|
||||
|
||||
// 固定录单人
|
||||
$enterUserId = 186;
|
||||
$enterUser = User::where('id', $enterUserId)->first();
|
||||
|
||||
// 固定渠道
|
||||
//$channel = 12;
|
||||
$channel = $mailbox['channel_id'];
|
||||
$channelInfo = InquiryChannel::where('id', $channel)->first();
|
||||
|
||||
// 从库实时获取
|
||||
$corp_id = $mailbox['corp_id'];
|
||||
// $users = \app\common\model\User::where('corp_id', $corp_id)->where('status', 1)->orderBy('id')->pluck('id')->toArray();
|
||||
if($corp_id == 1){ //合塑
|
||||
$users = \app\common\model\UserGroupMembers::where('group_id', 10)
|
||||
->orderBy('user_id')->pluck('user_id')->toArray();
|
||||
}else if($corp_id == 3){
|
||||
$users = \app\common\model\UserGroupMembers::where('group_id', 9)
|
||||
->orderBy('user_id')->pluck('user_id')->toArray();
|
||||
}
|
||||
|
||||
// 移除不参与分配人员
|
||||
$users = array_values(array_diff($users, [186]));
|
||||
|
||||
// 初始化 UID 轮询器
|
||||
$uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804');
|
||||
$uidRotator->setUids($users, false);
|
||||
|
||||
EmailPullParsed::where('status', 0)
|
||||
->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) {
|
||||
$count = 0;
|
||||
|
||||
foreach ($records as $record) {
|
||||
try {
|
||||
// 获取国家信息
|
||||
$countryName = $record->iplocation ? removeParentheses($record->iplocation) : '未知';
|
||||
$countryInfo = Country::where('cn_addr', $countryName)->first();
|
||||
|
||||
// 获取下一个分配的UID
|
||||
$assignedUid = $uidRotator->next();
|
||||
|
||||
// 获取分配的用户信息
|
||||
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
|
||||
if (!$recipientInfo) {
|
||||
flg("用户ID=" . $assignedUid . " 已离职 跳过自动分配,需要手动移除 UID", false,'mailbox');
|
||||
|
||||
$assignedUid = $uidRotator->next();
|
||||
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
|
||||
}
|
||||
|
||||
// 生成录单
|
||||
$log = [
|
||||
'order_no' => orderNum(),
|
||||
'enter_user_id' => $enterUser->id,
|
||||
'recipient' => $assignedUid,
|
||||
'quality' => 2,
|
||||
'channel' => $channelInfo->id,
|
||||
'email' => $record->email,
|
||||
'tel' => $record->tel,
|
||||
'whatsapp' => '',
|
||||
'country' => $countryInfo->id,
|
||||
'contacts' => $record->name,
|
||||
'content' => $record->message,
|
||||
'r_time' => time(),
|
||||
't_time' => time(),
|
||||
'status' => 2,
|
||||
'sync_crm' => 1
|
||||
];
|
||||
$inquiryLogResult = InquiryLog::create($log);
|
||||
if (!$inquiryLogResult) {
|
||||
throw new BusinessException('录单失败~');
|
||||
}
|
||||
|
||||
// 同步客户
|
||||
$crmCustomer = new CrmCustomer();
|
||||
$crmCustomer->sn = $inquiryLogResult->order_no;
|
||||
$crmCustomer->corp_id = $recipientInfo->corp_id;
|
||||
$crmCustomer->user_id = $assignedUid;
|
||||
$crmCustomer->type = 1;
|
||||
$crmCustomer->channel_id = $inquiryLogResult->channel;
|
||||
$crmCustomer->country_id = $inquiryLogResult->country;
|
||||
$crmCustomer->magnitude = $inquiryLogResult->quality;
|
||||
$crmCustomer->email = $inquiryLogResult->email;
|
||||
$crmCustomer->phone = $inquiryLogResult->tel;
|
||||
$crmCustomer->WhatsApp = $inquiryLogResult->whatsapp;
|
||||
$crmCustomer->contacts = $inquiryLogResult->contacts;
|
||||
$crmCustomer->desc_collect = $inquiryLogResult->content;
|
||||
$crmCustomer->enter_user_id = $inquiryLogResult->enter_user_id;
|
||||
if (!$crmCustomer->save()) {
|
||||
throw new BusinessException('同步失败~');
|
||||
}
|
||||
|
||||
// 9.2 修改消息发送
|
||||
$data = [
|
||||
'id' => $inquiryLogResult->id
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send('inquiry-to-crm-new', $data);
|
||||
|
||||
// 更新状态
|
||||
$record->update(['status' => 1]);
|
||||
flg('====== 编号:'.$inquiryLogResult->order_no.' 分配给:'.$recipientInfo->name.' email:'.$recipientInfo->email, false,'mailbox');
|
||||
|
||||
// 每封邮件后延迟
|
||||
if ($delaySeconds > 0) {
|
||||
sleep($delaySeconds);
|
||||
}
|
||||
|
||||
// 每发送一定数量后额外休息
|
||||
if (++$count % $throttleAfter === 0) {
|
||||
sleep($throttleSeconds);
|
||||
flg("已发送 {$count} 封邮件,休息 {$throttleSeconds} 秒", false,'mailbox');
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
flg("邮件发送失败 ID:{$record->id} - " . $e->getMessage(), false,'mailbox');
|
||||
// 更新状态
|
||||
$record->update(['status' => 3]);
|
||||
// 失败后也加入延迟,避免快速连续失败
|
||||
sleep($delaySeconds);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $th) {
|
||||
flg(dpr($th), false,'mailbox');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user