# 天瀚國際科技 資深後端工程師技術問卷 (20210114) 1. 請舉例在你過去的工作經驗中,覺得面臨過最大的技術挑戰或最令你印象深刻的技術性問題,並分享最後如何解決他? --- 問題回應: 在過往的工作經驗上,比較少有一些較深入的技術問題需要解決,但在工作上面對一些較不熟悉的技術,去深入了解及解決需求的情形還不少。 如藉由串接 Apple Sign In 去了解 JWT 及 OAuth 的運作機制,在進行登入驗證時需要對取得的 token 做 RSA 解密驗證時,也是耗費了一些時間研究。 1. 需要先打 api 取得 public key 2. 將 public key 部分資訊做 base64 解碼後組成 RSA key 3. 將取得的 token 透過 RSA key 做解碼 4. 將解碼的值與 token 透過 base64 解開的值做比對 而在進行集團 SSO 串接的時候,也透過 Laravel Socialite 來實作 OAuth 的串接,藉由 library 化的方式快速實作在多個入口端程式中。 在鴻海時期開發雲端服務時,也有過要實作網頁版遠端桌面的需求,後來是透過架設 guacamole server 來完成需求。 以上是比較有深刻印象的部分。 --- 2. 如果要實作一個黑名單檢查器來檢查一個 URL 是否存在黑名單清單內,而黑名單的資料量可能高達數百萬筆,請問在不使用外部服務(如:MySQL、Reids)的前提、並可以允許有一定機率內的誤判情況下,你會如何以最低的時間及空間複雜度來實作以下的 BlacklistChecker 來判斷該 URL 是否在黑名單之內?(使用文字描述即可,不需要寫出完整程式) ```php public function checkUrl(string $url) { $blacklistChecker = new BlacklistChecker(); if (! $blacklistChecker->checkUrl($url)) { throw new BlacklistException('This url is in blacklist.'); } return true; } ``` --- 問題回應: 如果要適用於大資料的檢索,又不透過 db 處理的話,可以使用 trie 來建立索引,建立每個節點在 O(m) 時間可以完成,在建立完樹後,搜尋時也只需要 O(m) 的時間及空間複雜度 (m 為字串長度)。 附上 wiki:https://en.wikipedia.org/wiki/Trie --- 3. 如下面範例 Topic 和 Comment 兩個 Model 之間為一對多的關聯,listTopicsWithComments 方法會列出依據建立時間倒序排序的 Topic,並列出每個 Topic 底下所有的 Comment,請問以下的程式可能存在什麼問題,如果是你會怎麼修改? 以下程式以 Laravel 框架為基礎 ```php public function listTopicsWithComments(Request $request) { $topics = Topic::latest()->paginate(); return TopicResource::collection($topics); } ``` ```php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class TopicResource extends JsonResource { public function toArray() { return [ 'id' => $this->resource->id, 'title' => $this->resource->title, 'comments' => $this->resource->comments ]; } } ``` --- 問題回應: 1. 可以建立 comment 的 Resource,再透過 Comment 物件來對應 ```php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class CommentResource extends JsonResource { public function toArray() { return [ // comment 內容 ]; } } ``` ```php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; use App\Models\Comment; class TopicResource extends JsonResource { public function toArray() { return [ 'id' => $this->resource->id, 'title' => $this->resource->title, 'comments' => new Comment($this->resource->comments) ]; } } ``` 2. 也可以透過 with 來設定關聯,就不用額外建立 Comment 的 Resource ```php public function listTopicsWithComments(Request $request) { $topics = Topic::with('comments')->latest()->paginate(); return TopicResource::collection($topics); } ``` ```php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; use App\Models\Comment; class TopicResource extends JsonResource { public function toArray() { return [ 'id' => $this->resource->id, 'title' => $this->resource->title, // 透過 with 額外加入 comment,不需要在 Resource 設定 ]; } } ``` --- 4. A, B 兩間不同第三方金流廠商各自提供了交易的 HTTP API,程式中 WalletA 與 WalletB 分別封裝 API 並且共同實做了 WalletContract 介面。如下有一個 transferMoney 的方法來達成將 A 錢包餘額轉帳至 B 錢包的功能,請問以下的程式可能存在什麼問題,如果是你會怎麼修改?(使用文字描述即可,不需要寫出完整程式) ```php interface WalletContract { // 依據交易單號進行存款操作,並回傳操作後餘額 public function deposit(string $transactionNumber, string $userId, float $money): float; // 依據交易單號進行提款操作,並回傳操作後餘額 public function withdraw(string $transactionNumber, string $userId, float $money): float; // 確認特定交易單號的交易是否成功執行 public function checkTransaction(string $transactionNumber): boolean; // 取得特定使用者的當下錢包餘額 public function getBalance(string $userId): float; } ``` ```php // 範例中假設只會 A 錢包單方向轉帳至 B 錢包 public function transferMoney(User $user, float $money) { $walletA = new WalletA(); $walletB = new WalletB(); // 檢查 A 錢包餘額是否足夠 if ($walletA->getBalance($user->id) < $money) { throw new WalletException('Wallet amount is not enough.'); } // 產生唯一的交易單號 uuid $transactionNumber = TransactionNumberGenerator::generate(); // 執行 A, B 錢包的轉帳交易 $walletA->withdraw($transactionNumber, $user->id, $money); $walletB->deposit($transactionNumber, $user->id, $money); return [ 'success' => true; ]; } ``` --- 問題回應: 1. 若是唯一的交易單號,應該 A 提款與 B 存款要有各自的交易編號才對,不應該共用 2. A 提款成功與 B 存款成功都沒有透過 checkTransaction 做檢查 3. 轉帳交易之方法應定義在 WalletContract 中,透過 A 錢包直接呼叫轉帳方法,於方法中傳入錢包 B 進行後續動作 4. 可將交易所需資訊包裝成物件,並將 checkTransaction 定義到物件中檢查 程式範例: ```php interface WalletContract { // 依據交易資訊進行存款操作,並回傳操作後餘額 public function deposit(User $user, float $money): float; // 依據交易資訊進行提款操作,並回傳操作後餘額 public function withdraw(User $user, float $money): float; // 依據交易資訊進行轉帳操作,傳入目標錢包 public function transferMoney(WalletContract $wallet, User $user, float $money): float; // 取得特定使用者的當下錢包餘額 public function getBalance(string $userId): float; } ``` ```php use TransactionNumberGenerator; // 交易物件 class Transaction { string $transactionNumber; User $user; float $money; /** * 建立時會建立 transaction number * 或是傳入已存在之 transaction number 做相關操作 */ public function __construct(User $user, float $money, string $transactionNumber = '') { // 產生唯一的交易單號 uuid $this->transactionNumber = (empty($transactionNumber)) ? TransactionNumberGenerator::generate() : $transactionNumber; $this->user = $user; $this->money = $money; } // 使用 transaction number 確認交易是否成功執行 public function checkTransaction(): boolean; } ``` ```php // 執行轉帳方法 public function transferMoney(User $user, float $money) { $walletA = new WalletA(); // 檢查 A 錢包餘額是否足夠 if ($walletA->getBalance($user->id) < $money) { throw new WalletException('Wallet amount is not enough.'); } $walletA->transferMoney(new WalletB(), $user, $money); return [ 'success' => true; ]; } ``` ```php class WalletA implement WalletContract { // 執行 A, B 錢包的轉帳交易 public function transferMoney(WalletContract $wallet, User $user, float $money): boolean { // A 取款 $this->withdraw($user, $money); // B 存款 $wallet->deposit($user, $money); return true; } // 提款流程 (存款架構相同) public function withdraw(User $user, float $money) { // 建立 Transaction $trans = new Transaction($user, $money); // 提款步驟 ... // 檢查交易狀態 if (!$trans->checkTransaction()) { throw new WalletException(__FUNCTION__ . ' transaction failed'); } // 回傳餘額 return $this->getBalance($user->id); } } ``` ---