> 演练模式下收集的下发/短信预览(供接口返回) */ protected $notifyDryRunPreview = []; /** * @var array */ protected $noNeedRight = [ 'review', 'reviewCompanies', 'outward_detail', 'export_month_outward', 'snapshotToProcure', 'pick', 'audit', 'confirm', 'pickreview', 'picksubmit', 'pickadd', 'auditissue', 'auditsubmit', ]; public function _initialize() { parent::_initialize(); $this->model = new \app\admin\model\Procuremen; } /** * 手机端外发明细首页直达链接(登录后打开对应明细,免搜索) * 配置 application/extra/mproc.php:mobile_base_url、mobile_index_path * * @param int $purchaseOrderDetailId purchase_order_detail 主键 */ protected function buildMprocMobileOrderUrl($purchaseOrderDetailId) { static $mprocCfgLoaded = false; if (!$mprocCfgLoaded) { $mprocCfgLoaded = true; if (is_file(APP_PATH . 'extra/mproc.php')) { Config::load(APP_PATH . 'extra/mproc.php', 'mproc'); } } $eid = (int)$purchaseOrderDetailId; $base = $this->resolveMprocMobilePublicBaseUrl(); if ($base === '') { throw new \Exception('无法生成手机端链接,请检查站点访问地址或 application/extra/mproc.php 中的 mobile_base_url'); } $path = trim((string)Config::get('mproc.mobile_index_path')); if ($path === '') { $path = '/index.php/index/index/index'; } else { $path = '/' . ltrim($path, '/'); } $query = ['main_tab' => 'orders']; if ($eid > 0) { $query['focus_eid'] = $eid; } return $base . $path . '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986); } /** * 邮件/短信外链:须为带点的公网域名 */ protected function procuremenOutboundBaseUrlLooksValid($baseUrl) { $u = trim((string)$baseUrl); if ($u === '' || !preg_match('#^https?://#i', $u)) { return false; } $host = parse_url($u, PHP_URL_HOST); if (!is_string($host) || $host === '') { return false; } if (strcasecmp($host, 'localhost') === 0) { return false; } if (preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) { return false; } if (strpos($host, '.') === false) { return false; } return true; } /** * 解析用于外发邮件/短信的手机端站点根 */ protected function resolveMprocMobilePublicBaseUrl() { $candidates = []; $candidates[] = trim((string)Config::get('mproc.mobile_base_url')); $candidates[] = trim((string)Config::get('site.indexurl')); $cdn = trim((string)Config::get('site.cdnurl')); if ($cdn !== '' && preg_match('#^https?://#i', $cdn)) { $candidates[] = rtrim($cdn, '/'); } $reqBase = rtrim($this->request->scheme() . '://' . $this->request->host(), '/'); $candidates[] = $reqBase; foreach ($candidates as $c) { if ($c === '') { continue; } $c = rtrim($c, '/'); if ($this->procuremenOutboundBaseUrlLooksValid($c)) { return $c; } } // 本地联调:127.0.0.1、内网 IP、虚拟主机等无法用「公网域名」规则时,退回当前请求根地址 $reqBase = rtrim($this->request->scheme() . '://' . $this->request->host(), '/'); if ($reqBase !== '' && preg_match('#^https?://#i', $reqBase)) { return $reqBase; } Log::write('外发邮件手机链接:配置地址失效', 'warning'); return ''; } /** * 排除 mcyd.dputrecord 空串、零日期等非法值,避免 MySQL 1525(DATE_FORMAT/BETWEEN 遇 '' 报错) */ protected function whereMcydDputrecordValid($query, $alias = 'a') { $c = $alias . '.dStamp'; // 先转成 CHAR 再判断,避免 DATETIME 列里存 '' / 0000-00-00 时参与日期函数报错 return $query->whereRaw( $c . ' IS NOT NULL' . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) <> \'\'' . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) NOT LIKE \'0000-00-00%\'' . ' AND TRIM(CAST(' . $c . ' AS CHAR(32))) REGEXP \'^[12][0-9]{3}-[01][0-9]-[0-3][0-9]\'' ); } /** * 获取 Redis 中 procuremen_redis */ protected function ProcuremenRedis(): array { try { $redis = redis(); $raw = $redis->get('procuremen_redis'); if ($raw === false || $raw === '') { return []; } $decoded = json_decode($raw, true); if (!is_array($decoded) || !isset($decoded['data']) || !is_array($decoded['data'])) { return []; } return $decoded['data']; } catch (\Throwable $e) { return []; } } /** * 左侧菜单 */ protected function GetIndexYearMonths() { $ymSet = []; //获取此方法调用缓存数据 foreach ($this->ProcuremenRedis() as $r) { if (!is_array($r)) { continue; } $ds = trim((string)($r['dputrecord'] ?? '')); if ($ds === '' || stripos($ds, '0000-00-00') === 0) { continue; } if (!preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $ds, $m)) { continue; } $mo = (int)$m[2]; if ($mo < 1 || $mo > 12) { continue; } $ymSet[$m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT)] = true; } $ymList = array_keys($ymSet); rsort($ymList, SORT_STRING); $byYear = []; foreach ($ymList as $ym) { $y = substr($ym, 0, 4); $mo = (int)substr($ym, 5, 2); if (!isset($byYear[$y])) { $byYear[$y] = []; } $byYear[$y][] = ['ym' => $ym, 'label' => $mo . '月']; } krsort($byYear, SORT_NUMERIC); foreach ($byYear as $y => $items) { usort($byYear[$y], function ($a, $b) { return strcmp($b['ym'], $a['ym']); }); } $sidebar = []; foreach ($byYear as $y => $months) { $sidebar[] = ['year' => $y, 'months' => $months]; } return $sidebar; } /** * 列表阶段:pick=下发(通知) audit=确认供应商 confirm=采购确认 */ protected function resolveProcuremenListStage(): string { $action = strtolower((string)$this->request->action()); if (in_array($action, ['pick', 'audit', 'confirm'], true)) { return $action; } $tab = trim((string)$this->request->request('wff_tab', 'pick')); return in_array($tab, ['pick', 'audit', 'confirm'], true) ? $tab : 'pick'; } protected function assignProcuremenListPage(string $stage, string $pageTitle): void { $rootTrue = rtrim((string)$this->request->root(true), '/'); $indexPhpRoot = preg_replace('#/[^/]+\.php$#i', '/index.php', $rootTrue); if ($indexPhpRoot === $rootTrue && !preg_match('#/index\.php$#i', $rootTrue)) { $indexPhpRoot = $rootTrue . '/index.php'; } $this->view->assign('procuremenRedisApi', $indexPhpRoot . '/api/procuremen/getprocuremen'); $this->view->assign('defaultYm', date('Y-m')); $this->view->assign('sidebarYearMonths', $this->GetIndexYearMonths()); $this->view->assign('procuremenStage', $stage); $this->view->assign('procuremenPageTitle', $pageTitle); } /** 外发下发(选工序+供应商,发短信邮件通知报价) */ public function pick() { $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { $this->assignProcuremenListPage('pick', '外发下发'); return $this->view->fetch('procuremen/index'); } return $this->index(); } /** 确认供应商(下发通知后,从报价中选定一家,进入采购确认) */ public function audit() { $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { $this->assignProcuremenListPage('audit', '确认供应商'); return $this->view->fetch('procuremen/index'); } return $this->index(); } /** 采购确认(定标) */ public function confirm() { $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { $this->assignProcuremenListPage('confirm', '采购确认'); return $this->view->fetch('procuremen/index'); } return $this->index(); } /** * 列表页(兼容旧菜单 procuremen/index,等同外发下发) */ public function index() { $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { $this->assignProcuremenListPage('pick', '外发下发'); return $this->view->fetch('procuremen/index'); } if ($this->request->request('keyField')) { return $this->selectpage(); } list(, $sort, $order, $offset, $limit) = $this->buildparams(); $limit = max(10, min(500, (int)$limit)); $ym = $this->request->request('ym', date('Y-m')); $ym = is_string($ym) ? trim($ym) : ''; if (!preg_match('/^\d{4}-\d{2}$/', $ym)) { $ym = date('Y-m'); } $monthStart = $ym . '-01 00:00:00'; $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart)); $search = trim((string)$this->request->get('search', '')); $hasActiveSearch = ($search !== ''); if (!$hasActiveSearch) { $filterStr = $this->request->get('filter', ''); if ($filterStr !== '' && $filterStr !== '[]' && $filterStr !== '{}') { $arr = json_decode($filterStr, true); if (is_array($arr) && count($arr) > 0) { foreach ($arr as $v) { if (is_array($v)) { if (array_filter($v, function ($x) { return $x !== '' && $x !== null; })) { $hasActiveSearch = true; break; } } elseif ($v !== '' && $v !== null) { $hasActiveSearch = true; break; } } } } } $applyMonthRange = !$hasActiveSearch; $filterArr = (array)json_decode($this->request->get('filter', ''), true); $opArr = (array)json_decode($this->request->get('op', ''), true); /* wff_tab / 菜单:pick=待下发 audit=待确认供应商 confirm=待采购确认 */ $wffTab = $this->resolveProcuremenListStage(); try { if ($wffTab === 'audit') { try { $dbRows = Db::table('purchase_order')->where('wflow_status', 1)->select(); } catch (\Throwable $e) { $dbRows = []; } $pool = $this->procuremenPoolFromPurchaseOrderDbRows(is_array($dbRows) ? $dbRows : []); } elseif ($wffTab === 'confirm') { try { $dbRows = Db::table('purchase_order') ->where('status', 0) ->where(function ($q) { $q->where('wflow_status', 2) ->whereOr(function ($q2) { $q2->where('wflow_status', 0)->whereRaw( 'EXISTS (SELECT 1 FROM purchase_order_detail d WHERE d.scydgy_id = purchase_order.scydgy_id LIMIT 1)' ); }); }) ->select(); } catch (\Throwable $e) { $dbRows = []; } $pool = $this->procuremenPoolFromPurchaseOrderDbRows(is_array($dbRows) ? $dbRows : []); } else { $pool = $this->ProcuremenRedis(); if (!is_array($pool)) { $pool = []; } // 下发列表:已通知供应商(有明细)或已进入审核/确认/完结的工序不再显示 $hideSet = $this->loadPickHiddenScydgySet(); if ($pool !== [] && $hideSet !== []) { $pool = array_values(array_filter($pool, function ($r) use ($hideSet) { if (!is_array($r)) { return false; } $id = (int)($r['ID'] ?? $r['id'] ?? 0); return $id <= 0 || !isset($hideSet[$id]); })); } $manualPool = $this->loadPickManualOrderRows(); if ($manualPool !== []) { $pool = array_merge($pool, $manualPool); } if ($pool === []) { return json([ 'total' => 0, 'rows' => [], 'msg' => '暂无缓存数据', ]); } } $filtered = $this->filterProcuremenIndexPool( $pool, $monthStart, $monthEnd, $applyMonthRange, $search, $filterArr, $opArr ); /* 确认供应商:同一订单号合并为一行(下发时一并提交的多道工序只显示一次) */ if ($wffTab === 'audit') { $filtered = $this->collapseProcuremenPoolByOrder($filtered); } $sortField = 'ID'; if (is_string($sort) && $sort !== '') { $parts = explode(',', $sort); $sortField = preg_replace('/^[ab]\./i', '', trim($parts[0])); if ($sortField === '') { $sortField = 'ID'; } } $ord = strtoupper((string)$order) === 'ASC' ? 1 : -1; $nFiltered = count($filtered); if ($nFiltered > 1) { if (in_array($sortField, ['pick_time', 'dputrecord', 'dStamp', 'createtime'], true)) { usort($filtered, function ($a, $b) use ($sortField, $ord) { $ta = $this->procuremenRowListSortTime($a, $sortField); $tb = $this->procuremenRowListSortTime($b, $sortField); if ($ta === $tb) { return ((int)($a['ID'] ?? 0) <=> (int)($b['ID'] ?? 0)) * $ord; } if ($ta === '') { return 1; } if ($tb === '') { return -1; } return strcmp($ta, $tb) * $ord; }); } elseif ($sortField === 'ID') { $sortKeys = []; foreach ($filtered as $i => $row) { $sortKeys[$i] = (int)($row['ID'] ?? 0); } $dir = $ord === 1 ? SORT_ASC : SORT_DESC; array_multisort($sortKeys, $dir, SORT_NUMERIC, $filtered); } else { usort($filtered, function ($a, $b) use ($sortField, $ord) { $va = $a[$sortField] ?? null; $vb = $b[$sortField] ?? null; $sa = (string)$va; $sb = (string)$vb; if ($sa === $sb) { return 0; } return strcmp($sa, $sb) * $ord; }); } } $offset = max(0, (int)$offset); $limit = max(1, (int)$limit); $rows = array_slice($filtered, $offset, $limit); foreach ($rows as &$rw) { if (!is_array($rw)) { continue; } $rid = (int)($rw['ID'] ?? 0); $rw['_iss_out'] = ($wffTab !== 'pick'); } unset($rw); if ($wffTab === 'confirm' && count($rows) > 0) { $idList = []; foreach ($rows as $rw) { if (!is_array($rw)) { continue; } $id = (int)($rw['ID'] ?? 0); if ($id > 0) { $idList[$id] = true; } } $sumMap = $this->OrderDetailSummary(array_keys($idList)); foreach ($rows as &$rw) { if (!is_array($rw)) { continue; } $rid = (int)($rw['ID'] ?? 0); $s = isset($sumMap[$rid]) ? $sumMap[$rid] : ['cnt' => 0, 'amt' => 0, 'deliv' => 0]; $rw['po_detail_count'] = $s['cnt']; $rw['po_amount_fill_cnt'] = $s['amt']; $rw['po_delivery_fill_cnt'] = $s['deliv']; } unset($rw); } if ($wffTab === 'audit' && count($rows) > 0) { $idList = []; foreach ($rows as $rw) { if (!is_array($rw)) { continue; } if (!empty($rw['_order_merge_rows']) && is_array($rw['_order_merge_rows'])) { foreach ($rw['_order_merge_rows'] as $mr) { $mid = (int)($mr['ID'] ?? 0); if ($mid > 0) { $idList[$mid] = true; } } } $id = (int)($rw['ID'] ?? 0); if ($id > 0) { $idList[$id] = true; } } $quoteBucket = $this->loadQuotedSupplierBucketByScydgyIds(array_keys($idList)); foreach ($rows as &$rw) { if (!is_array($rw)) { continue; } $mergeIds = []; if (!empty($rw['_order_merge_rows']) && is_array($rw['_order_merge_rows'])) { foreach ($rw['_order_merge_rows'] as $mr) { $mid = (int)($mr['ID'] ?? 0); if ($mid > 0) { $mergeIds[$mid] = true; } } } $rid = (int)($rw['ID'] ?? 0); if ($rid > 0) { $mergeIds[$rid] = true; } $buckets = []; foreach (array_keys($mergeIds) as $mid) { if (isset($quoteBucket[$mid])) { $buckets[] = $quoteBucket[$mid]; } } $rw['picked_supplier_name'] = $buckets !== [] ? $this->formatQuotedSupplierLines($this->mergeQuotedSupplierBuckets($buckets)) : ''; } unset($rw); } if ($wffTab === 'all' && count($rows) > 0) { $this->mergePurchaseOrder($rows); } return json([ 'total' => count($filtered), 'rows' => $rows, ]); } catch (\Throwable $e) { return json([ 'total' => 0, 'rows' => [], 'msg' => $e->getMessage(), ]); } } /** * 已外发/不可再审核判定(与「未发」列表隐藏规则一致,避免仅保存数量却被当成已下发) * - purchase_order.status 为 0 或 1:已审核下发或已完结 * - 或存在 purchase_order_detail:已向供应商下过明细 * 仅写入 purchase_order 且 status 为空:视为未发(如只填本次数量、最高限价) * * @return array scydgy_id => true */ protected function loadIssuedScydgySet() { $issuedSet = []; try { $poList = Db::table('purchase_order') ->whereRaw('(`status` = 0 OR `status` = 1)') ->column('scydgy_id'); if (is_array($poList)) { foreach ($poList as $ids) { $n = (int)$ids; if ($n > 0) { $issuedSet[$n] = true; } } } } catch (\Throwable $e) { } try { $detList = Db::table('purchase_order_detail')->group('scydgy_id')->column('scydgy_id'); if (is_array($detList)) { foreach ($detList as $ids) { $n = (int)$ids; if ($n > 0) { $issuedSet[$n] = true; } } } } catch (\Throwable $e) { } return $issuedSet; } /** * 外发下发列表需隐藏的工序 scydgy_id(已下发通知或已进入后续流程) * * @return array */ protected function loadPickHiddenScydgySet(): array { $hideSet = []; try { $poIds = Db::table('purchase_order') ->where(function ($q) { $q->where('wflow_status', '>=', 1)->whereOr('status', 1); }) ->column('scydgy_id'); if (is_array($poIds)) { foreach ($poIds as $pid) { $k = (int)$pid; if ($k > 0) { $hideSet[$k] = true; } } } } catch (\Throwable $e) { } try { $detIds = Db::table('purchase_order_detail')->column('scydgy_id'); if (is_array($detIds)) { foreach ($detIds as $pid) { $k = (int)$pid; if ($k > 0) { $hideSet[$k] = true; } } } } catch (\Throwable $e) { } try { $timeIds = Db::table('purchase_order') ->whereNotNull('pick_time') ->where('pick_time', '<>', '') ->where('pick_time', '<>', '0000-00-00 00:00:00') ->column('scydgy_id'); if (is_array($timeIds)) { foreach ($timeIds as $pid) { $k = (int)$pid; if ($k > 0) { $hideSet[$k] = true; } } } } catch (\Throwable $e) { } return $hideSet; } /** * 外发下发列表:手工新增工序(purchase_order.scydgy_id 为负数,与 ERP 缓存行区分) * * @return array> */ protected function loadPickManualOrderRows(): array { try { $dbRows = Db::table('purchase_order')->where('scydgy_id', '<', 0)->order('scydgy_id', 'asc')->select(); } catch (\Throwable $e) { return []; } if (!is_array($dbRows) || $dbRows === []) { return []; } $pool = []; foreach ($dbRows as $dbRow) { if (!is_array($dbRow)) { continue; } $sid = (int)($dbRow['scydgy_id'] ?? 0); if ($sid >= 0) { continue; } $wf = (int)($dbRow['wflow_status'] ?? 0); if ($wf >= 1) { continue; } $st = trim((string)($dbRow['status'] ?? '')); if ($st === '1' || $st === '0') { continue; } try { $detCnt = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->count(); if ($detCnt > 0) { continue; } } catch (\Throwable $e) { } $r = $this->buildListRowFromPurchaseOrderDbRow($dbRow); $r['_is_manual'] = 1; $pool[] = $r; } return $pool; } /** * 列表排序用时间:下发时间 pick_time 优先,其次 createtime / dputrecord / dStamp */ protected function procuremenRowListSortTime(array $row, string $primary = 'dputrecord'): 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 ''; } /** * 分配手工新增工序行 ID(负数递减) */ protected function allocateManualScydgyId(): int { try { $min = Db::table('purchase_order')->where('scydgy_id', '<', 0)->min('scydgy_id'); $min = (int)$min; return $min < 0 ? $min - 1 : -1; } catch (\Throwable $e) { return -1; } } /** * 工序行主键:ERP 为正 scydgy.ID;手工新增为负 purchase_order.scydgy_id */ protected function extractScydgyRowId(array $row): int { return (int)($row['ID'] ?? $row['id'] ?? $row['scydgy_id'] ?? 0); } protected function isValidScydgyRowId(int $id): bool { return $id !== 0; } protected function isManualScydgyRowId(int $id): bool { return $id < 0; } /** * 采购确认已选定供应商:明细 purchase_order_detail.status = 1 的工序行 scydgy_id 集合 * * @return array */ protected function loadScydgyIdsWithPickedSupplierDetail(): array { $set = []; try { $rows = Db::table('purchase_order_detail')->where('status', 1)->field('scydgy_id')->select(); } catch (\Throwable $e) { $rows = []; } if (!is_array($rows)) { return $set; } foreach ($rows as $r) { if (!is_array($r)) { continue; } $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0); if ($sid > 0) { $set[$sid] = true; } } return $set; } /** * 每个工序行对应已选中供应商名称(取 status=1 的首条明细 company_name) * * @param int[] $scydgyIds * @return array scydgy_id => company_name */ protected function loadPickedSupplierCompanyByScydgyIds(array $scydgyIds): array { $out = []; $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds)))); if ($scydgyIds === []) { return $out; } try { $rows = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', $scydgyIds) ->where('status', 1) ->field('scydgy_id,company_name') ->order('id', 'asc') ->select(); } catch (\Throwable $e) { try { $rows = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', $scydgyIds) ->where('status', 1) ->field('scydgy_id,company_name') ->order('ID', 'asc') ->select(); } catch (\Throwable $e2) { $rows = []; } } if (!is_array($rows)) { return $out; } foreach ($rows as $r) { if (!is_array($r)) { continue; } $sid = (int)($r['scydgy_id'] ?? $r['SCYDGY_ID'] ?? 0); if (!$this->isValidScydgyRowId($sid) || isset($out[$sid])) { continue; } $name = trim((string)($r['company_name'] ?? '')); $out[$sid] = $name; } return $out; } /** * 审核列表:工序行已通知/已报价供应商(purchase_order_detail 有加工金额视为已报价) * * @param int[] $scydgyIds * @return array, quoted: array}> */ protected function loadQuotedSupplierBucketByScydgyIds(array $scydgyIds): array { $out = []; $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds)))); if ($scydgyIds === []) { return $out; } try { $rows = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', $scydgyIds) ->field('scydgy_id,company_name,amount') ->select(); } catch (\Throwable $e) { $rows = []; } if (!is_array($rows)) { return $out; } foreach ($rows as $r) { if (!is_array($r)) { continue; } $sid = (int)($r['scydgy_id'] ?? 0); $cn = trim((string)($r['company_name'] ?? '')); if (!$this->isValidScydgyRowId($sid) || $cn === '') { continue; } if (!isset($out[$sid])) { $out[$sid] = ['all' => [], 'quoted' => []]; } $out[$sid]['all'][$cn] = true; $am = trim((string)($r['amount'] ?? '')); if ($am !== '' && $am !== '0' && $am !== '0.00') { $out[$sid]['quoted'][$cn] = true; } } return $out; } /** * @param array, quoted: array}> $buckets * @return array{all: array, quoted: array} */ protected function mergeQuotedSupplierBuckets(array $buckets): array { $merged = ['all' => [], 'quoted' => []]; foreach ($buckets as $b) { if (!is_array($b)) { continue; } foreach ($b['all'] ?? [] as $cn => $_) { $merged['all'][$cn] = true; } foreach ($b['quoted'] ?? [] as $cn => $_) { $merged['quoted'][$cn] = true; } } return $merged; } /** * @param array{all: array, quoted: array} $bucket */ protected function formatQuotedSupplierLines(array $bucket): string { $all = array_keys($bucket['all'] ?? []); if ($all === []) { return ''; } sort($all, SORT_STRING); $lines = []; foreach ($all as $cn) { $status = !empty($bucket['quoted'][$cn]) ? '已报价' : '未报价'; $lines[] = $cn . '(' . $status . ')'; } return implode("\n", $lines); } /** * @param int[] $scydgyIds * @return array */ protected function loadQuotedSupplierSummaryByScydgyIds(array $scydgyIds): array { $out = []; foreach ($this->loadQuotedSupplierBucketByScydgyIds($scydgyIds) as $sid => $bucket) { $text = $this->formatQuotedSupplierLines($bucket); if ($text !== '') { $out[$sid] = $text; } } return $out; } /** * 审核弹窗:按供应商汇总报价明细 * * @param array{ccydh:string, pos:array, merge_rows:array} $bundle * @return array> */ protected function loadAuditSupplierQuoteGroups(array $bundle): array { $sids = []; foreach ($bundle['pos'] ?? [] as $po) { if (!is_array($po)) { continue; } $sid = (int)($po['scydgy_id'] ?? 0); if ($sid > 0) { $sids[$sid] = true; } } if ($sids === []) { return []; } $gymcMap = []; foreach ($bundle['merge_rows'] ?? [] as $mr) { if (!is_array($mr)) { continue; } $gid = (int)($mr['ID'] ?? 0); if ($gid > 0) { $gymcMap[$gid] = trim((string)($mr['CGYMC'] ?? '')); } } foreach ($bundle['pos'] ?? [] as $po) { if (!is_array($po)) { continue; } $gid = (int)($po['scydgy_id'] ?? 0); if ($gid > 0) { $nm = trim((string)($po['CGYMC'] ?? '')); if ($nm !== '') { $gymcMap[$gid] = $nm; } } } try { $details = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', array_keys($sids)) ->order('company_name', 'asc') ->order('id', 'asc') ->select(); } catch (\Throwable $e) { try { $details = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', array_keys($sids)) ->order('company_name', 'asc') ->order('ID', 'asc') ->select(); } catch (\Throwable $e2) { $details = []; } } if (!is_array($details)) { $details = []; } $byCompany = []; foreach ($details as $d) { if (!is_array($d)) { continue; } $cn = trim((string)($d['company_name'] ?? '')); if ($cn === '') { continue; } if (!isset($byCompany[$cn])) { $ph = trim((string)($d['phone'] ?? '')); $byCompany[$cn] = [ 'name' => $cn, 'email' => trim((string)($d['email'] ?? '')), 'phone' => $ph, 'username' => $this->resolveCustomerContactName($ph, $cn), 'has_quote' => false, 'lines' => [], ]; } $sid = (int)($d['scydgy_id'] ?? 0); $am = trim((string)($d['amount'] ?? '')); $gymc = $gymcMap[$sid] ?? trim((string)($d['CGYMC'] ?? $d['cgymc'] ?? '')); if ($gymc === '') { $gymc = '工序'; } $deliveryRaw = trim((string)($d['delivery'] ?? '')); $deliveryShow = $this->formatDeliveryYmd($deliveryRaw); if ($deliveryShow === '' && $deliveryRaw !== '') { $deliveryShow = $deliveryRaw; } $amountFilled = ($am !== '' && $am !== '0' && $am !== '0.00'); $deliveryFilled = ($deliveryShow !== ''); $byCompany[$cn]['lines'][] = [ 'cgymc' => $gymc, 'amount' => $am, 'amount_show' => $amountFilled ? $am : '', 'amount_filled' => $amountFilled, 'delivery' => $deliveryRaw, 'delivery_show' => $deliveryFilled ? $deliveryShow : '', 'delivery_filled' => $deliveryFilled, 'status_name' => trim((string)($d['status_name'] ?? '')), 'is_quoted' => $amountFilled, 'quote_label' => $amountFilled ? '已报价' : '未报价', ]; } foreach ($byCompany as $cn => $g) { $total = count($g['lines']); $quoted = 0; foreach ($g['lines'] as $ln) { $am = trim((string)($ln['amount'] ?? '')); if ($am !== '' && $am !== '0' && $am !== '0.00') { $quoted++; } } $byCompany[$cn]['has_quote'] = $total > 0 && $quoted === $total; } return array_values($byCompany); } /** * purchase_order 表行 → 列表/弹窗用的工序行(用表内已有字段,不依赖 row_json) * * @param array $dbRow * @param array $dStampMap scydgy_id => dStamp */ protected function buildListRowFromPurchaseOrderDbRow(array $dbRow, array $dStampMap = []): array { $sid = (int)($dbRow['scydgy_id'] ?? 0); $r = [ 'ID' => $sid, 'CCYDH' => $dbRow['CCYDH'] ?? '', 'CYJMC' => $dbRow['CYJMC'] ?? '', 'CDXMC' => $dbRow['CDXMC'] ?? '', 'CGYBH' => $dbRow['CGYBH'] ?? '', 'CGYMC' => $dbRow['CGYMC'] ?? '', 'CDW' => $dbRow['CDW'] ?? '', 'NGZL' => $dbRow['NGZL'] ?? '', 'CDF' => $dbRow['CDF'] ?? '', 'cGzzxMc' => $dbRow['cGzzxMc'] ?? '', 'MBZ' => $dbRow['MBZ'] ?? '', 'bwjg' => $dbRow['bwjg'] ?? '', 'iStatus' => $dbRow['iStatus'] ?? '', 'dputrecord' => $dbRow['dputrecord'] ?? '', 'cywyxm' => $dbRow['cywyxm'] ?? '', 'This_quantity' => $dbRow['This_quantity'] ?? $dbRow['this_quantity'] ?? '', 'ceilingPrice' => $dbRow['ceilingPrice'] ?? $dbRow['ceiling_price'] ?? '', 'dStamp' => $dbRow['dStamp'] ?? '', 'pick_time' => $dbRow['pick_time'] ?? '', ]; if ($sid > 0 && isset($dStampMap[$sid])) { $t = trim((string)$dStampMap[$sid]); if ($t !== '' && stripos($t, '0000-00-00') !== 0) { $r['dStamp'] = $dStampMap[$sid]; } } $dsOut = trim((string)($r['dStamp'] ?? '')); if (($dsOut === '' || stripos($dsOut, '0000-00-00') === 0) && !empty($dbRow['createtime'])) { $ct = $dbRow['createtime']; if (is_numeric($ct) && (int)$ct > 946684800) { $r['dStamp'] = date('Y-m-d H:i:s', (int)$ct); } elseif (is_string($ct) && trim($ct) !== '' && stripos(trim($ct), '0000-00-00') !== 0) { $r['dStamp'] = trim($ct); } } // 列表月份筛选读 dputrecord;手工行无 ERP 提交日时回退 dStamp/createtime $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; } } if (array_key_exists('id', $dbRow)) { $r['purchase_order_id'] = (int)$dbRow['id']; } $r['createtime'] = $this->formatProcuremenDetailTime($dbRow['createtime'] ?? null); $pickNm = trim((string)($dbRow['pick_company_name'] ?? '')); if ($pickNm !== '') { $r['pick_company_name'] = $pickNm; $r['picked_supplier_name'] = $pickNm; } return $r; } /** * 与列表「已下发 / 已选中 / 已完结」同源:将 purchase_order 行还原为工序列表行 * * @return array> */ protected function procuremenPoolFromPurchaseOrderDbRows(array $dbRows): array { $pool = []; if (!is_array($dbRows) || $dbRows === []) { return $pool; } $dStampMap = []; $sidList = []; foreach ($dbRows as $tmpRow) { if (is_array($tmpRow) && isset($tmpRow['scydgy_id'])) { $sid = (int)$tmpRow['scydgy_id']; if ($sid > 0) { $sidList[$sid] = true; } } } if ($sidList !== []) { try { $dStampMap = Db::table('scydgy')->where('ID', 'in', array_keys($sidList))->column('dStamp', 'ID'); } catch (\Throwable $e) { $dStampMap = []; } } foreach ($dbRows as $dbRow) { if (!is_array($dbRow)) { continue; } $pool[] = $this->buildListRowFromPurchaseOrderDbRow($dbRow, $dStampMap); } return $pool; } /** * 已下发列表汇总:按工序行 ID 统计 purchase_order_detail 条数、已填金额条数、已填货期条数 * * @param int[] $scydgyIds * @return array */ protected function OrderDetailSummary(array $scydgyIds) { $out = []; $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds)))); if ($scydgyIds === []) { return $out; } $list = []; try { $list = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', $scydgyIds) ->field('id,scydgy_id,amount,delivery') ->select(); } catch (\Throwable $e) { $list = []; } if (!is_array($list)) { return $out; } foreach ($list as $r) { if (!is_array($r)) { continue; } $ids = (int)($r['scydgy_id'] ?? 0); if (!$this->isValidScydgyRowId($ids)) { continue; } if (!isset($out[$ids])) { $out[$ids] = ['cnt' => 0, 'amt' => 0, 'deliv' => 0]; } $out[$ids]['cnt']++; $am = $r['amount'] ?? null; if ($am !== null && $am !== '') { if (!(is_string($am) && trim($am) === '')) { $out[$ids]['amt']++; } } $dv = $r['delivery'] ?? null; if ($dv !== null && trim((string)$dv) !== '') { $out[$ids]['deliv']++; } } return $out; } /** * 列表筛选:月份按 dputrecord(提交日期)、快速搜索、Bootstrap Table filter * * @return array */ protected function filterProcuremenIndexPool(array $pool, $monthStart, $monthEnd, $applyMonthRange, $search, array $filterArr, array $opArr) { $strContains = function ($haystack, $needle) { if ($needle === '') { return false; } $haystack = (string)$haystack; if (function_exists('mb_stripos')) { return mb_stripos($haystack, $needle, 0, 'UTF-8') !== false; } return stripos($haystack, $needle) !== false; }; $stripAlias = function ($f) { return preg_replace('/^[ab]\./i', '', (string)$f); }; $searchColNames = []; foreach (explode(',', $this->searchFields) as $colRaw) { $c = $stripAlias(trim($colRaw)); if ($c !== '') { $searchColNames[] = $c; } } $filtered = []; foreach ($pool as $r) { if (!is_array($r)) { continue; } $ds = isset($r['dputrecord']) ? trim((string)$r['dputrecord']) : ''; if ($ds === '' || stripos($ds, '0000-00-00') === 0) { $ds = isset($r['dStamp']) ? trim((string)$r['dStamp']) : ''; } $isManual = !empty($r['_is_manual']); if (!$isManual && ($ds === '' || stripos($ds, '0000-00-00') === 0 || !preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $ds, $m))) { continue; } if ($applyMonthRange && !$isManual) { $mo = (int)$m[2]; if ($mo < 1 || $mo > 12) { continue; } $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)); if (strcmp($rowMonthEnd, $monthStart) < 0 || strcmp($rowMonthStart, $monthEnd) > 0) { continue; } } if ($search !== '') { $hitSearch = false; foreach ($searchColNames as $c) { $cell = isset($r[$c]) ? (string)$r[$c] : ''; if ($cell !== '' && $strContains($cell, $search)) { $hitSearch = true; break; } } if (!$hitSearch) { continue; } } foreach ($filterArr as $fk => $fv) { if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', (string)$fk)) { continue; } if (is_array($fv)) { continue; } if ($fv === '' && $fv !== '0' && $fv !== 0) { continue; } $sym = strtoupper(trim((string)($opArr[$fk] ?? '='))); $sym = str_replace(['LIKE %...%', 'NOT LIKE %...%'], ['LIKE', 'NOT LIKE'], $sym); $field = $stripAlias($fk); $cell = array_key_exists($field, $r) ? $r[$field] : null; $cellStr = trim((string)$cell); switch ($sym) { case '=': if ((string)$cell !== (string)$fv) { continue 3; } break; case '<>': if ((string)$cell === (string)$fv) { continue 3; } break; case 'LIKE': case 'NOT LIKE': $needle = trim((string)$fv); $hit = ($needle !== '' && $strContains($cellStr, $needle)); if ($sym === 'LIKE' && !$hit) { continue 3; } if ($sym === 'NOT LIKE' && $hit) { continue 3; } break; case '>': case '>=': case '<': case '<=': if (!is_numeric($cell) && $cellStr === '') { continue 3; } $cv = is_numeric($cell) ? (float)$cell : (float)$cellStr; $vv = (float)$fv; if ($sym === '>' && !($cv > $vv)) { continue 3; } if ($sym === '>=' && !($cv >= $vv)) { continue 3; } if ($sym === '<' && !($cv < $vv)) { continue 3; } if ($sym === '<=' && !($cv <= $vv)) { continue 3; } break; case 'BETWEEN': case 'NOT BETWEEN': case 'BETWEEN TIME': case 'NOT BETWEEN TIME': $rawR = is_array($fv) ? implode(',', $fv) : (string)$fv; $rawR = str_replace(' - ', ',', $rawR); $arr = array_slice(array_map('trim', explode(',', $rawR)), 0, 2); if (count($arr) < 2 || $arr[0] === '' || $arr[1] === '') { break; } $lo = $arr[0]; $hi = $arr[1]; $in = ($cellStr >= $lo && $cellStr <= $hi); $isNot = (strpos($sym, 'NOT') !== false); if ($isNot ? $in : !$in) { continue 3; } break; case 'NULL': case 'IS NULL': if ($cell !== null && $cell !== '') { continue 3; } break; case 'NOT NULL': case 'IS NOT NULL': if ($cell === null || $cell === '') { continue 3; } break; default: break; } } $filtered[] = $r; } return $filtered; } /** * 查询订单 purchase_order 数据查有无记录,无则插、有则改(全部写在本方法内) */ public function snapshotToProcure() { if (!$this->request->isPost() || !$this->request->isAjax()) { $this->error(__('Invalid parameters')); } $row = json_decode($this->request->post('row_json', ''), true); if (!is_array($row)) { $this->error(__('Invalid parameters')); } try { $ids = $this->extractScydgyRowId($row); if (!$this->isValidScydgyRowId($ids)) { throw new \Exception('无效的行主键 ID'); } $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); $data = [ 'scydgy_id' => $ids, 'CCYDH' => $row['CCYDH'] ?? null, 'CYJMC' => $row['CYJMC'] ?? null, 'CDXMC' => $row['CDXMC'] ?? null, 'CGYBH' => $row['CGYBH'] ?? null, 'CGYMC' => $row['CGYMC'] ?? null, 'CDW' => $row['CDW'] ?? null, 'NGZL' => $row['NGZL'] ?? null, 'CDF' => $row['CDF'] ?? null, 'cGzzxMc' => $row['cGzzxMc'] ?? null, 'MBZ' => $row['MBZ'] ?? null, 'bwjg' => $row['bwjg'] ?? null, 'iStatus' => $row['iStatus'] ?? null, 'dStamp' => $row['dStamp'] ?? null, 'dputrecord' => $row['dputrecord'] ?? null, 'cywyxm' => $row['cywyxm'] ?? null, 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null, 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null, ]; $qtySnap = trim((string)($data['This_quantity'] ?? '')); $priceSnap = trim((string)($data['ceilingPrice'] ?? '')); if (!$exists && $qtySnap === '' && $priceSnap === '') { $this->success('操作成功'); return; } if ($exists) { $upd = $data; unset($upd['scydgy_id'], $upd['status']); Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd); } else { // 新增:不写 status(空/走库默认);仅写创建时间 $data['createtime'] = date('Y-m-d H:i:s'); Db::table('purchase_order')->insert($data); } } catch (\Throwable $e) { $this->error($e->getMessage()); } // 操作记录仅在「审核提交」时写入(review POST + addOrderLog),此处同步不写日志,避免与下发记录重复 $this->success('操作成功'); } /** * 未发列表 * 「完结」或「仅保存本次数量/最高限价」:有则改、无则插 * POST finish=1(默认):并置 status=1;finish=0:更新时不改 status;新增不写 status。 */ public function completeDirectly() { if (!$this->request->isPost() || !$this->request->isAjax()) { $this->error(__('参数错误')); } $row = json_decode($this->request->post('row_json', ''), true); if (!is_array($row)) { $this->error(__('Invalid parameters')); } $finishRaw = $this->request->post('finish', '1'); $asComplete = ($finishRaw === '1' || $finishRaw === 1 || $finishRaw === true); try { // 获取工序行 ID,对应 purchase_order.scydgy_id;有则改、无则插 $ids = $this->extractScydgyRowId($row); if (!$this->isValidScydgyRowId($ids)) { throw new \Exception('无效的行主键 ID'); } $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); $data = [ 'scydgy_id' => $ids, 'CCYDH' => $row['CCYDH'] ?? null, 'CYJMC' => $row['CYJMC'] ?? null, 'CDXMC' => $row['CDXMC'] ?? null, 'CGYBH' => $row['CGYBH'] ?? null, 'CGYMC' => $row['CGYMC'] ?? null, 'CDW' => $row['CDW'] ?? null, 'NGZL' => $row['NGZL'] ?? null, 'CDF' => $row['CDF'] ?? null, 'cGzzxMc' => $row['cGzzxMc'] ?? null, 'MBZ' => $row['MBZ'] ?? null, 'bwjg' => $row['bwjg'] ?? null, 'iStatus' => $row['iStatus'] ?? null, 'dStamp' => $row['dStamp'] ?? null, 'dputrecord' => $row['dputrecord'] ?? null, 'cywyxm' => $row['cywyxm'] ?? null, 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null, 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null, ]; if ($asComplete) { // 主表 status 为 varchar 时统一写 '1',档案 procuremenarchive 按 status=1 查询 $data['status'] = '1'; } $qtyCd = trim((string)($data['This_quantity'] ?? '')); $priceCd = trim((string)($data['ceilingPrice'] ?? '')); if (!$asComplete && !$exists && $qtyCd === '' && $priceCd === '') { $this->success('操作成功'); return; } if ($exists) { $upd = $data; unset($upd['scydgy_id']); if (!$asComplete) { unset($upd['status']); } Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd); } else { // 新增:不写 status;完结时 $data 已含 status=1 $data['createtime'] = date('Y-m-d H:i:s'); Db::table('purchase_order')->insert($data); } } catch (\Throwable $e) { $this->error($e->getMessage()); } $poIdLog = null; try { $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); if (is_array($rpo)) { $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0); $poIdLog = $tid > 0 ? $tid : null; } } catch (\Throwable $e) { $poIdLog = null; } $q = isset($data['This_quantity']) ? trim((string)$data['This_quantity']) : ''; $p = isset($data['ceilingPrice']) ? trim((string)$data['ceilingPrice']) : ''; if ($asComplete) { $this->addOrderLog( $ids, 'mark_complete', '点击「完结」,已标记为已完结', $poIdLog ); try { $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($ids, (int)($poIdLog ?? 0)); if ($pdfPublicPath === '') { Log::write('完结存证PDF/OSS未生成 scydgy_id=' . $ids, 'notice'); } } catch (\Throwable $e) { Log::write('完结存证PDF异常 scydgy_id=' . $ids . ' ' . $e->getMessage(), 'error'); } } else { $this->addOrderLog( $ids, 'save_qty_price', '保存本次数量、最高限价:本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」', $poIdLog ); } $this->success("操作成功"); } /** * 未发列表:从 purchase_order 合并已填的本次数量、最高限价(若表中有对应列) * * @param array $rows 引用传递当前页行 */ protected function mergePurchaseOrder(array &$rows) { if ($rows === []) { return; } $ids = []; foreach ($rows as $rw) { if (!is_array($rw)) { continue; } $id = (int)($rw['ID'] ?? 0); if ($id > 0) { $ids[$id] = true; } } $idList = array_keys($ids); if ($idList === []) { return; } try { $list = Db::table('purchase_order') ->where('scydgy_id', 'in', $idList) ->field('scydgy_id,This_quantity,ceilingPrice') ->select(); } catch (\Throwable $e) { return; } if (!is_array($list)) { return; } $byId = []; foreach ($list as $dbRow) { if (!is_array($dbRow) || !isset($dbRow['scydgy_id'])) { continue; } $byId[(int)$dbRow['scydgy_id']] = $dbRow; } foreach ($rows as &$rw) { if (!is_array($rw)) { continue; } $sid = (int)($rw['ID'] ?? 0); if (!$this->isValidScydgyRowId($sid) || !isset($byId[$sid])) { continue; } $db = $byId[$sid]; if (array_key_exists('This_quantity', $db) && $db['This_quantity'] !== null && $db['This_quantity'] !== '') { $rw['This_quantity'] = $db['This_quantity']; } if (array_key_exists('ceilingPrice', $db) && $db['ceilingPrice'] !== null && $db['ceilingPrice'] !== '') { $rw['ceilingPrice'] = $db['ceilingPrice']; } elseif (array_key_exists('ceiling_price', $db) && $db['ceiling_price'] !== null && $db['ceiling_price'] !== '') { $rw['ceilingPrice'] = $db['ceiling_price']; } } unset($rw); } /** * 采购确认:明细选中 status=1、未选 status=0;同时将 purchase_order 主表 status 置为 1 */ public function purchaseConfirmPick() { if (!$this->request->isPost() || !$this->request->isAjax()) { $this->error(__('Invalid parameters')); } $scydgyId = trim((string)$this->request->post('scydgy_id', '')); $sid = (int)$scydgyId; if (!$this->isValidScydgyRowId($sid)) { $this->error('参数无效'); } $parseIdList = function ($raw) { if (is_array($raw)) { $arr = $raw; } else { $decoded = json_decode((string)$raw, true); $arr = is_array($decoded) ? $decoded : []; } return array_values(array_unique(array_filter(array_map('intval', $arr)))); }; $selectedRaw = $this->request->post('selected_ids', null); if ($selectedRaw === null || $selectedRaw === '') { $legacy = (int)$this->request->post('selected_id', 0); $selectedIds = $legacy > 0 ? [$legacy] : []; } else { $selectedIds = $parseIdList($selectedRaw); } $unselectedIds = $parseIdList($this->request->post('unselected_ids', '[]')); if ($selectedIds === []) { $this->error('请至少勾选一条明细'); } $inter = array_intersect($selectedIds, $unselectedIds); if ($inter !== []) { $this->error('选中与未选中 ID 不能重复'); } $allIds = $this->purchaseOrderDetail($sid); if ($allIds === []) { $this->error('未找到该工序行的下发明细'); } foreach ($selectedIds as $id) { if (!in_array($id, $allIds, true)) { $this->error('选中 ID 不属于当前工序行'); } } foreach ($unselectedIds as $id) { if (!in_array($id, $allIds, true)) { $this->error('未选中 ID 不属于当前工序行'); } } $union = array_values(array_unique(array_merge($selectedIds, $unselectedIds))); sort($union, SORT_NUMERIC); $expect = $allIds; sort($expect, SORT_NUMERIC); if ($union !== $expect) { $this->error('请提交当前工序下全部明细 ID(选中 + 未选中)'); } $purchaseOrderId = (int)$this->request->post('purchase_order_id', 0); Db::startTrans(); try { Db::table('purchase_order_detail')->where('scydgy_id', $sid)->update(['status' => 0]); $aff = 0; try { $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('id', 'in', $selectedIds)->update(['status' => 1]); } catch (\Throwable $e) { $aff = (int)Db::table('purchase_order_detail')->where('scydgy_id', $sid)->where('ID', 'in', $selectedIds)->update(['status' => 1]); } if ($aff < 1) { throw new \Exception('更新选中状态失败'); } if ($purchaseOrderId > 0) { $poAff = (int)Db::table('purchase_order') ->where('id', $purchaseOrderId) ->where('scydgy_id', $sid) ->update(['status' => 1]); if ($poAff < 1) { throw new \Exception('主表订单不存在或与工序行不匹配'); } } else { $poAff = (int)Db::table('purchase_order')->where('scydgy_id', $sid)->update(['status' => '1']); if ($poAff < 1) { throw new \Exception('未找到'); } } Db::commit(); } catch (\Throwable $e) { Db::rollback(); $this->error($e->getMessage()); } $poIdFinal = $purchaseOrderId; if ($poIdFinal <= 0) { try { $rpo = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); if (is_array($rpo)) { $poIdFinal = (int)($rpo['id'] ?? $rpo['ID'] ?? 0); } } catch (\Throwable $e) { $poIdFinal = 0; } } $pickNames = []; foreach ($selectedIds as $pid) { $dr = $this->purchaseOrderDetai($sid, $pid); $nm = trim((string)($dr['company_name'] ?? '')); $pickNames[] = $nm !== '' ? $nm : ('明细#' . $pid); } $this->addOrderLog( $sid, 'purchase_confirm', '采购确认:已选中供应商「' . implode('、', $pickNames) . '」;', $poIdFinal > 0 ? $poIdFinal : null ); $ccydh = ''; $cyjmc = ''; try { $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); if (is_array($po)) { $ccydh = trim((string)($po['CCYDH'] ?? '')); $cyjmc = trim((string)($po['CYJMC'] ?? '')); } } catch (\Throwable $e) { } if ($ccydh === '' || $cyjmc === '') { $any = $this->purchaseOrderDetai($sid, $selectedIds[0] ?? 0); if (is_array($any)) { if ($ccydh === '') { $ccydh = trim((string)($any['CCYDH'] ?? '')); } if ($cyjmc === '') { $cyjmc = trim((string)($any['CYJMC'] ?? '')); } } } $sendSmsSafe = function ($phone, $content) { $phone = trim((string)$phone); if ($phone === '') { return; } try { $this->smsbao($phone, $content); } catch (\Throwable $e) { Log::write('采购确认短信失败 phone=' . $phone . ' ' . $e->getMessage(), 'error'); } }; //批量发送手机短信:采购确认通过 foreach ($selectedIds as $pid) { $dr = $this->purchaseOrderDetai($sid, $pid); if (!is_array($dr)) { continue; } $cname = trim((string)($dr['company_name'] ?? '')); $ph = trim((string)($dr['phone'] ?? '')); $sms = $this->renderNotifyTemplate('confirm_ok', [ 'company_name' => $cname, 'contact_name' => $this->resolveCustomerContactName($ph, $cname), 'phone' => $ph, 'ccydh' => $ccydh, 'cyjmc' => $cyjmc, ]); $sendSmsSafe((string)($dr['phone'] ?? ''), $sms); } //批量发送手机短信:采购确认未通过 foreach ($unselectedIds as $pid) { $dr = $this->purchaseOrderDetai($sid, $pid); if (!is_array($dr)) { continue; } $cname = trim((string)($dr['company_name'] ?? '')); $ph = trim((string)($dr['phone'] ?? '')); $sms = $this->renderNotifyTemplate('confirm_fail', [ 'company_name' => $cname, 'contact_name' => $this->resolveCustomerContactName($ph, $cname), 'phone' => $ph, 'ccydh' => $ccydh, 'cyjmc' => $cyjmc, ]); $sendSmsSafe((string)($dr['phone'] ?? ''), $sms); } $line = sprintf( 'purchaseConfirmPick scydgy_id=%s purchase_order_id=%d selected_ids=%s unselected_ids=%s', $scydgyId, $purchaseOrderId, json_encode($selectedIds, JSON_UNESCAPED_UNICODE), json_encode($unselectedIds, JSON_UNESCAPED_UNICODE) ); Log::write($line, 'notice'); $pdfPublicPath = ''; try { $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($sid, $poIdFinal); } catch (\Throwable $e) { Log::write('采购确认PDF异常: ' . $e->getMessage(), 'error'); } $this->success('操作成功', '', [ 'scydgy_id' => $scydgyId, 'purchase_order_id' => $purchaseOrderId, 'selected_ids' => $selectedIds, 'unselected_ids' => $unselectedIds, 'purchase_confirm_pdf' => $pdfPublicPath, ]); } /** * @return int[] */ protected function purchaseOrderDetail(int $scydgyId): array { if (!$this->isValidScydgyRowId($scydgyId)) { return []; } try { $list = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->select(); } catch (\Throwable $e) { return []; } if (!is_array($list)) { return []; } $ids = []; foreach ($list as $r) { if (!is_array($r)) { continue; } $pk = (int)($r['id'] ?? $r['ID'] ?? 0); if ($pk > 0) { $ids[] = $pk; } } return array_values(array_unique($ids)); } /** * 获取当前登录用户信息 [id, 展示名] */ protected function GetUseName(): array { $id = 0; $name = ''; try { if ($this->auth && $this->auth->isLogin()) { $u = $this->auth->getUserInfo(); if (is_array($u)) { $id = (int)($u['id'] ?? 0); $name = trim((string)($u['nickname'] ?? '')); if ($name === '') { $name = trim((string)($u['username'] ?? '')); } } } } catch (\Throwable $e) { } if ($name === '') { $name = '未知用户'; } return [$id, $name]; } /** * 外发采购操作日志(表未建时仅写 runtime 日志,不中断业务) */ protected function addOrderLog(int $scydgyId, string $action, string $content, ?int $purchaseOrderId = null): void { if (!$this->isValidScydgyRowId($scydgyId) || $content === '') { return; } list($adminId, $adminName) = $this->GetUseName(); $cut = function ($s, $max) { if (function_exists('mb_substr')) { return mb_substr($s, 0, $max, 'UTF-8'); } return strlen($s) <= $max ? $s : substr($s, 0, $max); }; try { Db::table('purchase_order_oper_log')->insert([ 'scydgy_id' => $scydgyId, 'purchase_order_id' => $purchaseOrderId, 'admin_id' => $adminId, 'admin_name' => $cut($adminName, 64), 'action' => $cut((string)$action, 64), 'content' => $cut($content, 1000), // 表结构 createtime 为 int Unix 时间戳;日期字符串会导致插入失败或时间为 0,详情「操作记录」为空 'createtime' => time(), ]); } catch (\Throwable $e) { Log::write('procuremen addOrderLog: ' . $e->getMessage(), 'error'); } } /** * 按主键取一条 */ protected function purchaseOrderDetai(int $scydgyId, int $pk): array { if (!$this->isValidScydgyRowId($scydgyId) || $pk <= 0) { return []; } try { $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('id', $pk)->find(); if (is_array($one)) { return $one; } } catch (\Throwable $e) { } try { $one = Db::table('purchase_order_detail')->where('scydgy_id', $scydgyId)->where('ID', $pk)->find(); if (is_array($one)) { return $one; } } catch (\Throwable $e) { } return []; } /** * 主表/明细时间字段统一为可读字符串(用于详情进度) */ protected function formatProcuremenDetailTime($v): string { if ($v === null || $v === '') { return ''; } if (is_numeric($v) && (int)$v > 946684800) { return date('Y-m-d H:i:s', (int)$v); } $s = trim((string)$v); if ($s !== '' && stripos($s, '0000-00-00') !== 0) { return $s; } return ''; } /** * 交货日期仅展示 YYYY-MM-DD(详情表、列表展示用) */ protected function formatDeliveryYmd($v): string { if ($v === null || $v === '') { return ''; } $s = trim((string)$v); if ($s === '' || preg_match('/^0000-00-00/i', $s)) { return ''; } if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) { return $m[1]; } $ts = strtotime($s); return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : $s; } /** * 供应商已接单:金额与交货日期均已填写(与列表汇总逻辑一致) */ protected function detailRowSupplierAccepted(array $r): bool { $am = $r['amount'] ?? null; $okAmt = $am !== null && $am !== '' && !(is_string($am) && trim($am) === ''); $dv = isset($r['delivery']) ? trim((string)$r['delivery']) : ''; $okDel = $dv !== '' && !preg_match('/^0000-00-00/i', $dv); return $okAmt && $okDel; } /** * 详情弹层:进度步骤 + 订单摘要 + 下发明细(已下发 / 已完结均可用) * * @param array $main purchase_order 一行 * @param array $details purchase_order_detail 多行(已预处理 createtime_text) * @return array{steps: array, orderSummary: array, orderSummaryGrid: array, detailRows: array} */ protected function buildProcuremenDetailsViewData(array $main, array $details): array { $issueCnt = count($details); $acceptCnt = 0; $pickedName = ''; $pickedTime = ''; foreach ($details as $dr) { if (!is_array($dr)) { continue; } if ($this->detailRowSupplierAccepted($dr)) { $acceptCnt++; } if ((int)($dr['status'] ?? 0) === 1) { if ($pickedName === '') { $pickedName = trim((string)($dr['company_name'] ?? '')); } if ($pickedTime === '') { $pickedTime = trim((string)($dr['createtime_text'] ?? '')); if ($pickedTime === '') { $pickedTime = $this->formatProcuremenDetailTime($dr['createtime'] ?? null); } } } } $hasMain = $main !== []; $poTime = $this->formatProcuremenDetailTime($main['createtime'] ?? null); $mainStatus = isset($main['status']) ? (int)$main['status'] : 0; $supplierTime = ''; if ($acceptCnt > 0) { foreach ($details as $dr) { if (!is_array($dr) || !$this->detailRowSupplierAccepted($dr)) { continue; } $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null); if ($t !== '' && ($supplierTime === '' || strcmp($t, $supplierTime) < 0)) { $supplierTime = $t; } } } $doneTime = ''; if ($mainStatus === 1) { $doneTime = $this->formatProcuremenDetailTime($main['updatetime'] ?? null); if ($doneTime === '') { foreach ($details as $dr) { if (!is_array($dr)) { continue; } $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null); if ($t !== '' && ($doneTime === '' || strcmp($t, $doneTime) > 0)) { $doneTime = $t; } } } if ($doneTime === '') { $doneTime = $poTime; } } $step1Done = $hasMain; $step2Done = $hasMain; $step3Done = $acceptCnt > 0; $step4Done = false; foreach ($details as $dr) { if (is_array($dr) && (int)($dr['status'] ?? 0) === 1) { $step4Done = true; break; } } $step5Done = $mainStatus === 1; // 主表已完结:中间环节按业务视为已全部完成(不显示灰色「未到达」) if ($mainStatus === 1) { $step3Done = true; $step4Done = true; if ($supplierTime === '') { $fillT = $doneTime !== '' ? $doneTime : $poTime; if ($fillT !== '') { $supplierTime = $fillT; } } if ($pickedTime === '') { $fillT = $doneTime !== '' ? $doneTime : $poTime; if ($fillT !== '') { $pickedTime = $fillT; } } } $steps = [ [ 'title' => '未发', 'subtitle' => '', 'time' => $step1Done ? '—' : '', 'done' => $step1Done, ], [ 'title' => '已发未结束', 'subtitle' => '', 'time' => $step2Done ? $poTime : '', 'done' => $step2Done, ], [ 'title' => '供应商接单', 'subtitle' => '下发 ' . $issueCnt . ' / 接单 ' . $acceptCnt, 'time' => $step3Done ? $supplierTime : '', 'done' => $step3Done, ], [ 'title' => '采购确认', 'subtitle' => $pickedName !== '' ? ('选中供应商:' . $pickedName) : '', 'time' => $step4Done ? ($pickedTime !== '' ? $pickedTime : '') : '', 'done' => $step4Done, ], [ 'title' => '已完结', 'subtitle' => '', 'time' => $step5Done ? $doneTime : '', 'done' => $step5Done, ], ]; $currentIdx = count($steps) - 1; foreach ($steps as $i => $s) { if (!$s['done']) { $currentIdx = $i; break; } } $nSteps = count($steps); foreach ($steps as $idx => &$s) { $s['current'] = ($idx === $currentIdx); if ($idx === 0) { $s['pdf_left_bg'] = ''; } else { $s['pdf_left_bg'] = !empty($steps[$idx - 1]['done']) ? '#1890ff' : '#e0e0e0'; } if ($idx >= $nSteps - 1) { $s['pdf_right_bg'] = ''; } else { $s['pdf_right_bg'] = !empty($s['done']) ? '#1890ff' : '#e0e0e0'; } } unset($s); $labelMap = [ 'CCYDH' => '订单号', 'CYJMC' => '印件名称', 'CGYMC' => '工序名称', 'CDW' => '单位', 'NGZL' => '工作量', 'CDF' => '单价', 'cGzzxMc' => '工作中心', 'MBZ' => '备注', 'This_quantity' => '本次数量', 'ceilingPrice' => '最高限价', ]; $orderSummary = []; foreach ($labelMap as $key => $lab) { if (!array_key_exists($key, $main)) { continue; } $val = $main[$key]; if ($val === null) { $val = ''; } elseif (!is_scalar($val)) { $val = json_encode($val, JSON_UNESCAPED_UNICODE); } else { $val = (string)$val; } $orderSummary[] = ['label' => $lab, 'value' => $val]; } $orderSummaryGrid = []; $n = count($orderSummary); for ($i = 0; $i < $n; $i += 2) { $left = $orderSummary[$i]; $hasRight = ($i + 1) < $n; $orderSummaryGrid[] = [ 'l1' => $left['label'], 'v1' => $left['value'], 'l2' => $hasRight ? $orderSummary[$i + 1]['label'] : '', 'v2' => $hasRight ? $orderSummary[$i + 1]['value'] : '', ]; } return [ 'steps' => $steps, 'orderSummary' => $orderSummary, 'orderSummaryGrid' => $orderSummaryGrid, 'detailRows' => $details, ]; } /** * 加载并 assign 外发采购「详情」弹窗所需变量(与 {@see details()} 页面一致,供模板 / PDF 共用)。 * * @return array{ok: bool, ccydh: string} ok 表示存在主表或至少一条下发明细(否则 PDF 无可写内容) */ protected function prepareProcuremenDetailsView(string $ids): array { $main = []; try { $one = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); $main = is_array($one) ? $one : []; } catch (\Throwable $e) { $main = []; } $details = []; try { $details = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'asc')->select(); } catch (\Throwable $e) { $details = []; } if (!is_array($details)) { $details = []; } foreach ($details as &$r) { if (is_array($r) && isset($r['ID']) && !isset($r['id'])) { $r['id'] = $r['ID']; } if (isset($r['createtime'])) { if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) { $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']); } else { $r['createtime_text'] = (string)$r['createtime']; } } else { $r['createtime_text'] = ''; } $r['delivery_ymd'] = $this->formatDeliveryYmd($r['delivery'] ?? null); } unset($r); $operLogs = []; try { $operLogs = Db::table('purchase_order_oper_log')->where('scydgy_id', $ids)->order('id', 'asc')->select(); } catch (\Throwable $e) { $operLogs = []; } if (!is_array($operLogs)) { $operLogs = []; } foreach ($operLogs as &$lg) { if (!is_array($lg)) { continue; } $ct = isset($lg['createtime']) ? (int)$lg['createtime'] : 0; $lg['createtime_text'] = $ct > 946684800 ? date('Y-m-d H:i:s', $ct) : ''; } unset($lg); $bundle = $this->buildProcuremenDetailsViewData($main, $details); $ccydh = isset($main['CCYDH']) ? trim((string)$main['CCYDH']) : ''; if ($ccydh === '') { foreach ($details as $r) { if (!is_array($r)) { continue; } $ccydh = trim((string)($r['CCYDH'] ?? '')); if ($ccydh !== '') { break; } } } $this->view->assign('ccydh', $ccydh); $this->view->assign('steps', $bundle['steps']); $this->view->assign('orderSummary', $bundle['orderSummary']); $this->view->assign('orderSummaryGrid', $bundle['orderSummaryGrid']); $this->view->assign('detailRows', $bundle['detailRows']); $this->view->assign('operLogs', $operLogs); return [ 'ok' => $main !== [] || $details !== [], 'ccydh' => $ccydh, ]; } /** * 详情弹层:状态进度 + 订单信息 + 下发明细(列表「已下发」「已完结」均可打开) */ public function details() { $ids = $this->request->param('ids', $this->request->param('id', '')); if (is_array($ids)) { $ids = isset($ids[0]) ? $ids[0] : ''; } $ids = trim((string)$ids); if ($ids === '') { $this->error(__('Invalid parameters')); } $this->prepareProcuremenDetailsView($ids); /* 弹层内不套 default 布局,避免出现「控制台 / Control panel」整块标题区 */ $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false; $this->view->engine->layout(false); try { return $this->view->fetch('procuremen/details_dialog_shell'); } finally { if ($restoreLayout) { $this->view->engine->layout($restoreLayout); } } } /** * 解析合并审核工序行(订单号须一致,且均未外发) * * @return array> */ protected function parseReviewMergeRows(string $rowJson, string $mergeRowsJson): array { $primary = json_decode($rowJson, true); if (!is_array($primary)) { $this->error(__('Invalid parameters')); } $merge = json_decode($mergeRowsJson, true); if (!is_array($merge) || $merge === []) { $merge = [$primary]; } $seen = []; $out = []; foreach ($merge as $r) { if (!is_array($r)) { continue; } $id = (int)($r['ID'] ?? $r['id'] ?? 0); if ($id <= 0 || isset($seen[$id])) { continue; } $seen[$id] = true; $out[] = $r; } if ($out === []) { $this->error('无效的工序行'); } $ccydh = null; foreach ($out as $r) { $dh = trim((string)($r['CCYDH'] ?? '')); if ($ccydh === null) { $ccydh = $dh; } elseif ($dh !== $ccydh) { $this->error('合并审核要求所选行的订单号一致'); } } foreach ($out as $r) { $id = (int)($r['ID'] ?? $r['id'] ?? 0); if ($id <= 0) { continue; } try { $po = Db::table('purchase_order')->where('scydgy_id', $id)->find(); } catch (\Throwable $e) { $po = null; } if (!is_array($po)) { continue; } $wf = (int)($po['wflow_status'] ?? 0); $st = $po['status'] ?? null; if ($wf >= 1 || $st === 1 || $st === '1') { $gymc = trim((string)($r['CGYMC'] ?? '')); $this->error('工序「' . ($gymc !== '' ? $gymc : ('#' . $id)) . '」已进入审批流程,不能重复下发'); } try { $detCnt = (int)Db::table('purchase_order_detail')->where('scydgy_id', $id)->count(); } catch (\Throwable $e) { $detCnt = 0; } if ($detCnt > 0 && $wf < 1) { $gymc = trim((string)($r['CGYMC'] ?? '')); $this->error('工序「' . ($gymc !== '' ? $gymc : ('#' . $id)) . '」已存在下发明细,请走采购确认或联系管理员'); } } return $out; } /** * 规范化下发/确认时勾选的供应商 * * @param array $companies * @return array> */ protected function normalizePickCompanies(array $companies): array { $out = []; foreach ($companies as $c) { if (!is_array($c)) { continue; } $name = trim((string)($c['name'] ?? $c['company_name'] ?? '')); if ($name === '') { continue; } $out[] = [ 'name' => $name, 'company_name' => $name, 'username' => trim((string)($c['username'] ?? '')), 'email' => trim((string)($c['email'] ?? '')), 'phone' => trim((string)($c['phone'] ?? '')), 'company_type' => trim((string)($c['company_type'] ?? $c['category'] ?? '')), ]; } return $out; } /** * 确认供应商列表:按订单号 CCYDH 合并为一行(一单一行,工序汇总) * * @param array> $pool * @return array> */ protected function collapseProcuremenPoolByOrder(array $pool): array { if ($pool === []) { return []; } $groups = []; foreach ($pool as $row) { if (!is_array($row)) { continue; } $dh = trim((string)($row['CCYDH'] ?? '')); $key = $dh !== '' ? $dh : ('_id_' . (int)($row['ID'] ?? 0)); if (!isset($groups[$key])) { $groups[$key] = []; } $groups[$key][] = $row; } $out = []; foreach ($groups as $ccydh => $rows) { if ($rows === []) { continue; } usort($rows, function ($a, $b) { return ((int)($a['ID'] ?? 0)) <=> ((int)($b['ID'] ?? 0)); }); $head = $rows[0]; $gymcList = []; foreach ($rows as $r) { $g = trim((string)($r['CGYMC'] ?? '')); if ($g !== '' && !in_array($g, $gymcList, true)) { $gymcList[] = $g; } } $merged = $head; $merged['order_key'] = strpos((string)$ccydh, '_id_') === 0 ? '' : (string)$ccydh; $merged['process_count'] = count($rows); $merged['_order_merge_rows'] = $rows; if (count($rows) > 1) { $merged['CGYMC'] = implode('、', $gymcList); } $mergedPickTime = ''; foreach ($rows as $r) { $t = $this->procuremenRowListSortTime($r, 'pick_time'); if ($t !== '' && ($mergedPickTime === '' || strcmp($t, $mergedPickTime) > 0)) { $mergedPickTime = $t; } } if ($mergedPickTime !== '') { $merged['pick_time'] = $mergedPickTime; } $out[] = $merged; } usort($out, function ($a, $b) { $ta = $this->procuremenRowListSortTime($a, 'pick_time'); $tb = $this->procuremenRowListSortTime($b, 'pick_time'); if ($ta === $tb) { $ida = (int)($a['purchase_order_id'] ?? 0); $idb = (int)($b['purchase_order_id'] ?? 0); if ($ida !== $idb) { return $idb <=> $ida; } return strcmp((string)($b['CCYDH'] ?? ''), (string)($a['CCYDH'] ?? '')); } if ($ta === '') { return 1; } if ($tb === '') { return -1; } return strcmp($tb, $ta); }); return $out; } /** * 待确认供应商订单下全部工序(同一 CCYDH,wflow_status=1) * * @return array{ccydh:string, pos:array, merge_rows:array} */ protected function loadAuditOrderBundleByScydgyId(string $scydgyId): array { $scydgyId = trim($scydgyId); $empty = ['ccydh' => '', 'pos' => [], 'merge_rows' => []]; if ($scydgyId === '') { return $empty; } try { $anchor = Db::table('purchase_order')->where('scydgy_id', $scydgyId)->find(); } catch (\Throwable $e) { $anchor = null; } if (!is_array($anchor) || (int)($anchor['wflow_status'] ?? 0) !== 1) { return $empty; } $ccydh = trim((string)($anchor['CCYDH'] ?? '')); if ($ccydh === '') { return ['ccydh' => '', 'pos' => [$anchor], 'merge_rows' => $this->scydgyRowsForPurchaseOrders([$anchor])]; } try { $pos = Db::table('purchase_order')->where('CCYDH', $ccydh)->where('wflow_status', 1)->order('scydgy_id', 'asc')->select(); } catch (\Throwable $e) { $pos = [$anchor]; } if (!is_array($pos) || $pos === []) { $pos = [$anchor]; } return [ 'ccydh' => $ccydh, 'pos' => $pos, 'merge_rows' => $this->scydgyRowsForPurchaseOrders($pos), ]; } /** * @param array> $purchaseOrders * @return array> */ protected function scydgyRowsForPurchaseOrders(array $purchaseOrders): array { $pool = $this->procuremenPoolFromPurchaseOrderDbRows($purchaseOrders); $byId = []; foreach ($pool as $r) { if (!is_array($r)) { continue; } $sid = (int)($r['ID'] ?? $r['id'] ?? 0); if ($this->isValidScydgyRowId($sid)) { $byId[$sid] = $r; } } $out = []; foreach ($purchaseOrders as $po) { if (!is_array($po)) { continue; } $sid = (int)($po['scydgy_id'] ?? 0); if ($this->isValidScydgyRowId($sid) && isset($byId[$sid])) { $out[] = $byId[$sid]; } } return $out; } /** * 审核弹窗工序表展示行(与下发弹窗列一致,同订单合并订单号/印件名称单元格) * * @param array> $mergeRows * @return array> */ protected function buildAuditProcessDisplayRows(array $mergeRows): array { $list = array_values($mergeRows); $n = count($list); $orderKey0 = $n > 0 ? trim((string)($list[0]['CCYDH'] ?? '')) : ''; $nameKey0 = $n > 0 ? trim((string)($list[0]['CYJMC'] ?? '')) : ''; $mergeSame = $n > 1 && $orderKey0 !== ''; if ($mergeSame) { foreach ($list as $rr) { if (!is_array($rr)) { $mergeSame = false; break; } if (trim((string)($rr['CCYDH'] ?? '')) !== $orderKey0 || trim((string)($rr['CYJMC'] ?? '')) !== $nameKey0) { $mergeSame = false; break; } } } $out = []; foreach ($list as $idx => $r) { if (!is_array($r)) { continue; } $qty = $r['This_quantity'] ?? $r['this_quantity'] ?? ''; if (is_scalar($qty) && trim((string)$qty) === '') { $gzl = $r['NGZL'] ?? ''; if (is_scalar($gzl) && trim((string)$gzl) !== '') { $qty = $gzl; } } $price = $r['ceilingPrice'] ?? $r['ceiling_price'] ?? ''; $out[] = [ 'seq' => $idx + 1, 'CCYDH' => trim((string)($r['CCYDH'] ?? '')), 'CYJMC' => trim((string)($r['CYJMC'] ?? '')), 'CGYMC' => trim((string)($r['CGYMC'] ?? '')), 'CDW' => trim((string)($r['CDW'] ?? '')), 'NGZL' => $r['NGZL'] ?? '', 'This_quantity' => is_scalar($qty) ? trim((string)$qty) : '', 'ceilingPrice' => is_scalar($price) ? trim((string)$price) : '', 'CDF' => trim((string)($r['CDF'] ?? '')), 'show_order_cells' => !$mergeSame || $idx === 0, 'order_rowspan' => $mergeSame ? $n : 1, ]; } return $out; } /** * 短信用工序明细(仅工序行,订单号/印件名称请在模版里用 {ccydh} {cyjmc} 自行排版) */ protected function buildProcessLinesPlain(array $mergeRows): string { $lines = []; $i = 1; foreach ($mergeRows as $r) { if (!is_array($r)) { continue; } $gymc = trim((string)($r['CGYMC'] ?? '')); $dw = trim((string)($r['CDW'] ?? '')); $gzl = trim((string)($r['NGZL'] ?? '')); $qty = trim((string)($r['This_quantity'] ?? $r['this_quantity'] ?? '')); $parts = [$i . '.工序名称:' . ($gymc !== '' ? $gymc : '—')]; if ($dw !== '') { $parts[] = '单位:' . $dw; } if ($gzl !== '') { $parts[] = '工作量:' . $gzl; } if ($qty !== '') { $parts[] = '本次数量:' . $qty; } $lines[] = implode(' ', $parts); $i++; } return implode("\n", $lines); } /** * 各工序手机端链接(纯文本,供短信模版 {platform_links}) * * @param array $detailLinks */ protected function buildPlatformLinksPlain(array $detailLinks): string { $lines = []; $i = 1; foreach ($detailLinks as $lk) { if (!is_array($lk)) { continue; } $url = trim((string)($lk['url'] ?? '')); if ($url === '') { continue; } $label = trim((string)($lk['cgymc'] ?? '')); if ($label === '') { $label = '工序' . $i; } $lines[] = $label . ':' . $url; $i++; } return implode("\n", $lines); } /** * 各工序手机端链接(HTML,供邮件模版 {platform_links_html}) * * @param array $detailLinks */ protected function buildPlatformLinksHtml(array $detailLinks): string { $parts = []; $i = 1; foreach ($detailLinks as $lk) { if (!is_array($lk)) { continue; } $url = trim((string)($lk['url'] ?? '')); if ($url === '') { continue; } $label = trim((string)($lk['cgymc'] ?? '')); if ($label === '') { $label = '工序' . $i; } $urlEsc = htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); $parts[] = htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ' — 点击查看'; $i++; } return implode("
\n", $parts); } /** * 发件配置:host/port 等来自 config.php;addr、pass 必须来自 purchase_email 表 * * @return array */ protected function loadMailerConfig(): array { try { $cfg = \app\admin\model\Purchaseemail::getActiveMailerConfig(); } catch (\Throwable $e) { $cfg = []; } return is_array($cfg) ? $cfg : []; } protected function loadNotifyTemplateRow(string $scene): ?array { try { $row = Db::table('purchase_sms_template') ->where('scene', $scene) ->where(function ($q) { $q->where('status', 1)->whereOr('status', '1')->whereOr('status', 'normal'); }) ->order('id', 'asc') ->find(); } catch (\Throwable $e) { return null; } if (!is_array($row) || $row === []) { return null; } return [ 'title' => trim((string)($row['title'] ?? '')), 'content' => trim((string)($row['content'] ?? '')), ]; } /** * 邮件用工序明细 HTML(仅工序块,其它字段请在模版里用 {ccydh} 等变量) */ protected function buildProcessLinesHtml(array $mergeRows): string { if (count($mergeRows) <= 1) { $r = $mergeRows[0] ?? []; if (!is_array($r)) { return ''; } $gymc = trim((string)($r['CGYMC'] ?? '')); $dw = trim((string)($r['CDW'] ?? '')); $gzl = trim((string)($r['NGZL'] ?? '')); $qty = trim((string)($r['This_quantity'] ?? $r['this_quantity'] ?? '')); $html = '工序名称:' . htmlspecialchars($gymc !== '' ? $gymc : '—', ENT_QUOTES, 'UTF-8') . '
'; if ($dw !== '') { $html .= '单位:' . htmlspecialchars($dw, ENT_QUOTES, 'UTF-8') . '
'; } if ($gzl !== '') { $html .= '工作量:' . htmlspecialchars($gzl, ENT_QUOTES, 'UTF-8') . '
'; } if ($qty !== '') { $html .= '本次数量:' . htmlspecialchars($qty, ENT_QUOTES, 'UTF-8') . '
'; } return $html; } $rows = ''; $i = 1; foreach ($mergeRows as $r) { if (!is_array($r)) { continue; } $gymc = htmlspecialchars(trim((string)($r['CGYMC'] ?? '')), ENT_QUOTES, 'UTF-8'); $dw = htmlspecialchars(trim((string)($r['CDW'] ?? '')), ENT_QUOTES, 'UTF-8'); $gzl = htmlspecialchars(trim((string)($r['NGZL'] ?? '')), ENT_QUOTES, 'UTF-8'); $qty = htmlspecialchars(trim((string)($r['This_quantity'] ?? $r['this_quantity'] ?? '')), ENT_QUOTES, 'UTF-8'); $rows .= '' . $i . '' . '' . ($gymc !== '' ? $gymc : '—') . '' . '' . ($dw !== '' ? $dw : '—') . '' . '' . ($gzl !== '' ? $gzl : '—') . '' . '' . ($qty !== '' ? $qty : '—') . ''; $i++; } if ($rows === '') { return ''; } return '' . '' . '' . '' . '' . '' . '' . '' . $rows . '
序号工序名称单位工作量本次数量
'; } /** * 写入/更新 purchase_order(单道工序行) */ protected function upsertPurchaseOrderFromRow(array $row, string $sysRqDb, ?int $wflowStatus = null): void { $ids = $this->extractScydgyRowId($row); if (!$this->isValidScydgyRowId($ids)) { throw new \Exception('无效的行主键 ID'); } $exists = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); $data = [ 'scydgy_id' => $ids, 'CCYDH' => $row['CCYDH'] ?? null, 'CYJMC' => $row['CYJMC'] ?? null, 'CDXMC' => $row['CDXMC'] ?? null, 'CGYBH' => $row['CGYBH'] ?? null, 'CGYMC' => $row['CGYMC'] ?? null, 'CDW' => $row['CDW'] ?? null, 'NGZL' => $row['NGZL'] ?? null, 'CDF' => $row['CDF'] ?? null, 'cGzzxMc' => $row['cGzzxMc'] ?? null, 'MBZ' => $row['MBZ'] ?? null, 'bwjg' => $row['bwjg'] ?? null, 'iStatus' => $row['iStatus'] ?? null, 'dStamp' => $row['dStamp'] ?? null, 'dputrecord' => $row['dputrecord'] ?? null, 'cywyxm' => $row['cywyxm'] ?? null, 'This_quantity' => $row['This_quantity'] ?? $row['this_quantity'] ?? null, 'ceilingPrice' => $row['ceilingPrice'] ?? $row['ceiling_price'] ?? null, 'status' => 0, 'sys_rq' => $sysRqDb, ]; if ($wflowStatus !== null) { $data['wflow_status'] = $wflowStatus; } if ($exists) { $upd = $data; unset($upd['scydgy_id']); Db::table('purchase_order')->where('scydgy_id', $ids)->update($upd); } else { $data['createtime'] = date('Y-m-d H:i:s'); Db::table('purchase_order')->insert($data); } } /** * 外发下发 — 写入下发明细并拼好短信/邮件内容(不发) * * @return array */ protected function issueBuildSupplierNotifyBundle(array $c, array $mergeRows, array $ctx): array { $toDb = function ($value) { if ($value === null) { return null; } if (is_scalar($value)) { return $value; } return json_encode($value, JSON_UNESCAPED_UNICODE); }; $toEmail = isset($c['email']) ? trim((string)$c['email']) : ''; $companyName = isset($c['name']) ? trim((string)$c['name']) : '外协单位'; $phone = isset($c['phone']) ? trim((string)$c['phone']) : ''; if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) { throw new \Exception($companyName . ' 未填写有效邮箱,无法发送邮件,未写入数据'); } if ($phone === '') { throw new \Exception(($companyName !== '' ? $companyName : '外协单位') . ' 未填写手机号,无法发送短信,未写入数据'); } $detailLinks = []; foreach ($mergeRows as $row) { if (!is_array($row)) { continue; } $sid = $this->extractScydgyRowId($row); $one = [ 'scydgy_id' => $toDb($sid), 'CCYDH' => $toDb($row['CCYDH'] ?? null), 'CYJMC' => $toDb($row['CYJMC'] ?? null), 'company_name' => isset($c['name']) ? (string)$c['name'] : null, 'email' => isset($c['email']) ? (string)$c['email'] : null, 'phone' => isset($c['phone']) ? (string)$c['phone'] : null, 'createtime' => date('Y-m-d H:i:s'), 'status' => 0, 'status_name' => '未提交', ]; Db::table('purchase_order_detail')->insert($one); $detailId = (int)Db::getLastInsID(); $mprocUrl = $this->buildMprocMobileOrderUrl($detailId); $detailLinks[] = [ 'detail_id' => $detailId, 'cgymc' => trim((string)($row['CGYMC'] ?? '')), 'url' => $mprocUrl, ]; } $platformUrl = isset($detailLinks[0]['url']) ? trim((string)$detailLinks[0]['url']) : ''; $platformLinksPlain = $this->buildPlatformLinksPlain($detailLinks); $platformLinksHtml = $this->buildPlatformLinksHtml($detailLinks); $ccydh = (string)($ctx['ccydh'] ?? ''); $cyjmc = (string)($ctx['cyjmc'] ?? ''); $sysRqNotify = (string)($ctx['deadline'] ?? ''); $processPlain = (string)($ctx['process_plain'] ?? ''); $isMerge = !empty($ctx['is_merge']); $contactName = trim((string)($c['username'] ?? '')); if ($contactName === '') { $contactName = $this->resolveCustomerContactName($phone, $companyName); } $notifyVars = [ 'company_name' => $companyName, 'contact_name' => $contactName, 'phone' => $phone, 'email' => $toEmail, 'ccydh' => $ccydh, 'cyjmc' => $cyjmc, 'cgymc' => $isMerge ? '' : (string)($ctx['cgymc_single'] ?? ''), 'category' => trim((string)($c['category'] ?? $c['company_type'] ?? '')), 'deadline' => $sysRqNotify, 'process_lines' => $processPlain, 'process_lines_html' => (string)($ctx['process_html'] ?? ''), 'platform_url' => $platformUrl, 'platform_links' => $platformLinksPlain, 'platform_links_html' => $platformLinksHtml, ]; $smsContent = $this->renderNotifyTemplate('review_sms', $notifyVars); $mailPlain = $this->renderNotifyTemplate('review_email', $notifyVars); $mailBody = $this->plainTextToHtmlEmailBody($mailPlain); return [ 'company_name' => $companyName, 'phone' => $phone, 'email' => $toEmail, 'sms_content' => $smsContent, 'mail_plain' => $mailPlain, 'mail_body' => $mailBody, 'notify_vars' => $notifyVars, 'detail_links' => $detailLinks, ]; } /** * 外发下发 — 发送短信(一家供应商) * * @param array $bundle issueBuildSupplierNotifyBundle 返回值 */ protected function issueSendSupplierSms(array $bundle): void { $companyName = (string)($bundle['company_name'] ?? ''); $phone = (string)($bundle['phone'] ?? ''); $smsContent = (string)($bundle['sms_content'] ?? ''); $this->logIssueNotifyDebug('短信', [ 'company' => $companyName, 'phone' => $phone, 'sms_content' => $smsContent, ]); if ($this->isProcuremenNotifyDryRun()) { $this->recordNotifyDryRunPreview([ 'scene' => 'issue_sms', 'company_name' => $companyName, 'phone' => $phone, 'sms_content' => $smsContent, ]); return; } $this->smsbao($phone, $smsContent); } /** * 外发下发 — 发送邮件(一家供应商) * * @param array $bundle * @param array $mailConfig */ protected function issueSendSupplierEmail(array $bundle, array $mailConfig, string $mailSubject): void { $companyName = (string)($bundle['company_name'] ?? ''); $toEmail = (string)($bundle['email'] ?? ''); $mailPlain = (string)($bundle['mail_plain'] ?? ''); $mailBody = (string)($bundle['mail_body'] ?? ''); $this->logIssueNotifyDebug('邮件', [ 'company' => $companyName, 'email' => $toEmail, 'mail_subject' => $mailSubject, 'mail_body' => $mailPlain, ]); if ($this->isProcuremenNotifyDryRun()) { $this->recordNotifyDryRunPreview([ 'scene' => 'issue_email', 'company_name' => $companyName, 'email' => $toEmail, 'mail_subject' => $mailSubject, 'mail_body' => $mailPlain, 'sms_content' => (string)($bundle['sms_content'] ?? ''), 'notify_vars' => $bundle['notify_vars'] ?? [], 'detail_links' => $bundle['detail_links'] ?? [], ]); return; } $mail = new PHPMailer(true); $mail->isSMTP(); $mail->Host = $mailConfig['host']; $mail->SMTPAuth = true; $mail->Username = $mailConfig['addr']; $mail->Password = $mailConfig['pass']; $mail->SMTPSecure = $mailConfig['security']; $mail->Port = $mailConfig['port']; $mail->CharSet = $mailConfig['charset']; $mail->setFrom($mailConfig['addr'], $mailConfig['name']); $mail->addAddress($toEmail, $companyName); $mail->isHTML(true); $mail->Subject = $mailSubject; $mail->Body = $mailBody; $mail->send(); } /** * 外发下发通知上下文(订单号、工序明细等,供短信/邮件共用) * * @param array> $mergeRows * @return array */ protected function buildIssueNotifyContext(array $mergeRows, string $sysRqNotify): array { $head = $mergeRows[0] ?? []; $isMerge = count($mergeRows) > 1; return [ 'ccydh' => isset($head['CCYDH']) ? (string)$head['CCYDH'] : '', 'cyjmc' => isset($head['CYJMC']) ? (string)$head['CYJMC'] : '', 'cgymc_single' => trim((string)($head['CGYMC'] ?? '')), 'is_merge' => $isMerge, 'process_plain' => $this->buildProcessLinesPlain($mergeRows), 'process_html' => $this->buildProcessLinesHtml($mergeRows), 'deadline' => $sysRqNotify, ]; } /** * 调试:app_debug 或 notify_dry_run 时把短信/邮件内容写入 runtime/log * * @param array $data */ protected function logIssueNotifyDebug(string $type, array $data): void { if (!Config::get('app_debug') && !$this->isProcuremenNotifyDryRun()) { return; } Log::write('[外发下发-' . $type . '] ' . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'notice'); } /** * 外发下发弹窗(选多家供应商并通知) */ public function pickReview() { if ($this->request->isPost()) { $this->error('请使用弹窗内「确认下发」提交'); } $this->view->assign('pickMode', 1); return $this->view->fetch('procuremen/review'); } /** * 外发下发 — 手工新增工序(弹窗) */ public function pickAdd() { if ($this->request->isPost()) { $params = $this->request->post('row/a'); if (!is_array($params)) { $this->error(__('Parameter %s can not be empty', '')); } $ccydh = trim((string)($params['CCYDH'] ?? '')); $cyjmc = trim((string)($params['CYJMC'] ?? '')); $cgymc = trim((string)($params['CGYMC'] ?? '')); if ($ccydh === '') { $this->error('请填写订单号'); } if ($cyjmc === '') { $this->error('请填写印件名称'); } if ($cgymc === '') { $this->error('请填写工序名称'); } $now = date('Y-m-d H:i:s'); $sid = $this->allocateManualScydgyId(); $data = [ 'scydgy_id' => $sid, 'CCYDH' => $ccydh, 'CYJMC' => $cyjmc, 'CGYMC' => $cgymc, 'CDW' => trim((string)($params['CDW'] ?? '')), 'NGZL' => trim((string)($params['NGZL'] ?? '')), 'CDF' => trim((string)($params['CDF'] ?? '')), 'cGzzxMc' => trim((string)($params['cGzzxMc'] ?? '')), 'MBZ' => trim((string)($params['MBZ'] ?? '')), 'cywyxm' => trim((string)($params['cywyxm'] ?? '')), 'This_quantity' => trim((string)($params['This_quantity'] ?? '')), 'ceilingPrice' => trim((string)($params['ceilingPrice'] ?? '')), 'wflow_status' => 0, 'createtime' => $now, 'dStamp' => $now, 'dputrecord' => $now, ]; try { Db::table('purchase_order')->insert($data); } catch (\Throwable $e) { $this->error($e->getMessage()); } $poId = (int)Db::getLastInsID(); $this->addOrderLog($sid, 'manual_add', '手工新增外发工序', $poId > 0 ? $poId : null); $this->success('新增成功', '', ['scydgy_id' => $sid]); } return $this->view->fetch('procuremen/pick_add'); } /** * 外发下发提交 — 弹窗「确认下发」→ POST procuremen/picksubmit */ public function pickSubmit() { if (!$this->request->isPost()) { $this->error(__('Invalid parameters')); } // ---------- 接口订单数据 ---------- $rowJson = $this->request->post('row_json', ''); $mergeRowsJson = $this->request->post('merge_rows_json', ''); $companiesJson = $this->request->post('companies_json', '[]'); $mergeRows = $this->parseReviewMergeRows($rowJson, $mergeRowsJson); $companies = json_decode($companiesJson, true); if (!is_array($companies)) { $this->error('供应商数据无效'); } $issueCompanies = $this->normalizePickCompanies($companies); if ($issueCompanies === []) { $this->error('请至少选择一家合作供应商'); } $issueNameLabels = []; foreach ($issueCompanies as $pc) { $issueNameLabels[] = $pc['name']; } $issueNamesSummary = implode('、', $issueNameLabels); $sysRq = trim((string)$this->request->post('sys_rq', '')); if ($sysRq === '') { $this->error('请选择截止时间'); } $sysRqTs = strtotime($sysRq); if ($sysRqTs === false || $sysRqTs <= 0) { $this->error('截止时间格式无效'); } $sysRqDb = date('Y-m-d H:i:s', $sysRqTs); $sysRqNotify = date('Y-m-d H:i', $sysRqTs); $ctx = $this->buildIssueNotifyContext($mergeRows, $sysRqNotify); $mailTplRow = $this->loadNotifyTemplateRow('review_email'); if ($mailTplRow === null || trim((string)($mailTplRow['title'] ?? '')) === '') { $this->error($this->notifyTemplateMissingMessage('review_email', true)); } $mailSubject = trim((string)$mailTplRow['title']); $mailConfig = $this->loadMailerConfig(); if (!$this->isProcuremenNotifyDryRun()) { if (empty($mailConfig['host'])) { $this->error('邮件 SMTP 未配置,请检查 config.php 中 Mailer.host'); } if (empty($mailConfig['addr']) || empty($mailConfig['pass'])) { $this->error('发件邮箱或授权码未配置,请至后台「邮箱配置」填写'); } } $isMerge = count($mergeRows) > 1; $logMsg = ($isMerge ? '合并下发(' . count($mergeRows) . ' 道工序)' : '外发下发') . ',通知供应商(' . count($issueCompanies) . ' 家):' . $issueNamesSummary; $notifyBundles = []; Db::startTrans(); try { $this->notifyDryRunPreview = []; // ---------- 存入数据库 ---------- // purchase_order 主表(工序行 + 截止时间 + wflow_status=1) foreach ($mergeRows as $row) { if (!is_array($row)) { continue; } $this->upsertPurchaseOrderFromRow($row, $sysRqDb, 1); } // purchase_order_detail 下发明细 + 拼好短信/邮件文案(含手机端链接) foreach ($issueCompanies as $c) { if (!is_array($c)) { continue; } $notifyBundles[] = $this->issueBuildSupplierNotifyBundle($c, $mergeRows, $ctx); } $issueTime = date('Y-m-d H:i:s'); foreach ($mergeRows as $row) { if (!is_array($row)) { continue; } $sid = $this->extractScydgyRowId($row); if (!$this->isValidScydgyRowId($sid)) { continue; } $upd = [ 'wflow_status' => 1, 'status' => 0, 'pick_time' => $issueTime, 'pick_company_name' => '', 'sys_rq' => $sysRqDb, ]; Db::table('purchase_order')->where('scydgy_id', $sid)->update($upd); } // // ---------- 短信通知 ---------- // foreach ($notifyBundles as $bundle) { // $this->issueSendSupplierSms($bundle); // } // // ---------- 邮箱通知 ---------- // foreach ($notifyBundles as $bundle) { // $this->issueSendSupplierEmail($bundle, $mailConfig, $mailSubject); // } Db::commit(); } catch (\Throwable $e) { Db::rollback(); $this->error($e->getMessage()); } // ---------- 操作日志记录 ---------- foreach ($mergeRows as $row) { if (!is_array($row)) { continue; } $ids = $this->extractScydgyRowId($row); if (!$this->isValidScydgyRowId($ids)) { continue; } $poIdLog = null; try { $rpo = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); if (is_array($rpo)) { $tid = (int)($rpo['id'] ?? $rpo['ID'] ?? 0); $poIdLog = $tid > 0 ? $tid : null; } } catch (\Throwable $e) { } $this->addOrderLog($ids, 'issue_submit', $logMsg, $poIdLog); } if ($this->isProcuremenNotifyDryRun()) { $this->success('【演练模式】已写入数据,未发送短信/邮件', '', [ 'notify_dry_run' => true, 'notify_preview' => $this->notifyDryRunPreview, ]); } $this->success('已下发短信与邮件,请供应商报价后至「确认供应商」选定一家'); } /** * 确认供应商弹窗:订单信息 + 供应商报价,选定一家 */ public function auditIssue() { $ids = $this->request->param('ids', $this->request->param('id', '')); if (is_array($ids)) { $ids = isset($ids[0]) ? $ids[0] : ''; } $ids = trim((string)$ids); if ($ids === '') { $this->error(__('Invalid parameters')); } if ($this->request->isPost()) { $this->error('请使用弹窗内「确认审核」提交'); } $bundle = $this->loadAuditOrderBundleByScydgyId($ids); $pos = $bundle['pos']; if ($pos === []) { $this->error('该订单不在待确认供应商状态'); } $po = $pos[0]; $supplierGroups = $this->loadAuditSupplierQuoteGroups($bundle); if ($supplierGroups === []) { $this->error('暂无供应商报价明细,请确认已下发且供应商已填报'); } $this->view->assign('po', $po); $this->view->assign('scydgyId', $ids); $this->view->assign('orderCcydh', $bundle['ccydh']); $mergeRows = $bundle['merge_rows']; $this->view->assign('processRows', $mergeRows); $this->view->assign('processDisplayRows', $this->buildAuditProcessDisplayRows($mergeRows)); $this->view->assign('processCount', count($mergeRows)); $this->view->assign('supplierGroups', $supplierGroups); $this->view->assign('supplierGroupsJson', json_encode($supplierGroups, JSON_UNESCAPED_UNICODE)); return $this->view->fetch('procuremen/audit_issue'); } /** * 确认供应商提交(选定一家,进入采购确认,不发通知) */ public function auditSubmit() { if (!$this->request->isPost()) { $this->error(__('Invalid parameters')); } $scydgyId = trim((string)$this->request->post('scydgy_id', '')); if ($scydgyId === '') { $this->error('参数无效'); } $bundle = $this->loadAuditOrderBundleByScydgyId($scydgyId); $pos = $bundle['pos']; $mergeRows = $bundle['merge_rows']; if ($pos === [] || $mergeRows === []) { $this->error('该订单不在待确认供应商状态'); } $selRaw = $this->request->post('company_json', ''); $sel = json_decode(is_string($selRaw) ? $selRaw : '', true); if (!is_array($sel)) { $this->error('请选择一家供应商'); } $selNorm = $this->normalizePickCompanies([$sel]); if ($selNorm === []) { $this->error('请选择有效的供应商'); } $chosen = $selNorm[0]; $chosenName = $chosen['name']; $supplierGroups = $this->loadAuditSupplierQuoteGroups($bundle); $matched = null; foreach ($supplierGroups as $g) { if (!is_array($g)) { continue; } if (($g['name'] ?? '') === $chosenName) { $matched = $g; break; } } if ($matched === null) { $this->error('所选供应商不在本单报价列表中'); } $contactName = trim((string)($chosen['username'] ?? '')); if ($contactName === '') { $contactName = $this->resolveCustomerContactName( trim((string)($chosen['phone'] ?? '')), $chosenName ); } $ccydhLog = trim((string)($bundle['ccydh'] ?? '')); $procCnt = count($mergeRows); $logMsg = '确认供应商(订单' . ($ccydhLog !== '' ? $ccydhLog : '') . ($procCnt > 1 ? ',' . $procCnt . ' 道工序' : '') . '),选定供应商:' . $chosenName; Db::startTrans(); try { foreach ($pos as $poRow) { if (!is_array($poRow)) { continue; } $sid = (int)($poRow['scydgy_id'] ?? 0); if (!$this->isValidScydgyRowId($sid)) { continue; } Db::table('purchase_order')->where('scydgy_id', $sid)->update([ 'wflow_status' => 2, 'status' => 0, 'pick_company_name' => $chosenName, ]); } Db::commit(); } catch (\Throwable $e) { Db::rollback(); $this->error($e->getMessage()); } foreach ($pos as $poRow) { if (!is_array($poRow)) { continue; } $sid = (int)($poRow['scydgy_id'] ?? 0); if (!$this->isValidScydgyRowId($sid)) { continue; } $poIdLog = (int)($poRow['id'] ?? $poRow['ID'] ?? 0); $this->addOrderLog($sid, 'audit_confirm', $logMsg, $poIdLog > 0 ? $poIdLog : null); } $this->success('已确认供应商,请至第三步「采购确认」定标并发送通过/未通过短信'); } /** * 审核弹窗(旧入口,POST 已关闭) */ public function review() { if ($this->request->isPost()) { $this->error('请使用第一步「外发下发」通知供应商,再在「确认供应商」中选定一家'); } $this->view->assign('pickMode', 0); return $this->view->fetch('procuremen/review'); } /** * 审核弹窗获取公司列表 */ public function reviewCompanies() { if (!$this->request->isAjax()) { $this->error(__('Invalid parameters')); } $list = []; try { // 仅「正常」外协(customer.status=1);0=禁止登录,与手机端 mprocCustomerUserActive 一致 $rows = Db::table('customer') ->where(function ($q) { $q->where('status', 1) ->whereOr('status', '1') ->whereOr('status', '') ->whereNull('status'); }) ->order('id', 'desc') ->select(); } catch (\Throwable $e) { $this->success('', '', []); return; } if (!is_array($rows)) { $this->success('', '', []); return; } $detailColCandidates = [ 'detail', 'mingxi', 'remark', 'memo', 'notes', 'description', 'company_detail', 'company_desc', 'address', ]; foreach ($rows as $row) { if (!is_array($row)) { continue; } $norm = []; foreach ($row as $k => $v) { $norm[is_string($k) ? strtolower($k) : $k] = $v; } $row = $norm; $st = $row['status'] ?? ''; if ($st !== '' && $st !== null && $st !== 1 && $st !== '1') { continue; } $id = isset($row['id']) ? (string)$row['id'] : ''; $companyName = ''; foreach (['company_name', 'name'] as $nk) { if (!empty($row[$nk])) { $companyName = trim((string)$row[$nk]); break; } } $username = ''; foreach (['username', 'contact', 'linkman', 'contacts'] as $uk) { if (isset($row[$uk]) && trim((string)$row[$uk]) !== '') { $username = trim((string)$row[$uk]); break; } } $email = isset($row['email']) ? trim((string)$row['email']) : ''; $phone = isset($row['phone']) ? trim((string)$row['phone']) : ''; if ($phone === '' && isset($row['account'])) { $phone = trim((string)$row['account']); } if ($phone === '' && isset($row['mobile'])) { $phone = trim((string)$row['mobile']); } /* 邮箱、手机号均为空则无法发短信/邮件,不在下发弹窗展示 */ if ($email === '' && $phone === '') { continue; } $category = ''; foreach (['company_type', 'category', 'type_name'] as $ck) { if (isset($row[$ck]) && trim((string)$row[$ck]) !== '') { $category = trim((string)$row[$ck]); break; } } $detail = ''; foreach ($detailColCandidates as $dk) { if (!isset($row[$dk])) { continue; } $dv = trim((string)$row[$dk]); if ($dv !== '') { $detail = $dv; break; } } $list[] = [ 'id' => $id, 'name' => $companyName, 'company_name' => $companyName, 'username' => $username, 'email' => $email, 'phone' => $phone, 'category' => $category, 'company_type' => $category, 'detail' => $detail, ]; } $this->success('', '', $list); } /** * 采购确认-下发明细弹窗: * 查 purchase_order_detail;ids 为工序行 scydgy.ID,对应明细 scydgy_id */ public function outward_detail() { $ids = $this->request->param('ids', $this->request->param('id', '')); if (is_array($ids)) { $ids = isset($ids[0]) ? $ids[0] : ''; } $ids = trim((string)$ids); if ($ids === '') { $this->error(__('Invalid parameters')); } $wffTab = trim((string)$this->request->param('wff_tab', 'all')); if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) { $wffTab = 'all'; } $headTitle = $wffTab === 'pending' ? '采购确认(明细)' : '下发明细'; $rows = []; try { $rows = Db::table('purchase_order_detail')->where('scydgy_id', $ids)->order('id', 'desc')->select(); } catch (\Throwable $e) { $rows = []; } foreach ($rows as &$r) { if (is_array($r) && isset($r['ID']) && !isset($r['id'])) { $r['id'] = $r['ID']; } if (isset($r['createtime'])) { if (is_numeric($r['createtime']) && (int)$r['createtime'] > 946684800) { $r['createtime_text'] = date('Y-m-d H:i:s', (int)$r['createtime']); } else { $r['createtime_text'] = (string)$r['createtime']; } } else { $r['createtime_text'] = ''; } } unset($r); $purchaseOrderId = 0; try { $po = Db::table('purchase_order')->where('scydgy_id', $ids)->find(); if (is_array($po)) { $purchaseOrderId = (int)($po['id'] ?? $po['ID'] ?? 0); } } catch (\Throwable $e) { $purchaseOrderId = 0; } $this->view->assign('rows', $rows ?: []); $this->view->assign('rowCount', count($rows ?: [])); $this->view->assign('scydgyId', $ids); $this->view->assign('purchaseOrderId', $purchaseOrderId); $this->view->assign('headTitle', $headTitle); $this->view->assign('showPurchaseConfirm', ($wffTab === 'pending' || $wffTab === 'confirm') ? 1 : 0); $this->view->assign('detailColspan', $wffTab === 'pending' ? 10 : 9); /* 采购确认(pending)需在 iframe 内加载 require-backend,以便勾选与提交;其它 tab 仅只读表格,用轻量壳避免整站 JS/CSS 二次初始化导致弹窗极慢 */ if ($wffTab === 'pending') { return $this->view->fetch(); } $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false; $this->view->engine->layout(false); try { $bodyHtml = $this->view->fetch('procuremen/outward_detail'); $this->view->assign('dialogBody', $bodyHtml); return $this->view->fetch('procuremen/outward_detail_lite_shell'); } finally { if ($restoreLayout) { $this->view->engine->layout($restoreLayout); } } } /** * 按月份导出:已完结且采购确认已选定供应商(明细 status=1),与列表「已完结」∩「已选中」一致。 * 表头固定 8 列(与历史「外发明细」模板一致):序号、传票号、传票名称、外加工单位、订法、客户名称、工序、加工金额。 */ public function export_month_outward() { $this->request->filter(['strip_tags', 'trim']); $ym = trim((string)$this->request->get('ym', date('Y-m'))); if (!preg_match('/^\d{4}-\d{2}$/', $ym)) { $ym = date('Y-m'); } $monthStart = $ym . '-01 00:00:00'; $monthEnd = date('Y-m-t 23:59:59', strtotime($monthStart)); $dbRows = []; try { $dbRows = Db::table('purchase_order')->where('status', 1)->select(); } catch (\Throwable $e) { $dbRows = []; } if (!is_array($dbRows)) { $dbRows = []; } $pickedSidSet = $this->loadScydgyIdsWithPickedSupplierDetail(); $dbRows = array_values(array_filter($dbRows, function ($dbRow) use ($pickedSidSet) { if (!is_array($dbRow)) { return false; } $sid = (int)($dbRow['scydgy_id'] ?? 0); if ($sid <= 0 && isset($dbRow['SCYDGY_ID'])) { $sid = (int)$dbRow['SCYDGY_ID']; } return $sid > 0 && isset($pickedSidSet[$sid]); })); $pool = $this->procuremenPoolFromPurchaseOrderDbRows($dbRows); $filtered = $this->filterProcuremenIndexPool($pool, $monthStart, $monthEnd, true, '', [], []); $mainBySid = []; foreach ($filtered as $rw) { if (!is_array($rw)) { continue; } $rid = (int)($rw['ID'] ?? 0); if ($rid > 0) { $mainBySid[$rid] = $rw; } } if ($mainBySid === []) { $detailRows = []; } else { try { $detailRows = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', array_keys($mainBySid)) ->where('status', 1) ->order('CCYDH', 'asc') ->order('company_name', 'asc') ->order('id', 'asc') ->select(); } catch (\Throwable $e) { try { $detailRows = Db::table('purchase_order_detail') ->where('scydgy_id', 'in', array_keys($mainBySid)) ->where('status', 1) ->order('CCYDH', 'asc') ->order('company_name', 'asc') ->order('ID', 'asc') ->select(); } catch (\Throwable $e2) { $detailRows = []; } } } if (!is_array($detailRows)) { $detailRows = []; } $exportLines = []; foreach ($detailRows as $dr) { if (!is_array($dr)) { continue; } $sid = (int)($dr['scydgy_id'] ?? $dr['SCYDGY_ID'] ?? 0); if ($sid <= 0 || !isset($mainBySid[$sid])) { continue; } $m = $mainBySid[$sid]; $exportLines[] = [ 'CCYDH' => (string)($dr['CCYDH'] ?? $m['CCYDH'] ?? ''), 'CYJMC' => (string)($dr['CYJMC'] ?? $m['CYJMC'] ?? ''), 'company_name' => (string)($dr['company_name'] ?? ''), 'CDF' => (string)($m['CDF'] ?? ''), 'cGzzxMc' => (string)($m['cGzzxMc'] ?? ''), 'gx' => $this->procuremenExportGxText($m), 'detail' => $dr, ]; } $groups = []; foreach ($exportLines as $line) { $k = $line['CCYDH'] . "\x1f" . $line['company_name']; if (!isset($groups[$k])) { $groups[$k] = []; } $groups[$k][] = $line; } ksort($groups, SORT_STRING); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('外发明细'); $mon = (int)substr($ym, 5, 2); $sheet->mergeCells('A1:H1'); $sheet->setCellValue('A1', $mon . '月外发明细'); $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14); $sheet->getStyle('A1')->getAlignment() ->setHorizontal(Alignment::HORIZONTAL_CENTER) ->setVertical(Alignment::VERTICAL_CENTER); $headers = ['序号', '传票号', '传票名称', '外加工单位', '订法', '客户名称', '工序', '加工金额']; $col = 'A'; foreach ($headers as $h) { $sheet->setCellValue($col . '2', $h); $col++; } $sheet->getStyle('A2:H2')->getFont()->setBold(true); $sheet->getStyle('A2:H2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $rowNum = 3; $sumSubtotalCounts = 0; $grandAmount = 0.0; if ($groups === []) { $sheet->mergeCells('A3:H3'); $sheet->setCellValue('A3', '(当月暂无符合条件的外发明细)'); $sheet->getStyle('A3')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $rowNum = 4; } else { foreach ($groups as $items) { if ($items === []) { continue; } $groupLineCount = count($items); $subAmount = 0.0; $i = 0; foreach ($items as $line) { $i++; $dr = $line['detail']; $amt = $this->procuremenExportAmount($dr); $subAmount += $amt; $sheet->setCellValue('A' . $rowNum, $i); $sheet->setCellValue('B' . $rowNum, $line['CCYDH']); $sheet->setCellValue('C' . $rowNum, $line['CYJMC']); $sheet->setCellValue('D' . $rowNum, $line['company_name']); $sheet->setCellValue('E' . $rowNum, $line['CDF']); $sheet->setCellValue('F' . $rowNum, $line['cGzzxMc']); $sheet->setCellValue('G' . $rowNum, $line['gx']); $sheet->setCellValue('H' . $rowNum, $amt); $sheet->getStyle('H' . $rowNum)->getNumberFormat()->setFormatCode('"¥"#,##0.00'); $rowNum++; } $sumSubtotalCounts += $groupLineCount; $grandAmount += $subAmount; $sheet->setCellValue('A' . $rowNum, $groupLineCount); $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum); $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($subAmount, 2, '.', ',')); $sheet->getStyle('G' . $rowNum)->getAlignment() ->setHorizontal(Alignment::HORIZONTAL_RIGHT) ->setVertical(Alignment::VERTICAL_CENTER); $rowNum++; } } $sheet->setCellValue('A' . $rowNum, '总计'); $sheet->setCellValue('B' . $rowNum, $sumSubtotalCounts); $sheet->mergeCells('G' . $rowNum . ':H' . $rowNum); $sheet->setCellValue('G' . $rowNum, '¥ ' . number_format($grandAmount, 2, '.', ',')); $sheet->getStyle('A' . $rowNum . ':H' . $rowNum)->getFont()->setBold(true); $sheet->getStyle('G' . $rowNum)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT); $lastRow = $rowNum; $sheet->getStyle('A1:H' . $lastRow)->applyFromArray([ 'borders' => [ 'allBorders' => [ 'borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => '000000'], ], ], ]); $sheet->getStyle('A3:H' . $lastRow)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER); if ($lastRow >= 3) { $sheet->getStyle('C3:C' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getStyle('D3:D' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getStyle('F3:F' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); $sheet->getStyle('G3:G' . $lastRow)->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP); } $sheet->getColumnDimension('A')->setWidth(7); $sheet->getColumnDimension('B')->setWidth(16); $sheet->getColumnDimension('C')->setWidth(52); $sheet->getColumnDimension('D')->setWidth(32); $sheet->getColumnDimension('E')->setWidth(12); $sheet->getColumnDimension('F')->setWidth(44); $sheet->getColumnDimension('G')->setWidth(26); $sheet->getColumnDimension('H')->setWidth(13); $sheet->getRowDimension(1)->setRowHeight(28); $fileBase = '外发明细_' . str_replace('-', '', $ym); $filename = $fileBase . '.xlsx'; try { list($adminId, $adminName) = $this->GetUseName(); Db::table('purchase_month_export_log')->insert([ 'ym' => $ym, 'admin_id' => (int)$adminId, 'admin_name' => mb_substr((string)$adminName, 0, 64, 'UTF-8'), 'row_count' => count($exportLines), 'total_amount' => round($grandAmount, 2), 'createtime' => time(), ]); } catch (\Throwable $e) { Log::write('month export log: ' . $e->getMessage(), 'error'); } if (ob_get_length()) { ob_end_clean(); } $asciiName = 'outward_' . str_replace('-', '', $ym) . '.xlsx'; header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment;filename="' . $asciiName . '"; filename*=UTF-8\'\'' . rawurlencode($filename)); header('Cache-Control: max-age=0'); $writer = new Xlsx($spreadsheet); $writer->save('php://output'); $spreadsheet->disconnectWorksheets(); unset($spreadsheet); exit; } /** * 导出用:工序列文案 */ protected function procuremenExportGxText(array $r) { $a = trim((string)($r['CDXMC'] ?? '')); $b = trim((string)($r['CGYMC'] ?? '')); if ($a !== '' && $b !== '') { return $a . ':' . $b; } return $a !== '' ? $a : $b; } /** * 导出用:加工金额(表中有 amount/jje/jgje 等则读取,否则 0) */ protected function procuremenExportAmount(array $r) { foreach (['amount', 'jje', 'jgje', 'processing_amount'] as $k) { if (!array_key_exists($k, $r)) { continue; } $v = $r[$k]; if ($v === null || $v === '') { continue; } if (is_numeric($v)) { return (float)$v; } $v = preg_replace('/[^\d\.\-]/', '', (string)$v); if ($v !== '' && is_numeric($v)) { return (float)$v; } } return 0.0; } /** * 订单号等用于 PDF 文件名片段(去掉路径非法字符) */ protected function sanitizePurchaseConfirmPdfOrderKey(string $ccydh): string { $s = trim($ccydh); if ($s === '') { return 'ORDER'; } $s = preg_replace('@[\\\\/:*?"<>|\\s]+@u', '_', $s); $s = trim($s, '._-'); if ($s === '') { return 'ORDER'; } if (function_exists('mb_substr')) { return mb_substr($s, 0, 80, 'UTF-8'); } return strlen($s) <= 80 ? $s : substr($s, 0, 80); } /** * 采购确认 PDF 相对路径(与 OSS 对象键一致,无前导斜杠): * xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf * * @return array{objectKey: string, webPath: string} */ protected function buildPurchaseConfirmPdfPaths(int $scydgyId, string $ccydhRaw): array { $sid = (int)$scydgyId; $safeOrder = $this->sanitizePurchaseConfirmPdfOrderKey($ccydhRaw); $y = date('Y'); $m = date('m'); $d = date('d'); $basename = $safeOrder . '_' . $sid . '.pdf'; $objectKey = 'xinhua/' . $y . '/' . $m . '/' . $d . '/' . $sid . '/' . $basename; return [ 'objectKey' => $objectKey, 'webPath' => '/' . $objectKey, ]; } /** * 采购确认成功后:用与「详情」弹窗相同的模板片段渲染 HTML,再存为 PDF(改 details_fragment 后 PDF 同步变化)。 * 优先上传至阿里云 OSS(application/config.php 的 oss 节点);失败或未配置时回退到 public 下与 objectKey 相同目录结构。 * 成功后将相对路径写入 purchase_order.pdf_url(形如 /xinhua/年/月/日/scydgy_id/订单号_scydgy_id.pdf)。 * * @return string OSS 返回 https 完整 URL;本地回退为以 / 开头的 Web 路径;失败返回空串 */ protected function savePurchaseConfirmDetailPdf(int $scydgyId, int $purchaseOrderId): string { if (!$this->isValidScydgyRowId($scydgyId)) { return ''; } $ids = trim((string)$scydgyId); $prep = $this->prepareProcuremenDetailsView($ids); if (!$prep['ok']) { return ''; } $ccydh = $prep['ccydh']; $paths = $this->buildPurchaseConfirmPdfPaths((int)$scydgyId, $ccydh); $objectKey = $paths['objectKey']; $webPath = $paths['webPath']; $meta = sprintf('工序行ID %s | 主表订单ID %d | PDF生成时间 %s', $ids, (int)$purchaseOrderId, date('Y-m-d H:i:s')); $this->view->assign([ 'pdf_export' => 1, 'pdfMetaLine' => $meta, ]); // 关闭后台 layout,避免 default 布局里的「控制台」面包屑等被打进 PDF $restoreLayout = !empty($this->layout) ? ('layout/' . $this->layout) : false; $this->view->engine->layout(false); try { $html = $this->view->fetch('procuremen/details_pdf_shell'); } catch (\Throwable $e) { if ($restoreLayout) { $this->view->engine->layout($restoreLayout); } Log::write('采购确认PDF模板渲染失败: ' . $e->getMessage(), 'error'); $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']); return ''; } if ($restoreLayout) { $this->view->engine->layout($restoreLayout); } $this->view->assign(['pdf_export' => '', 'pdfMetaLine' => '']); $tempDir = ROOT_PATH . 'runtime' . DIRECTORY_SEPARATOR . 'mpdf_tmp'; if (!is_dir($tempDir)) { @mkdir($tempDir, 0755, true); } $tempPdf = $tempDir . DIRECTORY_SEPARATOR . uniqid('pc_pdf_', true) . '.pdf'; try { $mpdf = new \Mpdf\Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'margin_left' => 12, 'margin_right' => 12, 'margin_top' => 14, 'margin_bottom' => 14, 'tempDir' => $tempDir, 'autoScriptToLang' => true, 'autoLangToFont' => true, ]); // 与当前站点 HTTP_HOST 不同的占位 base,使 basepathIsLocal=false,避免 mPDF CssManager // 在 parse_url 得到有 scheme 无 host 时对 $tr['host'] 触发「Undefined index: host」(日志已复现)。 $mpdf->SetBasePath('http://127.0.0.1/'); $mpdf->WriteHTML($html); $mpdf->Output($tempPdf, \Mpdf\Output\Destination::FILE); } catch (\Throwable $e) { Log::write('采购确认PDF写入失败: ' . $e->getMessage(), 'error'); if (is_file($tempPdf)) { @unlink($tempPdf); } return ''; } $ossUrl = AliyunOss::uploadLocalFile($tempPdf, $objectKey); if ($ossUrl !== '') { @unlink($tempPdf); $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath); return $ossUrl; } $pi = pathinfo($objectKey); $dirRel = isset($pi['dirname']) ? (string)$pi['dirname'] : 'xinhua'; $baseFile = isset($pi['basename']) ? (string)$pi['basename'] : ''; if ($baseFile === '') { @unlink($tempPdf); return ''; } $dir = ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $dirRel); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { Log::write('采购确认PDF目录创建失败: ' . $dir, 'error'); @unlink($tempPdf); return ''; } $fullPath = $dir . DIRECTORY_SEPARATOR . $baseFile; if (!@copy($tempPdf, $fullPath)) { Log::write('采购确认PDF复制到本地失败: ' . $fullPath, 'error'); @unlink($tempPdf); return ''; } @unlink($tempPdf); $this->persistPurchaseOrderPdfUrl((int)$scydgyId, $webPath); return $webPath; } /** * 采购确认 PDF 生成成功后写入 purchase_order.pdf_url(列不存在时仅记日志) */ protected function persistPurchaseOrderPdfUrl(int $scydgyId, string $webPath): void { if ($scydgyId <= 0 || $webPath === '') { return; } try { Db::table('purchase_order')->where('scydgy_id', $scydgyId)->update(['pdf_url' => $webPath]); } catch (\Throwable $e) { Log::write('purchase_order.pdf_url 更新失败 scydgy_id=' . $scydgyId . ' ' . $e->getMessage(), 'notice'); } } /** * 整理短信变量:去空格;联系人姓名为空时用公司名称 * * @param array $vars * @return array */ protected function normalizeSmsTemplateVars(array $vars): array { $out = []; foreach ($vars as $k => $v) { $out[(string)$k] = trim((string)$v); } if (($out['contact_name'] ?? '') === '' && ($out['company_name'] ?? '') !== '') { $out['contact_name'] = $out['company_name']; } return $out; } /** * 按手机号或公司名称查 customer 表联系人姓名 */ protected function resolveCustomerContactName(string $phone, string $companyName): string { $phone = trim($phone); $companyName = trim($companyName); try { if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) { $row = Db::table('customer') ->where(function ($q) use ($phone) { $q->where('phone', $phone)->whereOr('account', $phone); }) ->order('id', 'asc') ->find(); if (is_array($row)) { $nm = trim((string)($row['username'] ?? '')); if ($nm !== '') { return $nm; } } } if ($companyName !== '') { $row = Db::table('customer')->where('company_name', $companyName)->order('id', 'asc')->find(); if (is_array($row)) { return trim((string)($row['username'] ?? '')); } } } catch (\Throwable $e) { } return ''; } /** * 模版纯文本转邮件 HTML:换行转 <br>,保留已替换进的 <a> 等标签 */ protected function plainTextToHtmlEmailBody(string $text): string { $text = str_replace(["\r\n", "\r"], "\n", (string)$text); $parts = explode("\n", $text); $out = []; foreach ($parts as $line) { if (preg_match('/]*href=/i', $line)) { $out[] = $line; } else { $out[] = htmlspecialchars($line, ENT_QUOTES, 'UTF-8'); } } return implode("
\n", $out); } /** * 短信场景:去掉链接类变量,避免误填 URL * * @param array $vars * @return array */ protected function stripLinkVarsForSmsScene(string $scene, array $vars): array { if (!in_array($scene, ['review_sms', 'confirm_ok', 'confirm_fail'], true)) { return $vars; } foreach (['platform_url', 'platform_links', 'platform_links_html', 'process_lines_html'] as $k) { $vars[$k] = ''; } return $vars; } /** * 读取通知模版并替换变量;无模版、正文为空或 status≠1 时抛异常(不使用代码内兜底文案) * * @param array $vars */ protected function renderNotifyTemplate(string $scene, array $vars): string { $vars = $this->normalizeSmsTemplateVars($vars); $vars = $this->stripLinkVarsForSmsScene($scene, $vars); $row = $this->loadNotifyTemplateRow($scene); if ($row === null) { throw new \Exception($this->notifyTemplateMissingMessage($scene)); } $tpl = trim((string)($row['content'] ?? '')); if ($tpl === '') { throw new \Exception($this->notifyTemplateMissingMessage($scene)); } foreach ($vars as $k => $v) { $tpl = str_replace('{' . $k . '}', (string)$v, $tpl); } return $tpl; } protected function notifyTemplateMissingMessage(string $scene, bool $titleRequired = false): string { $map = [ 'review_email' => '外发下发-邮箱', 'review_sms' => '外发下发-短信', 'confirm_ok' => '采购确认-通过', 'confirm_fail' => '采购确认-未通过', ]; $label = $map[$scene] ?? $scene; if ($titleRequired) { return "通知模版「{$label}」({$scene})未配置、已禁用或缺少邮件标题,请在后台「短信模版配置」维护后再操作"; } return "通知模版「{$label}」({$scene})未配置、已禁用或正文为空,请在后台「短信模版配置」维护后再操作"; } /** @deprecated 使用 renderNotifyTemplate */ protected function renderSmsTemplate(string $scene, array $vars): string { return $this->renderNotifyTemplate($scene, $vars); } /** * 是否开启外发通知演练(不发真实短信/邮件) */ protected function isProcuremenNotifyDryRun(): bool { return (bool)Config::get('procuremen_notify_dry_run'); } /** * @param array $payload */ protected function recordNotifyDryRunPreview(array $payload): void { $this->notifyDryRunPreview[] = $payload; Log::write('[外发通知演练] ' . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'notice'); } /** * 短信宝发送;失败抛异常(供事务回滚,避免「已入库但未通知」与「通知失败仍入库」) * @throws \Exception */ protected function smsbao($phone, $content) { if ($this->isProcuremenNotifyDryRun()) { $this->recordNotifyDryRunPreview([ 'scene' => 'sms_only', 'phone' => $phone, 'sms_content' => $content, ]); return; } $statusStr = [ '0' => '短信发送成功', '-1' => '参数不全', '-2' => '服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间!', '30' => '密码错误', '40' => '账号不存在', '41' => '余额不足', '42' => '帐户已过期', '43' => 'IP地址限制', '50' => '内容含有敏感词', ]; $smsapi = 'http://api.smsbao.com/'; $user = 'zhuwei123'; $pass = md5('1d1e605c101e4c1f8a156c6d7b19f126'); $sendurl = $smsapi . 'sms?u=' . $user . '&p=' . $pass . '&m=' . $phone . '&c=' . urlencode($content); $result = @file_get_contents($sendurl); if ($result === false) { \think\Log::record('smsbao 请求失败 phone=' . $phone, 'error'); throw new \Exception('短信接口请求失败,请检查网络或稍后再试(未写入数据)'); } $result = trim((string)$result); if ($result !== '0') { $msg = isset($statusStr[$result]) ? $statusStr[$result] : ('返回码 ' . $result); \think\Log::record('smsbao 发送失败 phone=' . $phone . ' code=' . $result . ' ' . $msg, 'error'); throw new \Exception('短信发送失败:' . $msg . '(' . $phone . '),未写入数据'); } } }