Repository navigation
Expand file tree
/
Copy pathmigration.html
More file actions
544 lines (498 loc) · 27.7 KB
/
Copy pathmigration.html
File metadata and controls
544 lines (498 loc) · 27.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
<meta charset="UTF-8">
<title>LAH-API 漸進式升級 PHP 8.5 (v85 雙軌架構) 完整技術與實作手冊</title>
<style>
@page {
size: A4;
margin: 18mm 16mm 18mm 16mm;
}
body {
font-family: "Microsoft JhengHei", "PingFang TC", "Noto Sans TC", sans-serif;
font-size: 10.5pt;
line-height: 1.65;
color: #1e293b;
margin: 0;
padding: 0;
}
h1 {
font-size: 20pt;
color: #0f172a;
border-bottom: 3px solid #2563eb;
padding-bottom: 8px;
margin-top: 0;
}
h2 {
font-size: 14.5pt;
color: #1d4ed8;
border-left: 5px solid #2563eb;
padding-left: 10px;
margin-top: 24px;
margin-bottom: 10px;
page-break-after: avoid;
}
h3 {
font-size: 12pt;
color: #0f172a;
margin-top: 16px;
margin-bottom: 6px;
page-break-after: avoid;
}
p, li {
margin-top: 4px;
margin-bottom: 6px;
}
code {
font-family: Consolas, "Courier New", monospace;
background: #f1f5f9;
color: #be123c;
padding: 1px 5px;
border-radius: 4px;
font-size: 9.5pt;
}
pre {
background: #0f172a;
color: #f8fafc;
padding: 12px 14px;
border-radius: 6px;
overflow-x: auto;
font-size: 9pt;
line-height: 1.45;
}
pre code {
background: transparent;
color: inherit;
padding: 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 9.5pt;
page-break-inside: avoid;
}
th, td {
border: 1px solid #cbd5e1;
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
th {
background-color: #f1f5f9;
color: #0f172a;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f8fafc;
}
.callout-warn {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
padding: 10px 14px;
border-radius: 0 6px 6px 0;
margin: 12px 0;
color: #991b1b;
page-break-inside: avoid;
}
.callout-tip {
background-color: #f0fdf4;
border-left: 4px solid #22c55e;
padding: 10px 14px;
border-radius: 0 6px 6px 0;
margin: 12px 0;
color: #166534;
page-break-inside: avoid;
}
.meta-box {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 20px;
font-size: 9.5pt;
color: #475569;
}
.mermaid {
text-align: center;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
page-break-inside: avoid;
}
@media print {
body {
color: #000;
}
h1, h2, h3 {
color: #000;
}
.mermaid {
background: #ffffff;
border: 1px solid #cbd5e1;
page-break-inside: avoid;
}
pre {
background: #f8fafc;
color: #0f172a;
border: 1px solid #cbd5e1;
}
a {
text-decoration: none;
color: #000;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
if (window.mermaid) {
mermaid.initialize({
startOnLoad: true,
theme: 'default',
flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis' },
securityLevel: 'loose'
});
}
});
</script>
</head>
<body>
<h1>LAH-API 漸進式升級 PHP 8.5(<code>v85</code> 雙軌架構)完整技術與實作手冊</h1>
<div class="meta-box">
<strong>文件版本:</strong>v1.1(2026-10-23)<br>
<strong>目標目錄:</strong><code>C:\AppServ\LAH-API\</code>(開發環境映射至 <code>c:\Temp\DEV\LAH-Workspace\LAH-API\API\</code>)<br>
<strong>採用架構:</strong>保留 Apache 2.4 (<code>mod_php7</code> + <code>mod_proxy_fcgi</code>) + PM2 託管 PHP 8.5 FastCGI (<code>127.0.0.1:8085</code>,16 子程序併發)<br>
<strong>資料庫環境:</strong>後端地政核心 Oracle 9i(搭配 <code>instantclient_11_2</code> 智慧雙模)+ 本地快取 SQLite 3 + 舊公文 MS SQL Server
</div>
<h2>第一章:整體升級策略與系統架構藍圖</h2>
<h3>1.1 核心設計決策</h3>
<ol>
<li><strong>新舊目錄並存(<code>www/</code> 與 <code>v85/</code>)</strong>:既有 <code>LAH-API/www/</code> 維持 PHP 7.3/7.4 穩定運作;新建 <code>LAH-API/API/</code> 作為 PHP 8.5 專屬目錄,逐支檢視、修正並遷移 API。</li>
<li><strong>保留 Apache 2.4 並啟用 <code>mod_proxy_fcgi</code> 分流</strong>:不替換現有 Apache 2.4 服務,各所防火牆與自動更新工具 <code>LAH-Updater</code> 無需大幅變動。Apache 以原本的 <code>mod_php7</code> 服務 <code>/</code>,同時將 <code>/v85/</code> 轉發給本機 Port <code>8085</code> 的 PHP 8.5 FastCGI。</li>
<li><strong>PM2 進程守護與 FastCGI 原生多程序併發</strong>:利用各所伺服器既有的 PM2 守護 <code>C:\AppServ\php85\php-cgi.exe</code>(設定 <code>exec_mode: "fork"</code>),並藉由 FastCGI 內部環境變數 <code>PHP_FCGI_CHILDREN: "16"</code> 由 Windows 核心自動衍生 16 個 Worker 程序,徹底解決 Windows 下 FastCGI 請求併發與崩潰重啟問題。</li>
</ol>
<h3>1.2 雙軌運作系統架構拓撲圖</h3>
<pre class="mermaid">
flowchart TD
subgraph Clients["客戶端層 (Clients)"]
FE["LAH-FE 智慧管控前端 (Nuxt 2)"]
CLIENT["lah-messenger 桌面客戶端 (Electron)"]
end
subgraph Gateway["Web 閘道 (Apache 2.4 - Port 80)"]
APACHE["Apache 2.4 (C:\\AppServ\\Apache24)"]
end
subgraph ServiceLayer["雙軌執行期 (Runtime Layer)"]
PHP7["舊軌 mod_php7 (C:\\AppServ\\php7)<br/>執行 www/ 原始碼"]
PHP85["新軌 PM2 FastCGI lah-api-v85 (127.0.0.1:8085)<br/>C:\\AppServ\\php85\\php-cgi.exe 執行 v85/ 原始碼"]
end
subgraph PersistenceLayer["資料持久層 (Shared Single Source of Truth)"]
ENV["共用組態 www/.env (強制 MOCK=true)"]
SQLITE[("共用 SQLite 3 快取與分庫<br/>www/assets/db/*.db")]
ORACLE[("地政核心 Oracle 9i 資料庫")]
MSSQL[("公文歷史 MS SQL Server")]
end
FE --> APACHE
CLIENT --> APACHE
APACHE -- "路徑 /api/*" --> PHP7
APACHE -- "路徑 /LAH-API/* (mod_proxy_fcgi)" --> PHP85
PHP7 --> ENV & SQLITE & ORACLE & MSSQL
PHP85 --> ENV & SQLITE & MSSQL
PHP85 -. "未載入 oci8 時透過 PHP 7 CLI 橋接" .-> PHP7
</pre>
<h2>第二章:Node.js v16 限制、PM2 託管與 <code>C:\AppServ\node</code> 隔離架構</h2>
<h3>2.1 關鍵解答:PM2 託管 PHP 8.5 完全不受 Node.js v16 限制!</h3>
<ul>
<li><strong>為什麼不需要升級 Node.js 或切換 NVM 就能用 PM2 管 PHP 8.5?</strong><br>
PM2 雖然執行於 Node.js v16 上,但當設定 <code>interpreter: "none"</code> 時,PM2 是直接透過 Windows 系統呼叫啟動原生的 <code>C:\AppServ\php85\php-cgi.exe</code> 執行檔,並監控其 PID。因此,<strong>現有跑在 Node.js v16 上的 PM2 可以直接同時管理 <code>LAH-FE</code> (Node 16)、<code>lah-messenger-server</code> 與 <code>lah-api-v85</code> (PHP 8.5)</strong>,完全不會互相干擾!</li>
</ul>
<h3>2.2 進階避坑與架構設計:<code>LAH-FE</code> 的 Cluster 限制與 <code>C:\AppServ\node</code> 專屬隔離</h3>
<div class="callout-tip">
<strong>💡 核心觀念:PM2 <code>cluster</code> 模式下的 <code>interpreter</code> 繼承原理</strong><br>
工程師常直覺想在 <code>LAH-FE</code> 的 <code>ecosystem.config.js</code> 中指定 <code>interpreter: "C:/AppServ/node/node.exe"</code> 來鎖定 Node 16。然而在 PM2 的底層架構中:
<ul>
<li><strong><code>exec_mode: "cluster"</code>(多行程負載均衡)</strong>:Node.js 核心的 <code>cluster.fork()</code> 深度依賴 PM2 Daemon 所在的執行緒,<strong>在 cluster 模式下 PM2 會忽略自訂 interpreter 路徑,強制繼承啟動 PM2 Daemon 本身的 Node 版本</strong>!</li>
<li><strong><code>exec_mode: "fork"</code>(單行程分支模式)</strong>:自訂 <code>interpreter</code> 才能生效,但會失去 PM2 內建的 Cluster 多核負載均衡與零停機熱重載優勢。</li>
</ul>
</div>
<div class="callout-warn">
<strong>⚠️ Windows 上全域升級或 <code>nvm-windows</code> 的潛在風險:</strong><br>
<code>nvm use <version></code> 在 Windows 的原理是動態切換全域符號連結 <code>C:\Program Files\nodejs</code>。若管理師日後在伺服器全域安裝或切換至 Node 20/22,當 PM2 守護服務重啟時,PM2 Daemon 便會誤以 Node 20/22 啟動,進而導致以 <code>cluster</code> 模式運行的 <code>LAH-FE</code> (Nuxt 2 / Vue 2) 因 Webpack 4 與原生模組 ABI 不相容而崩潰!
</div>
<p><strong>🎯 官方最佳實踐(實體專屬隔離方案):將 Node 16.20.2 集中部署至 <code>C:\AppServ\node\</code></strong></p>
<ol>
<li><strong>集中放置綠色執行檔</strong>:
下載 Node.js 官方最終版本 <code>node-v16.20.2-win-x64.zip</code> 解壓至 <code>C:\AppServ\node\</code>(內含 <code>node.exe</code>、<code>npm</code>、<code>yarn</code>、<code>pm2</code>)。
</li>
<li><strong>鎖定 PM2 守護服務啟動源</strong>:
在 Windows 服務或 PM2 啟動腳本中,<strong>明確由 <code>C:\AppServ\node\node.exe</code> 來喚醒 PM2 Daemon</strong>(例如 <code>"C:\AppServ\node\node.exe" "C:\AppServ\node\node_modules\pm2\bin\pm2" resurrect</code>)。
</li>
<li><strong>徹底免疫全域環境變更</strong>:
如此一來,PM2 Daemon 本身永遠 100% 固定跑在 Node 16.20.2 上,底下的 <code>LAH-FE</code>(Nuxt 2)與 <code>lah-messenger-server</code> 在 <code>cluster</code> 模式下自然繼承穩定的 Node 16 執行期。無論整台伺服器全域安裝了什麼軟體或如何切換 NVM,地政智慧管控系統皆能永久穩定運行!
</li>
</ol>
<h3>2.3 PM2 FastCGI 服務設定檔(<code>C:\AppServ\LAH-API\ecosystem.config.js</code>)</h3>
<p>以下為專案標準 <code>ecosystem.config.js</code> 配置:</p>
<pre><code>module.exports = {
apps: [
{
name: "lah-api-v85",
script: "C:/AppServ/php85/php-cgi.exe",
args: "-b 127.0.0.1:8085",
instances: 1,
exec_mode: "fork",
autorestart: true,
max_restarts: 10,
restart_delay: 2000,
interpreter: "none",
env: {
PHP_FCGI_CHILDREN: "16",
PHP_FCGI_MAX_REQUESTS: "10000",
PHPRC: "C:/AppServ/php85",
PATH: "C:/AppServ/php85;C:/AppServ/php7;C:/Tools/instantclient_11_2;" + process.env.PATH
}
}
]
};</code></pre>
<h2>第三章:破解「PHP 8.5 ↔ Oracle 9i」雙重死結的實戰方案</h2>
<h3>3.1 為什麼 PHP 8.5 連 Oracle 9i 會有雙重死結?</h3>
<ol>
<li><strong>死結 1(Client 版本限制)</strong>:Oracle Instant Client 12c / 19c 已在底層移除對 Oracle 9i (<code>9.2</code>) 的連線支援,連線必噴 <code>ORA-03134: Connections to this server version are no longer supported</code>。因此<strong>唯一能連上 Oracle 9i 的 Client 只有 <code>instantclient_11_2</code> (11.2.0.4)</strong>。</li>
<li><strong>死結 2(PHP 8.5 DLL 函式限制)</strong>:PHP 8.5 官方預編譯僅提供 <code>php_oci8_12c.dll</code> / <code>19.dll</code>,其二進位匯入表強制要求 Oracle 12c 才新增的 <code>OCIStmtGetNextResult</code> 函式,因此無法載入 <code>instantclient_11_2\oci.dll</code>。</li>
</ol>
<h3>3.2 現行採用方案:智慧雙模(本機 PHP 7.4 CLI Worker 內部橋接)</h3>
<p>由於整個專案對 Oracle 9i 的呼叫 100% 封裝於 <code>API/include/OraDB.class.php</code> 單一類別內,本系統已實作並全面採用<strong>本機 PHP 7.4 CLI Worker 智慧雙模內部橋接機制</strong>:</p>
<div class="callout-tip">
<strong>⭐ 核心優勢與採用理由:</strong>
<ul>
<li><strong>零編譯成本、立即可用</strong>:無需在 Windows 伺服器建置 Visual Studio C++ 工具鏈重新編譯非官方 DLL。</li>
<li><strong>業務 API 100% 零修改</strong>:現行 30 多支業務 API 的 SQL 語法與 <code>:bv_...</code> 具名參數綁定完全相容保留。</li>
<li><strong>程序隔離與安全穩定</strong>:PHP 8.5 FastCGI 程序與 PHP 7.4 CLI 獨立隔離,Oracle 9i 查詢透過標準輸入/輸出 (STDIN/STDOUT) 傳遞資料,避免記憶體洩漏互相干擾。</li>
</ul>
</div>
<p><strong>運作架構與資料流:</strong></p>
<ol>
<li>在 <code>API/include/OraDB.class.php</code> 建構子中自動偵測執行環境:
<pre><code>$this->use_cli_bridge = !extension_loaded('oci8');</code></pre>
</li>
<li><strong>現階段(後端為 Oracle 9i)</strong>:
<ul>
<li>因 PHP 8.5 官方未提供支援 Oracle 9i 的 <code>php_oci8_11g.dll</code>,因此 <code>extension_loaded('oci8')</code> 為 <code>false</code>,<code>$this->use_cli_bridge</code> 自動判定為 <code>true</code>。</li>
<li>當業務 API 呼叫 <code>$db->execute()</code> 時,<code>OraDB</code> 透過 <code>proc_open</code> 呼叫 <code>C:\AppServ\php7\php.exe</code> 執行同目錄下的 <code>API/include/ora_cli_worker.php</code>。</li>
<li>該 Worker 搭配舊軌穩定運作的 <code>instantclient_11_2</code> 與 <code>php_oci8_11g.dll</code> 連線 Oracle 9i 執行查詢,並將原始 BIG5 欄位以 Base64 編碼透過 STDOUT 回傳。</li>
<li><code>OraDB</code> 接收後自動調用 <code>UConverter::transcode</code> 轉為 UTF-8,達成 100% 正確的中文編碼轉換。</li>
</ul>
</li>
</ol>
<h3>3.3 📌 未來升級備忘:資料庫升級至 Oracle 12c 時之原生 PHP 8.5 OCI8 直連無縫切換步驟</h3>
<div class="callout-warn">
<strong>🔔 給未來的提醒(當後端資料庫從 Oracle 9i 正式升級為 Oracle 12c / 19c 時請執行此步驟):</strong><br>
將來後端資料庫升級為 <strong>Oracle 12c 或更高版本</strong> 後,3.1 節所述的「雙重死結」將徹底消失!屆時不必再透過 PHP 7.4 CLI 橋接,而是可以直接啟用 <strong>純 PHP 8.5 官方原生 OCI8 高速直連</strong>。
</div>
<p><strong>切換為「純 PHP 8.5 官方原生直連」的 3 個操作步驟(零程式碼修改):</strong></p>
<ol>
<li><strong>更換伺服器 Instant Client 為 19c</strong>:
<ul>
<li>Oracle 官方規範(Doc ID 207303.1)明訂 <code>Oracle Instant Client 19c</code> 完全向下相容連線 Oracle 12c (12.1 / 12.2) 資料庫。</li>
<li>請將伺服器上的 Instant Client 升級為 19c(例如解壓至 <code>C:\AppServ\instantclient_19_25</code>),並同步更新 <code>API/ecosystem.config.js</code> 中的 <code>PATH</code> 環境變數。</li>
</ul>
</li>
<li><strong>放入 PHP 8.5 官方 <code>php_oci8_19.dll</code> 並啟用設定</strong>:
<ul>
<li>前往 PECL 官方下載頁面取得 <code>php_oci8-3.4.1-8.5-nts-vs17-x64.zip</code>,將裡面的 <code>php_oci8_19.dll</code> 複製到 <code>C:\AppServ\php85\ext\</code>。</li>
<li>編輯 <code>C:\AppServ\php85\php.ini</code>,加入或取消註解:<code>extension=oci8_19</code>。</li>
</ul>
</li>
<li><strong>重啟 PM2 服務即自動完成切換</strong>:
<ul>
<li>執行 <code>pm2 restart lah-api-v85 --update-env</code>。</li>
<li>開啟 <code>http://127.0.0.1/LAH-API/ver.php</code> 確認 <code>"oci8": true</code>。</li>
<li>一旦 <code>extension_loaded('oci8')</code> 為 <code>true</code>,<code>API/include/OraDB.class.php</code> 的 <code>$this->use_cli_bridge</code> 會自動變為 <code>false</code>,<strong>一行 PHP 程式碼都不用改</strong>,系統就會自動跳過 PHP 7.4 橋接,全面改走純 PHP 8.5 原生 <code>oci_connect()</code>、<code>oci_parse()</code> 與 <code>oci_bind_by_name()</code> 高速直連 Oracle 12c,屆時即可正式退役移除舊版 PHP 7.4!</li>
</ul>
</li>
</ol>
<h2>第四章:<code>LAH-API\API\</code> 目錄結構與共用資料庫設計</h2>
<p>為了讓新舊 API 在過渡期讀寫同一份本地快取與系統設定,<code>v85/</code> 僅獨立程式碼與 <code>vendor/</code>,其餘資產與 SQLite 資料庫均指向 <code>../www/</code>:</p>
<pre><code><?php
// API/include/GlobalConstants.inc.php
define('V85_ROOT_DIR', dirname(__DIR__));
define('ROOT_DIR', dirname(V85_ROOT_DIR) . DIRECTORY_SEPARATOR . 'www'); // 指向既有 www/
define('INC_DIR', V85_ROOT_DIR . DIRECTORY_SEPARATOR . 'include'); // 引入 v85 修正版類別
define('ASSETS_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'assets'); // 共用 www/assets
define('DB_DIR', ASSETS_DIR . DIRECTORY_SEPARATOR . 'db'); // 共用 www/assets/db/*.db
define('LOG_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'log'); // 共用 www/log
define('EXPORT_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'export'); // 共用 www/export
define('IMPORT_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'import'); // 共用 www/import</code></pre>
<h2>第五章:<code>LAH-API</code> 移植至 PHP 8.5 程式碼修改檢核表</h2>
<table>
<thead>
<tr>
<th style="width:24%">目標檔案與行號</th>
<th style="width:32%">原本寫法 (PHP 7.3)</th>
<th style="width:44%">PHP 8.5 修正寫法與原因</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>include/init.php</code><br>(L11-14)</td>
<td><code>session_set_cookie_params(["SameSite" => "None"]);</code> (分三次呼叫且大寫)</td>
<td><code>session_set_cookie_params(['samesite' => 'Lax', 'secure' => false, 'httponly' => true]);</code><br>(PHP 8 陣列 Key 強制小寫,且布林不可用字串)</td>
</tr>
<tr>
<td><code>SQLiteOFFICES.class.php</code> (L17)<br><code>SQLiteSYSAUTH1.class.php</code> (L17, 45)<br><code>SQLiteImage.class.php</code> (L84, 85)<br><code>Temperature.class.php</code> (L126)</td>
<td><code>$stm->bindParam(':alias', str_replace(...));</code></td>
<td>改為 <code>$stm->bindValue(...);</code><br>(PHP 8 嚴格禁止將函式回傳值或運算式以傳址參考傳入 <code>bindParam</code> 第二參數,否則拋出 Fatal Error)</td>
</tr>
<tr>
<td><code>System.class.php</code> (L626-664)<br><code>MSDB.class.php</code> (L48)<br><code>Logger.class.php</code> (L99-101)<br><code>Message.class.php</code><br><code>MOIEXP.class.php</code></td>
<td><code>"(HOST=${ip})(PORT=${port})"</code><br><code>"log-${date}.zip"</code></td>
<td>全部改為 <code>"{$ip}"</code>、<code>"{$port}"</code>、<code>"log-{$date}.zip"</code><br>(PHP 8.2 起棄用 <code>${var}</code> 字串插值語法)</td>
</tr>
<tr>
<td><code>OraDB.class.php</code><br>(L110, L141, L152)</td>
<td><code>trigger_error(..., E_USER_ERROR);</code></td>
<td><code>throw new \RuntimeException($e['message']);</code><br>(PHP 8.4 / 8.5 正式棄用 <code>E_USER_ERROR</code>)</td>
</tr>
<tr>
<td><code>GlobalFunctions.inc.php</code> (L130, 380)<br><code>ADService.class.php</code> (L986)<br><code>DGXLandCaseParser.class.php</code> (L569)<br><code>MOISMS.class.php</code> (L62, 87, 115, 140)</td>
<td><code>curl_close($ch);</code><br><code>imagedestroy($image);</code></td>
<td>直接移除或改為 <code>unset($ch);</code><br>(<strong>PHP 8.5 最新棄用</strong>:<code>CurlHandle</code> 與 <code>GdImage</code> 物件已自動管理記憶體)</td>
</tr>
<tr>
<td><code>SQLSRV_DataBase.class.php</code><br>(L286, 321, 399, 438)</td>
<td><code>utf8_decode($value)</code></td>
<td><code>mb_convert_encoding($value, 'ISO-8859-1', 'UTF-8')</code><br>(PHP 8.2+ 棄用 <code>utf8_decode</code>)</td>
</tr>
<tr>
<td><code>API/composer.json</code></td>
<td><code>"phpoffice/phpspreadsheet": "^1.14"</code></td>
<td>升級為 <code>"^2.0"</code> 或更高版本(舊版 1.14 在 PHP 8.5 會因舊語法 Fatal Error)</td>
</tr>
</tbody>
</table>
<h2>第六章:Apache 2.4 (<code>httpd.conf</code>) <code>mod_proxy_fcgi</code> 雙軌與安全防護設定</h2>
<p>為支援 PHP 8.5 FastCGI 雙軌運行,同時<strong>防範 Web 伺服器目錄瀏覽(Index of /... 資訊洩漏)</strong>與機敏原始碼窺探,必須於 <code>C:\AppServ\Apache24\conf\httpd.conf</code> 進行以下設定:</p>
<h3>6.1 啟用 Apache 必要模組</h3>
<p>在 <code>httpd.conf</code> 中搜尋並取消註解(移除行首 <code>#</code>)以下核心模組:</p>
<pre><code>LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule headers_module modules/mod_headers.so</code></pre>
<h3>6.2 既有全站與 DocumentRoot 目錄清單瀏覽關閉</h3>
<p>預設 AppServ 安裝時會開啟 <code>Indexes</code>,導致若目錄無首頁檔案時將列出伺服器磁區全部內容。請在 <code>httpd.conf</code> 中找到 <code><Directory "C:/AppServ/www"></code> 區塊,將 <code>Options Indexes FollowSymLinks</code> 調整為 <code>-Indexes</code>:</p>
<pre><code><Directory "C:/AppServ/www">
Header set Access-Control-Allow-Origin "*"
# 關鍵防護:將 Indexes 改為 -Indexes,徹底阻絕 /export/、/include/ 等目錄清單洩漏
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory></code></pre>
<h3>6.3 LAH-API 雙軌轉發與安全沙盒設定(加入 httpd.conf 最末端)</h3>
<p>於 <code>httpd.conf</code> 檔案最下方加入以下完整設定區塊:</p>
<pre><code># =====================================================================
# LAH-API (PHP 8.5 FastCGI via PM2 on 127.0.0.1:8085)
# =====================================================================
# 1. 優先定義相容 Alias:將舊呼叫端之 /api/ 映射至實體 /v1/ 目錄
Alias /LAH-API/api "C:/AppServ/LAH-API/v1"
Alias /lah-api/api "C:/AppServ/LAH-API/v1"
# 2. 服務根目錄映射
Alias /LAH-API "C:/AppServ/LAH-API"
Alias /lah-api "C:/AppServ/LAH-API"
<Directory "C:/AppServ/LAH-API">
Header set Access-Control-Allow-Origin "*"
# 關鍵防護:關閉目錄自動索引 (禁止 Index of /LAH-API 瀏覽)
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
# 關鍵防護:禁止外網直接下載機敏副檔名檔案 (設定檔、套件鎖定檔、測試腳本、文件等)
<FilesMatch "^\.|\.(json|lock|xml|bat|ps1|md|ini|config|yml|yaml)$">
Require all denied
</FilesMatch>
<FilesMatch "\.php$">
# 關鍵防護:確認實體 .php 檔案存在才轉發,避免路徑穿透
<If "-f %{REQUEST_FILENAME}">
# Windows 下結尾務必帶斜線 /,並以 ProxyFCGISetEnvIf 剝除 proxy:fcgi:// 前綴供 php-cgi.exe 讀取實體路徑
SetHandler "proxy:fcgi://127.0.0.1:8085/"
ProxyFCGISetEnvIf "reqenv('SCRIPT_FILENAME') =~ m#^proxy:fcgi://127\.0\.0\.1:8085/(.*)$#" SCRIPT_FILENAME "$1"
</If>
</FilesMatch>
</Directory>
# 關鍵防護:禁止外網直接存取內部核心目錄 (原始碼 include、單元測試 tests、套件庫 vendor)
<Directory "C:/AppServ/LAH-API/include">
Require all denied
</Directory>
<Directory "C:/AppServ/LAH-API/tests">
Require all denied
</Directory>
<Directory "C:/AppServ/LAH-API/vendor">
Require all denied
</Directory>
# 建立 FastCGI 連線池,最大 16 併發,逾時 300 秒(避免跨所大筆地籍查詢 Timeout)
<Proxy "fcgi://127.0.0.1:8085/" enablereuse=on max=16>
ProxySet timeout=300
</Proxy></code></pre>
<div class="callout-warn">
<strong>⚠️ 重要指令解析與設定原則:</strong>
<ul>
<li><code>Options -Indexes +FollowSymLinks</code>:移除 <code>Indexes</code>,當存取目錄(如 <code>http://127.0.0.1/LAH-API/</code>)時,伺服器不再生成目錄清單,直接返回 <code>403 Forbidden</code>。</li>
<li><code><FilesMatch></code> 機敏檔案阻擋:阻擋 <code>composer.json</code>、<code>composer.lock</code>、<code>phpunit.xml</code>、<code>ecosystem.config.js</code>、<code>*.bat</code>、<code>*.md</code> 等檔案被直接存取或下載。</li>
<li><code><Directory ...> Require all denied</code>:Apache 規則中 <code><Directory></code> <strong>不可巢狀置於另一個 <code><Directory></code> 內</strong>,必須各自獨立宣告,將 <code>include/</code>(類別庫)、<code>tests/</code>(單元測試)、<code>vendor/</code>(Composer 相依)對外網路完全封閉。</li>
<li><code>ProxyFCGISetEnvIf ... SCRIPT_FILENAME "$1"</code>:Windows 環境下 <code>mod_proxy_fcgi</code> 預設會將 SCRIPT_FILENAME 帶上 <code>proxy:fcgi://127.0.0.1:8085/</code> 前綴,導致 Windows 上的 <code>php-cgi.exe</code> 無法正確辨識磁區實體路徑(回傳 404 Primary script unknown),此指令能將前綴剝除,精確還原實體路徑。</li>
</ul>
</div>
<h3>6.4 雙層縱深防禦:應用層守門員腳本 (index.php Guards)</h3>
<p>除 Web 伺服器層配置外,於 <code>API/index.php</code>、<code>API/v1/index.php</code>、<code>API/include/index.php</code> 與 <code>API/tests/index.php</code> 皆配置有守門員腳本。當訪客直接存取資料夾路徑時,由 PHP 8.5 FastCGI 即時攔截並返回標準 JSON 阻擋訊息,兼具即時生效與多層防禦優點:</p>
<pre><code><?php
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
echo json_encode([
'status' => 403,
'message' => 'Forbidden: Directory listing is disabled.'
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;</code></pre>
<h3>6.5 設定檢查與服務重啟流程</h3>
<ol>
<li><strong>語法測試(必做)</strong>:
<pre><code>C:\AppServ\Apache24\bin\httpd.exe -t</code></pre>
確認輸出為 <code>Syntax OK</code> 方可重啟,避免 Apache 因語法錯誤停止服務。
</li>
<li><strong>重啟 Apache 服務</strong>:
<ul>
<li>以<strong>系統管理員身分 (Run as Administrator)</strong> 開啟命令提示字元執行:
<pre><code>net stop Apache2.4 && net start Apache2.4</code></pre>
</li>
<li>或執行:
<pre><code>C:\AppServ\Apache24\bin\httpd.exe -k restart</code></pre>
</li>
</ul>
</li>
<li><strong>端點防護測試</strong>:
<pre><code># 1. 驗證目錄瀏覽已成功阻擋 (應回傳 HTTP 403)
curl -i http://127.0.0.1/LAH-API/
curl -i http://127.0.0.1/LAH-API/v1/
# 2. 驗證 API 服務正常運作 (應回傳正常 JSON)
curl -s -d "type=test" http://127.0.0.1/LAH-API/v1/system_json_api.php
curl -s http://127.0.0.1/LAH-API/ver.php</code></pre>
</li>
</ol>
<h2>第七章:開發筆電(<code>c:\Temp\DEV\LAH-Workspace</code>)實驗步驟</h2>
<ol>
<li><strong>安裝 PHP 8.5 NTS</strong>:下載 Windows PHP 8.5 (VS17 x64 Non Thread Safe) 解壓至 <code>C:\AppServ\php85</code>,複製 <code>php.ini-development</code> 為 <code>php.ini</code>,開啟 <code>curl, fileinfo, gd, intl, mbstring, odbc, openssl, pdo_odbc, pdo_sqlite, sqlite3, sockets, zip</code>。</li>
<li><strong>建立 <code>LAH-API\API</code> 目錄</strong>:將 <code>LAH-API\v1</code> 重新命名為 <code>LAH-API\API</code>,放入 <code>ecosystem.config.js</code> 與測試用 <code>ver.php</code>。</li>
<li><strong>使用 PM2 啟動 PHP 8.5 FastCGI</strong>:
<pre><code>cd C:\AppServ\LAH-API
pm2 start ecosystem.config.js
pm2 status</code></pre>
</li>
<li><strong>驗證雙軌連線</strong>:
<ul>
<li>若已啟動 Apache 2.4:瀏覽器開啟 <code>http://127.0.0.1/ver.php</code> (顯示 PHP 7.4) 與 <code>http://127.0.0.1/LAH-API/ver.php</code> (顯示 PHP 8.5 <code>cgi-fcgi</code>)。</li>
<li>若筆電未開 Apache,想快速單獨測 PHP 8.5:執行 <code>C:\AppServ\php85\php.exe -S 127.0.0.1:8085 -t c:\Temp\DEV\LAH-Workspace\LAH-API\API</code>,開啟 <code>http://127.0.0.1:8085/ver.php</code>。</li>
</ul>
</li>
</ol>
</body>
</html>