Procuremenarchive.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\ProcuremenStatus;
  5. use app\common\library\ProcuremenTime;
  6. use think\Db;
  7. /**
  8. * 协助采购 — 历史存证档案查询(审批通过即可查;含操作记录详情)
  9. *
  10. * @icon fa fa-archive
  11. */
  12. class Procuremenarchive extends Backend
  13. {
  14. protected $searchFields = 'CCYDH,CYJMC,CGYMC,pick_company_name';
  15. public function _initialize()
  16. {
  17. parent::_initialize();
  18. $canArchiveAbandon = $this->canArchiveAbandon();
  19. $canDetails = $this->hasProcuremenSiblingPerm(['details']);
  20. $this->view->assign('canArchiveAbandon', $canArchiveAbandon);
  21. $this->assignconfig('procuremenAuth', [
  22. 'details' => $canDetails,
  23. 'auditabandon' => $canArchiveAbandon,
  24. 'archiveabandon' => $canArchiveAbandon,
  25. ]);
  26. if (session_status() === PHP_SESSION_ACTIVE) {
  27. @session_write_close();
  28. }
  29. }
  30. /**
  31. * 历史存证「重新下发」权限:与审批列表统一为 auditabandon
  32. */
  33. protected function canArchiveAbandon(): bool
  34. {
  35. if ($this->hasProcuremenSiblingPerm(['auditabandon', 'auditAbandon'])) {
  36. return true;
  37. }
  38. return $this->hasProcuremenSiblingPerm(['archiveabandon', 'archiveAbandon']);
  39. }
  40. /**
  41. * @param string[] $actions
  42. */
  43. protected function hasProcuremenSiblingPerm(array $actions): bool
  44. {
  45. foreach ($actions as $action) {
  46. $action = strtolower(trim((string)$action));
  47. if ($action === '') {
  48. continue;
  49. }
  50. if (strpos($action, 'procuremen/') !== 0) {
  51. $action = 'procuremen/' . $action;
  52. }
  53. if ($this->auth->check($action)) {
  54. return true;
  55. }
  56. }
  57. $suffixes = [];
  58. foreach ($actions as $action) {
  59. $action = strtolower(trim((string)$action));
  60. $action = preg_replace('#^procuremen/#', '', $action);
  61. if ($action !== '') {
  62. $suffixes[$action] = true;
  63. }
  64. }
  65. if ($suffixes === []) {
  66. return false;
  67. }
  68. try {
  69. foreach ($this->auth->getRuleList() as $rule) {
  70. $rule = strtolower((string)$rule);
  71. if (strpos($rule, 'procuremen/') !== 0) {
  72. continue;
  73. }
  74. foreach (array_keys($suffixes) as $suffix) {
  75. if ($rule === 'procuremen/' . $suffix || strpos($rule, 'procuremen/' . $suffix . '/') === 0) {
  76. return true;
  77. }
  78. }
  79. }
  80. } catch (\Throwable $e) {
  81. }
  82. return false;
  83. }
  84. /**
  85. * 历史存证范围:审批通过(wflow=已审核)即可显示,不要求入库合格/已完结
  86. * (兼容旧数据仅有 status=已完结)
  87. */
  88. protected function applyPurchaseOrderArchiveWhere($query): void
  89. {
  90. $query->where(function ($q) {
  91. $q->whereIn('wflow_status', ProcuremenStatus::wflowApprovedValues())
  92. ->whereOr('status', 'in', ProcuremenStatus::poCompletedValues());
  93. });
  94. }
  95. /**
  96. * 与 collapseArchiveRowsByOrder 一致:有订单号按 CCYDH 合并,空单号按 id
  97. */
  98. protected function archiveOrderGroupKeySql(): string
  99. {
  100. return "IF(TRIM(IFNULL(`CCYDH`,''))='', CONCAT('_id_', `id`), TRIM(`CCYDH`))";
  101. }
  102. public function index()
  103. {
  104. $this->relationSearch = false;
  105. $this->request->filter(['strip_tags', 'trim']);
  106. if ($this->request->isAjax()) {
  107. list($where, $sort, $order, $offset, $limit) = $this->buildparams();
  108. $limit = max(10, min(500, (int)$limit));
  109. $offset = max(0, (int)$offset);
  110. $applyFilters = function ($query) use ($where) {
  111. $this->applyPurchaseOrderArchiveWhere($query);
  112. if (!empty($where)) {
  113. $query->where($where);
  114. }
  115. };
  116. $sortField = preg_match('/^[a-zA-Z0-9_]+$/', (string)$sort) ? $sort : 'complete_time';
  117. $orderDir = strtoupper((string)$order) === 'ASC' ? 'ASC' : 'DESC';
  118. $groupKeySql = $this->archiveOrderGroupKeySql();
  119. $total = 0;
  120. try {
  121. $countQuery = Db::table('purchase_order');
  122. $applyFilters($countQuery);
  123. $subSql = $countQuery->fieldRaw($groupKeySql . ' AS gk')->group($groupKeySql)->buildSql();
  124. $cntRows = Db::query('SELECT COUNT(*) AS c FROM ' . $subSql . ' _archive_g');
  125. $total = (int)($cntRows[0]['c'] ?? 0);
  126. } catch (\Throwable $e) {
  127. try {
  128. $countQuery = Db::table('purchase_order');
  129. $applyFilters($countQuery);
  130. $total = (int)$countQuery->count('DISTINCT CCYDH');
  131. } catch (\Throwable $e2) {
  132. $total = 0;
  133. }
  134. }
  135. if ($total <= 0) {
  136. return json(['total' => 0, 'rows' => []]);
  137. }
  138. $orderSql = 'MAX(`id`) ' . $orderDir;
  139. if ($sortField === 'CCYDH') {
  140. $orderSql = $groupKeySql . ' ' . $orderDir;
  141. } elseif (in_array($sortField, ['CYJMC', 'CGYMC', 'pick_company_name'], true)) {
  142. $orderSql = 'MAX(`' . $sortField . '`) ' . $orderDir;
  143. }
  144. $pageQuery = Db::table('purchase_order');
  145. $applyFilters($pageQuery);
  146. try {
  147. $groups = $pageQuery
  148. ->fieldRaw($groupKeySql . ' AS gk, MAX(`id`) AS max_id')
  149. ->group($groupKeySql)
  150. ->orderRaw($orderSql)
  151. ->limit($offset, $limit)
  152. ->select();
  153. } catch (\Throwable $e) {
  154. $groups = [];
  155. }
  156. if (!is_array($groups) || $groups === []) {
  157. return json(['total' => $total, 'rows' => []]);
  158. }
  159. $ccydhs = [];
  160. $orphanIds = [];
  161. foreach ($groups as $g) {
  162. if (!is_array($g)) {
  163. continue;
  164. }
  165. $gk = trim((string)($g['gk'] ?? ''));
  166. if ($gk === '') {
  167. continue;
  168. }
  169. if (strpos($gk, '_id_') === 0) {
  170. $oid = (int)substr($gk, 4);
  171. if ($oid > 0) {
  172. $orphanIds[$oid] = true;
  173. }
  174. } else {
  175. $ccydhs[$gk] = true;
  176. }
  177. }
  178. $ccydhList = array_keys($ccydhs);
  179. $orphanIdList = array_keys($orphanIds);
  180. if ($ccydhList === [] && $orphanIdList === []) {
  181. return json(['total' => $total, 'rows' => []]);
  182. }
  183. $detailQuery = Db::table('purchase_order');
  184. $applyFilters($detailQuery);
  185. $detailQuery->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_company_name,createtime,dStamp,pick_time');
  186. $detailQuery->where(function ($q) use ($ccydhList, $orphanIdList) {
  187. if ($ccydhList !== []) {
  188. $quoted = [];
  189. foreach ($ccydhList as $dh) {
  190. $quoted[] = "'" . str_replace("'", "''", (string)$dh) . "'";
  191. }
  192. $q->whereRaw('TRIM(`CCYDH`) IN (' . implode(',', $quoted) . ')');
  193. }
  194. if ($orphanIdList !== []) {
  195. if ($ccydhList !== []) {
  196. $q->whereOr('id', 'in', $orphanIdList);
  197. } else {
  198. $q->where('id', 'in', $orphanIdList);
  199. }
  200. }
  201. });
  202. $rows = $detailQuery->select();
  203. if (!is_array($rows)) {
  204. $rows = [];
  205. }
  206. $sidList = [];
  207. foreach ($rows as $r) {
  208. if (!is_array($r)) {
  209. continue;
  210. }
  211. $sid = (int)($r['scydgy_id'] ?? 0);
  212. if ($sid !== 0) {
  213. $sidList[$sid] = true;
  214. }
  215. }
  216. // 审批时间 = 审核确认供应商;完结时间 = 直接完结 / 质量评分(与详情「已完结」一致)
  217. $approveTsMap = ProcuremenTime::loadOperLogTimestampMap(
  218. array_keys($sidList),
  219. ['purchase_confirm']
  220. );
  221. $completeTsMap = ProcuremenTime::loadOperLogTimestampMap(
  222. array_keys($sidList),
  223. ['mark_complete', 'inbound_score']
  224. );
  225. $inboundMap = [];
  226. $inboundTimeMap = [];
  227. if ($sidList !== []) {
  228. try {
  229. $inRows = Db::table('purchase_order_inbound_score')
  230. ->where('scydgy_id', 'in', array_keys($sidList))
  231. ->field('scydgy_id,result,updatetime,createtime')
  232. ->select();
  233. } catch (\Throwable $e) {
  234. $inRows = [];
  235. }
  236. if (is_array($inRows)) {
  237. foreach ($inRows as $ir) {
  238. if (!is_array($ir)) {
  239. continue;
  240. }
  241. $isid = (int)($ir['scydgy_id'] ?? 0);
  242. if ($isid === 0) {
  243. continue;
  244. }
  245. $inboundMap[$isid] = trim((string)($ir['result'] ?? ''));
  246. $its = ProcuremenTime::parseToTimestamp($ir['updatetime'] ?? $ir['createtime'] ?? null);
  247. if ($its > 946684800) {
  248. $inboundTimeMap[$isid] = $its;
  249. }
  250. }
  251. }
  252. }
  253. $out = [];
  254. foreach ($rows as $r) {
  255. if (!is_array($r)) {
  256. continue;
  257. }
  258. $sid = (int)($r['scydgy_id'] ?? 0);
  259. $approve = ProcuremenTime::resolveCompletedDone($r, $approveTsMap);
  260. $completeMap = $completeTsMap;
  261. if ($sid > 0 && empty($completeMap[$sid]) && isset($inboundTimeMap[$sid])) {
  262. $completeMap[$sid] = $inboundTimeMap[$sid];
  263. }
  264. $complete = ProcuremenTime::resolveCompletedDone($r, $completeMap);
  265. if ($sid > 0 && empty($completeTsMap[$sid]) && empty($inboundTimeMap[$sid])) {
  266. $complete = ['ts' => 0, 'text' => ''];
  267. }
  268. $out[] = [
  269. 'id' => (int)($r['id'] ?? 0),
  270. 'scydgy_id' => $sid,
  271. 'CCYDH' => trim((string)($r['CCYDH'] ?? '')),
  272. 'CYJMC' => trim((string)($r['CYJMC'] ?? '')),
  273. 'CGYMC' => trim((string)($r['CGYMC'] ?? '')),
  274. 'pick_company_name' => trim((string)($r['pick_company_name'] ?? '')),
  275. 'inbound_result' => $inboundMap[$sid] ?? '',
  276. 'createtime' => $approve['ts'],
  277. 'createtime_text' => $approve['text'],
  278. 'complete_time' => $complete['ts'],
  279. 'complete_time_text' => $complete['text'],
  280. ];
  281. }
  282. $merged = $this->collapseArchiveRowsByOrder($out);
  283. $pageRows = $this->reorderArchiveRowsByGroupKeys($merged, $groups);
  284. return json(['total' => $total, 'rows' => $pageRows]);
  285. }
  286. return $this->view->fetch();
  287. }
  288. /**
  289. * @param array<int, array<string, mixed>> $merged
  290. * @param array<int, array<string, mixed>> $groups
  291. * @return array<int, array<string, mixed>>
  292. */
  293. protected function reorderArchiveRowsByGroupKeys(array $merged, array $groups): array
  294. {
  295. $pos = [];
  296. foreach ($groups as $i => $g) {
  297. if (!is_array($g)) {
  298. continue;
  299. }
  300. $gk = trim((string)($g['gk'] ?? ''));
  301. if ($gk !== '') {
  302. $pos[$gk] = $i;
  303. }
  304. }
  305. if ($pos === []) {
  306. return $merged;
  307. }
  308. usort($merged, function ($a, $b) use ($pos) {
  309. $ka = trim((string)($a['CCYDH'] ?? ''));
  310. if ($ka === '') {
  311. $ka = '_id_' . (int)($a['id'] ?? 0);
  312. }
  313. $kb = trim((string)($b['CCYDH'] ?? ''));
  314. if ($kb === '') {
  315. $kb = '_id_' . (int)($b['id'] ?? 0);
  316. }
  317. $ia = $pos[$ka] ?? 999999;
  318. $ib = $pos[$kb] ?? 999999;
  319. if ($ia === $ib) {
  320. return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
  321. }
  322. return $ia <=> $ib;
  323. });
  324. return array_values($merged);
  325. }
  326. /**
  327. * 历史存证:同一订单号 CCYDH 合并为一行(与审批/确认列表一致)
  328. *
  329. * @param array<int, array<string, mixed>> $rows
  330. * @return array<int, array<string, mixed>>
  331. */
  332. protected function collapseArchiveRowsByOrder(array $rows): array
  333. {
  334. if ($rows === []) {
  335. return [];
  336. }
  337. $groups = [];
  338. foreach ($rows as $row) {
  339. if (!is_array($row)) {
  340. continue;
  341. }
  342. $dh = trim((string)($row['CCYDH'] ?? ''));
  343. $key = $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
  344. if (!isset($groups[$key])) {
  345. $groups[$key] = [];
  346. }
  347. $groups[$key][] = $row;
  348. }
  349. $out = [];
  350. foreach ($groups as $groupRows) {
  351. if ($groupRows === []) {
  352. continue;
  353. }
  354. usort($groupRows, function ($a, $b) {
  355. return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
  356. });
  357. $head = $groupRows[0];
  358. $gymcList = [];
  359. $supplierList = [];
  360. $inboundList = [];
  361. foreach ($groupRows as $r) {
  362. $g = trim((string)($r['CGYMC'] ?? ''));
  363. if ($g !== '' && !in_array($g, $gymcList, true)) {
  364. $gymcList[] = $g;
  365. }
  366. $sn = trim((string)($r['pick_company_name'] ?? ''));
  367. if ($sn !== '' && !in_array($sn, $supplierList, true)) {
  368. $supplierList[] = $sn;
  369. }
  370. $ir = trim((string)($r['inbound_result'] ?? ''));
  371. if ($ir !== '' && !in_array($ir, $inboundList, true)) {
  372. $inboundList[] = $ir;
  373. }
  374. }
  375. $merged = $head;
  376. if (count($groupRows) > 1) {
  377. $merged['CGYMC'] = implode('、', $gymcList);
  378. }
  379. $merged['pick_company_name'] = implode('、', $supplierList);
  380. $merged['inbound_result'] = $inboundList !== [] ? implode('、', $inboundList) : '';
  381. $merged['process_count'] = count($groupRows);
  382. $latestApproveTs = 0;
  383. $latestApproveText = '';
  384. $latestCompleteTs = 0;
  385. $latestCompleteText = '';
  386. foreach ($groupRows as $r) {
  387. $ts = (int)($r['createtime'] ?? 0);
  388. if ($ts <= 0 && !empty($r['createtime_text'])) {
  389. $ts = (int)strtotime((string)$r['createtime_text']);
  390. }
  391. if ($ts > $latestApproveTs) {
  392. $latestApproveTs = $ts;
  393. $latestApproveText = trim((string)($r['createtime_text'] ?? ''));
  394. }
  395. $cts = (int)($r['complete_time'] ?? 0);
  396. if ($cts <= 0 && !empty($r['complete_time_text'])) {
  397. $cts = (int)strtotime((string)$r['complete_time_text']);
  398. }
  399. if ($cts > $latestCompleteTs) {
  400. $latestCompleteTs = $cts;
  401. $latestCompleteText = trim((string)($r['complete_time_text'] ?? ''));
  402. }
  403. }
  404. if ($latestApproveTs > 0) {
  405. $merged['createtime'] = $latestApproveTs;
  406. if ($latestApproveText === '') {
  407. $latestApproveText = date('Y-m-d H:i:s', $latestApproveTs);
  408. }
  409. $merged['createtime_text'] = ProcuremenTime::formatDisplayDateTime($latestApproveText);
  410. } else {
  411. $merged['createtime'] = 0;
  412. $merged['createtime_text'] = '';
  413. }
  414. if ($latestCompleteTs > 0) {
  415. $merged['complete_time'] = $latestCompleteTs;
  416. if ($latestCompleteText === '') {
  417. $latestCompleteText = date('Y-m-d H:i:s', $latestCompleteTs);
  418. }
  419. $merged['complete_time_text'] = ProcuremenTime::formatDisplayDateTime($latestCompleteText);
  420. } else {
  421. $merged['complete_time'] = 0;
  422. $merged['complete_time_text'] = '';
  423. }
  424. $maxId = (int)($head['id'] ?? 0);
  425. foreach ($groupRows as $r) {
  426. $maxId = max($maxId, (int)($r['id'] ?? 0));
  427. }
  428. $merged['id'] = $maxId;
  429. $out[] = $merged;
  430. }
  431. return $out;
  432. }
  433. }