|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'); } $hours = (int)Config::get('mproc.session_hours'); if ($hours > 0) { $this->mprocTtlSeconds = max(1, min(720, $hours)) * 3600; } else { $days = (int)(Config::get('mproc.session_days') ?: 3); $days = max(1, min(30, $days)); $this->mprocTtlSeconds = $days * 86400; } if (PHP_VERSION_ID >= 70300) { ini_set('session.cookie_lifetime', (string)$this->mprocTtlSeconds); ini_set('session.gc_maxlifetime', (string)$this->mprocTtlSeconds); } } /** 登录有效小时数(用于前端 localStorage 过期时间) */ protected function mprocKeepHours(): int { return max(1, (int)round($this->mprocTtlSeconds / 3600)); } /** * 当前手机端登录用户;未登录返回 null(支持 Cookie 令牌 + 7 天记住登录) */ protected function mprocGetUser() { $token = $this->mprocReadTokenFromRequest(); if ($token !== '') { $user = $this->mprocLoadUserByToken($token); if ($user) { $token = $this->mprocTouchLoginState($user, $token); $user['token'] = $token; return $user; } } $user = $this->mprocUserFromRememberCookie(); if (!$user) { return null; } $token = $this->mprocPackSignedAuthToken($user); $token = $this->mprocTouchLoginState($user, $token); $user['token'] = $token; return $user; } protected function mprocReadTokenFromRequest(): string { $token = Session::get('mproc_token'); if ($token === null || $token === '') { $token = Cookie::get('mproc_token'); } if ($token === null || $token === '') { $token = $this->request->header('X-Mproc-Token'); } if ($token === null || $token === '') { $token = $this->request->request('mproc_token', ''); } $token = trim((string)$token); if ($token === '') { return ''; } if ($this->mprocIsSignedAuthToken($token)) { return $token; } $token = preg_replace('/[^a-f0-9]/i', '', $token); return strlen($token) >= 16 ? $token : ''; } protected function mprocIsSignedAuthToken(string $token): bool { return strpos($token, '.') !== false && preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/', $token) === 1; } protected function mprocAuthCacheKey(string $token): string { return 'mproc_u_' . md5($token); } /** * @return array|null */ protected function mprocLoadUserByToken(string $token): ?array { if ($this->mprocIsSignedAuthToken($token)) { $user = $this->mprocUserFromSignedToken($token); if (!$user) { return null; } if (time() - (int)($user['login_time'] ?? 0) > $this->mprocTtlSeconds) { $this->mprocClearLogin($token); return null; } $user['token'] = $token; return $user; } $user = Cache::get('mproc_u_' . $token); if (!is_array($user)) { $user = $this->mprocUserFromRememberCookie(); if (!$user) { return null; } } if (empty($user['phone']) && empty($user['account']) && empty($user['username'])) { return null; } if (time() - (int)($user['login_time'] ?? 0) > $this->mprocTtlSeconds) { $this->mprocClearLogin($token); return null; } $user['token'] = $token; return $user; } /** * 滑动续期:刷新签名令牌 / Cache / Session / Cookie(保留 7 天) * * @param array $user */ protected function mprocTouchLoginState(array $user, string $token): string { $user['login_time'] = time(); if ($this->mprocIsSignedAuthToken($token)) { $token = $this->mprocPackSignedAuthToken($user); } Cache::set($this->mprocAuthCacheKey($token), $user, $this->mprocTtlSeconds + 86400); if (!$this->mprocIsSignedAuthToken($token)) { Cache::set('mproc_u_' . $token, $user, $this->mprocTtlSeconds + 86400); } Session::set('mproc_token', $token); $this->mprocSetTokenCookie($token); $this->mprocSetRememberCookie($user, $token); return $token; } /** * @return array */ protected function mprocCookieOptions(): array { $opts = [ 'expire' => $this->mprocTtlSeconds, 'path' => '/', 'httponly' => true, ]; if ($this->request->isSsl()) { $opts['secure'] = true; } if (PHP_VERSION_ID >= 70300) { $opts['samesite'] = 'Lax'; } return $opts; } protected function mprocSetTokenCookie(string $token): void { Cookie::set('mproc_token', $token, $this->mprocCookieOptions()); } /** * @param array $user */ protected function mprocSetRememberCookie(array $user, ?string $token = null): void { $val = $token !== null && $token !== '' ? $token : $this->mprocPackSignedAuthToken($user); Cookie::set('mproc_remember', $val, $this->mprocCookieOptions()); } protected function mprocAuthSignSecret(): string { $key = trim((string)Config::get('mproc.auth_sign_key')); if ($key === '') { $key = (string)Config::get('database.database') . '|' . (string)Config::get('database.hostname') . '|mproc'; } return hash('sha256', $key); } /** * @param array $user */ protected function mprocPackSignedAuthToken(array $user): string { $payload = [ 'uid' => (int)($user['customer_user_id'] ?? $user['customer_id'] ?? 0), 'phone' => trim((string)($user['phone'] ?? '')), 'uname' => trim((string)($user['username'] ?? $user['account'] ?? '')), 'admin' => !empty($user['is_admin']) ? 1 : 0, 'lt' => time(), ]; $b64 = rtrim(strtr(base64_encode(json_encode($payload, JSON_UNESCAPED_UNICODE)), '+/', '-_'), '='); $sig = hash_hmac('sha256', $b64, $this->mprocAuthSignSecret()); return $b64 . '.' . $sig; } /** @deprecated 使用 mprocPackSignedAuthToken */ protected function mprocPackRememberCookie(array $user): string { return $this->mprocPackSignedAuthToken($user); } /** * @return array|null */ protected function mprocUserFromSignedToken(string $raw): ?array { $parts = explode('.', trim($raw), 2); if (count($parts) !== 2) { return null; } $b64 = $parts[0]; $sig = $parts[1]; if (!hash_equals(hash_hmac('sha256', $b64, $this->mprocAuthSignSecret()), $sig)) { return null; } $pad = strlen($b64) % 4; if ($pad > 0) { $b64 .= str_repeat('=', 4 - $pad); } $json = base64_decode(strtr($b64, '-_', '+/'), true); if ($json === false || $json === '') { return null; } $payload = json_decode($json, true); if (!is_array($payload)) { return null; } $lt = (int)($payload['lt'] ?? 0); if ($lt <= 0 || time() - $lt > $this->mprocTtlSeconds) { return null; } return $this->mprocRebuildUserFromRememberPayload($payload, $lt); } /** * @return array|null */ protected function mprocUserFromRememberCookie(): ?array { $raw = Cookie::get('mproc_remember'); if ($raw === null || $raw === '') { $raw = Cookie::get('mproc_token'); } if ($raw === null || $raw === '') { return null; } return $this->mprocUserFromSignedToken((string)$raw); } /** * @param array $payload * @return array|null */ protected function mprocRebuildUserFromRememberPayload(array $payload, int $loginTime): ?array { if (!empty($payload['admin'])) { $uname = trim((string)($payload['uname'] ?? '')); if ($uname === '') { return null; } try { $row = Db::name('admin')->where('username', $uname)->find(); } catch (\Throwable $e) { $row = null; } if (!is_array($row) || ($row['status'] ?? '') === 'hidden') { return null; } return [ 'phone' => trim((string)($row['mobile'] ?? '')), 'company_name' => '', 'username' => $uname, 'customer_user_id' => 0, 'login_type' => 'remember', 'is_admin' => 1, 'login_time' => $loginTime, ]; } $cid = (int)($payload['uid'] ?? 0); if ($cid > 0) { try { $cu = Db::table('customer')->where('id', $cid)->find(); } catch (\Throwable $e) { $cu = null; } if (is_array($cu) && $cu !== [] && $this->mprocCustomerUserActive($cu)) { $user = $this->mprocLoginPayloadFromCustomer($cu, 'remember'); $user['login_time'] = $loginTime; return $user; } } $phone = trim((string)($payload['phone'] ?? '')); if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) { $cu = $this->mprocFindCustomerUserByMobile($phone); if ($cu) { $user = $this->mprocLoginPayloadFromCustomer($cu, 'remember'); $user['login_time'] = $loginTime; return $user; } } return null; } protected function mprocClearLogin($token) { if ($token !== null && $token !== '') { Cache::rm($this->mprocAuthCacheKey((string)$token)); if (!$this->mprocIsSignedAuthToken((string)$token)) { Cache::rm('mproc_u_' . $token); } } Session::delete('mproc_token'); Cookie::delete('mproc_token'); Cookie::delete('mproc_remember'); } /** * 登录成功后的回跳地址校验(仅允许本站「协助明细订单页」路径,防止开放重定向) * * @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; } protected function mprocRememberFocusEid(int $focusEid): void { if ($focusEid > 0) { Session::set('mproc_focus_eid', $focusEid); } } protected function mprocPullSessionFocusEid(): int { $fe = (int)Session::get('mproc_focus_eid', 0); if ($fe > 0) { Session::delete('mproc_focus_eid'); } return $fe; } /** * 从 URI/query 中解析 focus_eid(兼容邮件客户端把 &focus_eid 拼进上一参数值的情况) */ protected function mprocParseFocusEidFromUriString(string $uri): int { if ($uri === '' || stripos($uri, 'focus_eid') === false) { return 0; } if (preg_match('/(?:[?&;]|%26)(?:amp;)*focus_eid=(\d+)/i', $uri, $m)) { return (int)$m[1]; } $query = parse_url($uri, PHP_URL_QUERY); if (!is_string($query) || $query === '') { return 0; } $q = []; parse_str($query, $q); if (!is_array($q)) { return 0; } if (isset($q['focus_eid'])) { $fe = (int)$q['focus_eid']; if ($fe > 0) { return $fe; } } foreach ($q as $k => $v) { $blob = (is_string($k) ? $k : '') . '=' . (is_scalar($v) ? (string)$v : ''); if (preg_match('/(?:^|[?&;]|%26)(?:amp;)*focus_eid=(\d+)/i', $blob, $m2)) { return (int)$m2[1]; } } return 0; } /** * 从请求中读取短信/邮件直达明细 ID(兼容 query、pathinfo、REQUEST_URI、登录回跳 Session) */ protected function mprocReadFocusEidFromRequest(): int { $fe = (int)$this->request->param('focus_eid', 0); if ($fe > 0) { $this->mprocRememberFocusEid($fe); return $fe; } $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''; $fe = $this->mprocParseFocusEidFromUriString($uri); if ($fe > 0) { $this->mprocRememberFocusEid($fe); return $fe; } $fe = (int)Session::get('mproc_focus_eid', 0); if ($fe > 0) { return $fe; } return 0; } /** * 手机端登录页 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 = []; $fe = 0; if (isset($q['focus_eid'])) { $fe = (int)$q['focus_eid']; } if ($fe <= 0) { $fe = $this->mprocParseFocusEidFromUriString($raw); } if ($fe <= 0) { $fe = (int)Session::get('mproc_focus_eid', 0); } if ($fe > 0) { $allowed['focus_eid'] = $fe; $this->mprocRememberFocusEid($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'; if (isset($allowed['focus_eid']) && (int)$allowed['focus_eid'] > 0 && !isset($allowed['q'])) { $fe = (int)$allowed['focus_eid']; if (!isset($allowed['tab']) && (!isset($allowed['main_tab']) || $allowed['main_tab'] === 'orders')) { return $base . $path . '?focus_eid=' . $fe . '#mproc_fe=' . $fe; } $ordered = ['focus_eid' => $fe]; if (isset($allowed['tab'])) { $ordered['tab'] = $allowed['tab']; } if (isset($allowed['main_tab'])) { $ordered['main_tab'] = $allowed['main_tab']; } return $base . $path . '?' . http_build_query($ordered, '', '&', PHP_QUERY_RFC3986) . '#mproc_fe=' . $fe; } $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 是否允许登录(status:1 / 正常;空视为可登录以兼容旧数据) */ protected function mprocCustomerUserActive(array $row): bool { $st = $row['status'] ?? ''; if ($st === '' || $st === null) { return true; } return $st === 1 || $st === '1'; } /** * @return array|null */ protected function mprocFindCustomerUserByMobile(string $phone): ?array { return $this->mprocFindCustomerRowByPhone($phone); } /** * 按登录账号(account)查找 customer;兼容旧会话把 11 位手机号写在 username 里 * * @return array|null */ protected function mprocFindCustomerUserByUsername(string $username): ?array { $username = trim($username); if ($username === '') { return null; } try { $row = Db::table('customer')->where('account', $username)->order('id', 'asc')->find(); } catch (\Throwable $e) { $row = null; } if ((!is_array($row) || $row === []) && preg_match('/^1\d{10}$/', $username)) { $row = $this->mprocFindCustomerRowByPhone($username); } if (!is_array($row) || $row === [] || !$this->mprocCustomerUserActive($row)) { return null; } return $row; } /** * customer 密码校验(md5(md5) 无 salt;兼容 bcrypt) */ protected function mprocVerifyCustomerUserPassword(array $row, string $password): bool { $stored = (string)($row['password'] ?? ''); if ($stored === '' || $password === '') { return false; } if (preg_match('/^\$2[ayb]\$/', $stored)) { return password_verify($password, $stored); } return hash_equals($stored, md5(md5($password))); } /** * 生成 customer 登录密码密文 */ protected function mprocHashCustomerUserPassword(string $password, string $existingSalt = ''): string { unset($existingSalt); return md5(md5($password)); } /** * @param array $cu * @return array */ protected function mprocLoginPayloadFromCustomer(array $cu, string $loginType): array { $phone = trim((string)($cu['phone'] ?? '')); $account = trim((string)($cu['account'] ?? '')); if ($phone === '' && preg_match('/^1\d{10}$/', $account)) { $phone = $account; } if ($account === '' && preg_match('/^1\d{10}$/', $phone)) { $account = $phone; } $companyName = trim((string)($cu['company_name'] ?? '')); if ($companyName === '' && $phone !== '') { $companyName = $this->mprocResolveCompanyForLoginPhone($phone); } $id = (int)($cu['id'] ?? 0); return [ 'phone' => $phone, 'account' => $account, 'company_name' => $companyName, 'username' => trim((string)($cu['username'] ?? '')), 'customer_id' => $id, 'customer_user_id' => $id, 'login_type' => $loginType, 'is_admin' => 0, ]; } /** * 写入手机端登录态并返回跳转 URL * * @param array $userData */ protected function mprocFinishLogin(array $userData): void { $old = Session::get('mproc_token'); if ($old) { $this->mprocClearLogin((string)$old); } $userData['login_time'] = time(); $token = $this->mprocPackSignedAuthToken($userData); $token = $this->mprocTouchLoginState($userData, $token); $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, [ 'mproc_token' => $token, 'keep_hours' => $this->mprocKeepHours(), 'keep_days' => max(1, (int)round($this->mprocTtlSeconds / 86400)), ]); } /** * 登录手机号对应的外协单位名称: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 或 account 单值相等) * * @return array|null */ protected function mprocFindCustomerRowByPhone(string $phone): ?array { $phone = trim($phone); if ($phone === '' || !preg_match('/^1\d{10}$/', $phone)) { return null; } try { $row = Db::table('customer') ->where(function ($q) use ($phone) { $q->where('phone', $phone)->whereOr('account', $phone); }) ->order('id', 'asc') ->find(); } catch (\Throwable $e) { return null; } if (!is_array($row) || $row === [] || !$this->mprocCustomerUserActive($row)) { return null; } return $row; } /** * 管理员表 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 表中匹配当前用户:优先手机号,否则按公司名 * * @return array|null */ protected function mprocFindCustomerRowForUser(array $user): ?array { $phone = trim((string)($user['phone'] ?? '')); if ($phone === '') { $phone = trim((string)($user['account'] ?? '')); } $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 ($this->mprocIsValidScydgyRowId($id)) { $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 行 * * @return array|null */ protected function mprocResolveCustomerUserForSession(array $user): ?array { if (!empty($user['is_admin'])) { return null; } $cuId = (int)($user['customer_id'] ?? $user['customer_user_id'] ?? 0); if ($cuId > 0) { try { $row = Db::table('customer')->where('id', $cuId)->find(); } catch (\Throwable $e) { $row = null; } if (is_array($row) && $row !== [] && $this->mprocCustomerUserActive($row)) { return $row; } } $account = trim((string)($user['account'] ?? '')); if ($account !== '') { $byAcc = $this->mprocFindCustomerUserByUsername($account); if ($byAcc !== null) { return $byAcc; } } $phone = trim((string)($user['phone'] ?? '')); if ($phone !== '' && preg_match('/^1\d{10}$/', $phone)) { return $this->mprocFindCustomerUserByMobile($phone); } $uname = trim((string)($user['username'] ?? '')); if ($uname !== '' && preg_match('/^1\d{10}$/', $uname)) { return $this->mprocFindCustomerUserByMobile($uname); } return null; } /** * customer 表字段 →「我的」展示结构 * * @param array $cu * @return array{company_name:string,contact_name:string,phone:string,email:string} */ protected function mprocProfileFromCustomerUserRow(array $cu): array { $nm = trim((string)($cu['username'] ?? '')); $phone = trim((string)($cu['phone'] ?? '')); if ($phone === '') { $phone = trim((string)($cu['account'] ?? '')); } return [ 'company_name' => trim((string)($cu['company_name'] ?? '')), 'contact_name' => $nm, 'phone' => $phone, 'email' => trim((string)($cu['email'] ?? '')), ]; } /** * 管理员「我的」:admin 表 * * @return array{company_name:string,contact_name:string,phone:string,email:string} */ protected function mprocProfileForAdmin(array $user): array { $uname = trim((string)($user['username'] ?? '')); $out = [ 'company_name' => '管理员', 'contact_name' => $uname !== '' ? $uname : '管理员', 'phone' => trim((string)($user['phone'] ?? '')), 'email' => '', ]; if ($uname === '') { return $out; } try { $row = Db::name('admin')->where('username', $uname)->find(); } catch (\Throwable $e) { $row = null; } if (!is_array($row) || $row === []) { return $out; } $nick = trim((string)($row['nickname'] ?? '')); if ($nick !== '') { $out['contact_name'] = $nick; } $mob = trim((string)($row['mobile'] ?? '')); if ($mob !== '') { $out['phone'] = $mob; } $em = trim((string)($row['email'] ?? '')); if ($em !== '') { $out['email'] = $em; } return $out; } /** * 旧会话补全 customer_id 等字段 */ protected function mprocSyncSessionCustomerUser(array $user): array { if (!empty($user['is_admin'])) { return $user; } $cu = $this->mprocResolveCustomerUserForSession($user); if (!$cu) { return $user; } $id = (int)($cu['id'] ?? 0); $user['customer_id'] = $id; $user['customer_user_id'] = $id; $user['username'] = trim((string)($cu['username'] ?? $user['username'] ?? '')); $user['company_name'] = trim((string)($cu['company_name'] ?? '')); $user['account'] = trim((string)($cu['account'] ?? '')); $mob = trim((string)($cu['phone'] ?? '')); if ($mob === '') { $mob = trim((string)($cu['account'] ?? '')); } if ($mob !== '') { $user['phone'] = $mob; } $token = Session::get('mproc_token'); if ($token) { $user['login_time'] = (int)($user['login_time'] ?? time()); $tok = trim((string)$token); Cache::set($this->mprocAuthCacheKey($tok), $user, $this->mprocTtlSeconds + 86400); if (!$this->mprocIsSignedAuthToken($tok)) { Cache::set('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', $tok), $user, $this->mprocTtlSeconds + 86400); } } return $user; } /** * 「我的」:普通用户 customer;管理员 admin */ protected function mprocProfileForUser(array $user) { if (!empty($user['is_admin'])) { return $this->mprocProfileForAdmin($user); } $cu = $this->mprocResolveCustomerUserForSession($user); if (is_array($cu) && $cu !== []) { return $this->mprocProfileFromCustomerUserRow($cu); } $phone = trim((string)($user['phone'] ?? '')); if ($phone === '') { $phone = trim((string)($user['account'] ?? '')); } return [ 'company_name' => trim((string)($user['company_name'] ?? '')), 'contact_name' => trim((string)($user['username'] ?? '')), 'phone' => $phone, 'email' => '', ]; } protected function mprocIsValidScydgyRowId($id): bool { return (int)$id !== 0; } /** * 将 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 列名大小写可能不一致 $qtyRaw = ''; foreach (['this_quantity', 'This_quantity'] as $qk) { if (array_key_exists($qk, $pl) && $pl[$qk] !== null && $pl[$qk] !== '') { $qtyRaw = trim((string)$pl[$qk]); break; } } if ($qtyRaw === '') { foreach (['This_quantity', 'this_quantity'] as $qk) { if (array_key_exists($qk, $poRow) && $poRow[$qk] !== null && $poRow[$qk] !== '') { $qtyRaw = trim((string)$poRow[$qk]); break; } } } if ($qtyRaw !== '') { $row['This_quantity'] = $qtyRaw; } $ceilRaw = ''; foreach (['ceilingprice', 'ceiling_price', 'CeilingPrice'] as $ck) { if (array_key_exists($ck, $pl) && $pl[$ck] !== null && $pl[$ck] !== '') { $ceilRaw = trim((string)$pl[$ck]); break; } } if ($ceilRaw === '') { foreach (['ceilingPrice', 'ceiling_price', 'CeilingPrice'] as $ck) { if (array_key_exists($ck, $poRow) && $poRow[$ck] !== null && $poRow[$ck] !== '') { $ceilRaw = trim((string)$poRow[$ck]); break; } } } if ($ceilRaw !== '') { $row['ceilingPrice'] = $ceilRaw; } } /** * 查询 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'); // 初选下发已向供应商发送通知后,手机端可见 wflow_status>=1 的明细 $userWhere = $this->mprocListWhereForLoginUser($user); if ($userWhere !== []) { $query->where($userWhere); } if (trim((string)$q) === '' && $tab === 'done' && $statusNameCol !== null) { $this->mprocSyncLegacyApprovedStatusNames($user, $statusNameCol); } $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 ($this->mprocIsValidScydgyRowId($sid0)) { $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 ($this->mprocIsValidScydgyRowId($sidk)) { $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 ($this->mprocIsValidScydgyRowId($sid) && isset($poBySid[$sid])) { $this->mprocMergePurchaseOrderIntoDetail($row, $poBySid[$sid]); } $poRow = ($this->mprocIsValidScydgyRowId($sid) && isset($poBySid[$sid])) ? $poBySid[$sid] : null; $oldSn = trim((string)($row['status_name'] ?? '')); $effectiveSn = $this->mprocResolveEffectiveStatusName($row, $poRow); $row['status_name'] = $effectiveSn; if ($statusNameCol !== null && $effectiveSn !== $oldSn && $row['eid'] > 0) { $this->mprocPersistDetailStatusName((int)$row['eid'], $statusNameCol, $effectiveSn); } // 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'] = ''; $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row); } unset($row); if (trim((string)$q) === '' && $statusNameCol !== null) { $tabLabelMap = [ 'draft' => '未提交', 'submitted' => '已提交', 'done' => '已完成', ]; $expectLabel = $tabLabelMap[$tab] ?? '未提交'; $rows = array_values(array_filter($rows, function ($r) use ($expectLabel) { return is_array($r) && trim((string)($r['status_name'] ?? '')) === $expectLabel; })); } return [ 'rows' => $rows ?: [], 'done_no_status' => (int)($statusNameCol === null), ]; } /** * status_name → 手机端左侧 Tab */ protected function mprocStatusNameToListTab(string $statusName): string { $map = ['未提交' => 'draft', '已提交' => 'submitted', '已完成' => 'done']; $sn = trim($statusName); return isset($map[$sn]) ? $map[$sn] : ''; } /** * 短信/邮件 focus_eid 应落在的列表 Tab */ protected function mprocResolveListTabForFocusEid(int $focusEid, array $user): string { if ($focusEid <= 0) { return ''; } $idCol = $this->mprocResolveProcuremenColumn(['id']); if ($idCol === null) { return ''; } try { $qrow = Db::table('purchase_order_detail')->where($idCol, $focusEid); $qw = $this->mprocListWhereForLoginUser($user); if ($qw !== []) { $qrow->where($qw); } $dr = $qrow->find(); } catch (\Throwable $e) { $dr = null; } if (!is_array($dr) || $dr === []) { return ''; } $sid = (int)($dr['scydgy_id'] ?? $dr['SCYDGY_ID'] ?? 0); $po = null; if ($this->mprocIsValidScydgyRowId($sid)) { try { $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); } catch (\Throwable $e) { $po = null; } } return $this->mprocStatusNameToListTab( $this->mprocResolveEffectiveStatusName($dr, is_array($po) ? $po : null) ); } /** * 短信/邮件直达链接:确保 focus 对应明细出现在当前列表(便于高亮定位) * * @param array $bundle * @param array $user * @return array */ protected function mprocEnsureFocusRowInList( array $bundle, int $focusEid, array $user, $statusNameCol, string $tab = 'draft', string $q = '' ): array { if ($focusEid <= 0) { return $bundle; } $rows = isset($bundle['rows']) && is_array($bundle['rows']) ? $bundle['rows'] : []; $foundIdx = -1; foreach ($rows as $idx => $r) { if (!is_array($r)) { continue; } if ((int)($r['eid'] ?? $r['id'] ?? $r['ID'] ?? 0) === $focusEid) { $foundIdx = (int)$idx; break; } } if ($foundIdx > 0) { $hit = $rows[$foundIdx]; array_splice($rows, $foundIdx, 1); array_unshift($rows, $hit); $bundle['rows'] = $rows; return $bundle; } if ($foundIdx === 0) { return $bundle; } $idCol = $this->mprocResolveProcuremenColumn(['id']); if ($idCol === null) { return $bundle; } try { $qrow = Db::table('purchase_order_detail')->where($idCol, $focusEid); $qw = $this->mprocListWhereForLoginUser($user); if ($qw !== []) { $qrow->where($qw); } $dr = $qrow->find(); } catch (\Throwable $e) { $dr = null; } if (!is_array($dr) || $dr === []) { return $bundle; } $row = $dr; $row['eid'] = (int)($row['id'] ?? $row['ID'] ?? $focusEid); $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0); $poRow = null; if ($this->mprocIsValidScydgyRowId($sid)) { try { $poRow = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); } catch (\Throwable $e) { $poRow = null; } if (is_array($poRow)) { $this->mprocMergePurchaseOrderIntoDetail($row, $poRow); } } $effectiveSn = $this->mprocResolveEffectiveStatusName($row, is_array($poRow) ? $poRow : null); if (trim((string)$q) === '') { $rowTab = $this->mprocStatusNameToListTab($effectiveSn); if ($rowTab !== '' && $rowTab !== $tab) { return $bundle; } } $row['status_name'] = $effectiveSn; $row['mproc_can_edit'] = $this->mprocCanEditRow($user, $row) ? 1 : 0; $am = $row['amount'] ?? null; $row['amount_display'] = ($am === null || $am === '' || (is_string($am) && trim($am) === '')) ? '' : (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'] = ''; $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row); array_unshift($rows, $row); $bundle['rows'] = $rows; return $bundle; } /** * 列表展示用「本次数量」:主表本次数量为空时回退显示 NGZL(工作量) * * @param array $row */ protected function mprocResolveDisplayThisQuantity(array $row): string { $qty = trim((string)($row['This_quantity'] ?? $row['this_quantity'] ?? '')); if ($qty !== '') { return $qty; } $gzl = $row['NGZL'] ?? $row['ngzl'] ?? ''; if ($gzl === null || $gzl === '') { return ''; } return is_scalar($gzl) ? trim((string)$gzl) : ''; } /** * 明细是否已填写单价或交货日期 * * @param array $row */ protected function mprocDetailQuoteSubmitted(array $row): bool { $am = $row['amount'] ?? null; $dv = isset($row['delivery']) ? trim((string)$row['delivery']) : ''; $amountFilled = !($am === null || $am === '' || (is_string($am) && trim($am) === '')); $deliveryFilled = ($dv !== '' && !preg_match('/^0000-00-00/i', $dv)); return $amountFilled || $deliveryFilled; } /** * 手机端列表 Tab 用 status_name;审批通过后主表 status=1 时按明细 status 纠偏 * * @param array $row * @param array|null $po */ protected function mprocResolveEffectiveStatusName(array $row, ?array $po): string { $sn = trim((string)($row['status_name'] ?? '')); if (in_array($sn, ['已完成', '未通过', '已废弃'], true)) { return $sn; } if (!is_array($po)) { if ($sn !== '') { return $sn; } return $this->mprocDetailQuoteSubmitted($row) ? '已提交' : '未提交'; } $poStatus = $po['status'] ?? $po['STATUS'] ?? ''; if (!ProcuremenStatus::isPoCompleted($poStatus)) { if ($sn !== '') { return $sn; } return $this->mprocDetailQuoteSubmitted($row) ? '已提交' : '未提交'; } $detailStatus = $row['status'] ?? $row['STATUS'] ?? ''; if (ProcuremenStatus::isPodPicked($detailStatus)) { return '已完成'; } if ($sn === '已提交') { return '未通过'; } return $sn !== '' ? $sn : '未提交'; } /** * 将纠偏后的 status_name 写回库表(兼容历史已审批数据) */ protected function mprocPersistDetailStatusName(int $detailId, string $statusNameCol, string $statusName): void { if ($detailId <= 0 || $statusNameCol === '') { return; } $idCol = $this->mprocResolveProcuremenColumn(['id']); if ($idCol === null || $idCol === '') { return; } try { Db::table('purchase_order_detail')->where($idCol, $detailId)->update([$statusNameCol => $statusName]); } catch (\Throwable $e) { } } /** * 纠偏历史数据:主表已审批(status=1)但明细 status_name 仍为「已提交」 * * @param array $user */ protected function mprocSyncLegacyApprovedStatusNames(array $user, string $statusNameCol): void { $userWhere = $this->mprocListWhereForLoginUser($user); try { $query = Db::table('purchase_order_detail')->where($statusNameCol, '已提交'); if ($userWhere !== []) { $query->where($userWhere); } $candidates = $query->limit(200)->select(); } catch (\Throwable $e) { return; } if (!is_array($candidates) || $candidates === []) { return; } $sidList = []; foreach ($candidates as $cr) { if (!is_array($cr)) { continue; } $sid = (int)($cr['scydgy_id'] ?? $cr['SCYDGY_ID'] ?? 0); if ($this->mprocIsValidScydgyRowId($sid)) { $sidList[$sid] = true; } } if ($sidList === []) { return; } $poBySid = []; try { $poRows = Db::table('purchase_order') ->where('scydgy_id', 'in', array_keys($sidList)) ->whereIn('status', ProcuremenStatus::poCompletedValues()) ->select(); if (is_array($poRows)) { foreach ($poRows as $pr) { $sidk = (int)($pr['scydgy_id'] ?? $pr['SCYDGY_ID'] ?? 0); if ($this->mprocIsValidScydgyRowId($sidk)) { $poBySid[$sidk] = $pr; } } } } catch (\Throwable $e) { return; } if ($poBySid === []) { return; } $idCol = $this->mprocResolveProcuremenColumn(['id']); if ($idCol === null || $idCol === '') { return; } foreach ($candidates as $cr) { if (!is_array($cr)) { continue; } $sid = (int)($cr['scydgy_id'] ?? $cr['SCYDGY_ID'] ?? 0); if (!isset($poBySid[$sid])) { continue; } $detailId = (int)($cr[$idCol] ?? $cr['id'] ?? $cr['ID'] ?? 0); if ($detailId <= 0) { continue; } $targetSn = $this->mprocResolveEffectiveStatusName($cr, $poBySid[$sid]); if ($targetSn === '已提交') { continue; } $this->mprocPersistDetailStatusName($detailId, $statusNameCol, $targetSn); } } /** * 协助明细首页(需登录) * GET:main_tab=orders|me,orders 时 tab=draft|submitted|done 对应 status_name:未提交|已提交|已完成;q 搜索词 */ public function index() { $user = $this->mprocGetUser(); if (!$user) { $pendingFocus = $this->mprocReadFocusEidFromRequest(); if ($pendingFocus > 0) { $this->mprocRememberFocusEid($pendingFocus); } $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''; $safe = $this->mprocSanitizeRedirectUrl($uri); if ($safe !== '' && $pendingFocus > 0 && stripos($safe, 'focus_eid') === false) { $safe .= (strpos($safe, '?') !== false ? '&' : '?') . 'focus_eid=' . $pendingFocus; } if ($safe !== '') { Session::set('mproc_intended_url', $safe); } $this->redirect($this->mprocBuildLoginUrl($safe)); return; } $user = $this->mprocSyncSessionCustomerUser($user); $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 = $this->mprocReadFocusEidFromRequest(); if ($focusEid > 0 && $mainTab === 'orders') { $mprocFocusEid = $focusEid; // 仅邮件/短信直链(URL 带 focus_eid)时自动切 Tab;Session 记忆不覆盖用户手动点的状态 $focusFromUrl = (int)$this->request->param('focus_eid', 0) > 0; if (!$focusFromUrl) { $uri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''; $focusFromUrl = $this->mprocParseFocusEidFromUriString($uri) > 0; } if ($focusFromUrl && trim((string)$q) === '') { $resolvedTab = $this->mprocResolveListTabForFocusEid($focusEid, $user); if ($resolvedTab !== '') { $tab = $resolvedTab; } } } // 左侧 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); $cid = (int)($user['customer_id'] ?? $user['customer_user_id'] ?? 0); $this->view->assign('mprocCanChangePwd', empty($user['is_admin']) && $cid > 0 ? 1 : 0); $this->view->assign('mprocFocusEid', $mprocFocusEid); $mprocFocusTab = $mprocFocusEid > 0 ? $this->mprocResolveListTabForFocusEid($mprocFocusEid, $user) : ''; $this->view->assign('mprocFocusTab', $mprocFocusTab); $this->view->assign('mprocBootstrapToken', trim((string)($user['token'] ?? ''))); $this->view->assign('mprocBootstrapKeepHours', $this->mprocKeepHours()); if ($mainTab === 'me') { $this->view->assign('rows', []); return $this->view->fetch(); } $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol); if ($mprocFocusEid > 0) { $bundle = $this->mprocEnsureFocusRowInList($bundle, $mprocFocusEid, $user, $statusNameCol, $tab, $q); } $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')); } $user = $this->mprocSyncSessionCustomerUser($user); $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']); $focusEid = $this->mprocReadFocusEidFromRequest(); $focusTab = $focusEid > 0 ? $this->mprocResolveListTabForFocusEid($focusEid, $user) : ''; $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol); if ($focusEid > 0) { $bundle = $this->mprocEnsureFocusRowInList($bundle, $focusEid, $user, $statusNameCol, $tab, $q); } $this->success('ok', '', array_merge([ 'main_tab' => 'orders', 'tab' => $tab, 'focus_tab' => $focusTab, 'is_admin' => !empty($user['is_admin']) ? 1 : 0, 'focus_eid' => $focusEid, ], $bundle)); } /** * 登录页(手机号验证码 / 账号密码) */ public function login() { $redirect = $this->mprocSanitizeRedirectUrl($this->request->get('redirect', '')); if ($this->mprocGetUser()) { $this->redirect($this->mprocBuildAfterLoginIndexUrl($redirect)); } if ($redirect !== '') { Session::set('mproc_intended_url', $redirect); } $this->view->assign('mprocLoginRedirect', $redirect); $this->view->assign('mprocCaptchaUrl', url('index/index/captcha')); $this->view->assign('mprocCaptchaLen', (int)(Config::get('captcha.length') ?: 4)); return $this->view->fetch(); } /** * 图形验证码(手机号登录用) */ public function captcha($id = '') { $captcha = new Captcha((array)Config::get('captcha')); return $captcha->entry($id); } /** * 发送登录验证码(POST:phone、captcha) */ public function sendSms() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $phone = trim((string)$this->request->post('phone', '')); $captcha = trim((string)$this->request->post('captcha', '')); if (!preg_match('/^1\d{10}$/', $phone)) { $this->error('请输入正确的11位手机号'); } if ($captcha === '') { $this->error('请输入图形验证码'); } if (!$this->mprocFindCustomerUserByMobile($phone)) { $this->error('该手机号未开通或已禁用,请联系管理员'); } $cd = (int)(Config::get('mproc.sms_resend_cd') ?: 55); if (Cache::get('mproc_sms_wait_' . $phone)) { $this->error('发送过于频繁,请稍后再试'); } if (!Validate::is($captcha, 'captcha')) { $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 { $tpl = trim((string)Config::get('mproc.sms_login_template')); if ($tpl === '') { $tpl = '【可集达】您的验证码是{code}。如非本人操作,请忽略本短信'; } $content = str_replace('{code}', $code, $tpl); $this->mprocSmsSend($phone, $content); } 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); } $cu = $this->mprocFindCustomerUserByMobile($phone); if (!$cu) { $this->error('该手机号未开通或已禁用,请联系管理员'); } $this->mprocFinishLogin($this->mprocLoginPayloadFromCustomer($cu, 'sms')); } /** * 用本地保存的 token 恢复登录态(POST:mproc_token) */ public function mprocRestore() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $token = $this->mprocReadTokenFromRequest(); $user = null; if ($token !== '') { $user = $this->mprocLoadUserByToken($token); } if (!$user) { $user = $this->mprocUserFromRememberCookie(); if ($user) { $token = $this->mprocPackSignedAuthToken($user); } } if (!$user) { $this->error('登录已过期,请重新登录', url('index/index/login')); } $token = $this->mprocTouchLoginState($user, $token !== '' ? $token : $this->mprocPackSignedAuthToken($user)); $redirect = $this->mprocSanitizeRedirectUrl($this->request->post('redirect', '')); $jump = $this->mprocBuildAfterLoginIndexUrl($redirect); $this->success('ok', $jump, [ 'mproc_token' => $token, 'keep_hours' => $this->mprocKeepHours(), 'keep_days' => max(1, (int)round($this->mprocTtlSeconds / 86400)), ]); } /** * 账号密码登录(POST:username、password) * 先 customer(account),未命中再 admin;admin 密码规则同 FastAdmin Auth::login */ 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('请输入账号和密码'); } $cu = $this->mprocFindCustomerUserByUsername($username); if ($cu) { if (!$this->mprocVerifyCustomerUserPassword($cu, $password)) { $this->error('账号或密码错误'); } $this->mprocFinishLogin($this->mprocLoginPayloadFromCustomer($cu, 'pwd')); } // 管理员:表 admin $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) { } } $this->mprocFinishLogin([ 'phone' => trim((string)($row['mobile'] ?? '')), 'company_name' => '', 'username' => $username, 'customer_user_id' => 0, 'login_type' => 'pwd', 'is_admin' => 1, ]); } /** * 是否允许当前登录用户修改该条 purchase_order_detail 的金额、交期 * 仅普通用户(customer)可改;管理员(admin)仅可查看 */ protected function mprocCanEditRow(array $user, array $row) { if (!empty($user['is_admin'])) { return false; } $sn = trim((string)($row['status_name'] ?? '')); if (in_array($sn, ['已完成', '未通过', '已废弃'], true)) { 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; } /** * 明细行对应最高限价(来自 purchase_order;无或无效则返回 null,不校验) * * @param array $detailRow */ protected function mprocResolveCeilingPriceForDetailRow(array $detailRow): ?float { $sid = (int)($detailRow['scydgy_id'] ?? $detailRow['SCYDGY_ID'] ?? 0); $raw = trim((string)($detailRow['ceilingPrice'] ?? $detailRow['ceiling_price'] ?? '')); if ($raw === '' && $this->mprocIsValidScydgyRowId($sid)) { try { $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); } catch (\Throwable $e) { $po = null; } if (is_array($po)) { $pl = array_change_key_case($po, CASE_LOWER); foreach (['ceilingprice', 'ceiling_price'] as $ck) { if (array_key_exists($ck, $pl) && $pl[$ck] !== null && $pl[$ck] !== '') { $raw = trim((string)$pl[$ck]); break; } } if ($raw === '') { $raw = trim((string)($po['ceilingPrice'] ?? $po['ceiling_price'] ?? '')); } } } if ($raw === '' || !preg_match('/^-?\d+(\.\d{1,5})?$/', $raw)) { return null; } return (float)$raw; } protected function mprocFormatCeilingPriceDisplay(float $n): string { $s = rtrim(rtrim(sprintf('%.5F', $n), '0'), '.'); return $s === '' ? '0' : $s; } /** * 保存单条协助明细的金额、交期(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('记录不存在'); } $sid = (int)($row['scydgy_id'] ?? $row['SCYDGY_ID'] ?? 0); $po = null; if ($this->mprocIsValidScydgyRowId($sid)) { try { $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); } catch (\Throwable $e) { $po = null; } } $effectiveSn = $this->mprocResolveEffectiveStatusName($row, is_array($po) ? $po : null); if (in_array($effectiveSn, ['已完成', '未通过', '已废弃'], true)) { $this->error('订单已结束,不能再修改单价与交货日期'); } if (!$this->mprocCanEditRow($user, array_merge($row, ['status_name' => $effectiveSn]))) { if (!empty($user['is_admin'])) { $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('单价格式不正确,最多五位小数'); } $ceilingLimit = $this->mprocResolveCeilingPriceForDetailRow($row); if ($ceilingLimit !== null && (float)$amountRaw > $ceilingLimit) { $this->error('单价不能超过最高限价 ' . $this->mprocFormatCeilingPriceDisplay($ceilingLimit)); } $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'); } $upCol = $this->mprocResolveProcuremenColumn(['updatetime']); if ($upCol !== null) { $data[$upCol] = 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('已保存'); } /** * 普通用户修改密码(POST:old_password、new_password、renew_password) */ public function mprocChangePwd() { if (!$this->request->isPost()) { $this->error('请使用 POST'); } $user = $this->mprocGetUser(); if (!$user) { $this->error('请先登录', url('index/index/login')); } $user = $this->mprocSyncSessionCustomerUser($user); if (!empty($user['is_admin'])) { $this->error('当前账号不支持修改密码'); } $cu = $this->mprocResolveCustomerUserForSession($user); if (!$cu) { $this->error('账号不存在或已禁用'); } $oldPwd = (string)$this->request->post('old_password', ''); $newPwd = (string)$this->request->post('new_password', ''); $renewPwd = (string)$this->request->post('renew_password', ''); if ($oldPwd === '' || $newPwd === '' || $renewPwd === '') { $this->error('请填写完整'); } if (strlen($newPwd) < 4) { $this->error('新密码至少4位'); } if ($newPwd !== $renewPwd) { $this->error('两次输入的新密码不一致'); } if ($oldPwd === $newPwd) { $this->error('新密码不能与旧密码相同'); } $cuId = (int)($cu['id'] ?? 0); if (!$this->mprocVerifyCustomerUserPassword($cu, $oldPwd)) { $this->error('原密码不正确'); } $data = [ 'password' => $this->mprocHashCustomerUserPassword($newPwd), 'updatetime' => date('Y-m-d H:i:s'), ]; try { Db::table('customer')->where('id', $cuId)->update($data); } catch (\Throwable $e) { $this->error('修改失败:' . $e->getMessage()); } $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) { $statusStr = [ '0' => '短信发送成功', '-1' => '参数不全', '-2' => '服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间!', '30' => '密码错误', '40' => '账号不存在', '41' => '余额不足', '42' => '帐户已过期', '43' => 'IP地址限制', '50' => '内容含有敏感词', ]; $smsapi = 'http://api.smsbao.com/'; $user = trim((string)Config::get('mproc.smsbao_user')); if ($user === '') { $user = 'zhuwei123'; } $passPlain = Config::get('mproc.smsbao_pass'); $pass = ($passPlain !== null && $passPlain !== '') ? md5((string)$passPlain) : md5('1d1e605c101e4c1f8a156c6d7b19f126'); $phone = trim((string)$phone); $content = trim((string)$content); if ($phone === '' || $content === '') { throw new \Exception('短信发送失败:参数不全'); } $sendurl = $smsapi . 'sms?u=' . rawurlencode($user) . '&p=' . $pass . '&m=' . rawurlencode($phone) . '&c=' . rawurlencode($content); $result = @file_get_contents($sendurl); if ($result === false) { Log::record('smsbao 请求失败 phone=' . $phone . ' content=' . $content, 'error'); throw new \Exception('短信发送失败:网络异常'); } $result = trim((string)$result); if ($result !== '0') { $msg = isset($statusStr[$result]) ? $statusStr[$result] : ('返回码 ' . $result); Log::record('smsbao 发送失败 phone=' . $phone . ' code=' . $result . ' ' . $msg . ' content=' . $content, 'error'); throw new \Exception('短信发送失败:' . $msg); } Log::record('smsbao 发送成功 phone=' . $phone . ' content=' . $content, 'info'); } }