尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#实现微信扫码登录全流程:从OAuth .NET Core到生产部署

C#实现微信扫码登录全流程:从OAuth .NET Core到生产部署 1. 项目缘起与核心价值最近在做一个内部管理系统需要集成第三方登录。考虑到用户几乎人手一个微信微信扫码登录自然成了首选。这玩意儿听起来简单不就是用户扫个码、点个授权然后我们拿到用户信息吗但真上手做尤其是用C#在后端实现整套流程你会发现从申请应用、配置参数到处理回调、管理会话每一步都有不少细节需要注意稍不留神就会卡在某个环节。网上教程不少但要么是代码片段不全要么是过时的老版本API照着做总差那么一口气。今天我就把自己从零搭建C#微信扫码登录服务端的过程包括踩过的坑和优化点完整地梳理出来。无论你是要做一个全新的网站还是给老系统加个登录功能这篇内容都能让你少走弯路快速跑通。微信扫码登录的本质是OAuth 2.0授权码模式的一种应用。整个过程不涉及用户密码安全性更高用户体验也更流畅。对于开发者而言我们核心要做的就是三件事生成一个带参数的二维码让用户扫提供一个回调地址接收微信服务器跳转回来的授权码用这个授权码去换取用户的唯一标识OpenID或UnionID和基础信息。下面我们就围绕这三个核心环节把C#服务端的实现掰开揉碎了讲清楚。2. 前期准备申请与配置在写第一行代码之前你得先把“场地”准备好。这个场地就是微信开放平台。这里有个关键点个人订阅号不支持网页授权企业号、服务号或者微信开放平台的“网站应用”才支持。我们通常走的是“网站应用”这个渠道。2.1 创建网站应用与获取关键参数首先访问微信开放平台并登录。如果你没有账号需要先注册一个开发者账号。登录后在“管理中心”找到“网站应用”栏目点击“创建网站应用”。填写应用名称、简介、网站域名等信息时有几点需要特别注意网站域名这里填的是你服务器的域名不能带http://或https://也不能带端口号。例如你的回调地址是https://api.yourdomain.com/wechat/callback那么这里就填api.yourdomain.com。本地开发时可以用内网穿透工具如ngrok、cpolar生成一个临时域名并配置到这里。授权回调域这是整个流程中最容易出错的地方之一。它指的是你的服务端接收微信回调的域名。同样只填域名不填完整路径。微信在跳转时只会验证域名是否匹配不会验证具体路径。所以如果你填了api.yourdomain.com那么你的回调地址可以是https://api.yourdomain.com/wechat/callback也可以是https://api.yourdomain.com/auth/wechat只要域名对就行。一个应用只能设置一个授权回调域设置后需要等待审核通常几分钟到几小时才会生效在生效前所有回调都会失败。应用创建并审核通过后你就能在应用详情页找到两个至关重要的参数AppID应用唯一标识相当于你的应用在微信平台的“用户名”。AppSecret应用密钥这是极其重要的私密信息绝对不能泄露或提交到代码仓库。它相当于“密码”用于后续换取访问令牌。注意AppSecret需要妥善保管。建议在服务器上通过环境变量、配置文件并排除在版本控制外或专业的密钥管理服务来存储切勿硬编码在代码中。2.2 服务端环境与项目结构我们使用C#的ASP.NET Core来构建服务端。我选择的是.NET 6或.NET 8 LTS版本它们性能更好也是长期支持版本。创建一个新的Web API项目即可。项目结构可以这样规划WeChatLoginDemo/ ├── Controllers/ │ ├── AuthController.cs # 处理登录相关请求 │ └── CallbackController.cs # 处理微信回调 ├── Services/ │ ├── IWeChatAuthService.cs # 微信认证服务接口 │ └── WeChatAuthService.cs # 微信认证服务实现 ├── Models/ │ ├── WeChatApiResponse.cs # 微信API通用响应模型 │ ├── AccessTokenResult.cs # 访问令牌结果 │ └── UserInfoResult.cs # 用户信息结果 ├── appsettings.json # 配置文件 └── Program.cs # 主程序在appsettings.json中配置我们的关键参数{ WeChat: { AppId: 你的AppID, AppSecret: 你的AppSecret, RedirectUri: https://api.yourdomain.com/api/callback/wechat, Scope: snsapi_login // 网页授权登录固定用这个 } }同样AppSecret在生产环境应该从环境变量WeChat__AppSecret读取。3. 核心流程一构造扫码登录链接与页面用户点击“微信登录”按钮后我们需要引导用户跳转到微信的二维码登录页面。这个页面是由微信官方提供的我们只需要生成一个符合规范的URL。3.1 生成授权URL的算法与参数微信扫码登录的授权URL有固定的格式https://open.weixin.qq.com/connect/qrconnect?appidAPPIDredirect_uriREDIRECT_URIresponse_typecodescopeSCOPEstateSTATE#wechat_redirect我们需要在服务端动态构造这个URL。在WeChatAuthService.cs中我创建了一个方法来生成它using System.Web; // 用于UrlEncode public class WeChatAuthService : IWeChatAuthService { private readonly string _appId; private readonly string _redirectUri; private readonly string _scope; public WeChatAuthService(IConfiguration configuration) { _appId configuration[WeChat:AppId]; _redirectUri configuration[WeChat:RedirectUri]; _scope configuration[WeChat:Scope]; // 通常是 snsapi_login } public string GenerateAuthorizationUrl(string state) { // 对回调地址进行编码这是必须的 var encodedRedirectUri HttpUtility.UrlEncode(_redirectUri, Encoding.UTF8); // 构造完整的授权URL var url $https://open.weixin.qq.com/connect/qrconnect? $appid{_appId} $redirect_uri{encodedRedirectUri} $response_typecode $scope{_scope} $state{state} $#wechat_redirect; return url; } }关键参数解析appid你的应用ID必须。redirect_uri用户授权后微信服务器跳转的地址。必须和开放平台设置的“授权回调域”同域名且必须经过UrlEncode编码。response_type固定为code表示我们需要一个授权码。scope应用授权作用域。对于网页扫码登录固定填写snsapi_login。这个作用域可以获取用户的OpenID如果需要获取用户昵称、头像等更多信息在后续步骤中也可以实现但基础登录流程不需要变更此处。state一个非常重要的安全参数。它应该是一个随机生成的、不可预测的字符串用于防止CSRF跨站请求伪造攻击。服务端生成后需要将其与当前用户的会话Session或缓存关联起来。当微信回调时会原样传回这个state服务端需要验证其有效性确保这次回调是合法的。#wechat_redirect这个片段直接拼接在URL末尾微信前端会用到我们照写即可。3.2 前端页面的简单实现在C#的Web API项目中我们可以用一个简单的Razor页面或直接通过API返回URL给前端。这里以API为例在AuthController中[ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly IWeChatAuthService _weChatAuthService; public AuthController(IWeChatAuthService weChatAuthService) { _weChatAuthService weChatAuthService; } [HttpGet(login-url)] public IActionResult GetWeChatLoginUrl() { // 1. 生成一个随机的state参数并存储例如存入分布式缓存Rediskey为state值value为userId或sessionId并设置较短过期时间如5分钟 var state Guid.NewGuid().ToString(N); // _cache.SetString($wechat_state_{state}, some_session_identifier, TimeSpan.FromMinutes(5)); // 2. 生成授权URL var authUrl _weChatAuthService.GenerateAuthorizationUrl(state); // 3. 返回给前端 return Ok(new { url authUrl, state state }); } }前端如Vue、React调用这个API拿到url和state后将state存储在前端如localStorage然后跳转到url用户就会看到微信的登录二维码页面。4. 核心流程二处理回调与换取AccessToken用户扫码并确认授权后微信服务器会带着code和state跳转到我们预设的redirect_uri。这是服务端逻辑的核心。4.1 创建回调接口与验证State首先创建一个控制器来处理回调[ApiController] [Route(api/[controller])] public class CallbackController : ControllerBase { private readonly IWeChatAuthService _weChatAuthService; private readonly ILoggerCallbackController _logger; // private readonly IDistributedCache _cache; // 如果用了缓存 public CallbackController(IWeChatAuthService weChatAuthService, ILoggerCallbackController logger) { _weChatAuthService weChatAuthService; _logger logger; } [HttpGet(wechat)] public async TaskIActionResult WeChatCallback([FromQuery] string code, [FromQuery] string state) { _logger.LogInformation($收到微信回调code: {code}, state: {state}); // 1. 验证state参数防止CSRF攻击 // var cachedState await _cache.GetStringAsync($wechat_state_{state}); // if (string.IsNullOrEmpty(cachedState)) // { // _logger.LogWarning($无效的state参数: {state}); // return BadRequest(无效的授权请求。); // } // await _cache.RemoveAsync($wechat_state_{state}); // 验证后立即移除一次性使用 // 模拟验证这里假设state是有效的。生产环境必须用上面的缓存逻辑。 if (string.IsNullOrEmpty(state)) { return BadRequest(缺少state参数。); } // 2. 用code换取access_token和openid var tokenResult await _weChatAuthService.GetAccessTokenAsync(code); if (tokenResult null || string.IsNullOrEmpty(tokenResult.AccessToken)) { _logger.LogError($换取AccessToken失败code: {code}); return Unauthorized(微信登录失败请重试。); } // 3. (可选) 用access_token获取用户详细信息 var userInfo await _weChatAuthService.GetUserInfoAsync(tokenResult.AccessToken, tokenResult.OpenId); // 4. 处理用户登录逻辑创建本地用户、生成JWT Token、设置Cookie等 // var jwtToken await _userService.ProcessWeChatLoginAsync(tokenResult.OpenId, userInfo); // 5. 重定向到前端页面并携带登录成功令牌注意安全不要直接传JWT在URL可以用短时效code // 例如生成一个一次性登录码重定向到前端前端再用这个码换JWT // var authCode GenerateAuthCode(tokenResult.OpenId); // return Redirect($https://www.yourdomain.com/login-success?code{authCode}); // 简化演示直接返回成功信息 return Ok(new { openid tokenResult.OpenId, nickname userInfo?.Nickname, headimgurl userInfo?.HeadImageUrl }); } }关键点state验证是必须的安全步骤绝不能省略。它确保了回调请求来源于你发起的授权请求。code是一次性的且有效期很短通常5分钟拿到后需要立即使用。4.2 实现换取AccessToken的方法现在来实现WeChatAuthService中的GetAccessTokenAsync方法。这一步需要向微信的API发起一个HTTP GET请求。using System.Text.Json; public async TaskAccessTokenResult? GetAccessTokenAsync(string code) { // 微信换取access_token的API地址 var url $https://api.weixin.qq.com/sns/oauth2/access_token? $appid{_appId} $secret{_appSecret} $code{code} $grant_typeauthorization_code; using var httpClient new HttpClient(); try { var response await httpClient.GetStringAsync(url); var result JsonSerializer.DeserializeAccessTokenResult(response); // 微信API错误处理 if (result ! null !string.IsNullOrEmpty(result.ErrCode)) { _logger.LogError($微信API返回错误: {result.ErrCode} - {result.ErrMsg}); return null; } return result; } catch (HttpRequestException ex) { _logger.LogError(ex, $请求微信AccessToken API失败URL: {url}); return null; } catch (JsonException ex) { _logger.LogError(ex, $解析微信AccessToken响应失败响应内容可能异常。); return null; } }对应的模型类AccessTokenResultpublic class AccessTokenResult { [JsonPropertyName(access_token)] public string AccessToken { get; set; } [JsonPropertyName(expires_in)] public int ExpiresIn { get; set; } // 过期时间秒 [JsonPropertyName(refresh_token)] public string RefreshToken { get; set; } [JsonPropertyName(openid)] public string OpenId { get; set; } [JsonPropertyName(scope)] public string Scope { get; set; } [JsonPropertyName(unionid)] public string UnionId { get; set; } // 如果用户在开放平台有绑定会返回 // 错误时返回的字段 [JsonPropertyName(errcode)] public string ErrCode { get; set; } [JsonPropertyName(errmsg)] public string ErrMsg { get; set; } }常见错误码与排查40029:code无效已使用过、过期或不存在。检查code是否被重复使用或授权操作时间过长。40163:code已被使用。确保你的服务端逻辑对同一个code只处理一次。41008: 缺少code参数。检查回调URL是否完整收到了code。40125: 无效的appsecret。检查AppSecret是否正确是否包含多余空格以及在开放平台是否重置过。如果遇到redirect_uri参数错误请百分之百确认redirect_uri的值编码前与开放平台设置的“授权回调域”的域名完全匹配并且已经编码。5. 核心流程三获取用户信息与本地会话管理拿到access_token和openid其实已经完成了登录的“认证”环节。openid是用户在当前这个微信应用下的唯一标识。但通常我们还需要用户的昵称、头像来完善本地用户资料。5.1 获取用户基本信息在WeChatAuthService中添加获取用户信息的方法public async TaskUserInfoResult? GetUserInfoAsync(string accessToken, string openId) { var url $https://api.weixin.qq.com/sns/userinfo? $access_token{accessToken} $openid{openId}; using var httpClient new HttpClient(); try { var response await httpClient.GetStringAsync(url); var result JsonSerializer.DeserializeUserInfoResult(response); if (result ! null !string.IsNullOrEmpty(result.ErrCode)) { _logger.LogWarning($获取用户信息失败: {result.ErrCode} - {result.ErrMsg}。可能用户未授权获取信息或token已过期。); // 即使获取失败也不影响登录可以返回一个只有OpenId的默认对象 return new UserInfoResult { OpenId openId }; } return result; } catch (Exception ex) { _logger.LogError(ex, $获取微信用户信息失败。); return null; } }对应的模型类UserInfoResultpublic class UserInfoResult { [JsonPropertyName(openid)] public string OpenId { get; set; } [JsonPropertyName(nickname)] public string Nickname { get; set; } [JsonPropertyName(sex)] public int Sex { get; set; } // 1男2女0未知 [JsonPropertyName(province)] public string Province { get; set; } [JsonPropertyName(city)] public string City { get; set; } [JsonPropertyName(country)] public string Country { get; set; } [JsonPropertyName(headimgurl)] public string HeadImageUrl { get; set; } // 头像URL最后一个数值代表正方形头像大小有0、46、64、96、132数值可选0代表640*640正方形头像 [JsonPropertyName(privilege)] public Liststring Privilege { get; set; } [JsonPropertyName(unionid)] public string UnionId { get; set; } [JsonPropertyName(errcode)] public string ErrCode { get; set; } [JsonPropertyName(errmsg)] public string ErrMsg { get; set; } }注意用户头像headimgurl是一个HTTP链接直接存储在数据库并前端展示可能存在防盗链问题。一个常见的做法是在获取后将其下载到自己的OSS对象存储或本地服务器并替换为自有域名下的链接。5.2 建立本地用户体系与会话这是业务逻辑部分因人而异。基本思路是用户匹配用openid或更通用的unionid如果应用在开放平台绑定去查询本地用户表。用户创建/更新如果用户不存在则用微信返回的信息昵称、头像等创建一条新的本地用户记录。如果用户已存在可以更新其昵称、头像等信息可选。生成会话JWT (JSON Web Token)生成一个包含用户ID等信息的JWT Token返回给前端。前端后续请求在Authorization头中携带此Token。这是无状态、可扩展的方案。Session在服务器端或分布式缓存如Redis创建Session将Session ID通过Cookie返回给浏览器。这是传统的有状态方案。一次性Code交换像上面回调控制器注释里提到的生成一个短时效的authCode重定向到前端前端再用这个authCode调用另一个安全接口换取真正的JWT。这样避免了Token出现在URL中。这里给出一个简单的JWT生成示例需要安装Microsoft.AspNetCore.Authentication.JwtBearer包// 在UserService或AuthService中 public string GenerateJwtToken(string userId, string openId) { var securityKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration[Jwt:SecretKey])); var credentials new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims new[] { new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(openid, openId), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var token new JwtSecurityToken( issuer: _configuration[Jwt:Issuer], audience: _configuration[Jwt:Audience], claims: claims, expires: DateTime.Now.AddHours(2), // 设置过期时间 signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); }6. 安全加固、性能优化与生产环境考量基础流程跑通后要上线还得考虑更多。这部分是区分“能用”和“好用”、“安全”的关键。6.1 安全加固措施State参数防CSRF前面已经强调必须使用高强度的随机数如Guid或Cryptographically Random生成的字符串并存储在服务器端如Redis验证后立即销毁。绝对不要相信客户端传来的任何未经验证的state。Token存储安全AppSecret、JWT签名密钥等敏感信息必须脱离代码库。使用环境变量、Azure Key Vault、AWS Secrets Manager等服务管理。HTTPS强制整个流程尤其是回调地址(redirect_uri)必须使用HTTPS。微信开放平台也要求回调域是HTTPS的。验证回调请求来源IP可选但推荐微信服务器的出口IP列表是固定的可以在微信官方文档查到。可以在回调接口中验证HttpContext.Connection.RemoteIpAddress是否在微信的IP白名单内防止伪造的回调请求。不过由于有state和code的验证这一步的优先级可以稍低。防止重放攻击code的一次性使用已经提供了一定防护。对于JWT可以加入jtiJWT ID声明并在服务端维护一个短期的已使用jti黑名单但会增加状态管理负担。通常设置合理的JWT过期时间如2小时并配合刷新令牌机制是更实用的做法。6.2 性能与可靠性优化HTTPClient工厂不要在每次请求时都new HttpClient()这会导致端口耗尽。在ASP.NET Core中应使用IHttpClientFactory。在Program.cs中注册服务并在WeChatAuthService中注入IHttpClientFactory来创建客户端。// Program.cs builder.Services.AddHttpClient(); // WeChatAuthService.cs public class WeChatAuthService { private readonly IHttpClientFactory _httpClientFactory; public WeChatAuthService(IHttpClientFactory httpClientFactory, ...) { _httpClientFactory httpClientFactory; } public async TaskAccessTokenResult? GetAccessTokenAsync(string code) { var client _httpClientFactory.CreateClient(); // ... 使用client发起请求 } }请求重试与超时网络请求可能失败。为微信API调用配置合理的超时如10秒和重试策略如Polly库。特别是换取access_token和userinfo的请求。缓存AccessToken谨慎微信的access_token有有效期通常7200秒且调用频率有限制。如果你的服务需要频繁获取同一个用户的信息不推荐应本地存储可以考虑缓存access_token。但绝对不能缓存全局唯一的“接口调用凭证”那是另一种token这里缓存的是用户级别的access_token。更佳实践是只在使用时获取用完后丢弃因为code换token的调用频率限制很宽松。异步全链路确保从控制器到服务方法全部使用async/await避免阻塞线程提高并发能力。6.3 生产环境部署与监控日志记录在所有关键步骤生成state、收到回调、换取token、获取用户信息、业务处理和异常处记录详细的日志。使用结构化日志如Serilog方便后续排查问题。监控与告警监控登录接口的响应时间、错误率特别是state无效、code无效、微信API调用失败。设置告警当错误率超过阈值时及时通知。容灾与降级考虑如果微信服务暂时不可用虽然概率极低你的登录功能如何降级是否可以启用备用登录方式或者给用户友好的提示多环境配置开发、测试、生产环境使用不同的AppID和AppSecret。确保配置文件或环境变量正确区分。7. 实战中遇到的典型问题与解决方案在实际开发和线上运维中我遇到了一些教科书里不会细讲但确实会卡住人的问题。7.1 扫码后页面提示“redirect_uri参数错误”这是最高频的错误。检查点1最可能redirect_uri的值必须与微信开放平台“网站应用”详情页里“授权回调域”设置的一模一样不包括http://或https://。比如你设置了api.abc.com那么redirect_uri参数里就应该是https://api.abc.com/some/path。多一个wwwwww.api.abc.com都不行检查点2redirect_uri参数在拼接URL前必须进行UrlEncode编码。C#中要用HttpUtility.UrlEncode(yourRedirectUri, Encoding.UTF8)。检查点3确保你的回调地址是外网可访问的。本地开发必须用内网穿透工具。检查点4修改“授权回调域”后不是立即生效的通常有几分钟的延迟缓存。耐心等待或清除浏览器缓存再试。7.2 回调时获取到的code为空或无效现象用户扫码授权后跳转回你的回调地址但code参数为空或者用这个code去换access_token时返回40029。排查检查回调接口的[HttpGet]路由是否正确能否被公网访问。在回调接口的第一行打日志输出收到的所有Query参数。可能code被截断或丢失。code的有效期非常短约5分钟。是否用户扫码后在手机上确认授权操作太慢或者你的服务器处理太慢拿到code后没有立即使用绝对不要在浏览器地址栏手动输入一个带code的URL来测试这样code已经被消费过一次了。必须走完整的扫码流程。7.3 如何获取UnionIdUnionId是用户在微信开放平台下的唯一标识同一个用户在同一个开放平台下的不同应用网站应用、移动应用、公众号等里UnionId是相同的而OpenId是不同的。前提你的“网站应用”必须已经绑定到某个微信开放平台账号下。获取在用户关注了某个同开放平台下的公众号或者在某个同开放平台下的移动应用授权登录过并且你当前网站应用的授权作用域包含了snsapi_login那么在调用/sns/oauth2/access_token接口时返回的JSON中就会包含unionid字段。如果用户没有满足上述条件则不会返回。策略设计用户表时可以同时存储openid和unionid。首次登录可能只有openid当某次登录带回了unionid时可以更新记录并尝试用unionid去关联同一用户在其他应用下的账号。7.4 前端页面如何与后端协同这是一个常见的架构问题。我们的后端是独立的API服务(api.domain.com)前端是单独的SPA应用(www.domain.com)。后端生成state和url前端点击登录按钮调用后端API如GET /api/auth/login-url后端生成state存入缓存和授权url返回给前端。前端引导跳转前端收到url后使用window.location.href url跳转到微信二维码页。微信回调后端用户授权后微信跳转到后端的回调地址api.domain.com/api/callback/wechat。后端处理并重定向到前端后端完成所有验证、换取token、创建本地会话后生成一个一次性的认证码authCode然后重定向302到前端的一个特定页面如www.domain.com/login-success?codeauthCode并将authCode存入缓存关联用户ID短时间过期。前端换取最终令牌前端页面/login-success被加载它从URL参数中拿到authCode再调用后端的一个安全接口如POST /api/auth/exchange-token传入authCode后端验证authCode有效后返回真正的JWT Token给前端。前端存储Token前端将JWT Token存储在内存或安全的存储中如HttpOnly的Cookie或localStorage但需防范XSS用于后续API请求。这种“Code换Code再换Token”的间接方式虽然多了一步但避免了敏感令牌直接暴露在URL中安全性更高。8. 扩展思考从扫码登录到用户体系设计实现扫码登录只是第一步它背后连接的是你整个系统的用户体系。这里有几个进阶思考点1. 绑定与解绑用户可能想用多个微信登录同一个账号或者将微信绑定到已有的手机号/邮箱账号上。这需要在你的用户表中建立“第三方授权”关联表记录平台类型(wechat)、openid、unionid、关联的用户ID。提供一个“账号设置”页面让用户管理绑定的第三方账号。2. 静默登录与Session维护微信网页授权有“静默授权”模式(scopesnsapi_base)用户无感知即可获取openid适合已登录用户验证当前微信身份。但对于登录态维护主要还是靠你自己的JWT或Session机制。JWT过期前前端可以尝试用refresh_token如果有刷新或者直接提示用户重新扫码登录。3. 多端统一如果你的产品还有App、小程序微信也提供了相应的登录方式App授权登录、小程序登录。它们的流程和API端点略有不同但核心思想一致获取code- 换access_token和openid。后端可以设计统一的“第三方登录服务”根据不同的platform参数路由到不同的处理逻辑但最终都映射到同一个本地用户体系通过unionid来打通。4. 用户信息更新用户可能会修改微信头像或昵称。一个常见的做法是每次用户通过微信扫码登录时都去拉取一次最新的用户信息并更新本地数据库。这样能保证用户资料的时效性。但要注意频率避免不必要的API调用。整个C#实现微信扫码登录的流程从准备到安全上线核心就是理解OAuth 2.0的授权码流程并严谨地处理每一个环节的参数和安全问题。代码本身不复杂但细节决定成败。希望这篇近万字的拆解能帮你把这条路走得更加顺畅。在实际项目中建议把微信相关的API调用封装成独立的、可配置的、带有重试和降级策略的服务类这样业务代码会更加清晰和健壮。
返回列表