1.1 定義
1、基礎接口:單一職責原則,每個接口只負責各自的業務,下接db,通用性強。
2、聚合接口:根據調用方需求聚合基礎接口數據,業務性強。
1.2 協議
1. 客戶端在通過 API 與后端服務通信的過程中, 應該使用 HTTPS(生產環境) 協議
2. 服務端響應的數據格式統一為JSON
1.3域名host
prd環境:https://xxx-xxx-api.example.com/
uat環境:https://xxx-xxx-api-uat.example.com/
test環境:https://xxx-xxx-api-test.example.com/
dev環境:https://xxx-xxx-api-dev.example.com/
將api放到子域名里,這種做法可以保持某些規模化上的靈活性。
1.4路徑path
path命名應該是以資源為導向的命名,對資源的操作是由HttpMethod(get、post、put、delete)來決定。所以一般來說url上的單詞都應該是名詞,一定不要是動詞。一般遵循以下約定:
(1)URL 的命名必須全部小寫;
(2) URL 必須 是易讀的 URL;
(3)一定不可 暴露服務器架構
(4)出現復合詞匯使用下划線分隔,例如:animal_types
舉幾個正面例子:
新增用戶:http://localhost/user post方法提交;
修改用戶:http://localhost/users put方法提交;
刪除文章:http://localhost/articles?author=1&category=2 delete方法提交;
查詢用戶:http://localhost/users get方法提交;
查詢文章:http://localhost/articles?author=1&category=2get方法提交;
錯誤的例子如下:
http://localhost/get_user
https://api.example.com/getUserInfo?userid=1
https://api.example.com/getusers
https://api.example.com/sv/u
https://api.example.com/cgi-bin/users/get_user.php?userid=1
1.5動詞
- RESTful 的核心思想就是,客戶端發出的數據操作指令都是"動詞 + 賓語"的結構,動詞通常就是四種 HTTP 方法,對應 CRUD 操作:
GET(SELECT):從服務器取出資源(一項或多項)。 POST(CREATE):在服務器新建一個資源。 PUT(UPDATE):在服務器更新資源(客戶端提供改變后的完整資源)。 PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。 DELETE(DELETE):從服務器刪除資源。 |
其中
(1)刪除資源 必須 用 DELETE 方法
(2)創建新的資源 必須 使用 POST 方法
(3)更新資源 應該 使用 PUT 方法
(4)獲取資源信息 必須 使用 GET 方法
針對每一個路徑來說,下面列出所有可行的 HTTP 動詞和端點的組合
請求方 |
URL |
描述 |
|
法 |
|
||
|
|
|
|
|
|
|
|
GET |
/zoos |
列出所有的動物園(ID和名稱,不要太詳細) |
|
|
|
|
|
POST |
/zoos |
新增一個新的動物園 |
|
|
|
|
|
GET |
/zoos/{zoo} |
獲取指定動物園詳情 |
|
|
|
|
|
PUT |
/zoos/{zoo} |
更新指定動物園(整個對象) |
|
|
|
|
|
PATCH |
/zoos/{zoo} |
更新動物園(部分對象) |
|
|
|
|
|
DELETE |
/zoos/{zoo} |
刪除指定動物園 |
|
|
|
|
|
GET |
/zoos/{zoo}/animals |
檢索指定動物園下的動物列表(ID和名稱,不要太詳 |
|
細) |
|
||
|
|
|
|
|
|
|
|
GET |
/animals |
列出所有動物(ID和名稱)。 |
|
|
|
|
|
POST |
/animals |
新增新的動物 |
|
|
|
|
|
GET |
/animals/{animal} |
獲取指定的動物詳情 |
|
|
|
|
|
PUT |
/animals/{animal} |
更新指定的動物(整個對象) |
|
|
|
|
|
PATCH |
/animals/{animal} |
更新指定的動物(部分對象) |
|
|
|
|
|
GET |
/animal_types |
獲取所有動物類型(ID和名稱,不要太詳細) |
|
|
|
|
|
GET |
/animal_types/{type} |
獲取指定的動物類型詳情 |
|
|
|
|
|
GET |
/employees |
檢索整個雇員列表 |
|
|
|
|
|
GET |
/employees/{employee} |
檢索指定特定的員工 |
|
|
|
|
|
GET |
/zoos/{zoo}/employees |
檢索在這個動物園工作的雇員的名單(身份證和姓名) |
|
|
|
|
|
POST |
/employees |
新增指定新員工 |
|
|
|
|
|
POST |
/zoos/{zoo}/employees |
在特定的動物園雇佣一名員工 |
|
|
|
|
|
DELETE |
/zoos/{zoo}/employees/{employee} |
從某個動物園解雇一名員工 |
|
|
|
|
|
1.6入參
1、如果記錄數量很多,服務器不可能都將它們返回給用戶。API 應該 提供參數,過濾返回結果。下面是一些常見的參數。
- ?limit=10:指定返回記錄的數量
- ?offset=10:指定返回記錄的開始位置。
- ?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。
- ?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
- ?animal_type_id=1:指定篩選條件
所有URL參數 必須是全小寫,必須使用下划線類型的參數形式。
分頁參數 必須 固定為 page 、 per_page
經常使用的、復雜的查詢 應該 標簽化,降低維護成本,如
GET /trades?status=closed&sort=sortby=name&order=asc
# 可為其定制快捷方式
GET /trades/recently_closed
2、入參可分為業務參數和公共參數;公共參數有:
參數 |
名稱 |
說明 |
timestamp |
時間戳 |
|
clientid |
調用方appid |
統一管理應用,否則不放行 |
token |
令牌 |
冪等情況可用 |
version |
版本號 |
|
1.7響應
1、出參(返回值):必須的字段有:
字段 |
類型 |
描述 |
code |
數值 |
狀態碼 |
msg |
字符串 |
信息描述 |
data |
結果集 |
返回結果集 |
2、如果請求處理完全正確,則狀態碼為0 ;
3、狀態碼暫定8位數數字,前4位為某一個應用(服務)擬的一個數字,后4位為具體的狀態值。狀態碼分為2種---公共和自定義,公共碼以0打頭+3位數。
比如:
99990400 --客戶端錯誤,比如請求語法格式錯誤、無效的請求、無效的簽名等。
99991001 -----用戶Id不能為空
響應的公共碼如下:
編碼 |
描述 |
說明 |
001 |
注解使用錯誤 |
|
002 |
微服務不在線,或網絡超時 |
|
003 |
TOKEN解析失敗 |
|
004 |
TOKEN無效或沒有對應的用戶 |
|
400 |
客戶端錯誤,比如請求語法格式錯誤、 |
服務器 應該 放棄該請求 |
401 |
需要身份認證,比如access_token 無效/過期 |
客戶端在收到 401 響應后, |
403 |
沒有權限訪問該請求 |
服務器收到請求但拒絕提供服務。 |
404 |
用戶請求的資源不存在 |
如獲取不存在的用戶信息 |
410 |
請求的資源不存在,並且未來也不會存在 |
在收到 410 狀態碼后, |
429 |
請求次數超過允許范圍 |
|
500 |
未知異常 |
應該 提供完整的錯誤信息支持,也方便跟蹤調試 |
1.8項目結構
1、采用經典DDD領域取到模型:(默認一個解決方案有5個項目)
5個項目分別為:
Web層為最外層接口定義;
Service為具體的應用服務處理;
Infrastructure基礎設施層,處理具體的業務邏輯和數據DB的處理;
Domain領域層為模型和倉庫接口interface;
Common為通用的一些Helper類;
2、一個解決方案創建5個項目(如上圖),並且里包含常用的基礎組件:Log4net日志,聽雲監聽;dockerfile,skywalking,全局異常捕捉,接口請求開始和結束的日志記錄,swagger,service層的依賴注入,Mapping等。
3、代碼全部采用依賴注入寫法,盡量少些靜態類;
4、HttpClient的寫法:使用采用.netcore官方提供的方法,采用工廠類+依賴注入方式:實例代碼如下:
1、SartUp類里添加代碼-- httpclient初始化: services.AddHttpClient("MsgApi", c => { c.BaseAddress = new Uri(Configuration["OuterApi:MsgApi:url"]); c.Timeout = TimeSpan.FromSeconds(30); }); //2 構造函注入 private IDbContext _dbContext; private IUnitOfWork _unitOfWork; private IordersRepository _ordersRepository; private IordercourseRepository _ordercourseRepository; private ILogger _logger; privatereadonly IConfiguration _config; privatereadonly IHttpClientFactory _clientFactory; public ordersService(IDbContext dbContext, ILogger<ordersService> logger, IConfiguration config, IHttpClientFactory clientFactory) { _dbContext = dbContext; _unitOfWork = new UnitOfWork(_dbContext); _ordersRepository = new ordersRepository(_dbContext); _ordercourseRepository = new ordercourseRepository(_dbContext); _mapper = mapper; _config = config; _logger = logger; _clientFactory = clientFactory; } //3使用 ///<summary> ///判斷此時該校區是否可以下單 ///</summary> ///<param name="req"></param> ///<returns></returns> publicasync Task<Result<string>> CheckDept(CheckSchoolDeptReq req) { Result<string> sendRet = new Result<string>(); try { HttpClient client = _clientFactory.CreateClient("ContractApi"); MyHttpClientHelper myHttpClientHelper = new MyHttpClientHelper(); MarketToUPCCheckReq checkreq = new MarketToUPCCheckReq(); sendRet = await myHttpClientHelper.GetData<Result<string>>(client, "MarketToUPCCheck", checkreq); } catch (Exception ex) { sendRet.state = false; sendRet.error_code = ErrorCode.SysExceptionError; sendRet.error_msg = "調用《是否可以下訂單接口》報錯了。請重試或者聯系管理員!"; _logger.LogError(ex, ErrorCode.SysExceptionError +"調用《是否可以下訂單》接口報錯了:" + ex.Message); } return sendRet; }
1.9日志
1、接口開始前和結束后都已在LogstashFilter里記錄,接口里就不需要再次記錄;
LogstashFilter里的代碼如下:
/// <summary> /// 記錄日志用過濾器 /// </summary> public class LogstashFilter : IActionFilter, IResultFilter { private string ActionArguments { get; set; } /// <summary> /// 請求體中的所有值 /// </summary> private string RequestBody { get; set; } private Stopwatch Stopwatch { get; set; } private ILogger _logger; public LogstashFilter(ILogger<LogstashFilter> logger ) { _logger = logger; } /// <summary> /// Action 調用前執行 /// </summary> /// <param name="context"></param> public void OnActionExecuting(ActionExecutingContext context) { long contentLen = context.HttpContext.Request.ContentLength == null ? 0 : context.HttpContext.Request.ContentLength.Value; if (contentLen > 0) { // 讀取請求體中所有內容 System.IO.Stream stream = context.HttpContext.Request.Body; if (context.HttpContext.Request.Method == "POST") { stream.Position = 0; } byte[] buffer = new byte[contentLen]; stream.Read(buffer, 0, buffer.Length); RequestBody = System.Text.Encoding.UTF8.GetString(buffer);// 轉化為字符串 } ActionArguments = JsonConvert.SerializeObject(context.ActionArguments); Stopwatch = new Stopwatch(); Stopwatch.Start(); string url = context.HttpContext.Request.Host + context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; string method = context.HttpContext.Request.Method; _logger.LogInformation($"地址:{url} \n " + $"方式:{method} \n " + $"請求體:{RequestBody} \n " + $"完整參數:{ActionArguments}\n " ); } /// <summary> /// Action 方法調用后,Result 方法調用前執行 /// </summary> /// <param name="context"></param> public void OnActionExecuted(ActionExecutedContext context) { // do nothing } /// <summary> /// Result 方法調用前(View 呈現前)執行 /// </summary> /// <param name="context"></param> public void OnResultExecuting(ResultExecutingContext context) { // do nothing } /// <summary> /// Result 方法調用后執行 /// </summary> /// <param name="context"></param> public void OnResultExecuted(ResultExecutedContext context) { Stopwatch.Stop(); string url = context.HttpContext.Request.Host + context.HttpContext.Request.Path + context.HttpContext.Request.QueryString; string method = context.HttpContext.Request.Method; string qs = ActionArguments; string res = "在返回結果前發生了異常"; if (context.Result is ObjectResult) { dynamic result = context.Result.GetType().Name == "EmptyResult" ? new { Value = "無返回結果" } : context.Result as dynamic; if (result != null) { res = JsonConvert.SerializeObject(result.Value); } } _logger.LogInformation($"地址:{url} \n " + $"方式:{method} \n " + $"請求體:{RequestBody} \n " + $"參數:{qs}\n " + $"結果:{res}\n " + $"耗時:{Stopwatch.Elapsed.TotalMilliseconds} 毫秒"); } }
2、try Catch日志必須要添加LogError日志,並且要將堆棧信息記錄,代碼如下:
catch (Exception ex) { _logger.LogError(ex, ErrorCode500 + ex.Message); }