Mark Ku's Blog
首頁 關於我
回顧打造德國電子商務網站,我們做了些特別架構的設計
回顧打造德國電子商務網站,我們做了些特別架構的設計
Mark Ku
Mark Ku
October 16, 2024
2 min

系統目標

此系統改版主要目標是對德國網站進行優化及重寫,透過這次的改版,不僅改進了舊有的系統架構上無法克服的問題及程式碼不易維護擴充的問題,希望進而提升德國網站的銷售業績,未來也可以拿新寫的網站,快速去其他國家擴展。

系統當時的狀況

  • 系統老舊、程式碼遺失,沒有人可以在本地執行。
  • 使用者不易維護原有的資料
  • 舊的 API 系統架構,缺乏彈性無法擴充,無法容器化,目前仍要手動佈署。
  • 舊的系統架構及擴充,也不利於 SEO 的優化。
  • 舊的網站無法銷售套裝電腦及配件,目前全塞在電腦選配頁面裡銷售。
  • 舊的網站三不五時因交易失敗,需要手動將暫存訂單轉成訂單。
  • 舊的網站,沒有模塊化,技術框架老舊無法容器化,無法輕鬆移到其他國家使用。

挑戰與困難

原本招募我進來的目的是規劃及重新打造德國網站,但因為美國網站一直有問題及有更急的專案在經歷了充滿挑戰的第一年半,美國網站的趨近於穩定。

想說終於可以開始做德國網站,內部有著不同的大目標,礙於我們就只有三個開發人員,好幾個專案,全部一起執行會變慢,專案切來切去大家也很痛苦,來來回回去溝通了幾次,最終可以同一時間專注在一個項目上開發。

德國電商網站 - 架構圖

架構設計
架構設計
P.S. 礙於我們各種資源的限制,前端在外國網站買了 Theme,後端找了一個開源 NetCore 的專案來改。

這次採用的技術是 NextJS App routing + Dotnet core 8 ,我主要負責分析釐清需求、規劃架構、拆解工作清單,同時也參與開發核心功能開發,並獨立建置整套佈署開發環境及正式佈署開發環境。

接著,回顧德國電商的技術細節,做了那些特別的設計

前端網站( NextJS )

選擇 App routing 還是 page routing ?

當時的時空背景下,NextJS 的版更很快,每個月變動也不小,且App routing 剛stable,因此還想了好一陣子,在考量Server sdie component 的好處,可以讓不需要互動的元件的JS 更小,我試著將購買的專案模板,從Page routing 昇級到 App routing,發現新做的專案Side effect , 應該沒這麼大,且如果採Page routing 做完之後要昇級,可能更痛。

為了加速開發,導入了Headless CMS - Strapi

在過去,我們將網站設定、SEO、多語系等參數儲存在後台,透過 JSON 資料進行操作。這種維護方式確實讓我們可以快速開發,但對使用者來說卻不夠友善,沒有學過程式的人,會對這些JSON設定的作用感到困惑,上傳圖片及設定還要分開來設定,網站也常因路徑設定錯誤,出現叉燒包。

原本是想採用自己打造的 Low code cms,來做德國網站,但礙於不夠成熟及時間不夠,最後採用了Strapi CMS 解決資料維護的問題。

多語系

一開始多語系原本是放在Strapi ,發現在開發階段時發現,覺得使用者在Strapi新增多語系很繁瑣,後來全部將其維護功能,移到 Excel 365,再用程式寫入Redis,使用者可以更輕鬆快速一次維護多個語系。

Proxy api (/api/proxy/[…path].ts)

在過去使用 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);
    }
}

解決過去NEXT JS Redis Connect pool 釋放不掉的問題

頁面的渲染我們用到了大量的Redis ,原本用 node-redis,因為 Connect pool 釋放不掉,Redis 三不五時就需要手動重啟Redis,請同事用Jmeter 做了次壓力測試後,後來換成 IO redis 套件,就再也沒這個問題了。

後端 (Dotnet core)

制訂統一的 api 接口 及錯誤代碼

制定一致的API 接口,前端可以一致性的處理

{
  "count": 0,
  "data": null,  
  "isSuccess": true,
  "code": 20000,  
  "errors": []
}

C# 非同步延申的問題

為了讓 CPU 和 IO 中達到最高的利用率,讓需要等待的執行緖,先去做其他事情,因此我們此後端API,全部採非同步的方式撰寫。

然而,後端在非同步處理,導致在查詢日誌檔的線程時可能出現混亂,變得不好追蹤。

為了解決這個問題,我們使用了 CorrelationId 這個套件,每次請求都會產生一個CorrelationId,並顯示在Response Headers。通過這個 CorrelationId 套件,並在nlog 設定後,就有利於讓我們將相關的Log 串聯起來,以便於追蹤生產環境發生的緊急問題。 correlationId

Token base authentication - JWT Token

過去採用的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);

暫存訂單 ( 用到抽像類別 abstract class 及物件繼承關係,讓程式可以覆用,只要維護一份程式碼 )

因為外國的金流,有些導來導去,很機會會發生交易失敗,過去的暫存訂單,會在結帳前暫存,直到真正結帳,才會寫入真的訂單,這就會有兩段類似的邏輯,且因為暫存訂單和訂單的關係,欄位相同。

繼承關係

[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();
}
...
} 

