liuhairui il y a 2 semaines
Parent
commit
2d58ee8e78

+ 5 - 3
application/admin/controller/Index.php

@@ -70,12 +70,14 @@ class Index extends Backend
         if ($this->auth->isLogin()) {
             $this->success(__("You've logged in, do not login again"), $url);
         }
-        //保持会话有效时长,单位:小时
-        $keeyloginhours = 72;
+        //保持会话有效时长,单位:小时(与 config fastadmin.keeplogin_hours 一致)
+        $keeyloginhours = (int)Config::get('fastadmin.keeplogin_hours');
+        if ($keeyloginhours <= 0) {
+            $keeyloginhours = 72;
+        }
         if ($this->request->isPost()) {
             $username = $this->request->post('username');
             $password = $this->request->post('password', '', null);
-            $keeplogin = $this->request->post('keeplogin');
             $token = $this->request->post('__token__');
             $rule = [
                 'username'  => 'require|length:3,30',

+ 226 - 34
application/admin/controller/Procuremen.php

@@ -197,31 +197,176 @@ class Procuremen extends Backend
     }
 
     /**
-     * 左侧菜单
+     * 从 ERP 库拉取外发工序池(与 api/procuremen/getprocuremen 一致),并尽力写入 Redis
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function procuremenFetchErpPoolFromDb(): array
+    {
+        try {
+            $startTime = date('Y-m-d 00:00:00', strtotime('-1 year'));
+            $endTime = date('Y-m-d 23:59:59');
+            $list = Db::table('scydgy')
+                ->alias('a')
+                ->join('mcyd b', 'b.ICYDID = a.ICYDID AND b.iStatus >= 10', 'inner')
+                ->field('a.ID, b.CCYDH, b.CYJMC, b.CCLBMMC, a.CDXMC, a.CGYBH, a.CGYMC, a.CDW, a.NGZL, b.CDF, a.cGzzxMc, a.MBZ, a.bwjg, a.iStatus, a.dStamp, b.dputrecord, b.cywyxm')
+                ->where([
+                    'a.bwjg'    => 1,
+                    'a.iEndBz'  => 0,
+                    'a.iType'   => 0,
+                    'a.iStatus' => 10,
+                ])
+                ->where('a.dStamp', '>=', $startTime)
+                ->where('a.dStamp', '<=', $endTime)
+                ->order('a.dStamp', 'desc')
+                ->select();
+            if (!is_array($list)) {
+                return [];
+            }
+            try {
+                $redis = redis();
+                $payload = [
+                    'code' => 200,
+                    'msg'  => 'success',
+                    'data' => $list,
+                ];
+                $redis->set('procuremen_redis', json_encode($payload, JSON_UNESCAPED_UNICODE));
+                $this->procuremenRedisCache = $list;
+            } catch (\Throwable $e) {
+            }
+
+            return $list;
+        } catch (\Throwable $e) {
+            Log::write('外发列表拉取 ERP 数据失败:' . $e->getMessage(), 'error');
+
+            return [];
+        }
+    }
+
+    /**
+     * Redis 为空时从库补一份缓存(同请求内只尝试一次)
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function procuremenEnsureRedisPool(): array
+    {
+        $pool = $this->ProcuremenRedis();
+        if (is_array($pool) && $pool !== []) {
+            return $pool;
+        }
+        static $warmed = false;
+        if ($warmed) {
+            return is_array($pool) ? $pool : [];
+        }
+        $warmed = true;
+
+        return $this->procuremenFetchErpPoolFromDb();
+    }
+
+    /**
+     * 从工序行提取年月(与列表筛选一致:dputrecord 优先,回退 dStamp 等)
+     *
+     * @param array<string, mixed> $row
+     */
+    protected function procuremenRowYearMonth(array $row, string $primary = 'dputrecord'): ?string
+    {
+        $t = $this->procuremenRowListSortTime($row, $primary);
+        if ($t === '' || !preg_match('/^([12]\d{3})-(\d{1,2})/', $t, $m)) {
+            return null;
+        }
+        $mo = (int)$m[2];
+        if ($mo < 1 || $mo > 12) {
+            return null;
+        }
+
+        return $m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT);
+    }
+
+    /**
+     * @param array<int, array{year:string, months:array}> $sidebar
+     * @return array<int, array{year:string, months:array}>
+     */
+    protected function ensureProcuremenSidebarHasYm(array $sidebar, string $ym): array
+    {
+        $ym = trim($ym);
+        if (!preg_match('/^\d{4}-\d{2}$/', $ym)) {
+            return $sidebar;
+        }
+        foreach ($sidebar as $block) {
+            if (!is_array($block) || empty($block['months']) || !is_array($block['months'])) {
+                continue;
+            }
+            foreach ($block['months'] as $item) {
+                if (is_array($item) && ($item['ym'] ?? '') === $ym) {
+                    return $sidebar;
+                }
+            }
+        }
+        $y = substr($ym, 0, 4);
+        $mo = (int)substr($ym, 5, 2);
+        $sidebar[] = [
+            'year'   => $y,
+            'months' => [['ym' => $ym, 'label' => $mo . '月']],
+        ];
+
+        return $this->buildYearMonthSidebar($this->flattenProcuremenSidebarYms($sidebar));
+    }
+
+    /**
+     * @param array<int, array{year:string, months:array}> $sidebar
+     * @return string[]
+     */
+    protected function flattenProcuremenSidebarYms(array $sidebar): array
+    {
+        $yms = [];
+        foreach ($sidebar as $block) {
+            if (!is_array($block) || empty($block['months']) || !is_array($block['months'])) {
+                continue;
+            }
+            foreach ($block['months'] as $item) {
+                if (!is_array($item)) {
+                    continue;
+                }
+                $ym = trim((string)($item['ym'] ?? ''));
+                if (preg_match('/^\d{4}-\d{2}$/', $ym)) {
+                    $yms[$ym] = true;
+                }
+            }
+        }
+
+        return array_keys($yms);
+    }
+
+    /**
+     * 左侧菜单(外发初选:Redis/ERP 工序按提交日期分组;与列表月份筛选字段一致)
      */
     protected function GetIndexYearMonths()
     {
         $ymSet = [];
-        //获取此方法调用缓存数据
-        foreach ($this->ProcuremenRedis() as $r) {
+        $pool = $this->procuremenEnsureRedisPool();
+        foreach ($pool as $r) {
             if (!is_array($r)) {
                 continue;
             }
-            $ds = trim((string)($r['dputrecord'] ?? ''));
-            if ($ds === '' || stripos($ds, '0000-00-00') === 0) {
-                continue;
+            $ym = $this->procuremenRowYearMonth($r, 'dputrecord');
+            if ($ym !== null) {
+                $ymSet[$ym] = true;
             }
-            if (!preg_match('/^([12]\d{3})-(\d{1,2})-(\d{1,2})/', $ds, $m)) {
+        }
+        foreach ($this->loadPickManualOrderRows() as $r) {
+            if (!is_array($r)) {
                 continue;
             }
-            $mo = (int)$m[2];
-            if ($mo < 1 || $mo > 12) {
-                continue;
+            $ym = $this->procuremenRowYearMonth($r, 'dputrecord');
+            if ($ym !== null) {
+                $ymSet[$ym] = true;
             }
-            $ymSet[$m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT)] = true;
         }
 
         $ymList = array_keys($ymSet);
+        if ($ymList === []) {
+            $ymList[] = $this->resolveProcuremenDefaultYm('pick');
+        }
 
         return $this->buildYearMonthSidebar($ymList);
     }
@@ -285,15 +430,10 @@ class Procuremen extends Backend
                     if (!is_array($r)) {
                         continue;
                     }
-                    $t = $this->procuremenRowListSortTime($r, 'pick_time');
-                    if ($t === '' || !preg_match('/^([12]\d{3})-(\d{1,2})/', $t, $m)) {
-                        continue;
-                    }
-                    $mo = (int)$m[2];
-                    if ($mo < 1 || $mo > 12) {
-                        continue;
+                    $ym = $this->procuremenRowYearMonth($r, 'pick_time');
+                    if ($ym !== null) {
+                        $ymSet[$ym] = true;
                     }
-                    $ymSet[$m[1] . '-' . str_pad((string)$mo, 2, '0', STR_PAD_LEFT)] = true;
                 }
             }
         } catch (\Throwable $e) {
@@ -502,11 +642,11 @@ class Procuremen extends Backend
             $indexPhpRoot = $rootTrue . '/index.php';
         }
         $this->view->assign('procuremenRedisApi', $indexPhpRoot . '/api/procuremen/getprocuremen');
-        $this->view->assign('defaultYm', $this->resolveProcuremenDefaultYm($stage));
-        $this->view->assign(
-            'sidebarYearMonths',
-            $stage === 'pick' ? $this->GetIndexYearMonths() : $this->GetIssuedOrderYearMonths($stage)
-        );
+        $defaultYm = $this->resolveProcuremenDefaultYm($stage);
+        $sidebar = $stage === 'pick' ? $this->GetIndexYearMonths() : $this->GetIssuedOrderYearMonths($stage);
+        $sidebar = $this->ensureProcuremenSidebarHasYm($sidebar, $defaultYm);
+        $this->view->assign('defaultYm', $defaultYm);
+        $this->view->assign('sidebarYearMonths', $sidebar);
         $this->view->assign('procuremenStage', $stage);
         $this->view->assign('procuremenPageTitle', $pageTitle);
     }
