liuhairui 2 semanas atrás
pai
commit
b6ef96dbaa

+ 208 - 38
application/index/controller/Index.php

@@ -50,7 +50,8 @@ class Index extends Frontend
         if ($token !== '') {
             $user = $this->mprocLoadUserByToken($token);
             if ($user) {
-                $this->mprocTouchLoginState($user, $token);
+                $token = $this->mprocTouchLoginState($user, $token);
+                $user['token'] = $token;
 
                 return $user;
             }
@@ -60,8 +61,8 @@ class Index extends Frontend
         if (!$user) {
             return null;
         }
-        $token = bin2hex(random_bytes(16));
-        $this->mprocTouchLoginState($user, $token);
+        $token = $this->mprocPackSignedAuthToken($user);
+        $token = $this->mprocTouchLoginState($user, $token);
         $user['token'] = $token;
 
         return $user;
@@ -79,16 +80,49 @@ class Index extends Frontend
         if ($token === null || $token === '') {
             $token = $this->request->request('mproc_token', '');
         }
-        $token = preg_replace('/[^a-f0-9]/i', '', (string)$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<string, mixed>|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();
@@ -110,40 +144,59 @@ class Index extends Frontend
     }
 
     /**
-     * 滑动续期:刷新 Cache / Session / Cookie(保留 7 天)
+     * 滑动续期:刷新签名令牌 / Cache / Session / Cookie(保留 7 天)
      *
      * @param array<string, mixed> $user
      */
-    protected function mprocTouchLoginState(array $user, string $token): void
+    protected function mprocTouchLoginState(array $user, string $token): string
     {
         $user['login_time'] = time();
-        Cache::set('mproc_u_' . $token, $user, $this->mprocTtlSeconds + 86400);
+        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);
+        $this->mprocSetRememberCookie($user, $token);
+
+        return $token;
     }
 
