| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986 |
- <?php
- namespace app\common\library;
- use think\Db;
- /**
- * 控制台 — 协助采购(采购供应商)统计
- */
- class ProcuremenDashboard
- {
- /**
- * @return array{
- * today: array{issued:int,confirm:int,approval:int,completed:int},
- * week: array{issued:int,confirm:int,approval:int,completed:int},
- * pending: array{confirm:int,approval:int},
- * supplierRank: array{quote: array, selected: array, period_label: string},
- * kpi: array<string, array{today:int,month:int,link:string,link_text:string}>,
- * updated: string
- * }
- */
- public static function buildStats(): array
- {
- $todayStart = date('Y-m-d 00:00:00');
- $todayEnd = date('Y-m-d H:i:s');
- $weekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
- $weekEnd = date('Y-m-d H:i:s');
- return [
- 'kpi' => self::buildKpiCards(),
- 'today' => self::countStageMetrics($todayStart, $todayEnd),
- 'week' => self::countStageMetrics($weekStart, $weekEnd),
- 'pending' => self::countCurrentPending(),
- 'supplierRank' => self::buildSupplierRankings(10, date('Y-m-01 00:00:00'), date('Y-m-t 23:59:59')),
- 'last7' => self::buildLast7DaysChart(),
- 'updated' => date('Y-m-d H:i:s'),
- ];
- }
- /**
- * 控制台首行 KPI:今日 / 本月
- *
- * @return array<string, array{today:int,month:int,link:string,link_text:string}>
- */
- public static function buildKpiCards(): array
- {
- $todayStart = date('Y-m-d 00:00:00');
- $todayEnd = date('Y-m-d H:i:s');
- $monthStart = date('Y-m-01 00:00:00');
- $monthEnd = date('Y-m-t 23:59:59');
- return [
- 'supplier' => [
- 'total' => self::countActiveSuppliers(),
- 'link' => '',
- 'link_text' => '',
- ],
- 'confirm' => [
- 'today' => self::countOperLogDistinctOrdersInRange(['issue_submit'], $todayStart, $todayEnd),
- 'month' => self::countPendingStageOrdersInMonth('confirm', $monthStart, $monthEnd),
- 'link' => 'procuremen/audit',
- 'link_text' => '供应商确认',
- ],
- 'approval' => [
- 'today' => self::countOperLogDistinctOrdersInRange(['audit_select'], $todayStart, $todayEnd),
- 'month' => self::countPendingStageOrdersInMonth('approval', $monthStart, $monthEnd),
- 'link' => 'procuremen/confirm',
- 'link_text' => '供应商审批',
- ],
- 'completed' => [
- 'today' => self::countCompletedInRange($todayStart, $todayEnd),
- 'month' => self::countCompletedInRange($monthStart, $monthEnd),
- 'link' => 'procuremenarchive/index',
- 'link_text' => '历史存证档案查询',
- ],
- ];
- }
- /**
- * @param string[] $wflowValues
- */
- protected static function countWflowOrdersInPickRange(array $wflowValues, string $start, string $end): int
- {
- try {
- $expr = self::pickTimeSqlExpr();
- $query = Db::table('purchase_order')->field('id,CCYDH');
- self::applyNotDeletedWhere($query);
- $query->whereIn('wflow_status', $wflowValues);
- $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
- $rows = $query->select();
- } catch (\Throwable $e) {
- return 0;
- }
- return self::countDistinctOrders(is_array($rows) ? $rows : []);
- }
- protected static function countQuoteSuppliersInPickRange(string $start, string $end): int
- {
- $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
- if ($scydgyIds === []) {
- return 0;
- }
- try {
- $rows = Db::table('purchase_order_detail')
- ->where('scydgy_id', 'in', $scydgyIds)
- ->field('company_name,amount')
- ->select();
- } catch (\Throwable $e) {
- return 0;
- }
- if (!is_array($rows)) {
- return 0;
- }
- $names = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $name = trim((string)($row['company_name'] ?? ''));
- $amount = trim((string)($row['amount'] ?? ''));
- if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
- continue;
- }
- $names[$name] = true;
- }
- return count($names);
- }
- protected static function countActiveSuppliers(): int
- {
- try {
- $query = Db::table('customer');
- $query->where(function ($q) {
- $q->where('status', 1)
- ->whereOr('status', '1')
- ->whereOr('status', '')
- ->whereNull('status');
- });
- return (int)$query->count();
- } catch (\Throwable $e) {
- return self::countDistinctSuppliersFromDetails();
- }
- }
- protected static function countDistinctSuppliersFromDetails(): int
- {
- try {
- $rows = Db::table('purchase_order_detail')
- ->field('company_name')
- ->select();
- } catch (\Throwable $e) {
- return 0;
- }
- if (!is_array($rows)) {
- return 0;
- }
- $names = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $name = trim((string)($row['company_name'] ?? ''));
- if ($name !== '') {
- $names[$name] = true;
- }
- }
- return count($names);
- }
- protected static function countDistinctOrdersInPickTimeRange(string $start, string $end): int
- {
- try {
- $expr = self::pickTimeSqlExpr();
- $query = Db::table('purchase_order')->field('id,CCYDH');
- self::applyNotDeletedWhere($query);
- $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
- $rows = $query->select();
- } catch (\Throwable $e) {
- return 0;
- }
- return self::countDistinctOrders(is_array($rows) ? $rows : []);
- }
- /**
- * 供应商排名(本月):参与报价次数 / 被选中次数
- *
- * @return array{
- * quote: array<int, array{rank:int,name:string,value:int,percent:int}>,
- * selected: array<int, array{rank:int,name:string,value:int,percent:int}>,
- * period_label: string,
- * supplier_total: int
- * }
- */
- public static function buildSupplierRankings(int $limit = 10, ?string $start = null, ?string $end = null): array
- {
- $start = $start ?: date('Y-m-d 00:00:00', strtotime('first day of this month'));
- $end = $end ?: date('Y-m-t 23:59:59');
- return [
- 'quote' => self::rankSuppliersByQuoteInRange($start, $end, $limit),
- 'selected' => self::rankSuppliersBySelectedInRange($start, $end, $limit),
- 'period_label' => '本月',
- 'supplier_total' => self::countActiveSuppliers(),
- ];
- }
- /**
- * @param array<string, int> $counts
- * @return array<int, array{rank:int,name:string,value:int,percent:int}>
- */
- protected static function formatSupplierRankListFromCounts(array $counts, int $limit): array
- {
- if ($counts === []) {
- return [];
- }
- arsort($counts);
- $top = array_slice($counts, 0, max(1, $limit), true);
- $max = $top ? max($top) : 0;
- $out = [];
- $rank = 1;
- foreach ($top as $name => $value) {
- $out[] = [
- 'rank' => $rank++,
- 'name' => (string)$name,
- 'value' => (int)$value,
- 'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
- ];
- }
- return $out;
- }
- /**
- * @param array<string, array<string|int, bool>> $buckets
- * @return array<int, array{rank:int,name:string,value:int,percent:int}>
- */
- protected static function formatSupplierRankList(array $buckets, int $limit): array
- {
- if ($buckets === []) {
- return [];
- }
- $counts = [];
- foreach ($buckets as $name => $keys) {
- $counts[$name] = is_array($keys) ? count($keys) : 0;
- }
- arsort($counts);
- $top = array_slice($counts, 0, max(1, $limit), true);
- $max = $top ? max($top) : 0;
- $out = [];
- $rank = 1;
- foreach ($top as $name => $value) {
- $out[] = [
- 'rank' => $rank++,
- 'name' => (string)$name,
- 'value' => (int)$value,
- 'percent' => $max > 0 ? (int)round($value / $max * 100) : 0,
- ];
- }
- return $out;
- }
- /**
- * 按已填写报价金额的供应商统计参与报价次数(明细行数)
- *
- * @return array<int, array{rank:int,name:string,value:int,percent:int}>
- */
- protected static function rankSuppliersByQuoteInRange(string $start, string $end, int $limit): array
- {
- $scydgyIds = self::loadScydgyIdsInPickTimeRange($start, $end);
- if ($scydgyIds === []) {
- return [];
- }
- try {
- $rows = Db::table('purchase_order_detail')
- ->where('scydgy_id', 'in', $scydgyIds)
- ->field('company_name,amount')
- ->select();
- } catch (\Throwable $e) {
- return [];
- }
- if (!is_array($rows)) {
- return [];
- }
- $counts = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $name = trim((string)($row['company_name'] ?? ''));
- $amount = trim((string)($row['amount'] ?? ''));
- if ($name === '' || $amount === '' || $amount === '0' || $amount === '0.00') {
- continue;
- }
- $counts[$name] = ($counts[$name] ?? 0) + 1;
- }
- return self::formatSupplierRankListFromCounts($counts, $limit);
- }
- /**
- * 按主表 pick_company_name 统计被选中订单数
- *
- * @return array<int, array{rank:int,name:string,value:int,percent:int}>
- */
- protected static function rankSuppliersBySelectedInRange(string $start, string $end, int $limit): array
- {
- try {
- $expr = self::pickTimeSqlExpr();
- $query = Db::table('purchase_order')
- ->field('CCYDH,pick_company_name');
- self::applyNotDeletedWhere($query);
- $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
- $query->whereRaw("pick_company_name IS NOT NULL AND TRIM(CAST(pick_company_name AS CHAR(255))) <> ''");
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- if (!is_array($rows)) {
- return [];
- }
- $buckets = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $name = trim((string)($row['pick_company_name'] ?? ''));
- if ($name === '') {
- continue;
- }
- $orderKey = self::resolveOrderKey($row);
- if (!isset($buckets[$name])) {
- $buckets[$name] = [];
- }
- $buckets[$name][$orderKey] = true;
- }
- return self::formatSupplierRankList($buckets, $limit);
- }
- /**
- * @return int[]
- */
- protected static function loadScydgyIdsInPickTimeRange(string $start, string $end): array
- {
- try {
- $expr = self::pickTimeSqlExpr();
- $query = Db::table('purchase_order')->field('scydgy_id');
- self::applyNotDeletedWhere($query);
- $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- if (!is_array($rows)) {
- return [];
- }
- $ids = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $sid = (int)($row['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $ids[$sid] = true;
- }
- }
- return array_keys($ids);
- }
- /**
- * 近 7 天每日趋势(按订单号去重)
- *
- * @return array{
- * labels: string[],
- * issued: int[],
- * confirm: int[],
- * approval: int[],
- * completed: int[]
- * }
- */
- public static function buildLast7DaysChart(): array
- {
- $dayKeys = [];
- $labels = [];
- for ($i = 6; $i >= 0; $i--) {
- $day = date('Y-m-d', strtotime('-' . $i . ' days'));
- $dayKeys[] = $day;
- $labels[] = date('m-d', strtotime($day));
- }
- if ($dayKeys === []) {
- return ['labels' => [], 'issued' => [], 'confirm' => [], 'approval' => [], 'completed' => []];
- }
- $rangeStart = $dayKeys[0] . ' 00:00:00';
- $rangeEnd = $dayKeys[count($dayKeys) - 1] . ' 23:59:59';
- $startTs = ProcuremenTime::parseToTimestamp($rangeStart);
- $endTs = ProcuremenTime::parseToTimestamp($rangeEnd);
- $issuedBuckets = self::bucketOperLogOrdersByDay(
- self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs),
- $dayKeys,
- $startTs,
- $endTs
- );
- $confirmBuckets = self::bucketOperLogOrdersByDay(
- self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs),
- $dayKeys,
- $startTs,
- $endTs
- );
- $approvalBuckets = self::bucketOperLogOrdersByDay(
- self::loadOperLogsInRange(['audit_select'], $startTs, $endTs),
- $dayKeys,
- $startTs,
- $endTs
- );
- $completedBuckets = self::bucketOperLogOrdersByDay(
- self::loadOperLogsInRange(['purchase_confirm', 'mark_complete'], $startTs, $endTs),
- $dayKeys,
- $startTs,
- $endTs
- );
- return [
- 'labels' => $labels,
- 'issued' => self::bucketCounts($issuedBuckets, $dayKeys),
- 'confirm' => self::bucketCounts($confirmBuckets, $dayKeys),
- 'approval' => self::bucketCounts($approvalBuckets, $dayKeys),
- 'completed' => self::bucketCounts($completedBuckets, $dayKeys),
- ];
- }
- /**
- * @param string[] $dayKeys
- * @return array<string, array<string, bool>>
- */
- protected static function emptyDayBuckets(array $dayKeys): array
- {
- $out = [];
- foreach ($dayKeys as $day) {
- $out[$day] = [];
- }
- return $out;
- }
- /**
- * @param array<string, array<string, bool>> $buckets
- * @param string[] $dayKeys
- * @return int[]
- */
- protected static function bucketCounts(array $buckets, array $dayKeys): array
- {
- $out = [];
- foreach ($dayKeys as $day) {
- $out[] = isset($buckets[$day]) ? count($buckets[$day]) : 0;
- }
- return $out;
- }
- /**
- * @param array<string, mixed> $row
- * @param string[] $dayKeys
- */
- protected static function resolveRowDayKey(array $row, array $dayKeys): ?string
- {
- $ts = 0;
- foreach (['pick_time', 'createtime', 'dputrecord', 'dStamp'] as $key) {
- $t = ProcuremenTime::parseToTimestamp($row[$key] ?? null);
- if ($t > $ts) {
- $ts = $t;
- }
- }
- if ($ts <= 0) {
- return null;
- }
- $day = date('Y-m-d', $ts);
- return in_array($day, $dayKeys, true) ? $day : null;
- }
- /**
- * @param array<string, mixed> $row
- */
- protected static function resolveOrderKey(array $row): string
- {
- $dh = trim((string)($row['CCYDH'] ?? ''));
- return $dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0));
- }
- /**
- * @param array<string, mixed> $row
- */
- protected static function isPendingConfirmRow(array $row): bool
- {
- return ProcuremenStatus::isWflowPendingConfirm($row['wflow_status'] ?? '');
- }
- /**
- * @param array<int, string> $actions
- * @return array<int, array{scydgy_id:int, createtime:mixed}>
- */
- protected static function loadOperLogsInRange(array $actions, int $startTs, int $endTs): array
- {
- try {
- $logs = Db::table('purchase_order_oper_log')
- ->where('action', 'in', $actions)
- ->field('scydgy_id,createtime')
- ->select();
- } catch (\Throwable $e) {
- return [];
- }
- if (!is_array($logs)) {
- return [];
- }
- $out = [];
- foreach ($logs as $log) {
- if (!is_array($log)) {
- continue;
- }
- $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
- if ($ts < $startTs || $ts > $endTs) {
- continue;
- }
- $sid = (int)($log['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $out[] = ['scydgy_id' => $sid, 'createtime' => $log['createtime'] ?? null];
- }
- }
- return $out;
- }
- /**
- * @param array<int, array{scydgy_id:int, createtime:mixed}> $logs
- * @param string[] $dayKeys
- * @return array<string, array<string, bool>>
- */
- protected static function bucketOperLogOrdersByDay(array $logs, array $dayKeys, int $startTs, int $endTs): array
- {
- $buckets = self::emptyDayBuckets($dayKeys);
- if ($logs === []) {
- return $buckets;
- }
- $sidSet = [];
- foreach ($logs as $log) {
- $sidSet[(int)$log['scydgy_id']] = true;
- }
- $orderMap = [];
- foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
- if (!is_array($row)) {
- continue;
- }
- $sid = (int)($row['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $orderMap[$sid] = self::resolveOrderKey($row);
- }
- }
- foreach ($logs as $log) {
- $sid = (int)$log['scydgy_id'];
- if ($sid <= 0 || !isset($orderMap[$sid])) {
- continue;
- }
- $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
- if ($ts < $startTs || $ts > $endTs) {
- continue;
- }
- $day = date('Y-m-d', $ts);
- if (!isset($buckets[$day])) {
- continue;
- }
- $buckets[$day][$orderMap[$sid]] = true;
- }
- return $buckets;
- }
- /**
- * 按操作日志统计时段内去重订单数(与近7天图口径一致)
- *
- * @param string[] $actions
- */
- protected static function countOperLogDistinctOrdersInRange(array $actions, string $start, string $end): int
- {
- $startTs = ProcuremenTime::parseToTimestamp($start);
- $endTs = ProcuremenTime::parseToTimestamp($end);
- if ($startTs <= 0 || $endTs <= 0 || $actions === []) {
- return 0;
- }
- $logs = self::loadOperLogsInRange($actions, $startTs, $endTs);
- if ($logs === []) {
- return 0;
- }
- $sidSet = [];
- foreach ($logs as $log) {
- $sid = (int)($log['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $sidSet[$sid] = true;
- }
- }
- if ($sidSet === []) {
- return 0;
- }
- $keys = [];
- foreach (self::loadOrdersByScydgyIds(array_keys($sidSet)) as $row) {
- if (!is_array($row)) {
- continue;
- }
- $keys[self::resolveOrderKey($row)] = true;
- }
- return count($keys);
- }
- /**
- * 本月待处理:与「确认供应商 / 采购确认」列表一致(当前队列 + 下发月份 + 订单号去重)
- */
- protected static function countPendingStageOrdersInMonth(string $stage, string $monthStart, string $monthEnd): int
- {
- $rows = self::loadPendingStageRowsWithTime($stage);
- if ($rows === []) {
- return 0;
- }
- $matched = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $normalized = self::normalizeRowForListMonth($row);
- if (!self::rowMatchesMonthRange($normalized, $monthStart, $monthEnd, 'pick_time')) {
- continue;
- }
- $matched[] = $normalized;
- }
- return self::countDistinctOrders($matched);
- }
- /**
- * @return array<int, array<string, mixed>>
- */
- protected static function loadPendingStageRowsWithTime(string $stage): array
- {
- try {
- $fields = 'id,CCYDH,scydgy_id,pick_time,createtime,dputrecord,dStamp,wflow_status,status';
- $query = Db::table('purchase_order')->field($fields);
- self::applyNotDeletedWhere($query);
- if ($stage === 'confirm') {
- $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingConfirmValues());
- } else {
- $vals = ProcuremenStatus::wflowPendingApprovalValues();
- $quoted = [];
- foreach ($vals as $v) {
- $quoted[] = "'" . str_replace("'", "''", (string)$v) . "'";
- }
- if ($quoted === []) {
- return [];
- }
- $query->whereRaw('TRIM(CAST(wflow_status AS CHAR)) IN (' . implode(',', $quoted) . ')');
- $query->whereRaw('NOT (' . ProcuremenStatus::sqlPoCompleted('status') . ')');
- }
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- return is_array($rows) ? $rows : [];
- }
- /**
- * 列表月份筛选前补齐时间字段(与 buildListRowFromPurchaseOrderDbRow 回退一致)
- *
- * @param array<string, mixed> $row
- * @return array<string, mixed>
- */
- protected static function normalizeRowForListMonth(array $row): array
- {
- $r = $row;
- $r['createtime'] = ProcuremenTime::formatDisplayDateTime($row['createtime'] ?? null);
- $dpOut = trim((string)($r['dputrecord'] ?? ''));
- if ($dpOut === '' || stripos($dpOut, '0000-00-00') === 0) {
- $fallback = trim((string)($r['dStamp'] ?? ''));
- if ($fallback !== '' && stripos($fallback, '0000-00-00') !== 0) {
- $r['dputrecord'] = $fallback;
- } elseif ($r['createtime'] !== '') {
- $r['dputrecord'] = $r['createtime'];
- }
- }
- $pickOut = trim((string)($r['pick_time'] ?? ''));
- if ($pickOut === '' || stripos($pickOut, '0000-00-00') === 0) {
- foreach (['createtime', 'dputrecord', 'dStamp'] as $pickFallbackKey) {
- $pickFallback = trim((string)($r[$pickFallbackKey] ?? ''));
- if ($pickFallback !== '' && stripos($pickFallback, '0000-00-00') !== 0) {
- $r['pick_time'] = $pickFallback;
- break;
- }
- }
- }
- return $r;
- }
- /**
- * @param array<string, mixed> $row
- */
- protected static function resolveRowListTime(array $row, string $primary = 'pick_time'): string
- {
- $keys = array_values(array_unique([$primary, 'pick_time', 'createtime', 'dputrecord', 'dStamp']));
- foreach ($keys as $k) {
- $t = trim((string)($row[$k] ?? ''));
- if ($t !== '' && stripos($t, '0000-00-00') !== 0) {
- return $t;
- }
- }
- return '';
- }
- /**
- * @param array<string, mixed> $row
- */
- protected static function rowMatchesMonthRange(array $row, string $monthStart, string $monthEnd, string $primary = 'pick_time'): bool
- {
- $listTime = self::resolveRowListTime($row, $primary);
- if ($listTime === '' || stripos($listTime, '0000-00-00') === 0
- || !preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $listTime, $m)) {
- return false;
- }
- $mo = (int)$m[2];
- if ($mo < 1 || $mo > 12) {
- return false;
- }
- $ymRow = $m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT);
- $rowMonthStart = $ymRow . '-01 00:00:00';
- $rowMonthEnd = date('Y-m-t 23:59:59', strtotime($rowMonthStart));
- return !(strcmp($rowMonthEnd, $monthStart) < 0 || strcmp($rowMonthStart, $monthEnd) > 0);
- }
- /**
- * @return array{issued:int,confirm:int,approval:int,completed:int}
- */
- protected static function countStageMetrics(string $start, string $end): array
- {
- return [
- 'issued' => self::countIssuedInRange($start, $end),
- 'confirm' => self::countOperLogDistinctOrdersInRange(['issue_submit'], $start, $end),
- 'approval' => self::countOperLogDistinctOrdersInRange(['audit_select'], $start, $end),
- 'completed' => self::countCompletedInRange($start, $end),
- ];
- }
- /**
- * @return array{confirm:int,approval:int}
- */
- protected static function countCurrentPending(): array
- {
- return [
- 'confirm' => self::countDistinctOrders(self::loadPendingConfirmRows()),
- 'approval' => self::countDistinctOrders(self::loadPendingApprovalRows()),
- ];
- }
- protected static function countIssuedInRange(string $start, string $end): int
- {
- $startTs = ProcuremenTime::parseToTimestamp($start);
- $endTs = ProcuremenTime::parseToTimestamp($end);
- if ($startTs <= 0 || $endTs <= 0) {
- return 0;
- }
- $confirmSidSet = self::loadPendingConfirmScydgySet();
- $keys = [];
- $logs = self::loadOperLogsInRange(['issue_submit'], $startTs, $endTs);
- foreach ($logs as $log) {
- $sid = (int)($log['scydgy_id'] ?? 0);
- if ($sid <= 0 || isset($confirmSidSet[$sid])) {
- continue;
- }
- foreach (self::loadOrdersByScydgyIds([$sid]) as $row) {
- if (!is_array($row)) {
- continue;
- }
- $keys[self::resolveOrderKey($row)] = true;
- }
- }
- if ($keys !== []) {
- return count($keys);
- }
- return self::countIssuedByPickTimePastConfirm($start, $end);
- }
- /**
- * 无下发日志时:本时段 pick_time 内且流程已进入待审批/已审核(不含仍停在待确认)
- */
- protected static function countIssuedByPickTimePastConfirm(string $start, string $end): int
- {
- try {
- $expr = self::pickTimeSqlExpr();
- $query = Db::table('purchase_order')->field('id,CCYDH,wflow_status');
- self::applyNotDeletedWhere($query);
- $query->whereRaw("({$expr}) >= ? AND ({$expr}) <= ?", [$start, $end]);
- $query->whereIn('wflow_status', array_merge(
- ProcuremenStatus::wflowPendingApprovalValues(),
- ProcuremenStatus::wflowApprovedValues()
- ));
- $rows = $query->select();
- } catch (\Throwable $e) {
- return 0;
- }
- return self::countDistinctOrders(is_array($rows) ? $rows : []);
- }
- /**
- * @return array<int, bool>
- */
- protected static function loadPendingConfirmScydgySet(): array
- {
- $set = [];
- foreach (self::loadPendingConfirmRows() as $row) {
- if (!is_array($row)) {
- continue;
- }
- $sid = (int)($row['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $set[$sid] = true;
- }
- }
- return $set;
- }
- protected static function countCompletedInRange(string $start, string $end): int
- {
- $startTs = ProcuremenTime::parseToTimestamp($start);
- $endTs = ProcuremenTime::parseToTimestamp($end);
- if ($startTs <= 0 || $endTs <= 0) {
- return 0;
- }
- try {
- $logs = Db::table('purchase_order_oper_log')
- ->where('action', 'in', ['purchase_confirm', 'mark_complete'])
- ->field('scydgy_id,createtime')
- ->select();
- } catch (\Throwable $e) {
- return 0;
- }
- if (!is_array($logs) || $logs === []) {
- return 0;
- }
- $sidSet = [];
- foreach ($logs as $log) {
- if (!is_array($log)) {
- continue;
- }
- $ts = ProcuremenTime::parseToTimestamp($log['createtime'] ?? null);
- if ($ts < $startTs || $ts > $endTs) {
- continue;
- }
- $sid = (int)($log['scydgy_id'] ?? 0);
- if ($sid > 0) {
- $sidSet[$sid] = true;
- }
- }
- if ($sidSet === []) {
- return 0;
- }
- return self::countDistinctOrders(self::loadOrdersByScydgyIds(array_keys($sidSet)));
- }
- /**
- * @param int[] $scydgyIds
- * @return array<int, array<string, mixed>>
- */
- protected static function loadOrdersByScydgyIds(array $scydgyIds): array
- {
- $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
- if ($scydgyIds === []) {
- return [];
- }
- try {
- $query = Db::table('purchase_order')
- ->where('scydgy_id', 'in', $scydgyIds)
- ->field('id,CCYDH,scydgy_id,wflow_status');
- self::applyNotDeletedWhere($query);
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- return is_array($rows) ? $rows : [];
- }
- /**
- * @return array<int, array<string, mixed>>
- */
- protected static function loadPendingConfirmRows(): array
- {
- try {
- $query = Db::table('purchase_order')->field('id,CCYDH,scydgy_id');
- self::applyNotDeletedWhere($query);
- $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingConfirmValues());
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- return is_array($rows) ? $rows : [];
- }
- /**
- * @return array<int, array<string, mixed>>
- */
- protected static function loadPendingApprovalRows(): array
- {
- try {
- $query = Db::table('purchase_order')->field('id,CCYDH');
- self::applyNotDeletedWhere($query);
- $query->whereIn('wflow_status', ProcuremenStatus::wflowPendingApprovalValues());
- $query->whereRaw('NOT (' . ProcuremenStatus::sqlPoCompleted('status') . ')');
- $rows = $query->select();
- } catch (\Throwable $e) {
- return [];
- }
- return is_array($rows) ? $rows : [];
- }
- /**
- * @param array<int, array<string, mixed>> $rows
- */
- protected static function countDistinctOrders(array $rows): int
- {
- if ($rows === []) {
- return 0;
- }
- $keys = [];
- foreach ($rows as $row) {
- if (!is_array($row)) {
- continue;
- }
- $dh = trim((string)($row['CCYDH'] ?? ''));
- $keys[$dh !== '' ? $dh : ('_id_' . (int)($row['id'] ?? 0))] = true;
- }
- return count($keys);
- }
- protected static function applyNotDeletedWhere($query): void
- {
- $query->whereRaw(
- '(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
- );
- }
- protected static function pickTimeSqlExpr(): string
- {
- $norm = function (string $col): string {
- return "NULLIF(NULLIF(TRIM(CAST({$col} AS CHAR(32))), ''), '0000-00-00')";
- };
- $createtime = 'IF(createtime > 946684800, FROM_UNIXTIME(createtime), ' . $norm('createtime') . ')';
- $parts = [
- $norm('pick_time'),
- $createtime,
- $norm('dputrecord'),
- $norm('dStamp'),
- ];
- return 'COALESCE(' . implode(', ', $parts) . ')';
- }
- }
|