@@ -617,7 +757,7 @@ class Procuremen extends Backend
                     $this->loadPurchaseOrderRowsForListStage('confirm', $ym, $applyMonthRange)
                 );
             } else {
-                $pool = $this->ProcuremenRedis();
+                $pool = $this->procuremenEnsureRedisPool();
                 if (!is_array($pool)) {
                     $pool = [];
                 }
@@ -642,7 +782,6 @@ class Procuremen extends Backend
                     return json([
                         'total' => 0,
                         'rows'  => [],
-                        'msg'   => '暂无缓存数据',
                     ]);
                 }
             }
@@ -697,6 +836,22 @@ class Procuremen extends Backend
                     }
                     $dir = $ord === 1 ? SORT_ASC : SORT_DESC;
                     array_multisort($sortKeys, $dir, SORT_NUMERIC, $filtered);
+                } elseif ($sortField === 'CCYDH') {
+                    usort($filtered, function ($a, $b) use ($ord) {
+                        $sa = trim((string)($a['CCYDH'] ?? ''));
+                        $sb = trim((string)($b['CCYDH'] ?? ''));
+                        if ($sa === $sb) {
+                            return ((int)($a['ID'] ?? 0) <=> (int)($b['ID'] ?? 0)) * $ord;
+                        }
+                        if ($sa === '') {
+                            return 1;
+                        }
+                        if ($sb === '') {
+                            return -1;
+                        }
+
+                        return strcmp($sa, $sb) * $ord;
+                    });
                 } else {
                     usort($filtered, function ($a, $b) use ($sortField, $ord) {
                         $va = $a[$sortField] ?? null;
@@ -901,10 +1056,11 @@ class Procuremen extends Backend
                 'rows'  => $rows,
             ]);
         } catch (\Throwable $e) {
+            Log::write('外发列表加载异常:' . $e->getMessage(), 'error');
+
             return json([
                 'total' => 0,
                 'rows'  => [],
-                'msg'   => $e->getMessage(),
             ]);
         }
     }