我們都知道,抽像類別是用來讓一系列類別有共用的實作,把暫存訂單和訂單共同的實作抽出來,透過抽像類別把原本兩份程式,透過抽像類別及繼承關係,變成只需要一份程式,讓程式碼開發維護起來更輕鬆。

Https Secure Distribution with ngnix

當我剛加入公司時,伺服器的架構是基於巢狀的 Hyper-V。然而,由於每台伺服器僅能使用一個 HTTPS (443) 埠,導致每次擴展 Web 應用服務時都需要啟動一個新的虛擬機器,這不僅效率低下,還浪費大量伺服器資源(如 CPU、記憶體、硬碟和外部 IP)。如今,只需要一行 Docker 指令,就能輕鬆部署多個服務,無需像過去那樣安裝作業系統和繁瑣的環境配置。由於短期內無法移轉到 K8S ,我們決定先通過 Nginx 簡化伺服器架構,提高部署效率。

程式產生器

為了加速開發,後端底層是採用開源的框架,自己整理過,並透過 程式產生器 ,建立模型及資料存取層,加速後端的開發。

解決SFTP不穩定的問題

舊有的FTP Server 連線進去常常會斷線,試了幾套 Docker FTP後,終於架了一套擁有 Web UI 管理界面,且開源 SFTP Server - sftpgo。

Google CAPTCHA 換成 Cloudflare Turnstile

在防止機器人攻擊方面,在登入或是查詢訂單頁面,我們將原本的Google CAPTCHA 替換了 Cloudflare Turnstile,一來可以省錢(免費),串接也更容易,且對使用者體驗更好,且比Google CAPTCHA V3又更少誤判。

Cloudflare Turnstile
Cloudflare Turnstile

SEO 的部份

歐洲對個資使用非常重視,因此不能隨意拿會員資料做行銷,因此SEO 就格外重要。

  • 提交網站索引 Sitemap 到 Google search console 及 bing webmaster tools。
  • 提供 Metadata Tag,更有利益爬蟲建立索引。
  • 提供結構化的資料( Google schema) 給爬蟲,有助於爬蟲更理解我們的網站,並有機會獲得額外的版面。
  • 修正Google search console 或是 Bring Webmaster Tool 的建議及錯誤, 調整Html,讓其友善於爬蟲 ( title 50-60 個字元、Descriptions 150-160 個字元、alt 、h1 只能有一個 … )
  • 產品頁,使用Azure AI 生成 SEO Meta Tag 資料。
  • 在頁面變更時,使用了 Bing的 Index API Now,可以讓爬蟲更快到我們的網站
  • 舊站的頁面永久轉址導轉(http status 308),並將不需要的存在的頁面提交給搜尋引擎移除。

網站排名,剛上線時 4350 到 3745 名

Web Rank
Web Rank

單元測試

避免未來改功能時改壞,此次針對結帳金額或算計算運費,前後後端都有寫測試,並依據3A原則撰寫測試。

  • Arrange: 初始化目標物件、相依物件、方法參數。
  • Act: 呼叫目標物件的方法。
  • Assert: 驗證是否符合預期,

功能面

客服軟體

因為德國網站規模不大,過去沒有客服軟體,看了歐洲的通訊軟體市佔率後,我建議德國公司用了What’s app 當客服軟體,增加和使用者互動的機會。

導入Stripe 金流

Stripe 的金流串接相當容易,只要串好一個就能透過後台開啟10幾種歐洲的金流,因此在上線後兩週就能很快速的擴充新金流,且可以啟用(3DS)簡訊驗證,讓交易更安全。

另一個要注意的是GDPR

根據 GDPR 的規範,凡是有歐盟境內人士造訪的網站,都得通過 Cookie 彈窗明確告知使用 GA 並徵得用戶同意,這個同意必須是具體的、自願的、明確的,且可撤回的。

避免詐欺保險

歐洲及美國,網路詐欺嚴重,過去也因為刷退容易太容易,將 Paypal 關閉,但因為美國擁有可以透過詐欺保險服務來避免,但不服務於歐洲市場,因此我透過電商朋友的朋友問到,Riskfield,但因每月基本費用有點高,每筆還要額外抽%,因此 先不考慮。

Riskfield
Riskfield

上線前 DevOps 遇到的一些比較特別問題

因為遇到的問題比較多,因此額外寫了一篇Blog

結論

德國專案其實並不是從2月中才開始的,因為本身就對架構規劃有興趣,早在我剛加入公司時,只要有空閒,我就開始著手研究架構及規劃,從舊有的程式碼中推導出需求,並思考如何縮短開發時間,儘管開發過程中遇到不少小插曲,但最終我們在7月1號成功上線,德國網站如期順利運行,歐洲市場也逐步進入正軌。

這次能夠順利上線,過程中也獲得了許多人的幫助,也非常感謝大家,由於一直沒有專職PM,設計主管和我一直兼任這個角色,加上資源有限,我們在一邊規劃、一邊開發的同時,還需要協調外部事務,最終能夠達成這個目標,真的很不容易。


Tags

architecture-designgermanye-commerce
Mark Ku

Mark Ku

Software Developer

9年以上豐富網站開發經驗,開發過各種網站,電子商務、平台網站、直播系統、POS系統、SEO 優化、金流串接、AI 串接,Infra 出身,帶過幾次團隊,也加入過大團隊一起開發。

Expertise

前端(React)
後端(C#)
網路管理
DevOps
溝通
領導

Social Media

facebook github website

Quick Links

關於我

Social Media