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 中配置外网可访问的正式域名'); } $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; } // 无点主机名(xh、erp 等)在小米/企业邮安全跳转中常报 Invalid url if (strpos($host, '.') === false) { return false; } return true; } /** * 解析用于外发邮件/短信的手机端站点根(https://域名 无末尾斜杠) */ 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; } } Log::write('外发邮件手机链接:未配置有效 mproc.mobile_base_url,且当前访问主机不适合作为外链,请在 application/extra/mproc.php 设置 https 正式域名', '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]\'' ); } /** * 左侧菜单:仅「近 12 个自然月(含当月)」 */ protected function getIndexSidebarYearMonths() { $ymSet = []; $anchor = strtotime(date('Y-m-01')); for ($i = 0; $i < 12; $i++) { $ym = date('Y-m', strtotime("-{$i} month", $anchor)); $ymSet[$ym] = true; } $windowKeys = array_keys($ymSet); $minYm = min($windowKeys); $maxYm = max($windowKeys); $rows = []; try { $query = Db::table('scydgy') ->alias('a') ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner') ->where([ 'a.bwjg' => 1, 'a.iEndBz' => 0, 'a.iType' => 0, 'a.iStatus' => 10, ]); $this->whereMcydDputrecordValid($query, 'a'); $rows = $query->field("DATE_FORMAT(a.dputrecord, '%Y-%m') as ym") ->group("DATE_FORMAT(a.dputrecord, '%Y-%m')") ->orderRaw("DATE_FORMAT(a.dputrecord, '%Y-%m') DESC") ->select(); } catch (\Exception $e) { $rows = []; } foreach ($rows as $r) { $ym = isset($r['ym']) ? trim((string)$r['ym']) : ''; if (!preg_match('/^\d{4}-\d{2}$/', $ym)) { continue; } if (strcmp($ym, $minYm) >= 0 && strcmp($ym, $maxYm) <= 0) { $ymSet[$ym] = true; } } $ymList = array_keys($ymSet); rsort($ymList, SORT_STRING); $byYear = []; foreach ($ymList as $ym) { if (!preg_match('/^\d{4}-\d{2}$/', $ym)) { continue; } $y = substr($ym, 0, 4); $m = (int)substr($ym, 5, 2); if (!isset($byYear[$y])) { $byYear[$y] = []; } $byYear[$y][] = ['ym' => $ym, 'label' => $m . '月']; } 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; } /** * 列表页 */ public function index(){ $this->request->filter(['strip_tags', 'trim']); if (!$this->request->isAjax()) { $this->view->assign('defaultYm', date('Y-m')); $this->view->assign('sidebarYearMonths', $this->getIndexSidebarYearMonths()); return $this->view->fetch(); } 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:all=未发;pending=已下发;picked=已选中(主表 status=1 且存在明细 status=1);done=已完结 */ $wffTab = trim((string)$this->request->request('wff_tab', 'all')); if (!in_array($wffTab, ['all', 'pending', 'picked', 'done'], true)) { $wffTab = 'all'; } try { $issuedSet = $this->loadIssuedScydgySet(); if (in_array($wffTab, ['pending', 'done', 'picked'], true)) { $wantStatus = $wffTab === 'pending' ? 0 : 1; $pool = []; $dbRows = []; try { if ($wantStatus === 0) { $dbRows = Db::table('purchase_order') ->where('status', $wantStatus) ->select(); } else { $dbRows = Db::table('purchase_order') ->where('status', $wantStatus) ->select(); } } catch (\Throwable $e) { $dbRows = []; } if (!is_array($dbRows)) { $dbRows = []; } if ($wffTab === 'picked') { $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]); })); } $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) { $rj = null; if (!empty($dbRow['row_json'])) { $decoded = json_decode($dbRow['row_json'], true); if (is_array($decoded)) { $rj = $decoded; } } if ($rj !== null && isset($rj['ID'])) { $rid = (int)$rj['ID']; $dsFix = isset($rj['dStamp']) ? trim((string)$rj['dStamp']) : ''; if (($dsFix === '' || stripos($dsFix, '0000-00-00') === 0) && isset($dStampMap[$rid]) && trim((string)$dStampMap[$rid]) !== '') { $rj['dStamp'] = $dStampMap[$rid]; } if (array_key_exists('id', $dbRow)) { $rj['purchase_order_id'] = (int)$dbRow['id']; } $pool[] = $rj; continue; } $r = []; foreach ($dbRow as $k => $v) { $lk = strtolower((string)$k); if ($lk === 'id' || $lk === 'row_json') { continue; } $r[$k] = $v; } if (isset($dbRow['scydgy_id'])) { $r['ID'] = (int)$dbRow['scydgy_id']; $sid = $r['ID']; $dsOut = ''; if (isset($dStampMap[$sid])) { $t = trim((string)$dStampMap[$sid]); if ($t !== '' && stripos($t, '0000-00-00') !== 0) { $dsOut = $dStampMap[$sid]; } } if ($dsOut === '' && !empty($dbRow['createtime'])) { $ct = $dbRow['createtime']; if (is_numeric($ct) && (int)$ct > 946684800) { $dsOut = date('Y-m-d H:i:s', (int)$ct); } elseif (is_string($ct) && trim($ct) !== '') { $tc = trim($ct); if ($tc !== '' && stripos($tc, '0000-00-00') !== 0) { $dsOut = $tc; } } } if ($dsOut !== '') { $r['dStamp'] = $dsOut; } } if (array_key_exists('id', $dbRow)) { $r['purchase_order_id'] = (int)$dbRow['id']; } $pool[] = $r; } } else { $redis = redis(); $raw = $redis->get('procuremen_redis'); if ($raw === false || $raw === '') { return json([ 'total' => 0, 'rows' => [], 'msg' => '暂无缓存数据', ]); } $decoded = json_decode($raw, true); $pool = (is_array($decoded) && isset($decoded['data']) && is_array($decoded['data'])) ? $decoded['data'] : []; // 未发:主表已有且 status 为 0/1 才从列表隐藏;status 为空(NULL)仍可在未发展示 try { $poIds = Db::table('purchase_order') ->whereRaw('(`status` = 0 OR `status` = 1)') ->column('scydgy_id'); } catch (\Throwable $e) { $poIds = []; } if ($pool !== [] && is_array($poIds) && $poIds !== []) { $poSet = []; foreach ($poIds as $pid) { $k = (int)$pid; if ($k > 0) { $poSet[$k] = true; } } if ($poSet !== []) { $pool = array_values(array_filter($pool, function ($r) use ($poSet) { if (!is_array($r)) { return false; } $id = (int)($r['ID'] ?? 0); if ($id <= 0) { return true; } return !isset($poSet[$id]); })); } } } $filtered = $this->filterProcuremenIndexPool( $pool, $monthStart, $monthEnd, $applyMonthRange, $search, $filterArr, $opArr ); $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) { // 默认按主键 ID 排序时,用 array_multisort 比 usort+闭包略省开销 if ($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); if ($wffTab === 'pending' || $wffTab === 'done' || $wffTab === 'picked') { $rw['_iss_out'] = true; } else { $rw['_iss_out'] = $rid > 0 && isset($issuedSet[$rid]); } } unset($rw); if ($wffTab === 'pending' && 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 === 'picked' && count($rows) > 0) { $idList = []; foreach ($rows as $rw) { if (!is_array($rw)) { continue; } $id = (int)($rw['ID'] ?? 0); if ($id > 0) { $idList[$id] = true; } } $nameMap = $this->loadPickedSupplierCompanyByScydgyIds(array_keys($idList)); foreach ($rows as &$rw) { if (!is_array($rw)) { continue; } $rid = (int)($rw['ID'] ?? 0); $rw['picked_supplier_name'] = $nameMap[$rid] ?? ''; } 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.scydgy_id ∪ 明细 purchase_order_detail.scydgy_id(与工序列表行 ID 对应) * * @return array 列表行 ID => true */ protected function loadIssuedScydgySet() { $issuedSet = []; foreach (['purchase_order', 'purchase_order_detail'] as $tbl) { try { $issuedList = Db::table($tbl)->group('scydgy_id')->column('scydgy_id'); if (!is_array($issuedList)) { continue; } foreach ($issuedList as $ids) { $n = (int)$ids; if ($n > 0) { $issuedSet[$n] = true; } } } catch (\Throwable $e) { } } return $issuedSet; } /** * 采购确认已选定供应商:明细 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 ($sid <= 0 || isset($out[$sid])) { continue; } $name = trim((string)($r['company_name'] ?? '')); $out[$sid] = $name; } return $out; } /** * 已下发列表汇总:按工序行 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 ($ids <= 0) { 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; } /** * 列表筛选:月份、dStamp 合法性、快速搜索、Bootstrap Table filter(与原先 Redis 分支一致) * * @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['dStamp']) ? trim((string)$r['dStamp']) : ''; if ($ds === '' || stripos($ds, '0000-00-00') === 0 || !preg_match('/^[12]\d{3}-\d{1,2}-\d{1,2}/', $ds)) { continue; } if ($applyMonthRange) { if (strcmp($ds, $monthStart) < 0 || strcmp($ds, $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 = (int)($row['ID'] ?? $row['id'] ?? 0); if ($ids <= 0) { 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 ($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 = (int)($row['ID'] ?? $row['id'] ?? 0); if ($ids <= 0) { 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) { $data['status'] = 1; } 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', '点击「完结」,已标记为已完结;本次数量「' . ($q !== '' ? $q : '') . '」,最高限价「' . ($p !== '' ? $p : '') . '」', $poIdLog ); } 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 ($sid <= 0 || !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 ($sid <= 0) { $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'] ?? '')); $sms = "您好,{$cname}:\n\n" . "您参与的外发加工订单采购确认结果:已通过。\n" . "订单号:{$ccydh}\n" . "印件名称:{$cyjmc}\n\n"; $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'] ?? '')); $sms = "您好,{$cname}:\n\n" . "您参与的外发加工订单采购确认结果:未通过。\n" . "订单号:{$ccydh}\n" . "印件名称:{$cyjmc}\n\n"; $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 ($scydgyId <= 0) { 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 ($scydgyId <= 0 || $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 ($scydgyId <= 0 || $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' => '印件名称', 'CDXMC' => '订单名称', 'CGYMC' => '工序名称', 'CGYBH' => '工序编号', '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); } } } /** * 审核弹窗提交:GET 打开弹窗;POST 保存下发(主表 + 明细 + 操作日志) */ public function review() { if ($this->request->isPost()) { $rowJson = $this->request->post('row_json', ''); $companiesJson = $this->request->post('companies_json', '[]'); $row = json_decode($rowJson, true); if (!is_array($row)) { $this->error(__('Invalid parameters')); } $companies = json_decode($companiesJson, true); if (!is_array($companies) || count($companies) === 0) { $this->error('请至少选择一个下发单位'); } $toDb = function ($value) { if ($value === null) { return null; } if (is_scalar($value)) { return $value; } return json_encode($value, JSON_UNESCAPED_UNICODE); }; Db::startTrans(); try { $ids = (int)($row['ID'] ?? $row['id'] ?? 0); if ($ids <= 0) { 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, ]; 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); } $row['scydgy_id'] = (int)($row['ID'] ?? $row['id'] ?? 0); unset($row['ID'], $row['id'], $row['_iss_out'], $row['ids'], $row['purchase_order_id']); foreach ($companies as $c) { if (!is_array($c)) { continue; } $one = [ 'scydgy_id' => $toDb($row['scydgy_id']), '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' => "未提交", ]; // 先校验邮箱与手机号,再写入明细(写入后根据主键生成手机端直达链接 focus_eid) $toEmail = isset($c['email']) ? trim((string)$c['email']) : ''; $companyName = isset($c['name']) ? trim((string)$c['name']) : '外协单位'; if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) { throw new \Exception($companyName . ' 未填写有效邮箱,无法发送邮件,未写入数据'); } $phone = isset($c['phone']) ? trim((string)$c['phone']) : ''; if ($phone === '') { $cname = isset($c['name']) ? trim((string)$c['name']) : ''; throw new \Exception(($cname !== '' ? $cname : '外协单位') . ' 未填写手机号,无法发送短信,未写入数据'); } Db::table('purchase_order_detail')->insert($one); $detailId = (int)Db::getLastInsID(); $mprocUrl = $this->buildMprocMobileOrderUrl($detailId); $mprocUrlEsc = htmlspecialchars($mprocUrl, ENT_QUOTES, 'UTF-8'); // 发送邮箱 $mailConfig = Config::get('Mailer'); if (!is_array($mailConfig) || empty($mailConfig['host']) || empty($mailConfig['addr'])) { throw new \Exception('邮件未配置'); } $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 = '【外发加工订单通知】'; $mail->Body = " 您好,{$companyName}:

您有新的外发加工订单待处理:
订单号:{$row['CCYDH']}
印件名称:{$row['CYJMC']}

请点击下面链接,登录后可直接打开本条订单(无需再搜索):
{$mprocUrlEsc}

登录后可在手机端填写金额与交货日期。

请及时查收并处理,谢谢! "; $mail->send(); $ccydh = isset($row['CCYDH']) ? (string)$row['CCYDH'] : ''; $cyjmc = isset($row['CYJMC']) ? (string)$row['CYJMC'] : ''; $smsContent = "您好,{$companyName}:\n\n" . "您有新的外发加工订单待处理:\n" . "订单号:{$ccydh}\n" . "印件名称:{$cyjmc}\n\n" . "手机打开链接(登录后直达本条):\n" . $mprocUrl . "\n\n" . '请及时查收并处理,谢谢!'; $this->smsbao($phone, $smsContent); } Db::commit(); } catch (\Throwable $e) { Db::rollback(); $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; } $names = []; foreach ($companies as $c) { if (!is_array($c)) { continue; } $nm = trim((string)($c['name'] ?? $c['company_name'] ?? '')); if ($nm !== '') { $names[] = $nm; } } $n = count($names); $nameStr = $n ? implode('、', $names) : '(未查询到单位名称)'; $this->addOrderLog( $ids, 'review_issue', '审核共选择 ' . $n . ' 家单位:' . $nameStr, $poIdLog ); $this->success('操作成功'); } return $this->view->fetch(); } /** * 审核弹窗获取公司列表 */ public function reviewCompanies() { if (!$this->request->isAjax()) { $this->error(__('Invalid parameters')); } $list = []; try { $rows = Db::table('customer')->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; $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']) : ''; $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' ? 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); } } } /** * 月份外发明细导出 */ 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)); $unixStart = (int)strtotime($monthStart); $unixEnd = (int)strtotime($monthEnd); $rows = []; try { $rows = Db::table('purchase_order_detail') ->whereRaw( '(TRIM(CAST(createtime AS CHAR(64))) LIKE ?)' . ' OR ((createtime REGEXP \'^[0-9]+$\') AND CAST(createtime AS UNSIGNED) BETWEEN ? AND ?)', [$ym . '%', $unixStart, $unixEnd] ) ->order('CCYDH', 'asc') ->order('company_name', 'asc') ->order('id', 'asc') ->select(); } catch (\Throwable $e) { $rows = []; } $groups = []; foreach ($rows as $r) { if (!is_array($r)) { continue; } $k = (string)($r['CCYDH'] ?? '') . "\x1f" . (string)($r['company_name'] ?? ''); if (!isset($groups[$k])) { $groups[$k] = []; } $groups[$k][] = $r; } 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 (empty($groups)) { $sheet->mergeCells('A3:H3'); $sheet->setCellValue('A3', '(当月暂无外发明细)'); $sheet->getStyle('A3')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $rowNum = 4; } else { foreach ($groups as $items) { if (empty($items)) { continue; } $groupLineCount = count($items); $subAmount = 0.0; $i = 0; foreach ($items as $r) { $i++; $amt = $this->procuremenExportAmount($r); $subAmount += $amt; $sheet->setCellValue('A' . $rowNum, $i); $sheet->setCellValue('B' . $rowNum, (string)($r['CCYDH'] ?? '')); $sheet->setCellValue('C' . $rowNum, (string)($r['CYJMC'] ?? '')); $sheet->setCellValue('D' . $rowNum, (string)($r['company_name'] ?? '')); $sheet->setCellValue('E' . $rowNum, (string)($r['CDF'] ?? '')); $sheet->setCellValue('F' . $rowNum, (string)($r['cGzzxMc'] ?? '')); $sheet->setCellValue('G' . $rowNum, $this->procuremenExportGxText($r)); $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'; 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 ($scydgyId <= 0) { 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'); } } /** * 短信宝发送;失败抛异常(供事务回滚,避免「已入库但未通知」与「通知失败仍入库」) * @throws \Exception */ protected function smsbao($phone, $content) { $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 . '),未写入数据'); } } }