@@ -2999,6 +3155,44 @@ class Procuremen extends Backend
         return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : $s;
     }
 
+    /**
+     * 供应商是否已在手机端填写单价或交货日期(任一有值即视为已操作)
+     */
+    protected function detailRowSupplierOperated(array $r): bool
+    {
+        $am = $r['amount'] ?? null;
+        $okAmt = $am !== null && $am !== '' && !(is_string($am) && trim($am) === '');
+        $dv = isset($r['delivery']) ? trim((string)$r['delivery']) : '';
+        $okDel = $dv !== '' && !preg_match('/^0000-00-00/i', $dv);
+
+        return $okAmt || $okDel;
+    }
+
+    /**
+     * 详情供应商表「操作时间」:仅手机端填写单价/交期后展示,不用下发明细的 createtime
+     */
+    protected function resolveDetailSupplierOperTime(array $r): string
+    {
+        if (!$this->detailRowSupplierOperated($r)) {
+            return '';
+        }
+        $t = $this->formatProcuremenDetailTime($r['updatetime'] ?? null);
+        if ($t !== '') {
+            return $t;
+        }
+        foreach (['delivery_createtime', 'deliverycreatetime'] as $col) {
+            if (!isset($r[$col]) || $r[$col] === '' || $r[$col] === null) {
+                continue;
+            }
+            $t = $this->formatProcuremenDetailTime($r[$col]);
+            if ($t !== '') {
+                return $t;
+            }
+        }
+
+        return '';
+    }
+
     /**
      * 供应商已接单:金额与交货日期均已填写(与列表汇总逻辑一致)
      */
