Procuremenexport.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. <?php
  2. namespace app\admin\controller;
  3. use app\common\controller\Backend;
  4. use app\common\library\ProcuremenOperLog;
  5. use app\common\library\ProcuremenStatus;
  6. use app\common\library\ProcuremenTime;
  7. use think\Db;
  8. /**
  9. * 协助采购 — 月度报表导出列表
  10. *
  11. * @icon fa fa-file-excel-o
  12. */
  13. class Procuremenexport extends Backend
  14. {
  15. protected $searchFields = 'CCYDH,CYJMC,CGYMC';
  16. public function _initialize()
  17. {
  18. parent::_initialize();
  19. $canExport = $this->hasProcuremenSiblingPerm(['export_month_outward', 'exportMonthOutward']);
  20. $canExportQuote = $this->hasProcuremenSiblingPerm(['export_month_quote', 'exportMonthQuote', 'export_month_outward', 'exportMonthOutward']);
  21. $canDetails = $this->hasProcuremenSiblingPerm(['details']);
  22. $this->view->assign('canExportMonthOutward', $canExport);
  23. $this->view->assign('canExportMonthQuote', $canExportQuote);
  24. $this->assignconfig('procuremenAuth', [
  25. 'details' => $canDetails,
  26. 'export_month_outward' => $canExport,
  27. 'export_month_quote' => $canExportQuote,
  28. ]);
  29. }
  30. /**
  31. * 月度导出页引用的采购子权限(规则名在 procuremen 控制器下)
  32. *
  33. * @param string[] $actions
  34. */
  35. protected function hasProcuremenSiblingPerm(array $actions): bool
  36. {
  37. foreach ($actions as $action) {
  38. $action = strtolower(trim((string)$action));
  39. if ($action === '') {
  40. continue;
  41. }
  42. if (strpos($action, 'procuremen/') !== 0) {
  43. $action = 'procuremen/' . $action;
  44. }
  45. if ($this->auth->check($action)) {
  46. return true;
  47. }
  48. }
  49. $suffixes = [];
  50. foreach ($actions as $action) {
  51. $action = strtolower(trim((string)$action));
  52. $action = preg_replace('#^procuremen/#', '', $action);
  53. if ($action !== '') {
  54. $suffixes[$action] = true;
  55. }
  56. }
  57. if ($suffixes === []) {
  58. return false;
  59. }
  60. try {
  61. foreach ($this->auth->getRuleList() as $rule) {
  62. $rule = strtolower((string)$rule);
  63. if (strpos($rule, 'procuremen/') !== 0) {
  64. continue;
  65. }
  66. foreach (array_keys($suffixes) as $suffix) {
  67. if ($rule === 'procuremen/' . $suffix || strpos($rule, 'procuremen/' . $suffix . '/') === 0) {
  68. return true;
  69. }
  70. }
  71. }
  72. } catch (\Throwable $e) {
  73. }
  74. return false;
  75. }
  76. public function index()
  77. {
  78. $this->relationSearch = false;
  79. $this->request->filter(['strip_tags', 'trim']);
  80. if ($this->request->isAjax()) {
  81. $ym = trim((string)$this->request->request('ym', date('Y-m')));
  82. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  83. $ym = date('Y-m');
  84. }
  85. try {
  86. // 有搜索条件时跨月份查询(用户常不知完结月份)
  87. $hasSearch = $this->exportListHasActiveSearch();
  88. $rows = $this->buildExportableOrderRows($hasSearch ? '' : $ym);
  89. $rows = $this->filterExportableRowsBySearch($rows);
  90. $total = count($rows);
  91. $offset = max(0, (int)$this->request->get('offset', 0));
  92. $limit = (int)$this->request->get('limit', 0);
  93. if ($limit > 0) {
  94. $rows = array_slice($rows, $offset, $limit);
  95. }
  96. return json([
  97. 'total' => $total,
  98. 'rows' => $rows,
  99. 'ym' => $ym,
  100. 'cross_month'=> $hasSearch ? 1 : 0,
  101. ]);
  102. } catch (\Throwable $e) {
  103. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  104. }
  105. }
  106. return $this->view->fetch();
  107. }
  108. /**
  109. * 列表是否带有有效搜索(快速搜索或通用搜索)
  110. */
  111. protected function exportListHasActiveSearch(): bool
  112. {
  113. $keyword = trim((string)$this->request->request('search', ''));
  114. if ($keyword === '') {
  115. $keyword = trim((string)$this->request->get('search', ''));
  116. }
  117. if ($keyword !== '') {
  118. return true;
  119. }
  120. $filter = $this->request->get('filter', '');
  121. if (is_string($filter) && $filter !== '') {
  122. $filterArr = (array)json_decode($filter, true);
  123. } else {
  124. $filterArr = is_array($filter) ? $filter : [];
  125. }
  126. foreach ($filterArr as $val) {
  127. if (trim((string)$val) !== '') {
  128. return true;
  129. }
  130. }
  131. return false;
  132. }
  133. /**
  134. * 已审批通过、可按月份导出的订单(合并为一单一行)
  135. */
  136. public function exportable()
  137. {
  138. if (!$this->request->isAjax()) {
  139. $this->error(__('Invalid parameters'));
  140. }
  141. $ym = trim((string)$this->request->get('ym', date('Y-m')));
  142. if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
  143. $ym = date('Y-m');
  144. }
  145. try {
  146. $rows = $this->buildExportableOrderRows($ym);
  147. return json(['total' => count($rows), 'rows' => $rows, 'ym' => $ym]);
  148. } catch (\Throwable $e) {
  149. return json(['total' => 0, 'rows' => [], 'ym' => $ym, 'msg' => $e->getMessage()]);
  150. }
  151. }
  152. /**
  153. * 快速搜索 / 通用搜索过滤(本页数据已按月聚合,在内存中筛选)
  154. *
  155. * @param array<int, array<string, mixed>> $rows
  156. * @return array<int, array<string, mixed>>
  157. */
  158. protected function filterExportableRowsBySearch(array $rows): array
  159. {
  160. $keyword = trim((string)$this->request->request('search', ''));
  161. if ($keyword === '') {
  162. $keyword = trim((string)$this->request->get('search', ''));
  163. }
  164. $filter = $this->request->get('filter', '');
  165. $op = $this->request->get('op', '');
  166. if (is_string($filter) && $filter !== '') {
  167. $filterArr = (array)json_decode($filter, true);
  168. } else {
  169. $filterArr = is_array($filter) ? $filter : [];
  170. }
  171. if (is_string($op) && $op !== '') {
  172. $opArr = (array)json_decode($op, true);
  173. } else {
  174. $opArr = is_array($op) ? $op : [];
  175. }
  176. if ($keyword === '' && $filterArr === []) {
  177. return $rows;
  178. }
  179. $searchFields = ['CCYDH', 'order_batch_no', 'CYJMC', 'CGYMC', 'picked_supplier', 'createtime_text', 'ym'];
  180. $out = [];
  181. foreach ($rows as $row) {
  182. if (!is_array($row)) {
  183. continue;
  184. }
  185. if ($keyword !== '') {
  186. $hit = false;
  187. foreach ($searchFields as $f) {
  188. if (mb_stripos((string)($row[$f] ?? ''), $keyword) !== false) {
  189. $hit = true;
  190. break;
  191. }
  192. }
  193. if (!$hit) {
  194. continue;
  195. }
  196. }
  197. $passFilter = true;
  198. foreach ($filterArr as $field => $val) {
  199. $field = (string)$field;
  200. $val = trim((string)$val);
  201. if ($field === '' || $val === '') {
  202. continue;
  203. }
  204. $cell = (string)($row[$field] ?? '');
  205. $mode = strtoupper((string)($opArr[$field] ?? 'LIKE'));
  206. if ($mode === '=' || $mode === 'EQUAL') {
  207. if (strcasecmp($cell, $val) !== 0) {
  208. $passFilter = false;
  209. break;
  210. }
  211. } elseif (mb_stripos($cell, $val) === false) {
  212. $passFilter = false;
  213. break;
  214. }
  215. }
  216. if (!$passFilter) {
  217. continue;
  218. }
  219. $out[] = $row;
  220. }
  221. return $out;
  222. }
  223. /**
  224. * 审批通过订单,按审批月份筛选后合并为一单一行(不要求已完结)
  225. *
  226. * @param string $ym 审批月份 Y-m;空字符串表示不限月份(用于跨月搜索)
  227. * @return array<int, array<string, mixed>>
  228. */
  229. protected function buildExportableOrderRows(string $ym): array
  230. {
  231. $approvedVals = ProcuremenStatus::wflowApprovedValues();
  232. $softDel = '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')';
  233. try {
  234. $dbRows = Db::table('purchase_order')
  235. ->where('wflow_status', 'in', $approvedVals)
  236. ->whereRaw($softDel)
  237. ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_time,createtime,dStamp,pick_company_name,wflow_status,status,order_batch_no')
  238. ->order('id', 'desc')
  239. ->select();
  240. } catch (\Throwable $e) {
  241. try {
  242. $dbRows = Db::table('purchase_order')
  243. ->where('wflow_status', 'in', $approvedVals)
  244. ->whereRaw($softDel)
  245. ->field('id,scydgy_id,CCYDH,CYJMC,CGYMC,pick_time,createtime,dStamp,pick_company_name,wflow_status,status')
  246. ->order('id', 'desc')
  247. ->select();
  248. } catch (\Throwable $e2) {
  249. \think\Log::write('月度导出列表查询异常:' . $e2->getMessage(), 'error');
  250. $dbRows = [];
  251. }
  252. }
  253. if (!is_array($dbRows)) {
  254. $dbRows = [];
  255. }
  256. $sidList = [];
  257. foreach ($dbRows as $r) {
  258. if (!is_array($r)) {
  259. continue;
  260. }
  261. $sid = (int)($r['scydgy_id'] ?? 0);
  262. if ($sid > 0) {
  263. $sidList[$sid] = true;
  264. }
  265. }
  266. // 审核确认 / 完结时间都可用于归属月份与展示
  267. $completeTsMap = ProcuremenTime::loadOperLogTimestampMap(
  268. array_keys($sidList),
  269. ['purchase_confirm', 'mark_complete']
  270. );
  271. $pool = [];
  272. foreach ($dbRows as $r) {
  273. if (!is_array($r)) {
  274. continue;
  275. }
  276. if (!ProcuremenStatus::isWflowApproved($r['wflow_status'] ?? '')) {
  277. continue;
  278. }
  279. $done = ProcuremenTime::resolveCompletedDone($r, $completeTsMap);
  280. if ($done['ts'] <= 0) {
  281. // 审批通过但缺时间戳:仍展示;有筛月时挂到查询月
  282. $rowYm = $ym !== '' ? $ym : '';
  283. $timeText = '';
  284. } else {
  285. $rowYm = date('Y-m', $done['ts']);
  286. $timeText = $done['text'];
  287. }
  288. if ($ym !== '' && $rowYm !== $ym) {
  289. continue;
  290. }
  291. $sid = (int)($r['scydgy_id'] ?? 0);
  292. // 已选供应商以采购单中标字段为准(一家),不拼明细里多条「已选中」
  293. $pickedName = trim((string)($r['pick_company_name'] ?? ''));
  294. $pool[] = [
  295. 'id' => (int)($r['id'] ?? 0),
  296. 'scydgy_id' => $sid,
  297. 'CCYDH' => trim((string)($r['CCYDH'] ?? '')),
  298. 'order_batch_no' => trim((string)($r['order_batch_no'] ?? '')),
  299. 'CYJMC' => trim((string)($r['CYJMC'] ?? '')),
  300. 'CGYMC' => trim((string)($r['CGYMC'] ?? '')),
  301. 'ym' => $rowYm,
  302. 'createtime_text' => $timeText,
  303. 'picked_supplier' => $pickedName,
  304. ];
  305. }
  306. $merged = $this->collapseExportRowsByOrder($pool);
  307. foreach ($merged as &$row) {
  308. $picked = '';
  309. $batchNo = trim((string)($row['order_batch_no'] ?? ''));
  310. if (!empty($row['_merge_rows']) && is_array($row['_merge_rows'])) {
  311. foreach ($row['_merge_rows'] as $mr) {
  312. if (!is_array($mr)) {
  313. continue;
  314. }
  315. if ($picked === '') {
  316. $n = trim((string)($mr['picked_supplier'] ?? ''));
  317. if ($n !== '') {
  318. $picked = $n;
  319. }
  320. }
  321. if ($batchNo === '') {
  322. $bn = trim((string)($mr['order_batch_no'] ?? ''));
  323. if ($bn !== '') {
  324. $batchNo = $bn;
  325. }
  326. }
  327. if (trim((string)($row['ym'] ?? '')) === '') {
  328. $ymv = trim((string)($mr['ym'] ?? ''));
  329. if ($ymv !== '') {
  330. $row['ym'] = $ymv;
  331. }
  332. }
  333. }
  334. } else {
  335. $picked = trim((string)($row['picked_supplier'] ?? ''));
  336. }
  337. $row['picked_supplier'] = $picked;
  338. $row['order_batch_no'] = $batchNo;
  339. unset($row['_merge_rows']);
  340. }
  341. unset($row);
  342. return $merged;
  343. }
  344. /**
  345. * 中标/已选供应商名称(按工序 scydgy_id)
  346. *
  347. * @param int[] $scydgyIds
  348. * @return array<int, string>
  349. */
  350. protected function loadPickedSupplierNamesByScydgyIds(array $scydgyIds): array
  351. {
  352. $out = [];
  353. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  354. if ($scydgyIds === []) {
  355. return $out;
  356. }
  357. try {
  358. $rows = Db::table('purchase_order_detail')
  359. ->where('scydgy_id', 'in', $scydgyIds)
  360. ->whereIn('status', ProcuremenStatus::podPickedValues())
  361. ->field('scydgy_id,company_name')
  362. ->select();
  363. } catch (\Throwable $e) {
  364. $rows = [];
  365. }
  366. if (!is_array($rows)) {
  367. return $out;
  368. }
  369. $namesBySid = [];
  370. foreach ($rows as $r) {
  371. if (!is_array($r)) {
  372. continue;
  373. }
  374. $sid = (int)($r['scydgy_id'] ?? 0);
  375. $cn = trim((string)($r['company_name'] ?? ''));
  376. if ($sid <= 0 || $cn === '') {
  377. continue;
  378. }
  379. if (!isset($namesBySid[$sid])) {
  380. $namesBySid[$sid] = [];
  381. }
  382. if (!in_array($cn, $namesBySid[$sid], true)) {
  383. $namesBySid[$sid][] = $cn;
  384. }
  385. }
  386. foreach ($namesBySid as $sid => $names) {
  387. $out[$sid] = implode('、', $names);
  388. }
  389. return $out;
  390. }
  391. /**
  392. * 工序行金额合计:优先已选定明细 status=1,否则汇总有效明细上的加工金额
  393. *
  394. * @param int[] $scydgyIds
  395. * @return array<int, float>
  396. */
  397. protected function loadDetailAmountSumByScydgyIds(array $scydgyIds): array
  398. {
  399. $picked = $this->loadPickedAmountSumByScydgyIds($scydgyIds);
  400. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  401. $missing = [];
  402. foreach ($scydgyIds as $sid) {
  403. if ($sid > 0 && !isset($picked[$sid])) {
  404. $missing[] = $sid;
  405. }
  406. }
  407. if ($missing === []) {
  408. return $picked;
  409. }
  410. try {
  411. $rows = Db::table('purchase_order_detail')
  412. ->where('scydgy_id', 'in', $missing)
  413. ->whereRaw(ProcuremenStatus::sqlPodNotVoid('status'))
  414. ->field('scydgy_id,amount')
  415. ->select();
  416. } catch (\Throwable $e) {
  417. $rows = [];
  418. }
  419. if (!is_array($rows)) {
  420. return $picked;
  421. }
  422. foreach ($rows as $r) {
  423. if (!is_array($r)) {
  424. continue;
  425. }
  426. $sid = (int)($r['scydgy_id'] ?? 0);
  427. $am = trim((string)($r['amount'] ?? ''));
  428. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  429. continue;
  430. }
  431. if (!isset($picked[$sid])) {
  432. $picked[$sid] = 0.0;
  433. }
  434. $picked[$sid] += (float)$am;
  435. }
  436. return $picked;
  437. }
  438. /**
  439. * @param int[] $scydgyIds
  440. * @return array<int, float>
  441. */
  442. protected function loadPickedAmountSumByScydgyIds(array $scydgyIds): array
  443. {
  444. $out = [];
  445. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  446. if ($scydgyIds === []) {
  447. return $out;
  448. }
  449. try {
  450. $rows = Db::table('purchase_order_detail')
  451. ->where('scydgy_id', 'in', $scydgyIds)
  452. ->whereIn('status', ProcuremenStatus::podPickedValues())
  453. ->field('scydgy_id,amount')
  454. ->select();
  455. } catch (\Throwable $e) {
  456. $rows = [];
  457. }
  458. if (!is_array($rows)) {
  459. return $out;
  460. }
  461. foreach ($rows as $r) {
  462. if (!is_array($r)) {
  463. continue;
  464. }
  465. $sid = (int)($r['scydgy_id'] ?? 0);
  466. $am = trim((string)($r['amount'] ?? ''));
  467. if ($sid <= 0 || $am === '' || $am === '0' || $am === '0.00') {
  468. continue;
  469. }
  470. if (!isset($out[$sid])) {
  471. $out[$sid] = 0.0;
  472. }
  473. $out[$sid] += (float)$am;
  474. }
  475. return $out;
  476. }
  477. /**
  478. * 归属月份:优先采购确认/完结操作时间,其次 pick_time、createtime、dStamp
  479. *
  480. * @param array<string, mixed> $row
  481. * @param array<int, int> $completeTsMap scydgy_id => unix 完结时间
  482. */
  483. protected function resolvePurchaseOrderRowYm(array $row, array $completeTsMap = []): string
  484. {
  485. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  486. if ($ts > 0) {
  487. return date('Y-m', $ts);
  488. }
  489. return '';
  490. }
  491. /**
  492. * @param array<string, mixed> $row
  493. * @param array<int, int> $completeTsMap
  494. */
  495. protected function resolvePurchaseOrderCompleteTimestamp(array $row, array $completeTsMap = []): int
  496. {
  497. $sid = (int)($row['scydgy_id'] ?? 0);
  498. if ($sid > 0 && isset($completeTsMap[$sid]) && (int)$completeTsMap[$sid] > 946684800) {
  499. return (int)$completeTsMap[$sid];
  500. }
  501. foreach (['pick_time', 'createtime', 'dStamp'] as $key) {
  502. $raw = $row[$key] ?? null;
  503. if ($raw === null || $raw === '') {
  504. continue;
  505. }
  506. if (is_numeric($raw) && (int)$raw > 946684800) {
  507. return (int)$raw;
  508. }
  509. $t = trim((string)$raw);
  510. if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
  511. $parsed = (int)strtotime($t);
  512. return $parsed > 946684800 ? $parsed : 0;
  513. }
  514. }
  515. return 0;
  516. }
  517. /**
  518. * @param array<int, string> $actions
  519. * @return array<int, int> scydgy_id => 最近操作 unix 时间
  520. */
  521. protected function loadOperLogTimestampMap(array $scydgyIds, array $actions): array
  522. {
  523. $out = [];
  524. $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
  525. $actions = array_values(array_filter(array_map('strval', $actions)));
  526. if ($scydgyIds === [] || $actions === []) {
  527. return $out;
  528. }
  529. try {
  530. $logs = Db::table('purchase_order_oper_log')
  531. ->where(ProcuremenOperLog::COL_SCYDGY_ID, 'in', $scydgyIds)
  532. ->where(ProcuremenOperLog::COL_ACTION, 'in', ProcuremenOperLog::expandActionQueryValues($actions))
  533. ->field(ProcuremenOperLog::COL_SCYDGY_ID . ',' . ProcuremenOperLog::COL_TIME)
  534. ->order('id', 'asc')
  535. ->select();
  536. } catch (\Throwable $e) {
  537. $logs = [];
  538. }
  539. if (!is_array($logs)) {
  540. return $out;
  541. }
  542. foreach ($logs as $log) {
  543. if (!is_array($log)) {
  544. continue;
  545. }
  546. $log = ProcuremenOperLog::normalizeRow($log);
  547. $sid = (int)($log['scydgy_id'] ?? 0);
  548. $ct = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
  549. if ($sid > 0 && $ct > 946684800) {
  550. $out[$sid] = isset($out[$sid]) ? max($out[$sid], $ct) : $ct;
  551. }
  552. }
  553. return $out;
  554. }
  555. /**
  556. * @param array<string, mixed> $row
  557. * @param array<int, int> $completeTsMap
  558. */
  559. protected function formatPurchaseOrderDoneTime(array $row, array $completeTsMap = []): string
  560. {
  561. $ts = $this->resolvePurchaseOrderCompleteTimestamp($row, $completeTsMap);
  562. if ($ts > 0) {
  563. return date('Y-m-d H:i:s', $ts);
  564. }
  565. return '';
  566. }
  567. /**
  568. * @param array<int, array<string, mixed>> $rows
  569. * @return array<int, array<string, mixed>>
  570. */
  571. protected function collapseExportRowsByOrder(array $rows): array
  572. {
  573. if ($rows === []) {
  574. return [];
  575. }
  576. $groups = [];
  577. foreach ($rows as $row) {
  578. if (!is_array($row)) {
  579. continue;
  580. }
  581. $dh = trim((string)($row['CCYDH'] ?? ''));
  582. $key = $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
  583. if (!isset($groups[$key])) {
  584. $groups[$key] = [];
  585. }
  586. $groups[$key][] = $row;
  587. }
  588. $out = [];
  589. foreach ($groups as $groupRows) {
  590. if ($groupRows === []) {
  591. continue;
  592. }
  593. usort($groupRows, function ($a, $b) {
  594. return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
  595. });
  596. $head = $groupRows[0];
  597. $gymcList = [];
  598. foreach ($groupRows as $r) {
  599. $g = trim((string)($r['CGYMC'] ?? ''));
  600. if ($g !== '' && !in_array($g, $gymcList, true)) {
  601. $gymcList[] = $g;
  602. }
  603. }
  604. $merged = $head;
  605. $merged['_merge_rows'] = $groupRows;
  606. if (count($groupRows) > 1) {
  607. $merged['CGYMC'] = implode('、', $gymcList);
  608. }
  609. $latestText = '';
  610. $latestTs = 0;
  611. foreach ($groupRows as $r) {
  612. $t = trim((string)($r['createtime_text'] ?? ''));
  613. $ts = ProcuremenTime::parseToTimestamp($t);
  614. if ($ts > $latestTs) {
  615. $latestTs = $ts;
  616. $latestText = ProcuremenTime::formatDisplayDateTime($t);
  617. }
  618. }
  619. if ($latestText !== '') {
  620. $merged['createtime_text'] = $latestText;
  621. }
  622. $maxId = (int)($head['id'] ?? 0);
  623. foreach ($groupRows as $r) {
  624. $maxId = max($maxId, (int)($r['id'] ?? 0));
  625. }
  626. $merged['id'] = $maxId;
  627. $out[] = $merged;
  628. }
  629. usort($out, function ($a, $b) {
  630. return ((int)($b['id'] ?? 0)) <=> ((int)($a['id'] ?? 0));
  631. });
  632. return $out;
  633. }
  634. }