Compare commits
5 Commits
c94cf75838
...
331e747ef3
| Author | SHA1 | Date |
|---|---|---|
|
|
331e747ef3 | |
|
|
3649e21ac9 | |
|
|
b9f6eb1237 | |
|
|
c44d3e170c | |
|
|
e5cb336667 |
|
|
@ -33,7 +33,8 @@
|
|||
"Bash(cat:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(git remote add:*)"
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(sqlite3:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -35,9 +35,17 @@ public class AuthController : ControllerBase
|
|||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Registration attempt for user: {Username}, Email: {Email}",
|
||||
request.Username, request.Email);
|
||||
|
||||
// 驗證請求
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Invalid model state for registration: {@ModelErrors}",
|
||||
ModelState.Where(x => x.Value?.Errors.Count > 0)
|
||||
.ToDictionary(x => x.Key, x => x.Value?.Errors.Select(e => e.ErrorMessage)));
|
||||
return BadRequest(new { Success = false, Error = "Invalid request data" });
|
||||
}
|
||||
|
||||
// 檢查Email是否已存在
|
||||
if (await _context.Users.AnyAsync(u => u.Email == request.Email))
|
||||
|
|
@ -90,11 +98,17 @@ public class AuthController : ControllerBase
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during user registration");
|
||||
_logger.LogError(ex, "Error during user registration. Request: {@Request}", new {
|
||||
Username = request.Username,
|
||||
Email = request.Email,
|
||||
StackTrace = ex.StackTrace,
|
||||
InnerException = ex.InnerException?.Message
|
||||
});
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Registration failed",
|
||||
Details = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
|
@ -105,9 +119,16 @@ public class AuthController : ControllerBase
|
|||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Login attempt for email: {Email}", request.Email);
|
||||
|
||||
// 驗證請求
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Invalid model state for login: {@ModelErrors}",
|
||||
ModelState.Where(x => x.Value?.Errors.Count > 0)
|
||||
.ToDictionary(x => x.Key, x => x.Value?.Errors.Select(e => e.ErrorMessage)));
|
||||
return BadRequest(new { Success = false, Error = "Invalid request data" });
|
||||
}
|
||||
|
||||
// 查找用戶
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
||||
|
|
@ -143,11 +164,16 @@ public class AuthController : ControllerBase
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during user login");
|
||||
_logger.LogError(ex, "Error during user login. Request: {@Request}", new {
|
||||
Email = request.Email,
|
||||
StackTrace = ex.StackTrace,
|
||||
InnerException = ex.InnerException?.Message
|
||||
});
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Login failed",
|
||||
Details = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
|
@ -155,8 +181,9 @@ public class AuthController : ControllerBase
|
|||
|
||||
private string GenerateJwtToken(User user)
|
||||
{
|
||||
var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_JWT_SECRET")
|
||||
?? "your-super-secret-jwt-key-that-should-be-at-least-256-bits-long";
|
||||
var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET")
|
||||
?? Environment.GetEnvironmentVariable("DRAMALING_JWT_SECRET")
|
||||
?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only";
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.UTF8.GetBytes(jwtSecret);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DramaLing.Api.Middleware;
|
||||
|
||||
public class ErrorHandlingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ErrorHandlingMiddleware> _logger;
|
||||
|
||||
public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An unhandled exception occurred during request execution. Request: {Method} {Path}",
|
||||
context.Request.Method, context.Request.Path);
|
||||
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var errorResponse = new
|
||||
{
|
||||
Success = false,
|
||||
Error = "An error occurred while processing your request",
|
||||
Details = exception.Message,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
RequestId = context.TraceIdentifier
|
||||
};
|
||||
|
||||
var statusCode = exception switch
|
||||
{
|
||||
ArgumentException => HttpStatusCode.BadRequest,
|
||||
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
|
||||
KeyNotFoundException => HttpStatusCode.NotFound,
|
||||
NotImplementedException => HttpStatusCode.NotImplemented,
|
||||
TimeoutException => HttpStatusCode.RequestTimeout,
|
||||
_ => HttpStatusCode.InternalServerError
|
||||
};
|
||||
|
||||
context.Response.StatusCode = (int)statusCode;
|
||||
|
||||
var jsonResponse = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
_logger.LogError("Error response sent: {StatusCode} - {Response}", statusCode, jsonResponse);
|
||||
|
||||
await context.Response.WriteAsync(jsonResponse);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using DramaLing.Api.Data;
|
||||
using DramaLing.Api.Services;
|
||||
using DramaLing.Api.Middleware;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
|
@ -8,7 +9,12 @@ using System.Text;
|
|||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||
options.JsonSerializerOptions.WriteIndented = true;
|
||||
});
|
||||
|
||||
// Entity Framework - 使用 SQLite 進行測試
|
||||
var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
|
||||
|
|
@ -111,6 +117,10 @@ builder.Services.AddSwaggerGen(c =>
|
|||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
// 全域錯誤處理中介軟體 (必須放在最前面)
|
||||
app.UseMiddleware<ErrorHandlingMiddleware>();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Information",
|
||||
"DramaLing.Api": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,502 @@
|
|||
# 🔍 DramaLing 後端錯誤日誌監控指南
|
||||
|
||||
## 📋 目錄
|
||||
1. [錯誤日誌系統概覽](#錯誤日誌系統概覽)
|
||||
2. [監控工具使用方法](#監控工具使用方法)
|
||||
3. [常見錯誤類型和排查](#常見錯誤類型和排查)
|
||||
4. [實際使用案例](#實際使用案例)
|
||||
5. [日誌級別說明](#日誌級別說明)
|
||||
6. [快速診斷檢查清單](#快速診斷檢查清單)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 錯誤日誌系統概覽
|
||||
|
||||
DramaLing 後端配備了完整的錯誤日誌系統,幫助開發者快速定位和解決問題。
|
||||
|
||||
### ✅ **已配置的功能**
|
||||
- **全域錯誤處理中介軟體**:捕獲所有未處理的異常
|
||||
- **詳細日誌配置**:Debug 級別記錄
|
||||
- **結構化日誌**:包含請求詳情、堆疊追蹤
|
||||
- **資料庫操作日誌**:SQL 查詢和執行時間
|
||||
- **認證流程日誌**:完整的登入註冊追蹤
|
||||
|
||||
### 🗂️ **相關檔案**
|
||||
- `Middleware/ErrorHandlingMiddleware.cs` - 全域錯誤處理
|
||||
- `appsettings.json` - 日誌配置
|
||||
- `Controllers/AuthController.cs` - 認證日誌
|
||||
- `Program.cs` - 中介軟體註冊
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 監控工具使用方法
|
||||
|
||||
### **1. 查看即時日誌**
|
||||
```bash
|
||||
# 查看當前後端服務的所有日誌
|
||||
BashOutput(bash_id: "33d454")
|
||||
```
|
||||
|
||||
### **2. 過濾特定錯誤**
|
||||
```bash
|
||||
# 只看錯誤和異常日誌
|
||||
BashOutput(bash_id: "33d454", filter: "Error|Exception|fail")
|
||||
|
||||
# 只看認證相關日誌
|
||||
BashOutput(bash_id: "33d454", filter: "Registration|Login|Auth")
|
||||
|
||||
# 只看資料庫錯誤
|
||||
BashOutput(bash_id: "33d454", filter: "Database|DbCommand|Entity")
|
||||
```
|
||||
|
||||
### **3. 監控特定功能**
|
||||
```bash
|
||||
# 監控 JWT 相關問題
|
||||
BashOutput(bash_id: "33d454", filter: "JWT|Token|Bearer")
|
||||
|
||||
# 監控模型驗證錯誤
|
||||
BashOutput(bash_id: "33d454", filter: "ModelState|Validation")
|
||||
```
|
||||
|
||||
### **4. 找出服務 ID**
|
||||
如果不知道當前的 bash_id,使用:
|
||||
```bash
|
||||
/bashes
|
||||
```
|
||||
然後找到運行 `dotnet run --urls=http://localhost:5000` 的服務 ID。
|
||||
|
||||
---
|
||||
|
||||
## 🚨 常見錯誤類型和排查
|
||||
|
||||
### **1. 認證錯誤**
|
||||
|
||||
#### **問題症狀**:
|
||||
- 用戶無法登入
|
||||
- JWT token 無效
|
||||
- 401 Unauthorized 錯誤
|
||||
|
||||
#### **排查方法**:
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Login attempt|Authentication|JWT|Bearer")
|
||||
```
|
||||
|
||||
#### **典型日誌**:
|
||||
```
|
||||
info: Login attempt for email: user@example.com
|
||||
warn: Bearer was not authenticated. Failure message: IDX10503: Signature validation failed
|
||||
info: User logged in successfully: 4111625f-e44c-4878-8fd0-4377eed45f04
|
||||
```
|
||||
|
||||
#### **解決步驟**:
|
||||
1. 檢查 JWT 密鑰配置
|
||||
2. 驗證 token 格式是否正確
|
||||
3. 確認用戶是否存在於資料庫
|
||||
|
||||
### **2. 資料庫錯誤**
|
||||
|
||||
#### **問題症狀**:
|
||||
- 註冊失敗
|
||||
- 資料無法儲存
|
||||
- 500 Internal Server Error
|
||||
|
||||
#### **排查方法**:
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "DbCommand|Database|EntityFramework")
|
||||
```
|
||||
|
||||
#### **典型日誌**:
|
||||
```
|
||||
dbug: Executed DbCommand (2ms) [Parameters=[@__request_Email_0='test@example.com']]
|
||||
SELECT EXISTS (SELECT 1 FROM "user_profiles" WHERE "u"."email" = @__request_Email_0)
|
||||
info: Database ensured created
|
||||
```
|
||||
|
||||
#### **解決步驟**:
|
||||
1. 檢查資料庫檔案是否存在
|
||||
2. 驗證 SQL 查詢是否正確
|
||||
3. 確認資料庫連線字串
|
||||
|
||||
### **3. 模型驗證錯誤**
|
||||
|
||||
#### **問題症狀**:
|
||||
- 表單驗證失敗
|
||||
- 400 Bad Request
|
||||
- 密碼格式錯誤
|
||||
|
||||
#### **排查方法**:
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "ModelState|Validation|BadRequest")
|
||||
```
|
||||
|
||||
#### **典型日誌**:
|
||||
```
|
||||
warn: Invalid model state for registration: {"Password": ["Password must be at least 8 characters"]}
|
||||
dbug: The request has model state errors, returning an error response
|
||||
```
|
||||
|
||||
#### **解決步驟**:
|
||||
1. 檢查前端表單驗證
|
||||
2. 確認後端模型註解
|
||||
3. 驗證請求格式
|
||||
|
||||
### **4. 應用程式異常**
|
||||
|
||||
#### **問題症狀**:
|
||||
- 系統崩潰
|
||||
- 未預期的錯誤
|
||||
- 堆疊溢出
|
||||
|
||||
#### **排查方法**:
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Exception|fail:|error:")
|
||||
```
|
||||
|
||||
#### **典型日誌**:
|
||||
```
|
||||
fail: Error during user registration
|
||||
System.InvalidOperationException: Unable to determine the relationship...
|
||||
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator...
|
||||
```
|
||||
|
||||
#### **解決步驟**:
|
||||
1. 查看完整堆疊追蹤
|
||||
2. 檢查相關程式碼
|
||||
3. 驗證依賴注入配置
|
||||
|
||||
---
|
||||
|
||||
## 📝 實際使用案例
|
||||
|
||||
### **案例 1:用戶註冊失敗排查**
|
||||
|
||||
**問題**:前端註冊表單提交後返回 500 錯誤
|
||||
|
||||
**排查步驟**:
|
||||
|
||||
1. **檢查認證日誌**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Registration attempt")
|
||||
```
|
||||
|
||||
2. **查看詳細錯誤**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Error during user registration")
|
||||
```
|
||||
|
||||
3. **檢查資料庫操作**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "INSERT INTO.*user_profiles")
|
||||
```
|
||||
|
||||
**可能的解決方案**:
|
||||
- 資料庫關聯配置錯誤
|
||||
- 模型驗證失敗
|
||||
- 密碼雜湊錯誤
|
||||
|
||||
### **案例 2:JWT Token 驗證失敗**
|
||||
|
||||
**問題**:用戶登入後無法訪問受保護的端點
|
||||
|
||||
**排查步驟**:
|
||||
|
||||
1. **查看 Token 生成**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "User logged in successfully")
|
||||
```
|
||||
|
||||
2. **檢查 Token 驗證**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Bearer.*authenticated|Token.*validation")
|
||||
```
|
||||
|
||||
3. **查看認證失敗原因**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Failed to validate the token")
|
||||
```
|
||||
|
||||
**可能的解決方案**:
|
||||
- JWT 密鑰不匹配
|
||||
- Token 格式錯誤
|
||||
- Token 過期
|
||||
|
||||
### **案例 3:API 端點無回應**
|
||||
|
||||
**問題**:API 請求沒有回應或超時
|
||||
|
||||
**排查步驟**:
|
||||
|
||||
1. **檢查請求是否到達**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Request starting.*POST.*register")
|
||||
```
|
||||
|
||||
2. **查看路由匹配**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Route matched|Executing endpoint")
|
||||
```
|
||||
|
||||
3. **檢查異常處理**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "An unhandled exception occurred")
|
||||
```
|
||||
|
||||
**可能的解決方案**:
|
||||
- 路由配置錯誤
|
||||
- 控制器異常
|
||||
- 中介軟體問題
|
||||
|
||||
---
|
||||
|
||||
## 📊 日誌級別說明
|
||||
|
||||
### **Debug**
|
||||
- **目的**:詳細的開發調試信息
|
||||
- **內容**:執行流程、參數值、內部狀態
|
||||
- **例子**:
|
||||
```
|
||||
dbug: Creating DbConnection.
|
||||
dbug: Executed DbCommand (2ms) [Parameters=[@p0='value']]
|
||||
```
|
||||
|
||||
### **Information**
|
||||
- **目的**:重要的業務事件記錄
|
||||
- **內容**:正常操作、成功事件
|
||||
- **例子**:
|
||||
```
|
||||
info: User registered successfully: 4111625f-e44c-4878-8fd0-4377eed45f04
|
||||
info: Application started. Press Ctrl+C to shut down.
|
||||
```
|
||||
|
||||
### **Warning**
|
||||
- **目的**:潛在問題警告
|
||||
- **內容**:不影響功能但需要注意的情況
|
||||
- **例子**:
|
||||
```
|
||||
warn: Failed to determine the https port for redirect.
|
||||
warn: Invalid model state for registration
|
||||
```
|
||||
|
||||
### **Error**
|
||||
- **目的**:錯誤和異常記錄
|
||||
- **內容**:需要立即處理的問題
|
||||
- **例子**:
|
||||
```
|
||||
fail: Error during user registration
|
||||
info: An unhandled exception occurred during request execution
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 快速診斷檢查清單
|
||||
|
||||
當遇到問題時,按以下順序檢查:
|
||||
|
||||
### **Step 1: 服務健康檢查**
|
||||
```bash
|
||||
curl "http://localhost:5000/health"
|
||||
```
|
||||
**預期結果**:`{"status":"Healthy","timestamp":"..."}`
|
||||
|
||||
### **Step 2: 查看最新錯誤**
|
||||
```bash
|
||||
BashOutput(bash_id: "33d454", filter: "Error|fail|Exception")
|
||||
```
|
||||
**目的**:快速找出最近發生的錯誤
|
||||
|
||||
### **Step 3: 檢查特定功能**
|
||||
根據問題類型選擇:
|
||||
|
||||
```bash
|
||||
# 🔐 認證問題
|
||||
BashOutput(bash_id: "33d454", filter: "Auth|Login|Register|JWT")
|
||||
|
||||
# 🗄️ 資料庫問題
|
||||
BashOutput(bash_id: "33d454", filter: "Database|DbCommand|Entity")
|
||||
|
||||
# 📝 模型驗證問題
|
||||
BashOutput(bash_id: "33d454", filter: "ModelState|Validation")
|
||||
|
||||
# 🌐 API 路由問題
|
||||
BashOutput(bash_id: "33d454", filter: "Route|Endpoint")
|
||||
```
|
||||
|
||||
### **Step 4: 深入分析**
|
||||
```bash
|
||||
# 查看詳細追蹤
|
||||
BashOutput(bash_id: "33d454", filter: "StackTrace|InnerException")
|
||||
|
||||
# 查看完整請求流程
|
||||
BashOutput(bash_id: "33d454", filter: "Request starting.*POST")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 進階技巧
|
||||
|
||||
### **1. 組合過濾器**
|
||||
```bash
|
||||
# 同時監控多個關鍵字
|
||||
BashOutput(bash_id: "33d454", filter: "Error.*Auth|Exception.*User")
|
||||
|
||||
# 排除某些日誌
|
||||
BashOutput(bash_id: "33d454", filter: "Error(?!.*Entity)")
|
||||
```
|
||||
|
||||
### **2. 時間範圍分析**
|
||||
日誌包含精確時間戳,可以:
|
||||
- 對比問題發生時間
|
||||
- 追蹤請求處理時長
|
||||
- 分析系統性能
|
||||
|
||||
### **3. 關聯分析**
|
||||
使用 TraceId 和 RequestId 追蹤單一請求的完整生命週期:
|
||||
```
|
||||
=> TraceId:8270820e8be6f0bdb0cc7ed6c8623004 => RequestId:0HNFL7QLPM33V:00000001
|
||||
```
|
||||
|
||||
### **4. 即時監控**
|
||||
可以定期檢查日誌來監控系統健康狀況:
|
||||
```bash
|
||||
# 每隔一段時間檢查錯誤
|
||||
BashOutput(bash_id: "33d454", filter: "Error|fail")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常用監控命令
|
||||
|
||||
### **日常監控**
|
||||
```bash
|
||||
# 檢查服務狀態
|
||||
curl "http://localhost:5000/health"
|
||||
|
||||
# 查看最新日誌
|
||||
BashOutput(bash_id: "33d454")
|
||||
|
||||
# 只看錯誤
|
||||
BashOutput(bash_id: "33d454", filter: "Error|Exception|fail")
|
||||
```
|
||||
|
||||
### **功能測試**
|
||||
```bash
|
||||
# 測試註冊功能
|
||||
curl -X POST "http://localhost:5000/api/auth/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "test", "email": "test@example.com", "password": "testpass123"}'
|
||||
|
||||
# 測試登入功能
|
||||
curl -X POST "http://localhost:5000/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com", "password": "testpass123"}'
|
||||
```
|
||||
|
||||
### **問題排查**
|
||||
```bash
|
||||
# 檢查認證問題
|
||||
BashOutput(bash_id: "33d454", filter: "Auth|Login|Register|JWT")
|
||||
|
||||
# 檢查資料庫問題
|
||||
BashOutput(bash_id: "33d454", filter: "Database|DbCommand|SQL")
|
||||
|
||||
# 檢查 API 路由問題
|
||||
BashOutput(bash_id: "33d454", filter: "Route|Endpoint|Controller")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 日誌範例解讀
|
||||
|
||||
### **成功註冊的日誌**
|
||||
```
|
||||
info: Registration attempt for user: logtest, Email: logtest@example.com
|
||||
dbug: Executed DbCommand (0ms) [Parameters=[@__request_Email_0='logtest@example.com']]
|
||||
SELECT EXISTS (SELECT 1 FROM "user_profiles" WHERE "u"."email" = @__request_Email_0)
|
||||
dbug: Executed DbCommand (1ms) [Parameters=[@p0='4111625f-e44c-4878-8fd0-4377eed45f04', ...]]
|
||||
INSERT INTO "user_profiles" (Id, username, email, password_hash, ...)
|
||||
info: User registered successfully: 4111625f-e44c-4878-8fd0-4377eed45f04
|
||||
```
|
||||
|
||||
### **驗證錯誤的日誌**
|
||||
```
|
||||
warn: Invalid model state for registration: {"Password": ["Password must be at least 8 characters"]}
|
||||
dbug: The request has model state errors, returning an error response.
|
||||
info: Executing BadRequestObjectResult, writing value of type 'ValidationProblemDetails'
|
||||
```
|
||||
|
||||
### **系統異常的日誌**
|
||||
```
|
||||
fail: Error during user registration
|
||||
System.InvalidOperationException: Unable to determine the relationship...
|
||||
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator...
|
||||
info: An unhandled exception occurred during request execution. Request: POST /api/auth/register
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 故障排除流程圖
|
||||
|
||||
```
|
||||
問題發生
|
||||
↓
|
||||
檢查服務是否運行 (/health)
|
||||
↓
|
||||
查看最新錯誤日誌 (Error|Exception)
|
||||
↓
|
||||
根據錯誤類型選擇:
|
||||
├─ 認證問題 → 檢查 Auth 日誌
|
||||
├─ 資料庫問題 → 檢查 DbCommand 日誌
|
||||
├─ 驗證問題 → 檢查 ModelState 日誌
|
||||
└─ 系統異常 → 檢查 StackTrace 日誌
|
||||
↓
|
||||
分析具體錯誤原因
|
||||
↓
|
||||
實施修復方案
|
||||
↓
|
||||
重新測試驗證
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 最佳實踐
|
||||
|
||||
### **1. 定期監控**
|
||||
- 每次部署後檢查日誌
|
||||
- 定期查看錯誤趨勢
|
||||
- 監控關鍵業務流程
|
||||
|
||||
### **2. 錯誤分類**
|
||||
- **緊急**:系統崩潰、資料庫連線失敗
|
||||
- **重要**:認證失敗、API 異常
|
||||
- **一般**:驗證錯誤、業務邏輯問題
|
||||
|
||||
### **3. 日誌保存**
|
||||
重要錯誤日誌應該保存下來用於:
|
||||
- 問題追蹤
|
||||
- 性能分析
|
||||
- 系統優化
|
||||
|
||||
---
|
||||
|
||||
## 📞 支援
|
||||
|
||||
如果遇到無法解決的問題:
|
||||
|
||||
1. **收集信息**:保存相關日誌
|
||||
2. **重現問題**:記錄重現步驟
|
||||
3. **檢查環境**:確認配置和依賴
|
||||
4. **尋求協助**:提供完整的錯誤資訊
|
||||
|
||||
---
|
||||
|
||||
**最後更新**:2025-09-16
|
||||
**版本**:v1.0
|
||||
**適用於**:DramaLing .NET Core API Backend
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相關文檔
|
||||
|
||||
- [API 開發指南](./api/README.md)
|
||||
- [資料庫配置](./setup/env-setup.md)
|
||||
- [開發環境設置](./setup/README.md)
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
# 🚀 DramaLing 下一階段開發計劃
|
||||
|
||||
## 📊 當前專案狀態
|
||||
|
||||
### ✅ **已完成功能**
|
||||
- **🔐 用戶認證系統**:註冊、登入、JWT token 驗證
|
||||
- **🏗️ 後端架構**:.NET Core API、SQLite 資料庫、錯誤日誌系統
|
||||
- **🎨 前端框架**:Next.js 15、認證上下文、保護路由
|
||||
- **📝 開發工具**:完整的錯誤監控和日誌系統
|
||||
|
||||
### 🎯 **技術債務**
|
||||
- 前端頁面使用 mock 資料,需要與後端 API 整合
|
||||
- 資料庫只有用戶表格,缺少詞卡相關資料
|
||||
- AI 生成功能尚未與 Gemini API 連接
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一階段開發重點
|
||||
|
||||
### **第一階段:核心詞卡功能 (1-2 週)**
|
||||
|
||||
#### **1.1 詞卡管理系統**
|
||||
**目標**:建立完整的詞卡 CRUD 功能
|
||||
|
||||
**後端任務**:
|
||||
- [x] `CardSetsController` - 詞卡集合管理
|
||||
- [x] `FlashcardsController` - 單張詞卡操作
|
||||
- [ ] 測試所有 API 端點
|
||||
- [ ] 建立預設詞卡資料
|
||||
|
||||
**前端任務**:
|
||||
- [ ] 整合 `/flashcards` 頁面與後端 API
|
||||
- [ ] 實作詞卡列表展示
|
||||
- [ ] 實作詞卡新增/編輯/刪除功能
|
||||
- [ ] 實作詞卡集合管理
|
||||
|
||||
**API 端點**:
|
||||
```
|
||||
GET /api/cardsets - 取得詞卡集合列表
|
||||
POST /api/cardsets - 建立新詞卡集合
|
||||
GET /api/flashcards - 取得詞卡列表
|
||||
POST /api/flashcards - 建立新詞卡
|
||||
PUT /api/flashcards/{id} - 更新詞卡
|
||||
DELETE /api/flashcards/{id} - 刪除詞卡
|
||||
```
|
||||
|
||||
#### **1.2 資料庫初始化**
|
||||
**目標**:建立測試用的詞卡資料
|
||||
|
||||
**任務**:
|
||||
- [ ] 建立種子資料(Seed Data)
|
||||
- [ ] 建立預設詞卡集合(如:商務英語、日常對話)
|
||||
- [ ] 建立範例詞卡(50-100 張)
|
||||
- [ ] 測試資料庫關聯完整性
|
||||
|
||||
---
|
||||
|
||||
### **第二階段:AI 生成功能 (1 週)**
|
||||
|
||||
#### **2.1 Google Gemini 整合**
|
||||
**目標**:實作從影劇對話生成詞卡
|
||||
|
||||
**後端任務**:
|
||||
- [ ] 測試 `GeminiService` 與 Google AI 連接
|
||||
- [ ] 實作 `AIController` 的生成端點
|
||||
- [ ] 配置 Gemini API 金鑰
|
||||
- [ ] 優化 AI 提示詞(Prompt Engineering)
|
||||
|
||||
**前端任務**:
|
||||
- [ ] 整合 `/generate` 頁面與 AI API
|
||||
- [ ] 實作影劇對話輸入介面
|
||||
- [ ] 實作生成詞卡結果展示
|
||||
- [ ] 實作生成詞卡儲存功能
|
||||
|
||||
**API 端點**:
|
||||
```
|
||||
POST /api/ai/generate-flashcards - 從對話生成詞卡
|
||||
POST /api/ai/analyze-difficulty - 分析詞彙難度
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **第三階段:學習功能 (1-2 週)**
|
||||
|
||||
#### **3.1 學習模式實作**
|
||||
**目標**:實作多種學習模式
|
||||
|
||||
**功能清單**:
|
||||
- [ ] **翻卡模式**:顯示英文→中文翻譯
|
||||
- [ ] **選擇題模式**:四選一題目
|
||||
- [ ] **拼寫模式**:聽音拼字
|
||||
- [ ] **間隔重複算法**:使用 SM2 演算法
|
||||
|
||||
**前端任務**:
|
||||
- [ ] 整合 `/learn` 頁面與學習 API
|
||||
- [ ] 實作學習模式選擇器
|
||||
- [ ] 實作學習進度追蹤
|
||||
- [ ] 實作學習結果回饋
|
||||
|
||||
**後端任務**:
|
||||
- [ ] 測試 `StudyController` 學習端點
|
||||
- [ ] 實作 `SM2Algorithm` 間隔重複
|
||||
- [ ] 建立學習工作階段(StudySession)
|
||||
- [ ] 實作學習統計記錄
|
||||
|
||||
---
|
||||
|
||||
### **第四階段:數據可視化 (1 週)**
|
||||
|
||||
#### **4.1 學習統計與儀表板**
|
||||
**目標**:提供詳細的學習分析
|
||||
|
||||
**功能清單**:
|
||||
- [ ] 學習進度圖表
|
||||
- [ ] 每日學習統計
|
||||
- [ ] 詞彙掌握度分析
|
||||
- [ ] 學習時間追蹤
|
||||
|
||||
**前端任務**:
|
||||
- [ ] 整合 `/dashboard` 頁面與統計 API
|
||||
- [ ] 實作圖表組件(Chart.js 或 Recharts)
|
||||
- [ ] 實作學習報告頁面
|
||||
- [ ] 實作成就徽章系統
|
||||
|
||||
**後端任務**:
|
||||
- [ ] 測試 `StatsController` 統計端點
|
||||
- [ ] 實作學習數據聚合
|
||||
- [ ] 實作成就計算邏輯
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 開發工具與流程
|
||||
|
||||
### **開發環境**
|
||||
```bash
|
||||
# 啟動前端(localhost:3000)
|
||||
cd frontend && npm run dev
|
||||
|
||||
# 啟動後端(localhost:5000)
|
||||
cd backend/DramaLing.Api && dotnet run --urls=http://localhost:5000
|
||||
|
||||
# 查看 API 文檔
|
||||
http://localhost:5000/swagger
|
||||
```
|
||||
|
||||
### **錯誤監控**
|
||||
```bash
|
||||
# 查看後端日誌
|
||||
BashOutput(bash_id: "bfb3f6")
|
||||
|
||||
# 查看認證錯誤
|
||||
BashOutput(bash_id: "bfb3f6", filter: "Auth|Login|Register")
|
||||
|
||||
# 查看 API 錯誤
|
||||
BashOutput(bash_id: "bfb3f6", filter: "Error|Exception")
|
||||
```
|
||||
|
||||
### **API 測試**
|
||||
```bash
|
||||
# 健康檢查
|
||||
curl "http://localhost:5000/health"
|
||||
|
||||
# 測試認證
|
||||
curl -X POST "http://localhost:5000/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com", "password": "testpass123"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 第一階段詳細任務分解
|
||||
|
||||
### **Week 1:詞卡管理系統**
|
||||
|
||||
#### **Day 1-2:後端 API 測試與資料建立**
|
||||
- [ ] 測試所有 CardSets API 端點
|
||||
- [ ] 測試所有 Flashcards API 端點
|
||||
- [ ] 建立種子資料(Seed Data)
|
||||
- [ ] 驗證資料庫關聯
|
||||
|
||||
#### **Day 3-4:前端詞卡列表整合**
|
||||
- [ ] 建立詞卡 API 服務檔案
|
||||
- [ ] 整合詞卡列表顯示
|
||||
- [ ] 實作詞卡搜尋與篩選
|
||||
- [ ] 實作詞卡集合切換
|
||||
|
||||
#### **Day 5-7:詞卡編輯功能**
|
||||
- [ ] 實作詞卡新增表單
|
||||
- [ ] 實作詞卡編輯功能
|
||||
- [ ] 實作詞卡刪除確認
|
||||
- [ ] 實作批量操作
|
||||
|
||||
---
|
||||
|
||||
## 🎯 成功指標
|
||||
|
||||
### **第一階段完成標準**
|
||||
- [ ] 用戶可以瀏覽所有詞卡集合
|
||||
- [ ] 用戶可以新增/編輯/刪除詞卡
|
||||
- [ ] 詞卡資料正確儲存到資料庫
|
||||
- [ ] 前端頁面完全不使用 mock 資料
|
||||
|
||||
### **整體專案完成標準**
|
||||
- [ ] 用戶可以從零開始生成學習詞卡
|
||||
- [ ] 用戶可以進行完整的學習循環
|
||||
- [ ] 用戶可以查看學習進度和統計
|
||||
- [ ] 系統穩定運行,錯誤處理完善
|
||||
|
||||
---
|
||||
|
||||
## 📅 時程規劃
|
||||
|
||||
| 階段 | 時間 | 主要功能 | 里程碑 |
|
||||
|------|------|----------|---------|
|
||||
| 第一階段 | Week 1-2 | 詞卡管理 | 完整 CRUD 功能 |
|
||||
| 第二階段 | Week 3 | AI 生成 | Gemini 整合完成 |
|
||||
| 第三階段 | Week 4-5 | 學習功能 | 三種學習模式 |
|
||||
| 第四階段 | Week 6 | 數據分析 | 統計儀表板 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技術考量
|
||||
|
||||
### **效能優化**
|
||||
- 實作詞卡分頁載入
|
||||
- 快取常用的詞卡集合
|
||||
- 優化 AI 生成回應時間
|
||||
|
||||
### **用戶體驗**
|
||||
- 響應式設計適配行動裝置
|
||||
- 載入狀態和錯誤提示
|
||||
- 鍵盤快捷鍵支援
|
||||
|
||||
### **資料安全**
|
||||
- 用戶詞卡資料隔離
|
||||
- API 端點權限控制
|
||||
- 敏感資料加密儲存
|
||||
|
||||
---
|
||||
|
||||
## 🚀 立即行動計劃
|
||||
|
||||
**今天就可以開始**:
|
||||
1. 測試現有的 CardSets API
|
||||
2. 建立第一個詞卡集合
|
||||
3. 在前端顯示真實的後端資料
|
||||
4. 實作基本的詞卡 CRUD
|
||||
|
||||
**預期成果**:
|
||||
在這個階段結束後,DramaLing 將從一個原型變成一個可用的詞卡學習應用,用戶可以管理自己的詞卡並開始基本的學習活動。
|
||||
|
||||
---
|
||||
|
||||
**建立日期**:2025-09-16
|
||||
**預計完成**:2025-10-27
|
||||
**開發者**:DramaLing Team
|
||||
|
||||
---
|
||||
|
||||
## 📖 相關文檔
|
||||
- [錯誤日誌監控指南](./error-logging-guide.md)
|
||||
- [API 開發文檔](./api/README.md)
|
||||
- [環境設置指南](./setup/README.md)
|
||||
|
|
@ -1,3 +1,40 @@
|
|||
{
|
||||
"pages": {}
|
||||
"pages": {
|
||||
"/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/css/app/layout.css",
|
||||
"static/chunks/app/layout.js"
|
||||
],
|
||||
"/learn/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/learn/page.js"
|
||||
],
|
||||
"/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/page.js"
|
||||
],
|
||||
"/register/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/register/page.js"
|
||||
],
|
||||
"/dashboard/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/dashboard/page.js"
|
||||
],
|
||||
"/login/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/login/page.js"
|
||||
],
|
||||
"/flashcards/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/flashcards/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,10 @@
|
|||
"static/development/_buildManifest.js",
|
||||
"static/development/_ssgManifest.js"
|
||||
],
|
||||
"rootMainFiles": [],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"rootMainFilesTree": {},
|
||||
"pages": {
|
||||
"/_app": []
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1,7 @@
|
|||
{}
|
||||
{
|
||||
"/register/page": "app/register/page.js",
|
||||
"/login/page": "app/login/page.js",
|
||||
"/learn/page": "app/learn/page.js",
|
||||
"/dashboard/page": "app/dashboard/page.js",
|
||||
"/flashcards/page": "app/flashcards/page.js"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,7 +5,10 @@ globalThis.__BUILD_MANIFEST = {
|
|||
"devFiles": [],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [],
|
||||
"rootMainFiles": [],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"rootMainFilesTree": {},
|
||||
"pages": {
|
||||
"/_app": []
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
||||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{\"/Users/jettcheng1018/code/dramaling-vocab-learning/frontend/app/layout\":[\"static/media/e4af272ccee01ff0-s.p.woff2\"]},\"appUsingSizeAdjust\":true,\"pagesUsingSizeAdjust\":false}"
|
||||
|
|
@ -1 +1 @@
|
|||
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
||||
{"pages":{},"app":{"/Users/jettcheng1018/code/dramaling-vocab-learning/frontend/app/layout":["static/media/e4af272ccee01ff0-s.p.woff2"]},"appUsingSizeAdjust":true,"pagesUsingSizeAdjust":false}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
{"c":["webpack"],"r":["app/_not-found/page"],"m":["(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?absolutePagePath=%2FUsers%2Fjettcheng1018%2Fcode%2Fdramaling-vocab-learning%2Ffrontend%2Fnode_modules%2Fnext%2Fdist%2Fclient%2Fcomponents%2Fbuiltin%2Fglobal-not-found.js&page=%2F_not-found%2Fpage!","(app-pages-browser)/./node_modules/next/dist/client/components/builtin/global-not-found.js","(app-pages-browser)/./node_modules/next/dist/client/components/http-access-fallback/error-fallback.js","(app-pages-browser)/./node_modules/next/dist/client/components/styles/access-error-styles.js"]}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"c":["webpack"],"r":[],"m":[]}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
"use strict";
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("1ba7acc777cf829e")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
)
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJpZ25vcmVMaXN0IjpbMF0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZXMiOlsid2VicGFjay1pbnRlcm5hbDovL25leHRqcy93ZWJwYWNrLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIFRoaXMgc291cmNlIHdhcyBnZW5lcmF0ZWQgYnkgTmV4dC5qcyBiYXNlZCBvZmYgb2YgdGhlIGdlbmVyYXRlZCBXZWJwYWNrIHJ1bnRpbWUuXG4vLyBUaGUgbWFwcGluZ3MgYXJlIGluY29ycmVjdC5cbi8vIFRvIGdldCB0aGUgY29ycmVjdCBsaW5lL2NvbHVtbiBtYXBwaW5ncywgdHVybiBvZmYgc291cmNlbWFwcyBpbiB5b3VyIGRlYnVnZ2VyLlxuXG5zZWxmW1wid2VicGFja0hvdFVwZGF0ZV9OX0VcIl0oXCJ3ZWJwYWNrXCIse30sXG4vKioqKioqLyBmdW5jdGlvbihfX3dlYnBhY2tfcmVxdWlyZV9fKSB7IC8vIHdlYnBhY2tSdW50aW1lTW9kdWxlc1xuLyoqKioqKi8gLyogd2VicGFjay9ydW50aW1lL2dldEZ1bGxIYXNoICovXG4vKioqKioqLyAoKCkgPT4ge1xuLyoqKioqKi8gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmggPSAoKSA9PiAoXCIxYmE3YWNjNzc3Y2Y4MjllXCIpXG4vKioqKioqLyB9KSgpO1xuLyoqKioqKi8gXG4vKioqKioqLyB9XG4pIl19
|
||||
;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
"use strict";
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("98616de372651724")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
)
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJpZ25vcmVMaXN0IjpbMF0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZXMiOlsid2VicGFjay1pbnRlcm5hbDovL25leHRqcy93ZWJwYWNrLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIFRoaXMgc291cmNlIHdhcyBnZW5lcmF0ZWQgYnkgTmV4dC5qcyBiYXNlZCBvZmYgb2YgdGhlIGdlbmVyYXRlZCBXZWJwYWNrIHJ1bnRpbWUuXG4vLyBUaGUgbWFwcGluZ3MgYXJlIGluY29ycmVjdC5cbi8vIFRvIGdldCB0aGUgY29ycmVjdCBsaW5lL2NvbHVtbiBtYXBwaW5ncywgdHVybiBvZmYgc291cmNlbWFwcyBpbiB5b3VyIGRlYnVnZ2VyLlxuXG5zZWxmW1wid2VicGFja0hvdFVwZGF0ZV9OX0VcIl0oXCJ3ZWJwYWNrXCIse30sXG4vKioqKioqLyBmdW5jdGlvbihfX3dlYnBhY2tfcmVxdWlyZV9fKSB7IC8vIHdlYnBhY2tSdW50aW1lTW9kdWxlc1xuLyoqKioqKi8gLyogd2VicGFjay9ydW50aW1lL2dldEZ1bGxIYXNoICovXG4vKioqKioqLyAoKCkgPT4ge1xuLyoqKioqKi8gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmggPSAoKSA9PiAoXCI5ODYxNmRlMzcyNjUxNzI0XCIpXG4vKioqKioqLyB9KSgpO1xuLyoqKioqKi8gXG4vKioqKioqLyB9XG4pIl19
|
||||
;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// File: /Users/jettcheng1018/code/dramaling-vocab-learning/frontend/app/flashcards/page.tsx
|
||||
import * as entry from '../../../../app/flashcards/page.js'
|
||||
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
||||
|
||||
type TEntry = typeof import('../../../../app/flashcards/page.js')
|
||||
|
||||
type SegmentParams<T extends Object = any> = T extends Record<string, any>
|
||||
? { [K in keyof T]: T[K] extends string ? string | string[] | undefined : never }
|
||||
: T
|
||||
|
||||
// Check that the entry is a valid entry
|
||||
checkFields<Diff<{
|
||||
default: Function
|
||||
config?: {}
|
||||
generateStaticParams?: Function
|
||||
revalidate?: RevalidateRange<TEntry> | false
|
||||
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
|
||||
dynamicParams?: boolean
|
||||
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
|
||||
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
|
||||
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
|
||||
maxDuration?: number
|
||||
|
||||
metadata?: any
|
||||
generateMetadata?: Function
|
||||
viewport?: any
|
||||
generateViewport?: Function
|
||||
experimental_ppr?: boolean
|
||||
|
||||
}, TEntry, ''>>()
|
||||
|
||||
|
||||
// Check the prop type of the entry function
|
||||
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
|
||||
|
||||
// Check the arguments and return type of the generateMetadata function
|
||||
if ('generateMetadata' in entry) {
|
||||
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
|
||||
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
|
||||
}
|
||||
|
||||
// Check the arguments and return type of the generateViewport function
|
||||
if ('generateViewport' in entry) {
|
||||
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
|
||||
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
|
||||
}
|
||||
|
||||
// Check the arguments and return type of the generateStaticParams function
|
||||
if ('generateStaticParams' in entry) {
|
||||
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
|
||||
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
|
||||
}
|
||||
|
||||
export interface PageProps {
|
||||
params?: Promise<SegmentParams>
|
||||
searchParams?: Promise<any>
|
||||
}
|
||||
export interface LayoutProps {
|
||||
children?: React.ReactNode
|
||||
|
||||
params?: Promise<SegmentParams>
|
||||
}
|
||||
|
||||
// =============
|
||||
// Utility types
|
||||
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
|
||||
|
||||
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
|
||||
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
|
||||
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
|
||||
|
||||
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
|
||||
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
|
||||
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
|
||||
|
||||
|
||||
|
||||
function checkFields<_ extends { [k in keyof any]: never }>() {}
|
||||
|
||||
// https://github.com/sindresorhus/type-fest
|
||||
type Numeric = number | bigint
|
||||
type Zero = 0 | 0n
|
||||
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
|
||||
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
|
||||
Loading…
Reference in New Issue