@@ -3156,7 +3350,7 @@ class Procuremen extends Backend
                 if (!is_array($dr) || !$this->detailRowSupplierAccepted($dr)) {
                     continue;
                 }
-                $t = $this->formatProcuremenDetailTime($dr['updatetime'] ?? $dr['createtime'] ?? null);
+                $t = $this->resolveDetailSupplierOperTime($dr);
                 if ($t !== '' && ($supplierTime === '' || strcmp($t, $supplierTime) < 0)) {
                     $supplierTime = $t;
                 }
@@ -3419,6 +3613,7 @@ class Procuremen extends Backend
             }
             $r['delivery_ymd'] = $this->formatDeliveryYmd($r['delivery'] ?? null);
             $r['is_void'] = is_array($r) && $this->isPurchaseOrderDetailVoid($r);
+            $r['oper_time_text'] = $this->resolveDetailSupplierOperTime($r);
         }
         unset($r);
 
@@ -4176,10 +4371,7 @@ class Procuremen extends Backend
                         $groupHas = true;
                     }
                 }
-                $ct = $d['createtime_text'] ?? '';
-                if ($ct === '' && isset($d['createtime'])) {
-                    $ct = $this->formatProcuremenDetailTime($d['createtime'] ?? null);
-                }
+                $ct = $this->resolveDetailSupplierOperTime($d);
                 $lines[] = [
                     'cgymc'           => $gymcBySid[$sid] ?? trim((string)($d['CGYMC'] ?? '')),
                     'unit_price_text' => $amountNum !== null ? $this->formatProcuremenMoneyDisplay($amountNum) : trim((string)($d['amount'] ?? '')),
@@ -4187,7 +4379,7 @@ class Procuremen extends Backend
                     'delivery_ymd'    => $d['delivery_ymd'] ?? $this->formatDeliveryYmd($d['delivery'] ?? null),
                     'status'          => (int)($d['status'] ?? 0),
                     'is_void'         => $isVoid,
-                    'createtime_text' => $ct,
+                    'oper_time_text'  => $ct,
                 ];
             };
             foreach ($orderedSids as $sid) {
@@ -4210,7 +4402,7 @@ class Procuremen extends Backend
                 'lines'        => $lines,
                 'line_count'   => count($lines),
                 'total_text'   => $groupHas ? $this->formatProcuremenMoneyDisplay($groupTotal) : '',
-                'has_total'    => $groupHas,
+                'has_total'    => count($lines) > 0,
             ];
         }
         usort($supplierGroups, function ($a, $b) {

+ 57 - 0
application/admin/library/Auth.php

@@ -64,10 +64,23 @@ class Auth extends \fast\Auth
         $admin->save();
         Session::set("admin", $admin->toArray());
         Session::set("admin.safecode", $this->getEncryptSafecode($admin));
+        if ($keeptime <= 0) {
+            $keeptime = $this->getKeeploginSeconds();
+        }
         $this->keeplogin($admin, $keeptime);
         return true;
     }
 
+    /**
+     * 后台保持登录有效秒数(默认 72 小时,见 config fastadmin.keeplogin_hours)
+     */
+    public function getKeeploginSeconds(): int
+    {
+        $hours = (int)Config::get('fastadmin.keeplogin_hours');
+
+        return ($hours > 0 ? $hours : 72) * 3600;
+    }
+
     /**
      * 退出登录
      */
@@ -228,6 +241,13 @@ class Auth extends \fast\Auth
         }
         $admin = Session::get('admin');
         if (!$admin) {
+            // Session 过期时尝试用 keeplogin Cookie 静默恢复(避免操作中途频繁跳登录页)
+            if ($this->autologin()) {
+                $this->logined = true;
+
+                return true;
+            }
+
             return false;
         }
         $my = Admin::get($admin['id']);
