|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['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; } 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 是否允许登录(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) { Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old)); } $token = bin2hex(random_bytes(16)); $userData['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); } /** * 登录手机号对应的外协单位名称: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 ($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 行 * * @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()); Cache::set('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$token), $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' => '', ]; } /** * 将 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'] = ''; $row['mproc_this_quantity_display'] = $this->mprocResolveDisplayThisQuantity($row); } unset($row); return [ 'rows' => $rows ?: [], 'done_no_status' => (int)($statusNameCol === null), ]; } /** * 列表展示用「本次数量」:主表本次数量为空时回退显示 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) : ''; } /** * 外发明细首页(需登录) * 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; } $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 = (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); $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); 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')); } $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']); $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); $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 ($captcha === '') { $this->error('请输入图形验证码'); } if (!Validate::is($captcha, 'captcha')) { $this->error('图形验证码不正确'); } if (!preg_match('/^1\d{10}$/', $phone)) { $this->error('请输入正确的11位手机号'); } if (!$this->mprocFindCustomerUserByMobile($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); } $cu = $this->mprocFindCustomerUserByMobile($phone); if (!$cu) { $this->error('该手机号未开通或已禁用,请联系管理员'); } $this->mprocFinishLogin($this->mprocLoginPayloadFromCustomer($cu, 'sms')); } /** * 账号密码登录(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; } $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 = ''; if ($sid !== 0) { try { $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find(); } catch (\Throwable $e) { $po = null; } if (is_array($po)) { $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('记录不存在'); } if (!$this->mprocCanEditRow($user, $row)) { 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'); } // 同步 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) { $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); } } }