|null purchase_order_detail 表字段:小写 => 真实列名 */ protected static $mprocProcuremenColumns = null; public function _initialize() { parent::_initialize(); if (is_file(APP_PATH . 'extra/mproc.php')) { Config::load(APP_PATH . 'extra/mproc.php', 'mproc'); } $days = (int)(Config::get('mproc.session_days') ?: 7); $days = max(1, min(30, $days)); $this->mprocTtlSeconds = $days * 86400; } /** * 当前手机端登录用户;未登录返回 null */ protected function mprocGetUser() { $token = Session::get('mproc_token'); if ($token === null || $token === '') { $token = Cookie::get('mproc_token'); } if ($token === null || $token === '') { return null; } $token = preg_replace('/[^a-f0-9]/i', '', (string)$token); if (strlen($token) < 16) { return null; } $user = Cache::get('mproc_u_' . $token); if (!is_array($user)) { return null; } // 手机登录必有 phone;账号密码登录必有 username if (empty($user['phone']) && empty($user['username'])) { return null; } if (time() - (int)($user['login_time'] ?? 0) > $this->mprocTtlSeconds) { $this->mprocClearLogin($token); return null; } $user['token'] = $token; return $user; } protected function mprocClearLogin($token) { if ($token !== null && $token !== '') { Cache::rm('mproc_u_' . $token); } Session::delete('mproc_token'); Cookie::delete('mproc_token'); } /** * 登录成功后的回跳地址校验(仅允许本站「外发明细订单页」路径,防止开放重定向) * * @param string $raw GET/POST 的 redirect 或当前 REQUEST_URI */ protected function mprocSanitizeRedirectUrl($raw) { $s = str_replace(["\r", "\n", "\0"], '', trim((string)$raw)); if ($s === '') { return ''; } if (preg_match('#^https?://#i', $s)) { $h = parse_url($s, PHP_URL_HOST); if (!is_string($h) || strcasecmp($h, (string)$this->request->host()) !== 0) { return ''; } $path = parse_url($s, PHP_URL_PATH); $query = parse_url($s, PHP_URL_QUERY); $s = (is_string($path) && $path !== '' ? $path : '/'); if (is_string($query) && $query !== '') { $s .= '?' . $query; } } if (strpos($s, '://') !== false) { return ''; } if ($s === '' || ($s[0] !== '/' && stripos($s, 'index.php') !== 0)) { return ''; } if ($s[0] !== '/') { $s = '/' . ltrim($s, '/'); } if (strpos($s, '//') === 0) { return ''; } if (stripos($s, 'index/index/index') === false) { return ''; } if (stripos($s, 'index/index/login') !== false) { return ''; } return $s; } /** * 手机端登录页 URL。注意:勿用 url('...login', ['redirect'=>]),在 url_html_suffix 下会把参数拼进 PATHINFO 导致 404。 * * @param string $redirectPath 已通过 {@see mprocSanitizeRedirectUrl} 的回跳路径(含 query),空则不带参数 */ protected function mprocBuildLoginUrl($redirectPath = '') { $root = rtrim($this->request->root(), '/'); $path = '/index/index/login'; $rp = trim((string)$redirectPath); if ($rp === '') { return $root . $path; } return $root . $path . '?' . http_build_query(['redirect' => $rp], '', '&', PHP_QUERY_RFC3986); } /** * 登录成功后跳转到订单首页:用当前入口 {@see Request::root} 拼 URL,并从原 redirect 中只保留白名单 query(避免子目录部署丢参、开放重定向) * * @param string $redirectPathOrUrl 已通过 {@see mprocSanitizeRedirectUrl} 的路径或完整 URL(可含 ?focus_eid=) */ protected function mprocBuildAfterLoginIndexUrl($redirectPathOrUrl) { $raw = trim((string)$redirectPathOrUrl); if ($raw === '') { return url('index/index/index', '', '', true); } $queryStr = ''; if (preg_match('#^https?://#i', $raw)) { $pq = parse_url($raw); $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : ''; } elseif (isset($raw[0]) && $raw[0] === '/') { $pq = parse_url('http://127.0.0.1' . $raw); $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : ''; } else { $pq = parse_url('http://127.0.0.1/' . ltrim($raw, '/')); $queryStr = (is_array($pq) && !empty($pq['query']) && is_string($pq['query'])) ? $pq['query'] : ''; } $q = []; if ($queryStr !== '') { parse_str($queryStr, $q); if (!is_array($q)) { $q = []; } } $allowed = []; if (isset($q['focus_eid'])) { $fe = (int)$q['focus_eid']; if ($fe > 0) { $allowed['focus_eid'] = $fe; } } $mt = isset($q['main_tab']) ? trim((string)$q['main_tab']) : ''; if ($mt === 'me' || $mt === 'orders') { $allowed['main_tab'] = $mt; } $tb = isset($q['tab']) ? trim((string)$q['tab']) : ''; if (in_array($tb, ['draft', 'submitted', 'done', 'me'], true)) { $allowed['tab'] = $tb; } if (isset($q['q']) && trim((string)$q['q']) !== '') { $allowed['q'] = substr(trim((string)$q['q']), 0, 120); } if (isset($allowed['focus_eid']) && !isset($allowed['main_tab'])) { $allowed['main_tab'] = 'orders'; } $base = rtrim($this->request->root(true), '/'); $path = '/index/index/index'; $qs = $allowed !== [] ? ('?' . http_build_query($allowed, '', '&', PHP_QUERY_RFC3986)) : ''; return $base . $path . $qs; } /** * 从 purchase_order_detail 表解析真实列名(SHOW COLUMNS 只查一次,按候选小写名匹配第一条) * * @param string[] $candidatesLower 如 ['status','istatus'] * @return string|null */ protected function mprocResolveProcuremenColumn(array $candidatesLower) { if (self::$mprocProcuremenColumns === null) { self::$mprocProcuremenColumns = []; try { $rows = Db::query('SHOW COLUMNS FROM `purchase_order_detail`'); if (is_array($rows)) { foreach ($rows as $c) { $name = isset($c['Field']) ? (string)$c['Field'] : ''; if ($name !== '') { self::$mprocProcuremenColumns[strtolower($name)] = $name; } } } } catch (\Throwable $e) { self::$mprocProcuremenColumns = []; } } foreach ($candidatesLower as $low) { $k = strtolower((string)$low); if (isset(self::$mprocProcuremenColumns[$k])) { return self::$mprocProcuremenColumns[$k]; } } return null; } /** * 列表:非管理员按 company_name 与登录时解析的单位名一致;管理员不加条件 * * @return array 可直接 $query->where($arr),空数组表示不加条件 */ protected function mprocListWhereForLoginUser(array $user) { if (!empty($user['is_admin'])) { return []; } $cCol = $this->mprocResolveProcuremenColumn(['company_name']); if ($cCol === null || $cCol === '') { return ['id' => 0]; } $cn = trim((string)($user['company_name'] ?? '')); if ($cn === '') { $phone = trim((string)($user['phone'] ?? '')); if ($phone !== '') { $cn = $this->mprocResolveCompanyForLoginPhone($phone); } } if ($cn === '') { return ['id' => 0]; } return [$cCol => $cn]; } /** * 登录手机号对应的外协单位名称:优先 customer 表公司名/名称;否则取 purchase_order_detail 该手机最新一条 */ protected function mprocResolveCompanyForLoginPhone(string $phone): string { $phone = trim($phone); if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) { return ''; } $cust = $this->mprocFindCustomerRowByPhone($phone); if (is_array($cust) && $cust !== []) { $co = $this->mprocCustomerPickField($cust, ['company_name', 'name']); if ($co !== '') { return $co; } } try { $one = Db::table('purchase_order_detail') ->where('phone', $phone) ->order('id', 'desc') ->find(); if (is_array($one)) { $n = trim((string)($one['company_name'] ?? '')); if ($n !== '') { return $n; } } } catch (\Throwable $e) { } return ''; } /** * 仅按手机号在 customer.phone(多号逗号分隔)中匹配一条客户记录 * * @return array|null */ protected function mprocFindCustomerRowByPhone(string $phone): ?array { $phone = trim($phone); if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) { return null; } try { $rows = Db::table('customer')->where('phone', '<>', '')->select(); } catch (\Throwable $e) { return null; } if (!is_array($rows)) { return null; } foreach ($rows as $r) { if (!is_array($r)) { continue; } $raw = (string)($r['phone'] ?? ''); $raw = str_replace(["\r", "\n", "\t", ' ', ' ', ','], ['', '', '', '', '', ','], $raw); foreach (explode(',', $raw) as $seg) { if (trim($seg) === $phone) { return $r; } } } return null; } /** * 管理员表 fa_admin.mobile 与当前手机号一致且未禁用(用于手机验证码管理员通道) * * @return array|null */ protected function mprocAdminRowByMobile(string $phone): ?array { $phone = trim($phone); if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) { return null; } try { $row = Db::name('admin') ->where('mobile', $phone) ->where('status', '<>', 'hidden') ->order('id', 'asc') ->find(); } catch (\Throwable $e) { return null; } return is_array($row) && $row !== [] ? $row : null; } /** * 在 customer 表中匹配当前用户:优先手机号(支持多号逗号分隔),否则按公司名与 company_name / name * * @return array|null */ protected function mprocFindCustomerRowForUser(array $user): ?array { $phone = trim((string)($user['phone'] ?? '')); $cn = trim((string)($user['company_name'] ?? '')); if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) { $byPhone = $this->mprocFindCustomerRowByPhone($phone); if ($byPhone !== null) { return $byPhone; } } if ($cn !== '') { try { $hit = Db::table('customer') ->where(function ($q) use ($cn) { $q->where('company_name', $cn)->whereOr('name', $cn); }) ->order('id', 'desc') ->find(); } catch (\Throwable $e) { $hit = null; } if (is_array($hit) && $hit !== []) { return $hit; } } return null; } /** * 从 customer 行取字段(兼容列名大小写) * * @param string[] $candidates */ protected function mprocCustomerPickField(array $row, array $candidates): string { foreach ($candidates as $want) { $lw = strtolower($want); foreach ($row as $k => $v) { if (!is_string($k)) { continue; } if (strtolower($k) !== $lw) { continue; } $s = trim((string)$v); if ($s !== '') { return $s; } } } return ''; } /** * 明细表关键字搜索:仅对 purchase_order_detail 真实存在的列 LIKE; * 主表 purchase_order 上的订单号、印件、工序等另查 scydgy_id 再 OR 进列表(避免引用不存在的列导致整页查失败)。 * * @param \think\db\Query $query */ protected function mprocApplySearchKeywordToDetailQuery($query, $search) { $kw = trim((string)$search); if ($kw === '') { return; } $map = self::$mprocProcuremenColumns; if (!is_array($map) || $map === []) { return; } $like = '%' . addcslashes($kw, '%_\\') . '%'; $wantLower = ['ccydh', 'cyjmc', 'cdxmc', 'company_name', 'cgzzxmc', 'cgymc', 'cdf', 'phone', 'email']; $detailCols = []; foreach ($wantLower as $low) { if (isset($map[$low])) { $detailCols[] = $map[$low]; } } $scydgyCol = isset($map['scydgy_id']) ? $map['scydgy_id'] : 'scydgy_id'; $idCol = isset($map['id']) ? $map['id'] : 'id'; $poSidList = []; $poWant = ['CCYDH', 'CYJMC', 'CDXMC', 'CGYMC', 'cGzzxMc', 'CDF']; try { $col = Db::table('purchase_order') ->where(function ($sub) use ($like, $poWant) { $firstPo = true; foreach ($poWant as $pf) { if ($firstPo) { $sub->where($pf, 'like', $like); $firstPo = false; } else { $sub->whereOr($pf, 'like', $like); } } }) ->column('scydgy_id'); if (is_array($col)) { foreach ($col as $v) { $id = (int)$v; if ($id > 0) { $poSidList[$id] = true; } } } $poSidList = array_keys($poSidList); } catch (\Throwable $e) { $poSidList = []; } $query->where(function ($q2) use ($like, $detailCols, $poSidList, $scydgyCol, $idCol) { $first = true; foreach ($detailCols as $col) { if ($first) { $q2->where($col, 'like', $like); $first = false; } else { $q2->whereOr($col, 'like', $like); } } if ($poSidList !== []) { if ($first) { $q2->where($scydgyCol, 'in', $poSidList); $first = false; } else { $q2->whereOr($scydgyCol, 'in', $poSidList); } } if ($first) { $q2->where($idCol, '=', 0); } }); } /** * 列表:按左侧 Tab 追加 status_name 条件(与数值 status 0/1/2 无关;值由后端维护) * - draft:status_name = 未提交 * - submitted:status_name = 已提交 * - done:status_name = 已完成 * 表无 status_name 列时不加条件(三个 Tab 数据相同,待库表补列后再筛) * * @param mixed $query * @param string $tab draft|submitted|done * @param string|null $statusNameCol 真实列名,如 status_name */ protected function mprocApplyListTabConditions($query, $tab, $statusNameCol) { if ($statusNameCol === null) { return; } $map = [ 'draft' => '未提交', 'submitted' => '已提交', 'done' => '已完成', ]; $label = $map[$tab] ?? '未提交'; $query->where($statusNameCol, '=', $label); } /** * 「我的」:公司名称、姓名、手机、邮箱以 customer 表为准;无匹配时回退 purchase_order_detail */ protected function mprocProfileForUser(array $user) { $phone = trim((string)($user['phone'] ?? '')); $out = [ 'company_name' => trim((string)($user['company_name'] ?? '')), 'contact_name' => '', 'phone' => $phone, 'email' => '', ]; if ($phone === '' && !empty($user['username'])) { if ($out['company_name'] === '') { $out['company_name'] = '账号:' . (string)$user['username']; } return $out; } $cust = $this->mprocFindCustomerRowForUser($user); if (is_array($cust) && $cust !== []) { $co = $this->mprocCustomerPickField($cust, ['company_name', 'name']); if ($co !== '') { $out['company_name'] = $co; } $out['contact_name'] = $this->mprocCustomerPickField($cust, ['username', 'contact', 'linkman', 'contacts']); $em = $this->mprocCustomerPickField($cust, ['email']); if ($em !== '') { $out['email'] = $em; } $rawP = $this->mprocCustomerPickField($cust, ['phone']); if ($rawP !== '') { $norm = str_replace(["\r", "\n", "\t", ' ', ' ', ','], ['', '', '', '', '', ','], $rawP); $segs = []; foreach (explode(',', $norm) as $seg) { $seg = trim($seg); if ($seg !== '') { $segs[] = $seg; } } if ($phone !== '' && $segs !== [] && in_array($phone, $segs, true)) { $out['phone'] = $phone; } elseif ($segs !== []) { $out['phone'] = implode('、', $segs); } else { $out['phone'] = $rawP; } } return $out; } try { $one = null; $cCol = $this->mprocResolveProcuremenColumn(['company_name']); $co = $out['company_name']; if ($cCol && $co !== '') { $one = Db::table('purchase_order_detail')->where($cCol, $co)->order('id', 'desc')->find(); } if (!is_array($one) && $phone !== '') { $one = Db::table('purchase_order_detail') ->where('phone', $phone) ->order('id', 'desc') ->find(); } if (is_array($one)) { if ($out['company_name'] === '') { $out['company_name'] = trim((string)($one['company_name'] ?? '')); } $out['email'] = trim((string)($one['email'] ?? '')); $rp = trim((string)($one['phone'] ?? '')); if ($rp !== '') { $out['phone'] = $rp; } } } catch (\Throwable $e) { } return $out; } /** * 将 purchase_order(工序行主表)快照合并进 purchase_order_detail 行:订单级信息以主表为准; * 金额、交期、外厂 company、明细 status 等仍保留明细表。 * * @param array $row 引用:明细行 * @param array $poRow purchase_order 一行 */ protected function mprocMergePurchaseOrderIntoDetail(array &$row, array $poRow) { $pl = array_change_key_case($poRow, CASE_LOWER); $hdr = [ 'ccydh' => 'CCYDH', 'cyjmc' => 'CYJMC', 'cdf' => 'CDF', 'cgzzxmc' => 'cGzzxMc', 'cgymc' => 'CGYMC', 'cdxmc' => 'CDXMC', 'ngzl' => 'NGZL', 'cdw' => 'CDW', 'cgybh' => 'CGYBH', ]; foreach ($hdr as $lk => $out) { if (!array_key_exists($lk, $pl)) { continue; } $v = $pl[$lk]; if ($v !== null && $v !== '') { $row[$out] = $v; } } // 本次数量、最高限价:仅存在于主表;PDO 列名可能为小写 ceilingprice if (array_key_exists('this_quantity', $pl)) { $row['This_quantity'] = $pl['this_quantity']; } if (array_key_exists('ceilingprice', $pl)) { $row['ceilingPrice'] = $pl['ceilingprice']; } elseif (array_key_exists('ceiling_price', $pl)) { $row['ceilingPrice'] = $pl['ceiling_price']; } } /** * 查询 purchase_order_detail 列表(订单页) * 无搜索词时:左侧 Tab 按 status_name 筛选(未提交/已提交/已完成) * 有搜索词时:不按 Tab 筛选,在本单位可见数据内全局关键字匹配 * * @param string $tab draft|submitted|done * @param string|null $statusNameCol status_name 真实列名;为 null 时不按 Tab 过滤 * @return array{rows: array, done_no_status: int} */ protected function mprocFetchProcuremenList(array $user, $tab, $q, $statusNameCol) { $query = Db::table('purchase_order_detail')->order('id', 'desc'); $userWhere = $this->mprocListWhereForLoginUser($user); if ($userWhere !== []) { $query->where($userWhere); } $this->mprocApplySearchKeywordToDetailQuery($query, $q); // 有搜索词时不在此按 status_name 分栏筛选,全局匹配;无搜索词时仍按左侧 Tab(未提交/已提交/已完成)筛选 if (trim((string)$q) === '') { $this->mprocApplyListTabConditions($query, $tab, $statusNameCol); } try { $rows = $query->limit(500)->select(); } catch (\Throwable $e) { $rows = []; } if (!is_array($rows)) { $rows = []; } $poBySid = []; $sidList = []; foreach ($rows as $r0) { if (!is_array($r0)) { continue; } $sid0 = (int)($r0['scydgy_id'] ?? $r0['SCYDGY_ID'] ?? 0); if ($sid0 > 0) { $sidList[$sid0] = true; } } if ($sidList !== []) { try { $poRows = Db::table('purchase_order') ->where('scydgy_id', 'in', array_values(array_keys($sidList))) ->select(); if (is_array($poRows)) { foreach ($poRows as $pr) { $sidk = (int)($pr['scydgy_id'] ?? $pr['SCYDGY_ID'] ?? 0); if ($sidk > 0) { $poBySid[$sidk] = $pr; } } } } catch (\Throwable $e) { } } foreach ($rows as &$row) { if (!is_array($row)) { continue; } $row['eid'] = (int)($row['id'] ?? $row['ID'] ?? 0); $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0); if ($sid > 0 && isset($poBySid[$sid])) { $this->mprocMergePurchaseOrderIntoDetail($row, $poBySid[$sid]); } // status_name 由库表/后端维护,不在此根据 amount 覆盖 if (!isset($row['status_name']) || $row['status_name'] === null) { $row['status_name'] = ''; } else { $row['status_name'] = trim((string)$row['status_name']); } $row['mproc_can_edit'] = $this->mprocCanEditRow($user, $row) ? 1 : 0; $am = $row['amount'] ?? null; if ($am === null || $am === '' || (is_string($am) && trim($am) === '')) { $row['amount_display'] = ''; } else { $row['amount_display'] = is_scalar($am) ? (string)$am : ''; } $dv = isset($row['delivery']) ? trim((string)$row['delivery']) : ''; if ($dv !== '' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $dv, $m)) { $row['delivery_display'] = $m[1]; } elseif ($dv !== '') { $row['delivery_display'] = $dv; } else { $row['delivery_display'] = ''; } $row['amount_missing'] = ($am === null || $am === '' || (is_string($am) && trim($am) === '')) ? 1 : 0; $row['delivery_missing'] = ($dv === '' || preg_match('/^0000-00-00/i', $dv)) ? 1 : 0; $row['mproc_fill_hint'] = ''; } unset($row); return [ 'rows' => $rows ?: [], 'done_no_status' => (int)($statusNameCol === null), ]; } /** * 外发明细首页(需登录) * GET:main_tab=orders|me,orders 时 tab=draft|submitted|done 对应 status_name:未提交|已提交|已完成;q 搜索词 */ public function index() { $user = $this->mprocGetUser(); if (!$user) { $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''; $safe = $this->mprocSanitizeRedirectUrl($uri); if ($safe !== '') { Session::set('mproc_intended_url', $safe); } $this->redirect($this->mprocBuildLoginUrl('')); return; } $tabParam = trim((string)$this->request->get('tab', 'draft')); $mainTab = trim((string)$this->request->get('main_tab', 'orders')); // 旧地址 ?tab=me 表示「我的」 if ($tabParam === 'me') { $mainTab = 'me'; } if (!in_array($mainTab, ['orders', 'me'], true)) { $mainTab = 'orders'; } $tab = $tabParam === 'me' ? 'draft' : $tabParam; if (!in_array($tab, ['draft', 'submitted', 'done'], true)) { $tab = 'draft'; } $q = trim((string)$this->request->get('q', '')); $mprocFocusEid = 0; $focusEid = (int)$this->request->get('focus_eid', 0); if ($focusEid > 0 && $mainTab === 'orders') { $idCol = $this->mprocResolveProcuremenColumn(['id']); if ($idCol) { $qw = $this->mprocListWhereForLoginUser($user); $dr = null; try { $qrow = Db::table('purchase_order_detail')->where($idCol, $focusEid); if ($qw !== []) { $qrow->where($qw); } $dr = $qrow->find(); } catch (\Throwable $e) { $dr = null; } if (is_array($dr) && $dr !== []) { $mprocFocusEid = $focusEid; $q = ''; $sn = trim((string)($dr['status_name'] ?? $dr['STATUS_NAME'] ?? '')); $mapTab = ['未提交' => 'draft', '已提交' => 'submitted', '已完成' => 'done']; if ($sn !== '' && isset($mapTab[$sn])) { $tab = $mapTab[$sn]; } } } } // 左侧 Tab 按 purchase_order_detail.status_name(未提交/已提交/已完成),与数值 status 无关 $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']); $profile = $this->mprocProfileForUser($user); $this->view->assign('mprocMainTab', $mainTab); $this->view->assign('mprocTab', $tab); $this->view->assign('mprocSearchQ', $q); $this->view->assign('mprocProfile', $profile); $this->view->assign('mprocIsAdmin', !empty($user['is_admin']) ? 1 : 0); $this->view->assign('mprocFocusEid', $mprocFocusEid); if ($mainTab === 'me') { $this->view->assign('rows', []); return $this->view->fetch(); } $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol); $this->view->assign('rows', $bundle['rows']); return $this->view->fetch(); } /** * 外发明细列表 JSON(需登录) * main_tab=orders|me;orders 时 tab=draft|submitted|done、q=搜索词 */ public function mprocList() { $user = $this->mprocGetUser(); if (!$user) { $this->error('请先登录', url('index/index/login')); } $tabParam = trim((string)$this->request->request('tab', 'draft')); $mainTab = trim((string)$this->request->request('main_tab', 'orders')); if ($tabParam === 'me') { $mainTab = 'me'; } if (!in_array($mainTab, ['orders', 'me'], true)) { $mainTab = 'orders'; } $tab = $tabParam === 'me' ? 'draft' : $tabParam; if (!in_array($tab, ['draft', 'submitted', 'done'], true)) { $tab = 'draft'; } $q = trim((string)$this->request->request('q', '')); if ($mainTab === 'me') { // Jump::success($msg, $url, $data, …) 第二参是 URL,数据必须放第三参 $this->success('ok', '', [ 'main_tab' => 'me', 'tab' => $tab, 'rows' => [], 'profile' => $this->mprocProfileForUser($user), 'done_no_status' => 0, ]); } $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']); $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol); $this->success('ok', '', array_merge([ 'main_tab' => 'orders', 'tab' => $tab, 'is_admin' => !empty($user['is_admin']) ? 1 : 0, ], $bundle)); } /** * 登录页(手机号验证码 / 账号密码) */ public function login() { if ($this->mprocGetUser()) { $this->redirect(url('index/index/index')); } $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', '')); if ($redirect !== '') { Session::set('mproc_intended_url', $redirect); } $this->view->assign('mprocLoginRedirect', $redirect); return $this->view->fetch(); } /** * 发送登录验证码(POST:phone) */ public function sendSms() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $phone = trim((string)$this->request->post('phone', '')); if (!preg_match('/^1\d{10}$/', $phone)) { $this->error('请输入正确的11位手机号'); } if (!$this->mprocFindCustomerRowByPhone($phone) && !$this->mprocAdminRowByMobile($phone)) { $this->error('账号未开通权限,请联系管理员开通'); } $cd = (int)(Config::get('mproc.sms_resend_cd') ?: 55); if (Cache::get('mproc_sms_wait_' . $phone)) { $this->error('发送过于频繁,请稍后再试'); } $code = (string)random_int(100000, 999999); $ttl = (int)(Config::get('mproc.sms_code_ttl') ?: 300); $ttl = max(60, min(600, $ttl)); Cache::set('mproc_code_' . $phone, $code, $ttl); Cache::set('mproc_sms_wait_' . $phone, 1, $cd); try { $this->mprocSmsSend($phone, '【登录验证】您的验证码为' . $code . ',' . (int)($ttl / 60) . '分钟内有效,请勿泄露。'); } catch (\Exception $e) { Cache::rm('mproc_code_' . $phone); Cache::rm('mproc_sms_wait_' . $phone); $this->error($e->getMessage()); } $this->success('验证码已发送'); } /** * 验证码登录(POST:phone、code) */ public function doLogin() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $phone = trim((string)$this->request->post('phone', '')); $code = trim((string)$this->request->post('code', '')); if (!preg_match('/^1\d{10}$/', $phone)) { $this->error('手机号格式不正确'); } if (!preg_match('/^\d{6}$/', $code)) { $this->error('请输入6位验证码'); } // 本地调试:application/extra/mproc.php 中配置 mock_sms_code 与输入一致时,不校验短信缓存(生产务必留空) $mock = Config::get('mproc.mock_sms_code'); if ($mock !== null && $mock !== '' && (string)$mock === $code) { Cache::rm('mproc_code_' . $phone); } else { $cached = Cache::get('mproc_code_' . $phone); if ($cached === false || $cached === null || (string)$cached !== $code) { $this->error('验证码错误或已过期'); } Cache::rm('mproc_code_' . $phone); } $cust = $this->mprocFindCustomerRowByPhone($phone); if (is_array($cust) && $cust !== []) { $isAdmin = 0; $companyName = $this->mprocCustomerPickField($cust, ['company_name', 'name']); if ($companyName === '') { try { $one = Db::table('purchase_order_detail')->where('phone', $phone)->order('id', 'desc')->find(); if (is_array($one)) { $companyName = trim((string)($one['company_name'] ?? '')); } } catch (\Throwable $e) { } } } elseif ($this->mprocAdminRowByMobile($phone)) { $isAdmin = 1; $companyName = ''; } else { $this->error('账号未开通权限,请联系管理员开通'); } $old = Session::get('mproc_token'); if ($old) { Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old)); } $token = bin2hex(random_bytes(16)); $userData = [ 'phone' => $phone, 'company_name' => $companyName, 'username' => '', 'login_type' => 'sms', 'is_admin' => $isAdmin ? 1 : 0, 'login_time' => time(), ]; // 缓存略长于逻辑有效期,过期以 login_time 为准 Cache::set('mproc_u_' . $token, $userData, $this->mprocTtlSeconds + 86400); Session::set('mproc_token', $token); Cookie::set('mproc_token', $token, $this->mprocTtlSeconds); $postR = $this->mprocSanitizeRedirectUrl($this->request->post('redirect', '')); $sessR = $this->mprocSanitizeRedirectUrl((string)Session::get('mproc_intended_url', '')); Session::delete('mproc_intended_url'); $raw = $postR !== '' ? $postR : $sessR; $jump = $this->mprocBuildAfterLoginIndexUrl($raw); $this->success('登录成功', $jump); } /** * 账号密码登录(POST:username、password) * 与后台 FastAdmin 一致:表 admin、密码 md5(md5(明文)+salt)、状态禁用与失败锁定规则同 admin/library/Auth::login * 成功后仅建立手机端外发明细会话(不写后台 Session,避免与 PC 后台互踢登录态) */ public function doLoginPwd() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $username = trim((string)$this->request->post('username', '')); $password = (string)$this->request->post('password', ''); if ($username === '' || $password === '') { $this->error('请输入账号和密码'); } // 直接用 Db 查 admin,避免加载 Admin 模型与 AdminAuth(连带 fast\Auth),缩短首包时间 $row = null; try { $row = Db::name('admin') ->field('id,username,password,salt,status,loginfailure,updatetime') ->where('username', $username) ->find(); } catch (\Throwable $e) { $row = null; } if (!$row || !is_array($row)) { $this->error('账号或密码错误'); } $id = (int)($row['id'] ?? 0); if (($row['status'] ?? '') == 'hidden') { $this->error('该账号已禁用'); } if (Config::get('fastadmin.login_failure_retry') && (int)($row['loginfailure'] ?? 0) >= 10 && time() - (int)($row['updatetime'] ?? 0) < 86400) { $this->error('登录失败次数过多,请24小时后再试'); } $salt = (string)($row['salt'] ?? ''); $hashStored = (string)($row['password'] ?? ''); $hashInput = md5(md5($password) . $salt); if ($hashStored === '' || $hashInput !== $hashStored) { if ($id > 0) { try { Db::name('admin')->where('id', $id)->update([ 'loginfailure' => (int)($row['loginfailure'] ?? 0) + 1, 'updatetime' => time(), ]); } catch (\Throwable $e) { } } $this->error('账号或密码错误'); } if ($id > 0) { try { Db::name('admin')->where('id', $id)->update([ 'loginfailure' => 0, 'updatetime' => time(), ]); } catch (\Throwable $e) { } } $old = Session::get('mproc_token'); if ($old) { Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old)); } $token = bin2hex(random_bytes(16)); $userData = [ 'phone' => '', 'company_name' => '', 'username' => $username, 'login_type' => 'pwd', 'is_admin' => 1, 'login_time' => time(), ]; Cache::set('mproc_u_' . $token, $userData, $this->mprocTtlSeconds + 86400); Session::set('mproc_token', $token); Cookie::set('mproc_token', $token, $this->mprocTtlSeconds); $postR = $this->mprocSanitizeRedirectUrl($this->request->post('redirect', '')); $sessR = $this->mprocSanitizeRedirectUrl((string)Session::get('mproc_intended_url', '')); Session::delete('mproc_intended_url'); $raw = $postR !== '' ? $postR : $sessR; $jump = $this->mprocBuildAfterLoginIndexUrl($raw); $this->success('登录成功', $jump); } /** * 是否允许当前登录用户修改该条 purchase_order_detail 的金额、交期 * 仅普通外协用户(短信登录且非管理员)可改;管理员手机号、账号密码登录仅可查看 */ protected function mprocCanEditRow(array $user, array $row) { if (!empty($user['is_admin'])) { return false; } if (($user['login_type'] ?? '') === 'pwd') { return false; } $uCo = trim((string)($user['company_name'] ?? '')); if ($uCo === '') { $uPhone = trim((string)($user['phone'] ?? '')); if ($uPhone !== '') { $uCo = $this->mprocResolveCompanyForLoginPhone($uPhone); } } $rCo = trim((string)($row['company_name'] ?? '')); if ($uCo !== '' && $rCo !== '' && strcmp($rCo, $uCo) === 0) { return true; } $uPhone = trim((string)($user['phone'] ?? '')); $rPhone = trim((string)($row['phone'] ?? '')); return $uPhone !== '' && $rPhone !== '' && strcasecmp($rPhone, $uPhone) === 0; } /** * 保存单条外发明细的金额、交期(POST:id、amount、delivery) */ public function mprocSave() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $user = $this->mprocGetUser(); if (!$user) { $this->error('请先登录', url('index/index/login')); } $id = (int)$this->request->post('id', 0); if ($id <= 0) { $this->error('参数错误'); } $row = null; try { $row = Db::table('purchase_order_detail')->where('id', $id)->find(); if (!$row) { $row = Db::table('purchase_order_detail')->where('ID', $id)->find(); } } catch (\Throwable $e) { $row = null; } if (!$row || !is_array($row)) { $this->error('记录不存在'); } if (!$this->mprocCanEditRow($user, $row)) { if (!empty($user['is_admin']) || (($user['login_type'] ?? '') === 'pwd')) { $this->error('当前账号仅可查看,不能修改金额与交货日期'); } $this->error('无权修改该记录'); } $amountRaw = trim((string)$this->request->post('amount', '')); $deliveryRaw = trim((string)$this->request->post('delivery', '')); $data = []; if ($amountRaw === '') { $data['amount'] = null; } else { if (!preg_match('/^-?\d+(\.\d{1,5})?$/', $amountRaw)) { $this->error('金额格式不正确,最多五位小数'); } $data['amount'] = $amountRaw; } if ($deliveryRaw === '') { $data['delivery'] = null; } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryRaw)) { // 仅选年月日:存 DATETIME,禁止写成 00:00:00——原记录有非零点时间则沿用,否则用当前服务器时分秒 $existingDel = isset($row['delivery']) ? trim((string)$row['delivery']) : ''; $timePart = date('H:i:s'); if ($existingDel !== '') { $tsEx = strtotime(str_replace('T', ' ', $existingDel)); if ($tsEx !== false) { $hms = date('H:i:s', $tsEx); if ($hms !== '00:00:00') { $timePart = $hms; } } } $data['delivery'] = $deliveryRaw . ' ' . $timePart; } else { $deliveryRaw = str_replace('T', ' ', $deliveryRaw); $ts = strtotime($deliveryRaw); if ($ts === false) { $this->error('交期时间格式不正确'); } $data['delivery'] = date('Y-m-d H:i:s', $ts); } $dcCol = $this->mprocResolveProcuremenColumn(['delivery_createtime', 'deliverycreatetime']); if ($dcCol !== null && array_key_exists('delivery', $data) && $data['delivery'] !== null && $data['delivery'] !== '') { $data[$dcCol] = date('Y-m-d H:i:s'); } // 同步 status_name(与列表 Tab 一致):金额或交期任一有有效数据 → 已提交,否则未提交;已是「已完成」不覆盖 $statusNameCol = $this->mprocResolveProcuremenColumn(['status_name', 'status_txt', 'status_text']); if ($statusNameCol !== null) { $curSn = ''; foreach ($row as $k => $v) { if (strcasecmp((string)$k, $statusNameCol) === 0) { $curSn = trim((string)$v); break; } } if ($curSn !== '已完成') { $effAm = array_key_exists('amount', $data) ? $data['amount'] : ($row['amount'] ?? null); $effDv = array_key_exists('delivery', $data) ? trim((string)$data['delivery']) : trim((string)($row['delivery'] ?? '')); $amountFilled = !($effAm === null || $effAm === '' || (is_string($effAm) && trim($effAm) === '')); $deliveryFilled = ($effDv !== '' && !preg_match('/^0000-00-00/i', $effDv)); $data[$statusNameCol] = ($amountFilled || $deliveryFilled) ? '已提交' : '未提交'; } } $pkField = isset($row['id']) ? 'id' : (isset($row['ID']) ? 'ID' : 'id'); $pkVal = (int)($row[$pkField] ?? $id); try { $aff = Db::table('purchase_order_detail')->where($pkField, $pkVal)->update($data); } catch (\Throwable $e) { $msg = $e->getMessage(); if (stripos($msg, 'Unknown column') !== false) { $msg = '请确认数据表 purchase_order_detail 已包含 amount、delivery 字段'; } $this->error('保存失败:' . $msg); } if ($aff === false) { $this->error('保存失败'); } $this->success('已保存'); } /** * 退出登录 */ public function logout() { $token = Session::get('mproc_token'); if ($token === null || $token === '') { $token = Cookie::get('mproc_token'); } if ($token) { $this->mprocClearLogin(preg_replace('/[^a-f0-9]/i', '', (string)$token)); } $this->redirect(url('index/index/login')); } /** * 短信宝(与后台外发审核一致,便于复用账号) * * @throws \Exception */ protected function mprocSmsSend($phone, $content) { $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) { throw new \Exception('短信发送失败:网络异常'); } $result = trim((string)$result); if ($result !== '0') { throw new \Exception('短信发送失败,错误码:' . $result); } } }