此系統改版主要目標是對德國網站進行優化及重寫,透過這次的改版,不僅改進了舊有的系統架構上無法克服的問題及程式碼不易維護擴充的問題,希望進而提升德國網站的銷售業績,未來也可以拿新寫的網站,快速去其他國家擴展。
原本招募我進來的目的是規劃及重新打造德國網站,但因為美國網站一直有問題及有更急的專案在經歷了充滿挑戰的第一年半,美國網站的趨近於穩定。
想說終於可以開始做德國網站,內部有著不同的大目標,礙於我們就只有三個開發人員,好幾個專案,全部一起執行會變慢,專案切來切去大家也很痛苦,來來回回去溝通了幾次,最終可以同一時間專注在一個項目上開發。
P.S. 礙於我們各種資源的限制,前端在外國網站買了 Theme,後端找了一個開源 NetCore 的專案來改。
這次採用的技術是 NextJS App routing + Dotnet core 8 ,我主要負責分析釐清需求、規劃架構、拆解工作清單,同時也參與開發核心功能開發,並獨立建置整套佈署開發環境及正式佈署開發環境。
當時的時空背景下,NextJS 的版更很快,每個月變動也不小,且App routing 剛stable,因此還想了好一陣子,在考量Server sdie component 的好處,可以讓不需要互動的元件的JS 更小,我試著將購買的專案模板,從Page routing 昇級到 App routing,發現新做的專案Side effect , 應該沒這麼大,且如果採Page routing 做完之後要昇級,可能更痛。
在過去,我們將網站設定、SEO、多語系等參數儲存在後台,透過 JSON 資料進行操作。這種維護方式確實讓我們可以快速開發,但對使用者來說卻不夠友善,沒有學過程式的人,會對這些JSON設定的作用感到困惑,上傳圖片及設定還要分開來設定,網站也常因路徑設定錯誤,出現叉燒包。
原本是想採用自己打造的 Low code cms,來做德國網站,但礙於不夠成熟及時間不夠,最後採用了Strapi CMS 解決資料維護的問題。
一開始多語系原本是放在Strapi ,發現在開發階段時發現,覺得使用者在Strapi新增多語系很繁瑣,後來全部將其維護功能,移到 Excel 365,再用程式寫入Redis,使用者可以更輕鬆快速一次維護多個語系。
在過去使用 Next.js 時,每當需要呼叫 C# API,我們都得在 NextJS 裡額外實作一個 API,現在透過使用 http-proxy-middleware 套件來客製的代理伺服器 ,我們不再需要費時維護 NextJS 額外的 API,大幅節省了開發時間及跨域存取(CORS)的問題。
import { CookieKeys } from '@/const/keys'; import { clientSideLog } from '@utils/log/client-side-log'; import { LogLevel } from '@utils/log/const-logging'; import { createProxyMiddleware } from 'http-proxy-middleware'; export const config = { api: { externalResolver: true, // true, means not use the default settings of Next.js bodyParser: false, // false, means not use Next.js bodyParser(same as express) }, }; const proxy: any = createProxyMiddleware({ target: process.env.NEXT_PUBLIC_API, changeOrigin: true, // change `request domain` to `target` pathRewrite: { '^/api/proxy': '' }, // remove `/api/proxy` prefix onProxyReq: relayRequestHeaders, onError: (err: any, req: any) => { clientSideLog( `[Proxy] Error for ${req.method} '${req.url}' to '${process.env.NEXT_PUBLIC_API}': ${err.message}`, LogLevel.Error ); }, }); function proxyServer(req: any, res: any) { proxy(req, res, (err: any) => { if (err) { throw err; } throw new Error(`Request '${req.url}' is not proxied! We should never reach here!`); }); } export default proxyServer; function relayRequestHeaders(proxyReq: any, req: any, res: any) { let cookie = req.headers.cookie; if (cookie) { proxyReq.setHeader('cookie', cookie); } }
頁面的渲染我們用到了大量的Redis ,原本用 node-redis,因為 Connect pool 釋放不掉,Redis 三不五時就需要手動重啟Redis,請同事用Jmeter 做了次壓力測試後,後來換成 IO redis 套件,就再也沒這個問題了。
制定一致的API 接口,前端可以一致性的處理
{ "count": 0, "data": null, "isSuccess": true, "code": 20000, "errors": [] }
為了讓 CPU 和 IO 中達到最高的利用率,讓需要等待的執行緖,先去做其他事情,因此我們此後端API,全部採非同步的方式撰寫。
然而,後端在非同步處理,導致在查詢日誌檔的線程時可能出現混亂,變得不好追蹤。
為了解決這個問題,我們使用了 CorrelationId 這個套件,每次請求都會產生一個CorrelationId,並顯示在Response Headers。通過這個 CorrelationId 套件,並在nlog 設定後,就有利於讓我們將相關的Log 串聯起來,以便於追蹤生產環境發生的緊急問題。
過去採用的Session,其實就不易做到橫向擴展,為了能讓後端API,未來伺服器能橫向擴展,降低api 偶合度,我們這次導入了,Token base(JWT)為辨別用戶身份的方式。
自動讀取 Config 注入相關的金流Service , 並透過界面將定義所有金流需要的界口,並透過 Autofac 將金流解析,達到依賴反轉及容易擴充並解耦合,傳入payment code 就能自動抽換不同的金流邏輯。
var paymentConfigs = AppSettingsHelper.GetSection<List<PaymentConfig>>("Payment", "PaymentOptions").Where(x => !x.Disable).ToList(); foreach (var item in paymentConfigs) { var serviceTypeName = item.PaymentCode; Type serviceType = assembly.GetType($"MemberSite.Services.{serviceTypeName}Service"); if (serviceType == null) { throw new Exception($"Payment {serviceType} service not found"); } builder.RegisterType(serviceType) .WithProperty("PaymentConfig", item) .WithParameter(new ResolvedParameter( (pi, ctx) => pi.ParameterType == typeof(IUnitOfWork), (pi, ctx) => ctx.Resolve<IUnitOfWork>())) .PropertiesAutowired() // 啟用屬性自動注入 .Named<IPaymentOptionService>(item.PaymentCode); }
傳入payment code 就能自動依據傳入命名抽換不同的金流邏輯
var params = new Dictionary<string,string>(); var paymentService = _context.ResolveNamed<IPaymentOptionService>(checkout.PaymentTypeCode); paymentService.pay(params);
因為外國的金流,有些導來導去,很機會會發生交易失敗,過去的暫存訂單,會在結帳前暫存,直到真正結帳,才會寫入真的訂單,這就會有兩段類似的邏輯,且因為暫存訂單和訂單的關係,欄位相同。
繼承關係
[SugarTable("TempOrders")] public class TempOrders: Orders { }
抽像類別
AbstractInsertOrderService { ... public T CreateOrderInstance<T>(Checkout checkout) where T : Orders, new() { return new T() { Name = checkout.Name, Email = checkout.Email, ... }; } public int InsertOrders<T>(Checkout checkout) where T : Orders, new() // 泛型約束 (Generic Constraint) { var order = CreateCustomerInstance<T>(checkout); return _unitOfWork.GetDbClient().Insertable<T>(order).ExecuteReturnIdentity(); } ... }
我們都知道,抽像類別是用來讓一系列類別有共用的實作,把暫存訂單和訂單共同的實作抽出來,透過抽像類別把原本兩份程式,透過抽像類別及繼承關係,變成只需要一份程式,讓程式碼開發維護起來更輕鬆。
當我剛加入公司時,伺服器的架構是基於巢狀的 Hyper-V。然而,由於每台伺服器僅能使用一個 HTTPS (443) 埠,導致每次擴展 Web 應用服務時都需要啟動一個新的虛擬機器,這不僅效率低下,還浪費大量伺服器資源(如 CPU、記憶體、硬碟和外部 IP)。如今,只需要一行 Docker 指令,就能輕鬆部署多個服務,無需像過去那樣安裝作業系統和繁瑣的環境配置。由於短期內無法移轉到 K8S ,我們決定先通過 Nginx 簡化伺服器架構,提高部署效率。
為了加速開發,後端底層是採用開源的框架,自己整理過,並透過 程式產生器 ,建立模型及資料存取層,加速後端的開發。
舊有的FTP Server 連線進去常常會斷線,試了幾套 Docker FTP後,終於架了一套擁有 Web UI 管理界面,且開源 SFTP Server - sftpgo。
在防止機器人攻擊方面,在登入或是查詢訂單頁面,我們將原本的Google CAPTCHA 替換了 Cloudflare Turnstile,一來可以省錢(免費),串接也更容易,且對使用者體驗更好,且比Google CAPTCHA V3又更少誤判。
歐洲對個資使用非常重視,因此不能隨意拿會員資料做行銷,因此SEO 就格外重要。
網站排名,剛上線時 4350 到 3745 名
避免未來改功能時改壞,此次針對結帳金額或算計算運費,前後後端都有寫測試,並依據3A原則撰寫測試。
因為德國網站規模不大,過去沒有客服軟體,看了歐洲的通訊軟體市佔率後,我建議德國公司用了What’s app 當客服軟體,增加和使用者互動的機會。
Stripe 的金流串接相當容易,只要串好一個就能透過後台開啟10幾種歐洲的金流,因此在上線後兩週就能很快速的擴充新金流,且可以啟用(3DS)簡訊驗證,讓交易更安全。
根據 GDPR 的規範,凡是有歐盟境內人士造訪的網站,都得通過 Cookie 彈窗明確告知使用 GA 並徵得用戶同意,這個同意必須是具體的、自願的、明確的,且可撤回的。
歐洲及美國,網路詐欺嚴重,過去也因為刷退容易太容易,將 Paypal 關閉,但因為美國擁有可以透過詐欺保險服務來避免,但不服務於歐洲市場,因此我透過電商朋友的朋友問到,Riskfield,但因每月基本費用有點高,每筆還要額外抽%,因此 先不考慮。
德國專案其實並不是從2月中才開始的,因為本身就對架構規劃有興趣,早在我剛加入公司時,只要有空閒,我就開始著手研究架構及規劃,從舊有的程式碼中推導出需求,並思考如何縮短開發時間,儘管開發過程中遇到不少小插曲,但最終我們在7月1號成功上線,德國網站如期順利運行,歐洲市場也逐步進入正軌。
這次能夠順利上線,過程中也獲得了許多人的幫助,也非常感謝大家,由於一直沒有專職PM,設計主管和我一直兼任這個角色,加上資源有限,我們在一邊規劃、一邊開發的同時,還需要協調外部事務,最終能夠達成這個目標,真的很不容易。