1.1 定義
1、基礎接口:單一職責原則,每個接口隻負責各自的業務,下接db,通用性強。
2、聚合接口:根據調用方需求聚合基礎接口資料,業務性強。
1.2 協定
- 用戶端在通過 API 與後端服務通信的過程中, 應該使用 HTTPS(生産環境) 協定
- 服務端響應的資料格式統一為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/userpost方法送出;
修改使用者:
http://localhost/usersput方法送出;
删除文章:
http://localhost/articles?author=1&category=2delete方法送出;
查詢使用者:
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=11.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/{zoo}
擷取指定動物園詳情
PUT
更新指定動物園(整個對象)
PATCH
更新動物園(部分對象)
DELETE
删除指定動物園
/zoos/{zoo}/animals
檢索指定動物園下的動物清單(ID和名稱,不要太詳
細)
/animals
列出所有動物(ID和名稱)。
新增新的動物
/animals/{animal}
擷取指定的動物詳情
更新指定的動物(整個對象)
更新指定的動物(部分對象)
/animal_types
擷取所有動物類型(ID和名稱,不要太詳細)
/animal_types/{type}
擷取指定的動物類型詳情
/employees
檢索整個雇員清單
/employees/{employee}
檢索指定特定的員工
/zoos/{zoo}/employees
檢索在這個動物園工作的雇員的名單(身份證和姓名)
新增指定新員工
在特定的動物園雇傭一名員工
/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 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使用
///
///判斷此時該校區是否可以下單
publicasync Task> 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>
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日志,并且要将堆棧資訊記錄,代碼如下:
{
_logger.LogError(ex, ErrorCode500 + ex.Message);
}