-    protected function mprocSetTokenCookie(string $token): void
+    /**
+     * @return array<string, mixed>
+     */
+    protected function mprocCookieOptions(): array
     {
-        Cookie::set('mproc_token', $token, [
+        $opts = [
             'expire'   => $this->mprocTtlSeconds,
             'path'     => '/',
             'httponly' => true,
-            'samesite' => 'Lax',
-        ]);
+        ];
+        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<string, mixed> $user
      */
-    protected function mprocSetRememberCookie(array $user): void
+    protected function mprocSetRememberCookie(array $user, ?string $token = null): void
     {
-        Cookie::set('mproc_remember', $this->mprocPackRememberCookie($user), [
-            'expire'   => $this->mprocTtlSeconds,
-            'path'     => '/',
-            'httponly' => true,
-            'samesite' => 'Lax',
-        ]);
+        $val = $token !== null && $token !== '' ? $token : $this->mprocPackSignedAuthToken($user);
+        Cookie::set('mproc_remember', $val, $this->mprocCookieOptions());
     }
 
     protected function mprocAuthSignSecret(): string
@@ -159,7 +212,7 @@ class Index extends Frontend
     /**
      * @param array<string, mixed> $user
      */
-    protected function mprocPackRememberCookie(array $user): string
+    protected function mprocPackSignedAuthToken(array $user): string
     {
         $payload = [
             'uid'   => (int)($user['customer_user_id'] ?? $user['customer_id'] ?? 0),
@@ -174,16 +227,18 @@ class Index extends Frontend
         return $b64 . '.' . $sig;
     }
 
+    /** @deprecated 使用 mprocPackSignedAuthToken */
+    protected function mprocPackRememberCookie(array $user): string
+    {
+        return $this->mprocPackSignedAuthToken($user);
+    }
+
     /**
      * @return array<string, mixed>|null
      */
-    protected function mprocUserFromRememberCookie(): ?array
+    protected function mprocUserFromSignedToken(string $raw): ?array
     {
-        $raw = Cookie::get('mproc_remember');
-        if ($raw === null || $raw === '') {
-            return null;
-        }
-        $parts = explode('.', (string)$raw, 2);
+        $parts = explode('.', trim($raw), 2);
         if (count($parts) !== 2) {
             return null;
         }
@@ -212,6 +267,22 @@ class Index extends Frontend
         return $this->mprocRebuildUserFromRememberPayload($payload, $lt);
     }
 
+    /**
+     * @return array<string, mixed>|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<string, mixed> $payload
      * @return array<string, mixed>|null
@@ -275,7 +346,10 @@ class Index extends Frontend
     protected function mprocClearLogin($token)
     {
         if ($token !== null && $token !== '') {
-            Cache::rm('mproc_u_' . $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');
@@ -575,11 +649,11 @@ class Index extends Frontend
     {
         $old = Session::get('mproc_token');
         if ($old) {
-            Cache::rm('mproc_u_' . preg_replace('/[^a-f0-9]/i', '', (string)$old));
+            $this->mprocClearLogin((string)$old);
         }
-        $token = bin2hex(random_bytes(16));
         $userData['login_time'] = time();
-        $this->mprocTouchLoginState($userData, $token);
+        $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', ''));
@@ -972,7 +1046,11 @@ class Index extends Frontend
         $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);
+            $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;
@@ -1204,6 +1282,81 @@ class Index extends Frontend
         ];
     }
 
+    /**
+     * 短信/邮件直达链接:确保 focus 对应明细出现在当前列表(便于高亮定位)
+     *
+     * @param array<string, mixed> $bundle
+     * @param array<string, mixed> $user
+     * @return array<string, mixed>
+     */
+    protected function mprocEnsureFocusRowInList(array $bundle, int $focusEid, array $user, $statusNameCol): array
+    {
+        if ($focusEid <= 0) {
+            return $bundle;
+        }
+        $rows = isset($bundle['rows']) && is_array($bundle['rows']) ? $bundle['rows'] : [];
+        foreach ($rows as $r) {
+            if (!is_array($r)) {
+                continue;
+            }
+            if ((int)($r['eid'] ?? $r['id'] ?? $r['ID'] ?? 0) === $focusEid) {
+                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);
+        $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(工作量)
      *
@@ -1424,7 +1577,16 @@ class Index extends Frontend
                 if (is_array($dr) && $dr !== []) {
                     $mprocFocusEid = $focusEid;
                     $q = '';
-                    $sn = trim((string)($dr['status_name'] ?? $dr['STATUS_NAME'] ?? ''));
+                    $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;
+                        }
+                    }
+                    $sn = $this->mprocResolveEffectiveStatusName($dr, is_array($po) ? $po : null);
                     $mapTab = ['未提交' => 'draft', '已提交' => 'submitted', '已完成' => 'done'];
                     if ($sn !== '' && isset($mapTab[$sn])) {
                         $tab = $mapTab[$sn];
@@ -1453,6 +1615,9 @@ class Index extends Frontend
         }
 
         $bundle = $this->mprocFetchProcuremenList($user, $tab, $q, $statusNameCol);
+        if ($mprocFocusEid > 0) {
+            $bundle = $this->mprocEnsureFocusRowInList($bundle, $mprocFocusEid, $user, $statusNameCol);
+        }
         $this->view->assign('rows', $bundle['rows']);
 
         return $this->view->fetch();
@@ -1624,21 +1789,26 @@ class Index extends Frontend
             $this->error('请使用 POST');
         }
         $token = $this->mprocReadTokenFromRequest();
-        if ($token === '') {
-            $this->error('登录已过期,请重新登录', url('index/index/login'));
+        $user = null;
+        if ($token !== '') {
+            $user = $this->mprocLoadUserByToken($token);
         }
-        $user = $this->mprocLoadUserByToken($token);
         if (!$user) {
             $user = $this->mprocUserFromRememberCookie();
             if ($user) {
-                $token = bin2hex(random_bytes(16));
+                $token = $this->mprocPackSignedAuthToken($user);
             }
         }
         if (!$user) {
             $this->error('登录已过期,请重新登录', url('index/index/login'));
         }
-        $this->mprocTouchLoginState($user, $token);
-        $this->success('ok', '', ['mproc_token' => $token]);
+        $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_days'   => max(1, (int)round($this->mprocTtlSeconds / 86400)),
+        ]);
     }
 
     /**

+ 16 - 7
application/index/view/index/index.html

@@ -293,7 +293,7 @@
     <div class="empty">暂无数据</div>
     {else /}
     {volist name="rows" id="r"}
-    <div class="card js-card"
+    <div class="card js-card {if $mprocFocusEid && $mprocFocusEid == $r.eid}mproc-card-highlight{/if}"
          data-id="{$r.eid|default=0}"
          data-ccydh="{$r.CCYDH|default=''|htmlentities}"
          data-amount="{$r.amount|default=''|htmlentities}"
@@ -463,14 +463,18 @@
             mprocClearLoginToken();
             return Promise.resolve(false);
         }
+        var body = 'mproc_token=' + encodeURIComponent(tok);
+        var curPath = window.location.pathname + window.location.search;
+        if (curPath) {
+            body += '&redirect=' + encodeURIComponent(curPath);
+        }
         return fetch(restoreUrl, {
             method: 'POST',
             credentials: 'same-origin',
-            headers: {
-                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
-                'X-Requested-With': 'XMLHttpRequest'
-            },
-            body: 'mproc_token=' + encodeURIComponent(tok)
+            headers: mprocAuthHeaders({
+                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+            }),
+            body: body
         }).then(function (res) {
             return res.json();
         }).then(function (ret) {
@@ -663,7 +667,9 @@
             var canEdit = parseInt(r.mproc_can_edit, 10) === 1;
             var ccydh = pick(r, ['CCYDH', 'ccydh']);
             var cname = pick(r, ['company_name', 'Company_name']);
-            return '<div class="card js-card" data-id="' + eid + '" data-ccydh="' + mprocEscAttr(ccydh) + '" data-amount="' + mprocEscAttr(amtRaw) + '" data-delivery="' + mprocEscAttr(delRaw) + '" data-title="' + mprocEscAttr(tit) + '" data-ceiling-price="' + mprocEscAttr(ceilP) + '" data-this-quantity="' + mprocEscAttr(thisQty) + '">'
+            var focusEid = parseInt(boot.focus_eid, 10) || 0;
+            var hl = focusEid > 0 && eid === focusEid ? ' mproc-card-highlight' : '';
+            return '<div class="card js-card' + hl + '" data-id="' + eid + '" data-ccydh="' + mprocEscAttr(ccydh) + '" data-amount="' + mprocEscAttr(amtRaw) + '" data-delivery="' + mprocEscAttr(delRaw) + '" data-title="' + mprocEscAttr(tit) + '" data-ceiling-price="' + mprocEscAttr(ceilP) + '" data-this-quantity="' + mprocEscAttr(thisQty) + '">'
                 + '<p class="title">' + mprocEsc(tit) + '</p>'
                 + '<div class="kv">' + mprocEsc(cname) + '</div>'
                 + '<div class="kv"><span>订单号</span>' + mprocEsc(pick(r, ['CCYDH', 'ccydh'])) + '</div>'
@@ -739,6 +745,8 @@
             if (listInner) {
                 listInner.innerHTML = renderListHtml(rows, dns);
             }
+            setTimeout(mprocTryScrollToFocusCard, 80);
+            setTimeout(mprocTryScrollToFocusCard, 400);
         }).catch(function (e) {
             if (myAbort && mprocListAbort !== myAbort) {
                 return;
@@ -1172,6 +1180,7 @@
             } catch (e2) {}
         }
     }
+    mprocTryRestoreLoginFromStorage();
     setTimeout(mprocTryScrollToFocusCard, 200);
     setTimeout(mprocTryScrollToFocusCard, 650);
 

+ 62 - 0
application/index/view/index/login.html

@@ -163,6 +163,7 @@
     var loginUrl = "{:url('index/index/doLogin')}";
     var loginPwdUrl = "{:url('index/index/doLoginPwd')}";
     var indexUrl = "{:url('index/index/index')}";
+    var restoreUrl = "{:url('index/index/mprocRestore')}";
     var captchaBaseUrl = {:json_encode(isset($mprocCaptchaUrl) ? $mprocCaptchaUrl : url('index/index/captcha'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
     var loginRedirect = {:json_encode(isset($mprocLoginRedirect) ? $mprocLoginRedirect : '', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
 
@@ -415,6 +416,67 @@
             }
         });
     });
+
+    /** 微信等移动端 Session/Cookie 易失效:用 localStorage 中的 token 自动恢复 7 天登录并回跳原链接(含 focus_eid) */
+    function mprocTryAutoRestoreLogin() {
+        var tok = '';
+        var exp = 0;
+        try {
+            tok = localStorage.getItem('mproc_token') || '';
+            exp = parseInt(localStorage.getItem('mproc_token_exp'), 10) || 0;
+        } catch (eRead) {
+            return;
+        }
+        if (!tok || !exp || Date.now() > exp) {
+            if (tok) {
+                try {
+                    localStorage.removeItem('mproc_token');
+                    localStorage.removeItem('mproc_token_exp');
+                } catch (eClr) {}
+            }
+            return;
+        }
+        setSubmitBusy($btnLogin, true, '正在恢复登录…');
+        if ($btnLoginPwd) {
+            $btnLoginPwd.disabled = true;
+        }
+        var body = 'mproc_token=' + encodeURIComponent(tok);
+        if (loginRedirect) {
+            body += '&redirect=' + encodeURIComponent(loginRedirect);
+        }
+        fetch(restoreUrl, {
+            method: 'POST',
+            credentials: 'same-origin',
+            headers: {
+                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+                'X-Requested-With': 'XMLHttpRequest',
+                'X-Mproc-Token': tok
+            },
+            body: body
+        }).then(function (r) {
+            return r.json();
+        }).then(function (ret) {
+            if (ret && (ret.code === 1 || ret.code === '1')) {
+                goHome(ret);
+                return;
+            }
+            setSubmitBusy($btnLogin, false);
+            if ($btnLoginPwd) {
+                $btnLoginPwd.disabled = false;
+            }
+            try {
+                localStorage.removeItem('mproc_token');
+                localStorage.removeItem('mproc_token_exp');
+            } catch (eDrop) {}
+        }).catch(function () {
+            setSubmitBusy($btnLogin, false);
+            if ($btnLoginPwd) {
+                $btnLoginPwd.disabled = false;
+            }
+        });
+    }
+
+    mprocTryAutoRestoreLogin();
 })();
 </script>
 </body>