@@ -237,12 +257,14 @@ class Auth extends \fast\Auth
         //校验安全码,可用于判断关键信息发生了变更需要重新登录
         if (!isset($admin['safecode']) || $this->getEncryptSafecode($my) !== $admin['safecode']) {
             $this->logout();
+
             return false;
         }
         //判断是否同一时间同一账号只能在一个地方登录
         if (Config::get('fastadmin.login_unique')) {
             if ($my['token'] != $admin['token']) {
                 $this->logout();
+
                 return false;
             }
         }
@@ -250,13 +272,48 @@ class Auth extends \fast\Auth
         if (Config::get('fastadmin.loginip_check')) {
             if (!isset($admin['loginip']) || $admin['loginip'] != request()->ip()) {
                 $this->logout();
+
                 return false;
             }
         }
+        $this->refreshKeeploginCookie($my);
         $this->logined = true;
+
         return true;
     }
 
+    /**
+     * 操作过程中滑动续期 keeplogin(72 小时内免重复登录)
+     *
+     * @param array|\think\Model $admin
+     */
+    protected function refreshKeeploginCookie($admin): void
+    {
+        $keeplogin = Cookie::get('keeplogin');
+        if (!$keeplogin) {
+            return;
+        }
+        $parts = explode('|', (string)$keeplogin);
+        if (count($parts) !== 4) {
+            return;
+        }
+        $id = (int)$parts[0];
+        $keeptime = (int)$parts[1];
+        $expiretime = (int)$parts[2];
+        if ($keeptime <= 0) {
+            $keeptime = $this->getKeeploginSeconds();
+        }
+        if ($expiretime <= time()) {
+            return;
+        }
+        $adminId = is_array($admin) ? (int)($admin['id'] ?? 0) : (int)($admin->id ?? 0);
+        if ($id <= 0 || $adminId !== $id) {
+            return;
+        }
+        $row = is_array($admin) ? $admin : $admin->toArray();
+        $this->keeplogin($row, $keeptime);
+    }
+
     /**
      * 获取当前请求的URI
      * @return string

+ 2 - 2
application/admin/view/procuremen/details_fragment.html

@@ -383,7 +383,7 @@
                     <span class="text-muted"> </span>
                     {/if}
                 </td>
-                <td>{$ln.createtime_text|default=''|htmlentities}</td>
+                <td>{$ln.oper_time_text|default=''|htmlentities}</td>
             </tr>
             {/volist}
             {if !empty($sg.has_total)}
@@ -412,7 +412,7 @@
                     <span class="text-muted"> </span>
                     {/if}
                 </td>
-                <td>{$dr.createtime_text|default=''|htmlentities}</td>
+                <td>{$dr.oper_time_text|default=''|htmlentities}</td>
             </tr>
             {/volist}
             {else /}

+ 6 - 1
application/admin/view/procuremen/index.html

@@ -42,6 +42,11 @@
             display: flex;
             flex-direction: column;
         }
+        #procuremen-layout .procuremen-layout > .procuremen-sidebar {
+            flex: 0 0 92px;
+            max-width: 92px;
+            width: 92px !important;
+        }
     }
     #procuremen-layout .procuremen-main {
         overflow-x: visible !important;
@@ -57,7 +62,7 @@
         border-right: 1px solid #e7e7e7;
         background: #fafafa;
     }
-    /* 左侧年月栏:桌面 md 用 1 列(略窄于 2 列);平板 sm 仍为 2 列 */
+    /* 左侧年月栏:桌面固定约 92px,略宽于纯数字月份便于显示「2026年」 */
     .procuremen-sidebar .year-title {
         font-weight: 600;
         padding: 6px 8px;

+ 8 - 2
application/common/controller/Backend.php

@@ -120,9 +120,15 @@ class Backend extends Controller
         $controllername = Loader::parseName($this->request->controller());
         $actionname = strtolower($this->request->action());
 
-        // 与后台「保持登录」时长一致,减少操作中 Session 过期被迫重新登录
+        // 与 fastadmin.keeplogin_hours 一致(默认 72 小时)
+        $keeploginSec = (int)Config::get('fastadmin.keeplogin_hours', 72);
+        if ($keeploginSec <= 0) {
+            $keeploginSec = 72;
+        }
+        $keeploginSec *= 3600;
         if (PHP_VERSION_ID >= 70300) {
-            ini_set('session.gc_maxlifetime', '259200');
+            ini_set('session.gc_maxlifetime', (string)$keeploginSec);
+            ini_set('session.cookie_lifetime', (string)$keeploginSec);
         }
 
         $path = str_replace('.', '/', $controllername) . '/' . $actionname;

+ 4 - 0
application/config.php

@@ -206,6 +206,8 @@ return [
         'type'           => '',
         // 是否自动开启 SESSION
         'auto_start'     => true,
+        // 登录态有效时长(秒):与 fastadmin.keeplogin_hours 保持一致
+        'expire'         => 72 * 3600,
         //'cache_limiter'=>''
     ],
     // +----------------------------------------------------------------------
@@ -279,6 +281,8 @@ return [
         'login_unique'          => false,
         //是否开启IP变动检测
         'loginip_check'         => false,
+        // 后台保持登录时长(小时),登录后 Session + Cookie 均按此时长续期
+        'keeplogin_hours'       => 72,
         //登录页默认背景图
         'login_background'      => "",
         //是否启用多级菜单导航

+ 1 - 1
application/database.php

@@ -15,7 +15,7 @@ use think\Env;
 return [
     // 数据库类型
     // 服务器地址
-    'hostname'        => Env::get('database.hostname', 'rm-bp10xvfpe5c9kz119eo.mysql.rds.aliyuncs.com'),
+    'hostname'        => Env::get('database.hostname', 'rm-bp17w41t6c09dl10ayo.mysql.rds.aliyuncs.com'),
     // 数据库名
     'database'        => Env::get('database.database', 'xinhua_erp'),
     // 用户名

+ 3 - 1
application/extra/mproc.php

@@ -10,7 +10,9 @@ return [
     'mobile_base_url'   => 'https://xh.7in6.com',
     // 手机端首页路径(相对上项),留空为 /index.php/index/index/index
     'mobile_index_path' => '',
-    'session_days'  => 7,
+    'session_hours' => 72,
+    // 兼容旧配置(未设 session_hours 时按天计)
+    'session_days'  => 3,
     'sms_code_ttl'  => 300,
     'sms_resend_cd' => 55,
     // 登录短信全文(须与短信宝 VIP 已审核模板一致:签名+正文,{code} 为 6 位验证码)

+ 21 - 4
application/index/controller/Index.php

@@ -36,15 +36,26 @@ class Index extends Frontend
         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;
+        $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 天记住登录)
      */
@@ -768,6 +779,7 @@ class Index extends Frontend
         $jump = $this->mprocBuildAfterLoginIndexUrl($raw);
         $this->success('登录成功', $jump, [
             'mproc_token' => $token,
+            'keep_hours'  => $this->mprocKeepHours(),
             'keep_days'   => max(1, (int)round($this->mprocTtlSeconds / 86400)),
         ]);
     }
@@ -1734,7 +1746,7 @@ class Index extends Frontend
         $this->view->assign('mprocCanChangePwd', empty($user['is_admin']) && $cid > 0 ? 1 : 0);
         $this->view->assign('mprocFocusEid', $mprocFocusEid);
         $this->view->assign('mprocBootstrapToken', trim((string)($user['token'] ?? '')));
-        $this->view->assign('mprocBootstrapKeepDays', max(1, (int)round($this->mprocTtlSeconds / 86400)));
+        $this->view->assign('mprocBootstrapKeepHours', $this->mprocKeepHours());
 
         if ($mainTab === 'me') {
             $this->view->assign('rows', []);
@@ -1940,6 +1952,7 @@ class Index extends Frontend
         $jump = $this->mprocBuildAfterLoginIndexUrl($redirect);
         $this->success('ok', $jump, [
             'mproc_token' => $token,
+            'keep_hours'  => $this->mprocKeepHours(),
             'keep_days'   => max(1, (int)round($this->mprocTtlSeconds / 86400)),
         ]);
     }
@@ -2187,6 +2200,10 @@ class Index extends Frontend
         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']);

+ 13 - 4
application/index/view/index/index.html

@@ -409,6 +409,16 @@
     var listUrl = mprocEndpointUrl('mprocList.html');
     var saveUrl = mprocEndpointUrl('mprocSave.html');
     var restoreUrl = mprocEndpointUrl('mprocRestore.html');
+    function mprocTokenExpireMs(d) {
+        d = d || {};
+        var hours = parseInt(d.keep_hours, 10) || 0;
+        if (hours > 0) {
+            return Date.now() + hours * 3600000;
+        }
+        var days = parseInt(d.keep_days, 10) || 3;
+        return Date.now() + days * 86400000;
+    }
+
     function mprocPersistLoginToken(ret) {
         var d = ret && ret.data;
         var tok = d && d.mproc_token;
@@ -416,9 +426,8 @@
             return;
         }
         try {
-            var days = parseInt(d.keep_days, 10) || 7;
             localStorage.setItem('mproc_token', tok);
-            localStorage.setItem('mproc_token_exp', String(Date.now() + days * 86400000));
+            localStorage.setItem('mproc_token_exp', String(mprocTokenExpireMs(d)));
         } catch (ignoreStore) {
         }
     }
@@ -498,7 +507,7 @@
     /** 服务端已登录时,把 Cookie 中的 token 同步进 localStorage(邮箱/微信内置浏览器 Cookie 易丢) */
     (function mprocBootstrapAuthFromServer() {
         var srvTok = {:json_encode(isset($mprocBootstrapToken) ? $mprocBootstrapToken : '', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
-        var keepDays = {:json_encode(isset($mprocBootstrapKeepDays) ? (int)$mprocBootstrapKeepDays : 7, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
+        var keepHours = {:json_encode(isset($mprocBootstrapKeepHours) ? (int)$mprocBootstrapKeepHours : 72, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)};
         if (!srvTok) {
             return;
         }
@@ -506,7 +515,7 @@
             code: 1,
             data: {
                 mproc_token: srvTok,
-                keep_days: keepDays
+                keep_hours: keepHours
             }
         });
     })();

+ 11 - 2
application/index/view/index/login.html

@@ -305,14 +305,23 @@
         return base + '?focus_eid=' + encodeURIComponent(String(fe)) + '#mproc_fe=' + fe;
     }
 
+    function mprocTokenExpireMs(d) {
+        d = d || {};
+        var hours = parseInt(d.keep_hours, 10) || 0;
+        if (hours > 0) {
+            return Date.now() + hours * 3600000;
+        }
+        var days = parseInt(d.keep_days, 10) || 3;
+        return Date.now() + days * 86400000;
+    }
+
     function goHome(ret) {
         if (ret && (ret.code === 1 || ret.code === '1')) {
             var d = ret.data || {};
             if (d.mproc_token) {
                 try {
-                    var days = parseInt(d.keep_days, 10) || 7;
                     localStorage.setItem('mproc_token', d.mproc_token);
-                    localStorage.setItem('mproc_token_exp', String(Date.now() + days * 86400000));
+                    localStorage.setItem('mproc_token_exp', String(mprocTokenExpireMs(d)));
                 } catch (eStore) {}
             }
             var jump = ret.url || indexUrl;

+ 50 - 16
public/assets/js/backend/procuremen.js

@@ -189,6 +189,29 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             }
             var listAction = Controller.wffTab;
 
+            /** 左侧年月栏为空时补默认月份,避免 Redis 未同步时整块空白 */
+            function procuremenEnsureSidebarMonths() {
+                var $sidebar = $('.procuremen-sidebar');
+                if (!$sidebar.length || $sidebar.find('.procuremen-ym-item').length) {
+                    return;
+                }
+                var ym = (Controller.currYm || $layout.data('defaultYm') || '').toString();
+                if (!/^\d{4}-\d{2}$/.test(ym)) {
+                    var d = new Date();
+                    ym = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
+                }
+                Controller.currYm = ym;
+                var y = ym.substring(0, 4);
+                var mo = parseInt(ym.substring(5, 7), 10) || 1;
+                $sidebar.html(
+                    '<div class="procuremen-year-block">'
+                    + '<div class="year-title">' + y + '年</div>'
+                    + '<a href="javascript:;" class="procuremen-ym-item active" data-ym="' + ym + '">' + mo + '月</a>'
+                    + '</div>'
+                );
+            }
+            procuremenEnsureSidebarMonths();
+
             Table.api.init({
                 extend: {
                     index_url: 'procuremen/' + listAction + location.search,
@@ -361,7 +384,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 //         return v != null && v !== '' && String(v) !== '0' ? String(v) : '';
                 //     }
                 // },
-                {field: 'CCYDH', title: __('订单号'), operate: 'LIKE', table: 'b', width: 100, align: 'center'},
+                {field: 'CCYDH', title: __('订单号'), operate: 'LIKE', table: 'b', width: 100, align: 'center', sortable: true},
                 {field: 'CYJMC', title: __('印件名称'), operate: 'LIKE', table: 'b', width: 270, align: 'left'},
                 {field: 'CCLBMMC', title: '承揽部门', operate: 'LIKE', table: 'b', width: 100, align: 'center'},
                 {field: 'CGYMC', title: __('工序名称'), operate: 'LIKE', table: 'a', width: 140, align: 'left',
@@ -522,7 +545,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
                 pk: 'ID',
-                sortName: indexInitWffTab === 'pick' ? 'b.dputrecord' : 'a.pick_time',
+                sortName: indexInitWffTab === 'pick' ? 'CCYDH' : 'a.pick_time',
                 sortOrder: 'desc',
                 width: 'auto',
                 height: procuremenInitTableHeight,
@@ -535,6 +558,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 showToggle: false,
                 showExport: false,
                 smartDisplay: false,
+                responseHandler: function (res) {
+                    if (res && typeof res === 'object' && res.msg && (res.total === 0 || !res.rows || !res.rows.length)) {
+                        delete res.msg;
+                    }
+                    return res;
+                },
                 queryParams: function (params) {
                     params.ym = Controller.currYm;
                     params.wff_tab = Controller.wffTab;
@@ -546,6 +575,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
 
             Table.api.bindevent(table);
 
+            // 刷新/切换月份时旧请求被中断会误触 load-error;列表已有数据时不打扰用户
+            table.off('load-error.bs.table').on('load-error.bs.table', function (status, res, jqXHR) {
+                var xhr = jqXHR || res;
+                if (xhr && (!xhr.status || xhr.status === 0 || xhr.statusText === 'abort')) {
+                    return;
+                }
+                try {
+                    if ((table.bootstrapTable('getData') || []).length > 0) {
+                        return;
+                    }
+                } catch (ignore) {
+                }
+            });
+
             table.on('load-success.bs.table', function () {
                 procuremenPlaceToolbar();
                 procuremenSyncTableHeight();
@@ -642,7 +685,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     sessionStorage.setItem('procuremen_merge_rows', JSON.stringify(selRows));
                 } catch (ignoreStore) {
                 }
-                var titlePick = '外发下发';
+                var titlePick = '协助下发';
                 var revUrl = Fast.api.fixurl('procuremen/pickreview?ids=' + encodeURIComponent(ids[0]) + '&merge=1');
                 var winPick = window;
                 if (!winPick.Backend || !winPick.Backend.api) {
@@ -1191,20 +1234,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         dataType: 'json',
                         timeout: 120000
                     }).done(function (ret) {
-
-                        if (ret && (ret.code === 200 || ret.code === '200')) {
-                            if (typeof Toastr !== 'undefined') {
-                                console.log('已刷新');
-                                // console.log(ret);
-                                // Toastr.success('已刷新');
-                            }
-                        } else if (typeof Toastr !== 'undefined') {
-                            Toastr.warning((ret && ret.msg) ? String(ret.msg) : '刷新接口返回异常');
-                        }
-                    }).fail(function (xhr) {
-                        if (typeof Toastr !== 'undefined') {
-                            Toastr.error(xhr && xhr.statusText ? ('刷新失败:' + xhr.statusText) : '刷新缓存请求失败');
+                        if (!(ret && (ret.code === 200 || ret.code === '200'))) {
+                            console.warn('procuremen redis refresh:', ret);
                         }
+                    }).fail(function () {
+                        console.warn('procuremen redis refresh failed');
                     }).always(function () {
                         table.bootstrapTable('refresh');
                     });