ProcuremenDashboard.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. <?php
  2. namespace app\common\library;
  3. use think\Db;
  4. /**
  5. * 控制台 — 协助采购(采购供应商)统计
  6. */
  7. class ProcuremenDashboard
  8. {
  9. /**
  10. * @return array{
  11. * today: array{issued:int,confirm:int,approval:int,completed:int},
  12. * week: array{issued:int,confirm:int,approval:int,completed:int},
  13. * pending: array{confirm:int,approval:int},
  14. * supplierRank: array{quote: array, selected: array, period_label: string},
  15. * kpi: array<string, array{today:int,month:int,link:string,link_text:string}>,
  16. * updated: string
  17. * }
  18. */
  19. public static function buildStats(): array
  20. {
  21. $todayStart = date('Y-m-d 00:00:00');
  22. $todayEnd = date('Y-m-d H:i:s');
  23. $weekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
  24. $weekEnd = date('Y-m-d H:i:s');
  25. return [
  26. 'kpi' => self::buildKpiCards(),
  27. 'today' => self::countStageMetrics($todayStart, $todayEnd),
  28. 'week' => self::countStageMetrics($weekStart, $weekEnd),
  29. 'pending' => self::countCurrentPending(),
  30. 'supplierRank' => self::buildSupplierRankings(10, date('Y-m-01 00:00:00'), date('Y-m-t 23:59:59')),
  31. 'last7' => self::buildLast7DaysChart(),
  32. 'updated' => date('Y-m-d H:i:s'),
  33. ];
  34. }
  35. /**
  36. * 控制台首行 KPI:今日 / 本月
  37. *
  38. * @return array<string, array{today:int,month:int,link:string,link_text:string}>
  39. */
  40. public static function buildKpiCards(): array
  41. {
  42. $todayStart = date('Y-m-d 00:00:00');
  43. $todayEnd = date('Y-m-d H:i:s');
  44. $monthStart = date('Y-m-01 00:00:00');
  45. $monthEnd = date('Y-m-t 23:59:59');
  46. $pending = self::countCurrentPending();
  47. return [
  48. 'supplier' => [
  49. 'total' => self::countActiveSuppliers(),
  50. 'link' => '',
  51. 'link_text' => '',
  52. ],
  53. 'confirm' => [
  54. 'today' => (int)$pending['confirm'],
  55. 'month' => self::countOperLogDistinctOrdersInRange(['issue_submit'], $monthStart, $monthEnd),
  56. 'link' => 'procuremen/audit',
  57. 'link_text' => '供应商确认',
  58. ],
  59. 'approval' => [
  60. 'today' => (int)$pending['approval'],
  61. 'month' => self::countOperLogDistinctOrdersInRange(['audit_select'], $monthStart, $monthEnd),
  62. 'link' => 'procuremen/confirm',
  63. 'link_text' => '供应商审批',
  64. ],
  65. 'completed' => [
  66. 'today' => self::countCompletedInRange($todayStart, $todayEnd),
  67. 'month' => self::countCompletedInRange($monthStart, $monthEnd),
  68. 'link' => 'procuremenarchive/index',
  69. 'link_text' => '历史存证档案查询',
  70. ],
  71. ];
  72. }
  73. /**
  74. * @param string[] $wflowValues
  75. */
  76. protected static function countWflowOrdersInPickRange(array $wflowValues, string $start, string $end): int
  77. {
  78. try {
  79. $expr = self::pickTimeSqlExpr();
  80. $query = Db::table('purchase_order')->field('id,CCYDH');
  81. self::applyNotDeletedWhere($query);
  82. $query->whereIn('wflow_status', $wflowValues);
  83. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
  84. $rows = $query->select();
  85. } catch (\Throwable $e) {
  86. return 0;
  87. }
  88. return self::countDistinctOrders(is_array($rows) ? $rows : []);
  89. }
  90. protected static function countQuoteSuppliersInPickRange(string $start, string $end): int
  91. {
  92. $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
  93. if ($scydgyIds === []) {
  94. return 0;
  95. }
  96. try {
  97. $rows = Db::table('purchase_order_detail')
  98. ->where('scydgy_id', 'in', $scydgyIds)
  99. ->field('company_name,amount')
  100. ->select();
  101. } catch (\Throwable $e) {
  102. return 0;
  103. }
  104. if (!is_array($rows)) {
  105. return 0;
  106. }
  107. $names = [];
  108. foreach ($rows as $row) {
  109. if (!is_array($row)) {
  110. continue;
  111. }
  112. $name = trim((string)($row['company_name'] ?? ''));
  113. $amount = trim((string)($row['amount'] ?? ''));
  114. if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
  115. continue;
  116. }
  117. $names[$name] = true;
  118. }
  119. return count($names);
  120. }
  121. protected static function countActiveSuppliers(): int
  122. {
  123. try {
  124. $query = Db::table('customer');
  125. $query->where(function ($q) {
  126. $q->where('status', 1)
  127. ->whereOr('status', '1')
  128. ->whereOr('status', '')
  129. ->whereNull('status');
  130. });
  131. return (int)$query->count();
  132. } catch (\Throwable $e) {
  133. return self::countDistinctSuppliersFromDetails();
  134. }
  135. }
  136. protected static function countDistinctSuppliersFromDetails(): int
  137. {
  138. try {
  139. $rows = Db::table('purchase_order_detail')
  140. ->field('company_name')
  141. ->select();
  142. } catch (\Throwable $e) {
  143. return 0;
  144. }
  145. if (!is_array($rows)) {
  146. return 0;
  147. }
  148. $names = [];
  149. foreach ($rows as $row) {
  150. if (!is_array($row)) {
  151. continue;
  152. }
  153. $name = trim((string)($row['company_name'] ?? ''));
  154. if ($name !== '') {
  155. $names[$name] = true;
  156. }
  157. }
  158. return count($names);
  159. }
  160. protected static function countDistinctOrdersInPickTimeRange(string $start, string $end): int
  161. {
  162. try {
  163. $expr = self::pickTimeSqlExpr();
  164. $query = Db::table('purchase_order')->field('id,CCYDH');
  165. self::applyNotDeletedWhere($query);
  166. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
  167. $rows = $query->select();
  168. } catch (\Throwable $e) {
  169. return 0;
  170. }
  171. return self::countDistinctOrders(is_array($rows) ? $rows : []);
  172. }
  173. /**
  174. * 供应商排名(本月):参与报价次数 / 被选中次数
  175. *
  176. * @return array{
  177. * quote: array<int, array{rank:int,name:string,value:int,percent:int}>,
  178. * selected: array<int, array{rank:int,name:string,value:int,percent:int}>,
  179. * period_label: string,
  180. * supplier_total: int
  181. * }
  182. */
  183. public static function buildSupplierRankings(int $limit = 10, ?string $start = null, ?string $end = null): array
  184. {
  185. $start = $start ?: date('Y-m-d 00:00:00', strtotime('first day of this month'));
  186. $end = $end ?: date('Y-m-t 23:59:59');
  187. return [
  188. 'quote' => self::rankSuppliersByQuoteInRange($start, $end, $limit),
  189. 'selected' => self::rankSuppliersBySelectedInRange($start, $end, $limit),
  190. 'period_label' => '本月',
  191. 'supplier_total' => self::countActiveSuppliers(),
  192. ];
  193. }
  194. /**
  195. * @param array<string, int> $counts
  196. * @return array<int, array{rank:int,name:string,value:int,percent:int}>
  197. */
  198. protected static function formatSupplierRankListFromCounts(array $counts, int $limit): array
  199. {
  200. if ($counts === []) {
  201. return [];
  202. }
  203. arsort($counts);
  204. $top = array_slice($counts, 0, max(1, $limit), true);
  205. $max = $top ? max($top) : 0;
  206. $out = [];
  207. $rank = 1;
  208. foreach ($top as $name => $value) {
  209. $out[] = [
  210. 'rank' => $rank++,
  211. 'name' => (string)$name,
  212. 'value' => (int)$value,
  213. 'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
  214. ];
  215. }
  216. return $out;
  217. }
  218. /**
  219. * @param array<string, array<string|int, bool>> $buckets
  220. * @return array<int, array{rank:int,name:string,value:int,percent:int}>
  221. */
  222. protected static function formatSupplierRankList(array $buckets, int $limit): array
  223. {
  224. if ($buckets === []) {
  225. return [];
  226. }
  227. $counts = [];
  228. foreach ($buckets as $name => $keys) {
  229. $counts[$name] = is_array($keys) ? count($keys) : 0;
  230. }
  231. arsort($counts);
  232. $top = array_slice($counts, 0, max(1, $limit), true);
  233. $max = $top ? max($top) : 0;
  234. $out = [];
  235. $rank = 1;
  236. foreach ($top as $name => $value) {
  237. $out[] = [
  238. 'rank' => $rank++,
  239. 'name' => (string)$name,
  240. 'value' => (int)$value,
  241. 'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
  242. ];
  243. }
  244. return $out;
  245. }
  246. /**
  247. * 按已填写报价金额的供应商统计参与报价次数(明细行数)
  248. *
  249. * @return array<int, array{rank:int,name:string,value:int,percent:int}>
  250. */
  251. protected static function rankSuppliersByQuoteInRange(string $start, string $end, int $limit): array
  252. {
  253. $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
  254. if ($scydgyIds === []) {
  255. return [];
  256. }
  257. try {
  258. $rows = Db::table('purchase_order_detail')
  259. ->where('scydgy_id', 'in', $scydgyIds)
  260. ->field('company_name,amount')
  261. ->select();
  262. } catch (\Throwable $e) {
  263. return [];
  264. }
  265. if (!is_array($rows)) {
  266. return [];
  267. }
  268. $counts = [];
  269. foreach ($rows as $row) {
  270. if (!is_array($row)) {
  271. continue;
  272. }
  273. $name = trim((string)($row['company_name'] ?? ''));
  274. $amount = trim((string)($row['amount'] ?? ''));
  275. if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
  276. continue;
  277. }
  278. $counts[$name] = ($counts[$name] ?? 0) + 1;
  279. }
  280. return self::formatSupplierRankListFromCounts($counts, $limit);
  281. }
  282. /**
  283. * 按主表 pick_company_name 统计被选中订单数
  284. *
  285. * @return array<int, array{rank:int,name:string,value:int,percent:int}>
  286. */
  287. protected static function rankSuppliersBySelectedInRange(string $start, string $end, int $limit): array
  288. {
  289. try {
  290. $expr = self::pickTimeSqlExpr();
  291. $query = Db::table('purchase_order')
  292. ->field('CCYDH,pick_company_name');
  293. self::applyNotDeletedWhere($query);
  294. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
  295. $query->whereRaw("pick_company_name IS NOT NULL AND TRIM(CAST(pick_company_name AS CHAR(255))) <> ''");
  296. $rows = $query->select();
  297. } catch (\Throwable $e) {
  298. return [];
  299. }
  300. if (!is_array($rows)) {
  301. return [];
  302. }
  303. $buckets = [];
  304. foreach ($rows as $row) {
  305. if (!is_array($row)) {
  306. continue;
  307. }
  308. $name = trim((string)($row['pick_company_name'] ?? ''));
  309. if ($name === '') {
  310. continue;
  311. }
  312. $orderKey = self::resolveOrderKey($row);
  313. if (!isset($buckets[$name])) {
  314. $buckets[$name] = [];
  315. }
  316. $buckets[$name][$orderKey] = true;
  317. }
  318. return self::formatSupplierRankList($buckets, $limit);
  319. }
  320. /**
  321. * @return int[]
  322. */
  323. protected static function loadScydgyIdsInPickTimeRange(string $start, string $end): array
  324. {
  325. try {
  326. $expr = self::pickTimeSqlExpr();
  327. $query = Db::table('purchase_order')->field('scydgy_id');
  328. self::applyNotDeletedWhere($query);
  329. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
  330. $rows = $query->select();
  331. } catch (\Throwable $e) {
  332. return [];
  333. }
  334. if (!is_array($rows)) {
  335. return [];
  336. }
  337. $ids = [];
  338. foreach ($rows as $row) {
  339. if (!is_array($row)) {
  340. continue;
  341. }
  342. $sid = (int)($row['scydgy_id'] ?? 0);
  343. if ($sid > 0) {
  344. $ids[$sid] = true;
  345. }
  346. }
  347. return array_keys($ids);
  348. }
  349. /**
  350. * 近 7 天每日趋势(按订单号去重)
  351. *
  352. * @return array{
  353. * labels: string[],
  354. * issued: int[],
  355. * confirm: int[],
  356. * approval: int[],
  357. * completed: int[]
  358. * }
  359. */
  360. public static function buildLast7DaysChart(): array
  361. {
  362. $dayKeys = [];
  363. $labels = [];
  364. for ($i = 6; $i >= 0; $i--) {
  365. $day = date('Y-m-d', strtotime('-' . $i . ' days'));
  366. $dayKeys[] = $day;
  367. $labels[] = date('m-d', strtotime($day));
  368. }
  369. if ($dayKeys === []) {
  370. return ['labels' => [], 'issued' => [], 'confirm' => [], 'approval' => [], 'completed' => []];
  371. }
  372. $rangeStart = $dayKeys[0] . ' 00:00:00';
  373. $rangeEnd = $dayKeys[count($dayKeys) - 1] . ' 23:59:59';
  374. $startTs = ProcuremenTime::parseToTimestamp($rangeStart);
  375. $endTs = ProcuremenTime::parseToTimestamp($rangeEnd);
  376. $issuedBuckets = self::bucketOperLogOrdersByDay(
  377. self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs),
  378. $dayKeys,
  379. $startTs,
  380. $endTs
  381. );
  382. $confirmBuckets = self::emptyDayBuckets($dayKeys);
  383. try {
  384. $expr = self::pickTimeSqlExpr();
  385. $query = Db::table('purchase_order')->field('CCYDH,pick_time,createtime,dputrecord,dStamp,wflow_status,status');
  386. self::applyNotDeletedWhere($query);
  387. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$rangeStart, $rangeEnd]);
  388. $rows = $query->select();
  389. } catch (\Throwable $e) {
  390. $rows = [];
  391. }
  392. if (is_array($rows)) {
  393. foreach ($rows as $row) {
  394. if (!is_array($row)) {
  395. continue;
  396. }
  397. $day = self::resolveRowDayKey($row, $dayKeys);
  398. if ($day === null) {
  399. continue;
  400. }
  401. $orderKey = self::resolveOrderKey($row);
  402. if (self::isPendingConfirmRow($row)) {
  403. $confirmBuckets[$day][$orderKey] = true;
  404. }
  405. }
  406. }
  407. $approvalBuckets = self::bucketOperLogOrdersByDay(
  408. self::loadOperLogsInRange(['audit_select'], $startTs, $endTs),
  409. $dayKeys,
  410. $startTs,
  411. $endTs
  412. );
  413. $completedBuckets = self::bucketOperLogOrdersByDay(
  414. self::loadOperLogsInRange(['purchase_confirm', 'mark_complete'], $startTs, $endTs),
  415. $dayKeys,
  416. $startTs,
  417. $endTs
  418. );
  419. return [
  420. 'labels' => $labels,
  421. 'issued' => self::bucketCounts($issuedBuckets, $dayKeys),
  422. 'confirm' => self::bucketCounts($confirmBuckets, $dayKeys),
  423. 'approval' => self::bucketCounts($approvalBuckets, $dayKeys),
  424. 'completed' => self::bucketCounts($completedBuckets, $dayKeys),
  425. ];
  426. }
  427. /**
  428. * @param string[] $dayKeys
  429. * @return array<string, array<string, bool>>
  430. */
  431. protected static function emptyDayBuckets(array $dayKeys): array
  432. {
  433. $out = [];
  434. foreach ($dayKeys as $day) {
  435. $out[$day] = [];
  436. }
  437. return $out;
  438. }
  439. /**
  440. * @param array<string, array<string, bool>> $buckets
  441. * @param string[] $dayKeys
  442. * @return int[]
  443. */
  444. protected static function bucketCounts(array $buckets, array $dayKeys): array
  445. {
  446. $out = [];
  447. foreach ($dayKeys as $day) {
  448. $out[] = isset($buckets[$day]) ? count($buckets[$day]) : 0;
  449. }
  450. return $out;
  451. }
  452. /**
  453. * @param array<string, mixed> $row
  454. * @param string[] $dayKeys
  455. */
  456. protected static function resolveRowDayKey(array $row, array $dayKeys): ?string
  457. {
  458. $ts = 0;
  459. foreach (['pick_time', 'createtime', 'dputrecord', 'dStamp'] as $key) {
  460. $t = ProcuremenTime::parseToTimestamp($row[$key] ?? null);
  461. if ($t > $ts) {
  462. $ts = $t;
  463. }
  464. }
  465. if ($ts <= 0) {
  466. return null;
  467. }
  468. $day = date('Y-m-d', $ts);
  469. return in_array($day, $dayKeys, true) ? $day : null;
  470. }
  471. /**
  472. * @param array<string, mixed> $row
  473. */
  474. protected static function resolveOrderKey(array $row): string
  475. {
  476. $dh = trim((string)($row['CCYDH'] ?? ''));
  477. return $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
  478. }
  479. /**
  480. * @param array<string, mixed> $row
  481. */
  482. protected static function isPendingConfirmRow(array $row): bool
  483. {
  484. return ProcuremenStatus::isWflowPendingConfirm($row['wflow_status'] ?? '');
  485. }
  486. /**
  487. * @param array<int, string> $actions
  488. * @return array<int, array{scydgy_id:int, createtime:mixed}>
  489. */
  490. protected static function loadOperLogsInRange(array $actions, int $startTs, int $endTs): array
  491. {
  492. try {
  493. $logs = Db::table('purchase_order_oper_log')
  494. ->where('action', 'in', $actions)
  495. ->field('scydgy_id,createtime')
  496. ->select();
  497. } catch (\Throwable $e) {
  498. return [];
  499. }
  500. if (!is_array($logs)) {
  501. return [];
  502. }
  503. $out = [];
  504. foreach ($logs as $log) {
  505. if (!is_array($log)) {
  506. continue;
  507. }
  508. $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
  509. if ($ts < $startTs || $ts > $endTs) {
  510. continue;
  511. }
  512. $sid = (int)($log['scydgy_id'] ?? 0);
  513. if ($sid > 0) {
  514. $out[] = ['scydgy_id' => $sid, 'createtime' => $log['createtime'] ?? null];
  515. }
  516. }
  517. return $out;
  518. }
  519. /**
  520. * @param array<int, array{scydgy_id:int, createtime:mixed}> $logs
  521. * @param string[] $dayKeys
  522. * @return array<string, array<string, bool>>
  523. */
  524. protected static function bucketOperLogOrdersByDay(array $logs, array $dayKeys, int $startTs, int $endTs): array
  525. {
  526. $buckets = self::emptyDayBuckets($dayKeys);
  527. if ($logs === []) {
  528. return $buckets;
  529. }
  530. $sidSet = [];
  531. foreach ($logs as $log) {
  532. $sidSet[(int)$log['scydgy_id']] = true;
  533. }
  534. $orderMap = [];
  535. foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
  536. if (!is_array($row)) {
  537. continue;
  538. }
  539. $sid = (int)($row['scydgy_id'] ?? 0);
  540. if ($sid > 0) {
  541. $orderMap[$sid] = self::resolveOrderKey($row);
  542. }
  543. }
  544. foreach ($logs as $log) {
  545. $sid = (int)$log['scydgy_id'];
  546. if ($sid <= 0 || !isset($orderMap[$sid])) {
  547. continue;
  548. }
  549. $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
  550. if ($ts < $startTs || $ts > $endTs) {
  551. continue;
  552. }
  553. $day = date('Y-m-d', $ts);
  554. if (!isset($buckets[$day])) {
  555. continue;
  556. }
  557. $buckets[$day][$orderMap[$sid]] = true;
  558. }
  559. return $buckets;
  560. }
  561. /**
  562. * 按操作日志统计时段内去重订单数(与近7天图口径一致)
  563. *
  564. * @param string[] $actions
  565. */
  566. protected static function countOperLogDistinctOrdersInRange(array $actions, string $start, string $end): int
  567. {
  568. $startTs = ProcuremenTime::parseToTimestamp($start);
  569. $endTs = ProcuremenTime::parseToTimestamp($end);
  570. if ($startTs <= 0 || $endTs <= 0 || $actions === []) {
  571. return 0;
  572. }
  573. $logs = self::loadOperLogsInRange($actions, $startTs, $endTs);
  574. if ($logs === []) {
  575. return 0;
  576. }
  577. $sidSet = [];
  578. foreach ($logs as $log) {
  579. $sid = (int)($log['scydgy_id'] ?? 0);
  580. if ($sid > 0) {
  581. $sidSet[$sid] = true;
  582. }
  583. }
  584. if ($sidSet === []) {
  585. return 0;
  586. }
  587. $keys = [];
  588. foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
  589. if (!is_array($row)) {
  590. continue;
  591. }
  592. $keys[self::resolveOrderKey($row)] = true;
  593. }
  594. return count($keys);
  595. }
  596. /**
  597. * @return array{issued:int,confirm:int,approval:int,completed:int}
  598. */
  599. protected static function countStageMetrics(string $start, string $end): array
  600. {
  601. $pending = self::countCurrentPending();
  602. return [
  603. // 已下发:本时段内通知下发,且当前已离开「待确认」队列(不与待确认重复)
  604. 'issued' => self::countIssuedInRange($start, $end),
  605. // 待确认 / 待审批:与列表一致,统计当前积压(不按 pick_time 截断)
  606. 'confirm' => $pending['confirm'],
  607. 'approval' => $pending['approval'],
  608. 'completed' => self::countCompletedInRange($start, $end),
  609. ];
  610. }
  611. /**
  612. * @return array{confirm:int,approval:int}
  613. */
  614. protected static function countCurrentPending(): array
  615. {
  616. return [
  617. 'confirm' => self::countDistinctOrders(self::loadPendingConfirmRows()),
  618. 'approval' => self::countDistinctOrders(self::loadPendingApprovalRows()),
  619. ];
  620. }
  621. protected static function countIssuedInRange(string $start, string $end): int
  622. {
  623. $startTs = ProcuremenTime::parseToTimestamp($start);
  624. $endTs = ProcuremenTime::parseToTimestamp($end);
  625. if ($startTs <= 0 || $endTs <= 0) {
  626. return 0;
  627. }
  628. $confirmSidSet = self::loadPendingConfirmScydgySet();
  629. $keys = [];
  630. $logs = self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs);
  631. foreach ($logs as $log) {
  632. $sid = (int)($log['scydgy_id'] ?? 0);
  633. if ($sid <= 0 || isset($confirmSidSet[$sid])) {
  634. continue;
  635. }
  636. foreach (self::loadOrdersByScydgyIds([$sid]) as $row) {
  637. if (!is_array($row)) {
  638. continue;
  639. }
  640. $keys[self::resolveOrderKey($row)] = true;
  641. }
  642. }
  643. if ($keys !== []) {
  644. return count($keys);
  645. }
  646. return self::countIssuedByPickTimePastConfirm($start, $end);
  647. }
  648. /**
  649. * 无下发日志时:本时段 pick_time 内且流程已进入待审批/已审核(不含仍停在待确认)
  650. */
  651. protected static function countIssuedByPickTimePastConfirm(string $start, string $end): int
  652. {
  653. try {
  654. $expr = self::pickTimeSqlExpr();
  655. $query = Db::table('purchase_order')->field('id,CCYDH,wflow_status');
  656. self::applyNotDeletedWhere($query);
  657. $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
  658. $query->whereIn('wflow_status', array_merge(
  659. ProcuremenStatus::wflowPendingApprovalValues(),
  660. ProcuremenStatus::wflowApprovedValues()
  661. ));
  662. $rows = $query->select();
  663. } catch (\Throwable $e) {
  664. return 0;
  665. }
  666. return self::countDistinctOrders(is_array($rows) ? $rows : []);
  667. }
  668. /**
  669. * @return array<int, bool>
  670. */
  671. protected static function loadPendingConfirmScydgySet(): array
  672. {
  673. $set = [];
  674. foreach (self::loadPendingConfirmRows() as $row) {
  675. if (!is_array($row)) {
  676. continue;
  677. }
  678. $sid = (int)($row['scydgy_id'] ?? 0);
  679. if ($sid > 0) {
  680. $set[$sid] = true;
  681. }
  682. }
  683. return $set;
  684. }
  685. protected static function countCompletedInRange(string $start, string $end): int
  686. {
  687. $startTs = ProcuremenTime::parseToTimestamp($start);
  688. $endTs = ProcuremenTime::parseToTimestamp($end);
  689. if ($startTs <= 0 || $endTs <= 0) {
  690. return 0;
  691. }
  692. try {
  693. $logs = Db::table('purchase_order_oper_log')
  694. ->where('action', 'in', ['purchase_confirm', 'mark_complete'])
  695. ->field('scydgy_id,createtime')
  696. ->select();
  697. } catch (\Throwable $e) {
  698. return 0;
  699. }
  700. if (!is_array($logs) || $logs === []) {
  701. return 0;
  702. }
  703. $sidSet = [];
  704. foreach ($logs as $log) {
  705. if (!is_array($log)) {
  706. continue;
  707. }
  708. $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
  709. if ($ts < $startTs || $ts > $endTs) {
  710. continue;
  711. }
  712. $sid = (int)($log['scydgy_id'] ?? 0);
  713. if ($sid > 0) {
  714. $sidSet[$sid] = true;
  715. }
  716. }
  717. if ($sidSet === []) {
  718. return 0;
  719. }
  720. return self::countDistinctOrders(self::loadOrdersByScydgyIds(array_keys($sidSet)));
  721. }
  722. /**
  723. * @param int[] $scydgyIds
  724. * @return array<int, array<string, mixed>>
  725. */
  726. protected static function loadOrdersByScydgyIds(array $scydgyIds): array
  727. {
  728. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  729. if ($scydgyIds === []) {
  730. return [];
  731. }
  732. try {
  733. $query = Db::table('purchase_order')
  734. ->where('scydgy_id', 'in', $scydgyIds)
  735. ->field('id,CCYDH,scydgy_id,wflow_status');
  736. self::applyNotDeletedWhere($query);
  737. $rows = $query->select();
  738. } catch (\Throwable $e) {
  739. return [];
  740. }
  741. return is_array($rows) ? $rows : [];
  742. }
  743. /**
  744. * @return array<int, array<string, mixed>>
  745. */
  746. protected static function loadPendingConfirmRows(): array
  747. {
  748. try {
  749. $query = Db::table('purchase_order')->field('id,CCYDH,scydgy_id');
  750. self::applyNotDeletedWhere($query);
  751. $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingConfirmValues());
  752. $rows = $query->select();
  753. } catch (\Throwable $e) {
  754. return [];
  755. }
  756. return is_array($rows) ? $rows : [];
  757. }
  758. /**
  759. * @return array<int, array<string, mixed>>
  760. */
  761. protected static function loadPendingApprovalRows(): array
  762. {
  763. try {
  764. $query = Db::table('purchase_order')->field('id,CCYDH');
  765. self::applyNotDeletedWhere($query);
  766. $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingApprovalValues());
  767. $query->whereRaw('NOT (' . ProcuremenStatus::sqlPoCompleted('status') . ')');
  768. $rows = $query->select();
  769. } catch (\Throwable $e) {
  770. return [];
  771. }
  772. return is_array($rows) ? $rows : [];
  773. }
  774. /**
  775. * @param array<int, array<string, mixed>> $rows
  776. */
  777. protected static function countDistinctOrders(array $rows): int
  778. {
  779. if ($rows === []) {
  780. return 0;
  781. }
  782. $keys = [];
  783. foreach ($rows as $row) {
  784. if (!is_array($row)) {
  785. continue;
  786. }
  787. $dh = trim((string)($row['CCYDH'] ?? ''));
  788. $keys[$dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0))] = true;
  789. }
  790. return count($keys);
  791. }
  792. protected static function applyNotDeletedWhere($query): void
  793. {
  794. $query->whereRaw(
  795. '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
  796. );
  797. }
  798. protected static function pickTimeSqlExpr(): string
  799. {
  800. $norm = function (string $col): string {
  801. return "NULLIF(NULLIF(TRIM(CAST({$col} AS CHAR(32))), ''), '0000-00-00')";
  802. };
  803. $createtime = 'IF(createtime > 946684800, FROM_UNIXTIME(createtime), ' . $norm('createtime') . ')';
  804. $parts = [
  805. $norm('pick_time'),
  806. $createtime,
  807. $norm('dputrecord'),
  808. $norm('dStamp'),
  809. ];
  810. return 'COALESCE(' . implode(', ', $parts) . ')';
  811. }
  812. }