[GitHub Global] Translate Vibe Coding 零基础教程/30 经验技巧/06 Vibe Coding 代码质量保障.md to zh-TW
This commit is contained in:
committed by
GitHub
parent
b4256b6717
commit
cb6b7b2732
@@ -0,0 +1,458 @@
|
|||||||
|
# Vibe Coding 程式碼品質保障
|
||||||
|
|
||||||
|
> 如何確保 AI 生成的程式碼品質
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
你好,我是魚皮。
|
||||||
|
|
||||||
|
很多同學在用 AI 開發時,會有這樣的擔心:AI 生成的程式碼靠譜嗎?會不會有隱藏的 bug?
|
||||||
|
|
||||||
|
這個擔心是有道理的。AI 雖然能快速生成程式碼,但它不能保證程式碼的品質。作為開發者,你需要建立一套品質保障體系。
|
||||||
|
|
||||||
|
本文我就來分享一些實用的方法,幫你確保 AI 生成的程式碼品質。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 一、什麼是好程式碼?
|
||||||
|
|
||||||
|
在講如何保障品質之前,我們先要明確什麼是好程式碼。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 好程式碼的特徵
|
||||||
|
|
||||||
|
什麼樣的程式碼才算好程式碼?
|
||||||
|
|
||||||
|
這個問題看似簡單,但很多人說不清楚。其實,除了能運行之外,好程式碼最重要的是 **可讀性好**:程式碼清晰易懂、符合團隊開發規範,別人(包括未來的你)能快速理解。在此基礎上,還要 **可維護性強**,修改和擴展都很容易,不會牽一髮而動全身。
|
||||||
|
|
||||||
|
當然,這些都要建立在 **功能正確** 的前提下,程式碼能正確實現需求,沒有 Bug。同時,**性能要合理**,在可接受的時間內完成任務,不浪費資源。此外,程式碼還要 **安全可靠**,沒有安全漏洞、能處理異常情況。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AI 生成程式碼的常見問題
|
||||||
|
|
||||||
|
那麼,AI 生成的程式碼通常會有哪些問題呢?
|
||||||
|
|
||||||
|
根據我的經驗,最常見的是 **過度複雜**,AI 為了實現功能,經常寫很多不必要的程式碼。另一個常見問題是 **缺少邊界處理**,AI 為了讓程式碼能快速運行,可能只考慮正常情況,忽略了空值、錯誤等特殊情況。
|
||||||
|
|
||||||
|
AI 生成的程式碼還經常出現 **重複程式碼** 的問題。特別是在前端開發中,如果你讓 AI 分別生成多個相似的頁面,它不會主動複用程式碼,而是為每個頁面生成獨立的程式碼。
|
||||||
|
|
||||||
|
舉個例子,假設你要做一個管理後台,有用戶列表頁、文章列表頁、評論列表頁。這三個頁面的佈局和功能都很相似:都是表格展示數據、都有搜索框、都有分頁。但如果你分 3 次讓 AI 生成,它會給你 3 套幾乎一樣的程式碼,只是數據字段不同。更好的做法是先讓 AI 生成一個通用的列表組件,然後用不同的配置來複用。這樣不僅程式碼量少,維護起來也容易。
|
||||||
|
|
||||||
|
有時候還會存在 **性能問題**,使用了低效的算法或數據結構。了解這些問題,你才能有針對性地檢查和改進。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 建立品質標準
|
||||||
|
|
||||||
|
知道了什麼是好程式碼,接下來就要為你的項目建立明確的品質標準。
|
||||||
|
|
||||||
|
在程式碼規範方面,建議使用 ESLint 或 Prettier 統一程式碼風格,定義清晰的命名規則(比如變數用 camelCase 駝峰式寫法,常量用 UPPER_SNAKE_CASE 寫法),並規定好文件和文件夾的組織方式。
|
||||||
|
|
||||||
|
在功能標準上,要求所有功能都要有測試、要處理邊界情況、錯誤要有友好的提示。
|
||||||
|
|
||||||
|
性能方面,可以設定具體的指標,比如頁面加載時間不超過 3 秒,API 響應時間不超過 1 秒,數據量大時使用虛擬滾動等。
|
||||||
|
|
||||||
|
可以把這些標準寫在項目文檔裡,讓 AI 也知道。
|
||||||
|
|
||||||
|
💡 不過實際開發中還是要靈活,如果你只是單純開發個 Demo 類的小項目,幹就完了,不用考慮那麼多。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 二、程式碼審查
|
||||||
|
|
||||||
|
程式碼審查(Code Review)是保障品質的第一道防線。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 為什麼要審查 AI 程式碼?
|
||||||
|
|
||||||
|
有些同學覺得:AI 生成的程式碼能跑就行了,為什麼還要審查?
|
||||||
|
|
||||||
|
這個想法其實挺危險的。
|
||||||
|
|
||||||
|
首先,AI 不是完美的,它會犯錯,會生成有 Bug 的程式碼。更重要的是,AI 只知道技術,**不了解你的具體業務邏輯**,它生成的程式碼可能在技術上沒問題,但在業務上不合理。
|
||||||
|
|
||||||
|
另外,AI 可能只關注當前功能,不考慮未來的擴展性。今天能跑的程式碼,明天可能就成了技術債。而且,審查程式碼也是一個學習的機會,能幫你理解程式碼的工作原理,提升自己的技術水平。所以,審查 AI 程式碼是必不可少的,尤其是對於有程式設計基礎的同學。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 審查的重點
|
||||||
|
|
||||||
|
那麼,審查 AI 生成的程式碼時,應該重點關注哪些方面呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 1、功能正確性
|
||||||
|
|
||||||
|
最基本的要求是:程式碼能不能正確實現需求?
|
||||||
|
|
||||||
|
這聽起來簡單,但很容易被忽視。你需要運行程式碼,**測試所有功能**,嘗試各種輸入,包括正常的和異常的。
|
||||||
|
|
||||||
|
特別要注意邊界情況,比如空值、極大值、極小值等,這些往往是 Bug 的高發區。
|
||||||
|
|
||||||
|
舉個例子:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// AI 生成的程式碼
|
||||||
|
function divide(a: number, b: number): number {
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
想想看,這段程式碼有什麼問題呢?
|
||||||
|
|
||||||
|
答案是:沒有處理除數為 0 的情況。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 改進後:
|
||||||
|
function divide(a: number, b: number): number {
|
||||||
|
if (b === 0) {
|
||||||
|
throw new Error('除數不能為 0');
|
||||||
|
}
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 2、程式碼可讀性
|
||||||
|
|
||||||
|
功能正確之後,接下來要看程式碼是否易讀。
|
||||||
|
|
||||||
|
記住,程式碼是給人看的,不只是給機器執行的。
|
||||||
|
|
||||||
|
檢查時要問自己幾個問題:
|
||||||
|
|
||||||
|
- 變數名是否清晰?
|
||||||
|
- 函數名是否表達了功能?
|
||||||
|
- 邏輯是否容易理解?
|
||||||
|
- 是否需要添加註釋?
|
||||||
|
|
||||||
|
如果你看程式碼時感到困惑,那別人也會困惑。
|
||||||
|
|
||||||
|
舉個例子:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不好的命名
|
||||||
|
function f(x: number): number {
|
||||||
|
return x * 2 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 好的命名
|
||||||
|
function calculateDiscountedPrice(originalPrice: number): number {
|
||||||
|
const discount = 0.2; // 8 折優惠
|
||||||
|
return originalPrice * (1 - discount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 3、錯誤處理
|
||||||
|
|
||||||
|
程式碼要能優雅地處理錯誤,不能一出錯就崩潰。
|
||||||
|
|
||||||
|
你要檢查 API 調用有沒有錯誤處理、用戶輸入有沒有驗證、異常情況有沒有友好的提示。很多 AI 生成的程式碼只考慮了正常流程,完全忽略了錯誤處理,這是很危險的。
|
||||||
|
|
||||||
|
舉個例子:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 不好的錯誤處理(根本沒做)
|
||||||
|
async function fetchUser(id: string) {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
const user = await response.json();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 好的錯誤處理
|
||||||
|
async function fetchUser(id: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`獲取用戶失敗: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await response.json();
|
||||||
|
return { data: user, error: null };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取用戶出錯:', error);
|
||||||
|
return { data: null, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 4、性能問題
|
||||||
|
|
||||||
|
然後是性能問題。程式碼要高效,不能浪費資源。
|
||||||
|
|
||||||
|
可以看看有沒有不必要的循環、有沒有重複的計算、數據結構選擇是否合理。AI 有時候為了快速實現功能,會選擇最簡單但不是最高效的方案。
|
||||||
|
|
||||||
|
舉個例子:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 性能不好
|
||||||
|
function findUser(users: User[], id: string): User | undefined {
|
||||||
|
// 每次都遍歷整個數組,O(n)
|
||||||
|
return users.find(user => user.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能更好
|
||||||
|
class UserManager {
|
||||||
|
private userMap: Map<string, User>;
|
||||||
|
|
||||||
|
constructor(users: User[]) {
|
||||||
|
// 用 Map 存儲,查找是 O(1)
|
||||||
|
this.userMap = new Map(users.map(u => [u.id, u]));
|
||||||
|
}
|
||||||
|
|
||||||
|
findUser(id: string): User | undefined {
|
||||||
|
return this.userMap.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 5、安全問題
|
||||||
|
|
||||||
|
對於商業項目來說,這點至關重要。程式碼要安全,不能有漏洞。
|
||||||
|
|
||||||
|
需要檢查有沒有 SQL 注入風險、有沒有 XSS 攻擊風險、敏感信息有沒有加密、API Key 有沒有暴露。AI 對安全的理解可能不夠深入,很容易留下安全隱患。
|
||||||
|
|
||||||
|
舉個 SQL 注入的例子。SQL 注入是指攻擊者通過在輸入中插入惡意的 SQL 程式碼,來執行非預期的數據庫操作。
|
||||||
|
|
||||||
|
比如下面這段程式碼是不安全的:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 不安全:直接拼接用戶輸入
|
||||||
|
const query = `SELECT * FROM users WHERE name = '${userName}'`;
|
||||||
|
```
|
||||||
|
|
||||||
|
假設用戶在登錄時輸入用戶名 `admin' OR '1'='1`,直接把這個輸入拼接到 SQL 語句裡,就會變成:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM users WHERE name = 'admin' OR '1'='1'
|
||||||
|
```
|
||||||
|
|
||||||
|
這個查詢會返回所有用戶,因為 `'1'='1'` 永遠為真。攻擊者就能繞過驗證登錄任何賬戶。
|
||||||
|
|
||||||
|
正確的做法是使用參數化查詢:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 安全:使用參數化查詢
|
||||||
|
const query = 'SELECT * FROM users WHERE name = ?';
|
||||||
|
db.execute(query, [userName]);
|
||||||
|
```
|
||||||
|
|
||||||
|
參數化查詢會自動轉義特殊字符,防止 SQL 注入。
|
||||||
|
|
||||||
|
如果你對 Web 安全感興趣,可以利用魚皮 [免費的網絡安全自學網](https://github.com/liyupi/ceshiya) 來學習這些知識:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 審查流程
|
||||||
|
|
||||||
|
可以按照下面的步驟,建立一個系統的審查流程:
|
||||||
|
|
||||||
|
1. 自己先看一遍:快速瀏覽程式碼,看有沒有明顯問題
|
||||||
|
|
||||||
|
2. 運行測試:測試所有功能,包括邊界情況
|
||||||
|
|
||||||
|
3. 逐行審查:仔細檢查每一行程式碼,思考是否有問題
|
||||||
|
|
||||||
|
4. 記錄問題:把發現的問題記下來
|
||||||
|
|
||||||
|
5. 要求 AI 改進:把問題反饋給 AI,讓它修復
|
||||||
|
|
||||||
|
6. 再次審查:確認修復後的程式碼沒有新問題
|
||||||
|
|
||||||
|
這個流程可能有點繁瑣,但能大大提高程式碼品質。
|
||||||
|
|
||||||
|
對於沒有程式設計基礎的朋友,如果自己看不懂程式碼,可以利用其他的 AI 大模型來幫忙審查。這是一個很實用的技巧:**用多個 AI 交叉驗證**。
|
||||||
|
|
||||||
|
比如用 Cursor(Claude)生成的程式碼,可以複製到 ChatGPT 或 Gemini,讓它們幫你審查一遍:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
請審查這段程式碼,找出潛在的問題,包括 bug、性能問題、安全隱患。
|
||||||
|
```
|
||||||
|
|
||||||
|
不同的 AI 有不同的視角和訓練數據,能互相補充,一個 AI 可能忽略的問題,另一個 AI 能發現。
|
||||||
|
|
||||||
|
我自己在做重要項目時,經常會讓 2 ~ 3 個不同的 AI 審查同一段程式碼,然後綜合它們的建議。這樣雖然多花了點時間,但能大大降低出錯的風險。特別是對於關鍵的業務邏輯、安全相關的程式碼、性能敏感的部分,多一層保障總是好的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 三、測試
|
||||||
|
|
||||||
|
測試是保障程式碼品質的關鍵手段。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 為什麼要寫測試?
|
||||||
|
|
||||||
|
很多同學覺得寫測試浪費時間,但其實恰恰相反。測試能在開發階段就發現問題,而不是等到上線後用戶發現。有了測試,你可以放心地重構程式碼,不怕改壞。而且,測試程式碼本身就是很好的文檔,展示了如何使用你的函數或組件。
|
||||||
|
|
||||||
|
此外,雖然寫測試要花時間,但能省下更多調試時間。想想看,如果你每次改程式碼都要手動測試所有功能,那得花多少時間?有了自動化測試,運行一下就知道有沒有問題。
|
||||||
|
|
||||||
|
所以,寫測試是值得的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 測試的類型
|
||||||
|
|
||||||
|
測試主要有 3 種類型。
|
||||||
|
|
||||||
|
- 單元測試:測試單個函數或組件,速度快,容易定位問題,覆蓋率要高。
|
||||||
|
- 集成測試:測試多個模塊的協作,確保模塊之間的接口正確,覆蓋主要流程。
|
||||||
|
- 端到端測試:模擬用戶的完整操作,測試整個系統,覆蓋關鍵場景。
|
||||||
|
|
||||||
|
對於 Vibe Coding 項目,我建議 **重點寫單元測試和集成測試**。端到端測試雖然也重要,但成本較高,可以只覆蓋最關鍵的場景。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 讓 AI 幫你寫測試
|
||||||
|
|
||||||
|
如今絕大多數的測試程式碼已經不需要人工編寫了,你可以直接讓 AI 幫你生成測試程式碼。
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
請為這個函數寫單元測試,覆蓋正常情況和邊界情況:
|
||||||
|
```typescript
|
||||||
|
function calculateTotal(items: CartItem[]): number {
|
||||||
|
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
AI 會生成類似這樣的測試程式碼:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('calculateTotal', () => {
|
||||||
|
it('應該正確計算總價', () => {
|
||||||
|
const items = [
|
||||||
|
{ price: 10, quantity: 2 },
|
||||||
|
{ price: 5, quantity: 3 }
|
||||||
|
];
|
||||||
|
expect(calculateTotal(items)).toBe(35);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('應該處理空數組', () => {
|
||||||
|
expect(calculateTotal([])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('應該處理數量為 0 的情況', () => {
|
||||||
|
const items = [{ price: 10, quantity: 0 }];
|
||||||
|
expect(calculateTotal(items)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('應該處理小數', () => {
|
||||||
|
const items = [{ price: 10.5, quantity: 2 }];
|
||||||
|
expect(calculateTotal(items)).toBe(21);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
這段測試程式碼的作用是:用 `describe` 定義一個測試組(測試 calculateTotal 函數),然後用多個 `it` 定義具體的測試用例。每個測試用例都會調用函數並檢查結果是否符合預期。比如第一個測試檢查正常情況、第二個測試檢查空數組、第三個測試檢查數量為 0 的情況、第四個測試檢查小數。運行這些測試時,如果所有 `expect` 都通過,說明函數工作正常;如果有任何一個失敗,就說明程式碼有問題。
|
||||||
|
|
||||||
|
有了這些測試,你就能確保函數在各種情況下都能正常工作。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 擴展知識 - 測試驅動開發(TDD)
|
||||||
|
|
||||||
|
你可以嘗試測試驅動開發(Test-Driven Development,簡稱 TDD)。這是一種 「先寫測試,再寫程式碼」 的開發方式。
|
||||||
|
|
||||||
|
聽起來很反直覺對吧?一般不都是先寫程式碼,再寫測試嗎?
|
||||||
|
|
||||||
|
但 TDD 的邏輯是:你先定義好函數應該如何工作(寫測試),然後讓 AI 根據測試來實現功能。這樣能確保程式碼從一開始就是符合需求的、可測試的。
|
||||||
|
|
||||||
|
具體流程是:
|
||||||
|
|
||||||
|
1. 先寫一個會失敗的測試(因為功能還沒實現)
|
||||||
|
2. 然後讓 AI 實現功能讓測試通過,運行測試確保所有測試都通過
|
||||||
|
3. 最後優化程式碼但保持測試通過
|
||||||
|
|
||||||
|
這樣一來,能避免寫出 「看起來能用但實際有問題」 的程式碼。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 四、調試技巧進階
|
||||||
|
|
||||||
|
即使有了審查和測試,還是難免會遇到 Bug。這時候,你需要掌握調試技巧。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1、使用斷點調試
|
||||||
|
|
||||||
|
很多同學調試程式碼時只會用 `console.log`,也就是在程式碼裡加一行 `console.log(變數名)` 來打印變數的值,然後在瀏覽器控制台查看。
|
||||||
|
|
||||||
|
這個方法雖然簡單,但效率不高,而且調試完還要把這些 log 刪掉。
|
||||||
|
|
||||||
|
其實斷點調試要高效得多。在 VS Code 或 Cursor 中,你只需要在程式碼行號左邊點擊設置斷點,然後按 F5 開始調試。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
程式碼會在斷點處暫停,這時你可以查看所有變數的值:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
還可以單步執行程式碼,看看每一步發生了什麼。這比到處加 `console.log` 然後再刪掉要方便多了。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 2、瀏覽器調試工具
|
||||||
|
|
||||||
|
前端開發時,瀏覽器的調試工具是你的好幫手。在瀏覽器中按 F12 就能打開開發者工具。
|
||||||
|
|
||||||
|
裡面有幾個常用的面板:
|
||||||
|
|
||||||
|
- Console(控制台)面板可以查看日誌和錯誤,執行 JavaScript 程式碼,查看變數值。
|
||||||
|
- Sources(源程式碼)面板可以設置斷點,單步執行,查看調用棧。
|
||||||
|
- Network(網絡)面板可以查看 API 請求,檢查請求和響應,分析加載時間。
|
||||||
|
- Performance(性能)面板可以分析性能瓶頸,查看渲染時間,找出慢的操作。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
掌握這些工具,你的調試效率會大大提高。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 3、二分法定位問題
|
||||||
|
|
||||||
|
如果你不確定問題在哪裡,可以試試二分法。
|
||||||
|
|
||||||
|
很好理解,直接把程式碼分成兩半,註釋掉其中一半,看問題是否還在。如果還在,說明問題在另一半;如果不在,說明問題在這一半。然後繼續把有問題的那一半再分成兩半,重複這個過程,直到找到問題。
|
||||||
|
|
||||||
|
這個方法雖然簡單,但很有效,特別是在處理大段程式碼時。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 4、橡皮鴨調試法
|
||||||
|
|
||||||
|
這是一個看似玄學呆板,實則有一定科學原理的方法。
|
||||||
|
|
||||||
|
當你被 bug 困住時,試著向別人(或者一隻橡皮鴨)解釋你的程式碼:阿巴阿巴,這個函數應該做什麼…… 它先做了這個…… 然後做了那個…… 咦,這裡好像不對……
|
||||||
|
|
||||||
|
很神奇的是,在解釋的過程中,你往往就會發現問題。因為解釋的過程會強迫你重新理清思路,從不同的角度看問題。
|
||||||
|
|
||||||
|
哪個
|
||||||
Reference in New Issue
Block a user