由于对接抖音使用了原生的HTTP Client ,为了简化操作 以及 让大家学习更多的开发技巧,这里使用Flurl.http进行 RPC的操作。
对接饿了么主要是三步:
- 生成授权URL ,这一步前端和后端做都可以。不过饿了么这一块是本人负责,因此由后端生成
- 商户点击生成的授权URL,进行授权
- 商户授权成功后,会向配置好的回调地址发送商户的code 以及自定义的state,有了code,我们就可以拿到该商户的access_token和refresh_token了
- 调用饿了么的api。饿了么使用的是nop协议,因此所有操作都在同一个地址,根据传入body 中的action和param返回相应的数据。
至于VO,由于代码在公司,VO需要大家自行建立。如果有些数据不需要后端进行操作,可以不用建立VO,把请求的返回体序列化为Dictionary即可。
授权URL类似于这样:
https://open-api-sandbox.shop.ele.me/authorize?response_type=code&client_id=[应用id]&redirect_uri=[回调地址]&state=[自定义信息]&scope=all
授权文档 : https://nest-fe.faas.ele.me/base/documents/isvoauth
API调用规范: https://nest-fe.faas.ele.me/base/documents/apiprotoco
using AlibabaCloud.OpenApiClient.Models;
using Aliyun.Credentials.Utils;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.Core.Config;
using CoreCms.Net.Core.Custom;
using CoreCms.Net.IServices;
using CoreCms.Net.Loging;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.Entities.Eleme.Product;
using CoreCms.Net.Model.Entities.Enum;
using CoreCms.Net.Model.Req.Eleme;
using CoreCms.Net.Model.Resp.Eleme;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Utility;
using CoreCms.Net.Utility.Extensions;
using Essensoft.Paylink.Alipay.Domain;
using Flurl.Http;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using NPOI.OpenXmlFormats.Dml.Chart;
using NPOI.Util;
using Org.BouncyCastle.Utilities.Encoders;
using Qiniu.CDN;
using SqlSugar;
using SqlSugar.Extensions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using static CoreCms.Net.Model.Resp.Eleme.GetUser;
using static SKIT.FlurlHttpClient.Wechat.Api.Models.ComponentTCBBatchGetEnvironmentIdResponse.Types;
using static System.Net.WebRequestMethods;
namespace CoreCms.Net.Services.Eleme
{
public class ElemeService : IElemeService
{
#region 依赖注入
private readonly Model.Entities.ElemeConfig _elemeConfig;
private readonly IHttpContextUser _user;
private readonly ICoreCmsStoreServices _storeServices;
private readonly DbManager _dbManager;
private readonly IServiceProvider _serviceProvider;
#endregion
#region 在构造函数中进行依赖注入,读取配置信息
public ElemeService(IHttpContextUser user, ICoreCmsStoreServices storeServices, DbManager dbManager, IServiceProvider serviceProvider)
{
_user = user;
_storeServices = storeServices;
_dbManager = dbManager;
_serviceProvider = serviceProvider;
var IsSandBox = AppSettingsHelper.GetContent("Eleme", "IsSandBox").ObjToBool(); // 是否沙箱环境
string Key = AppSettingsHelper.GetContent("Eleme", IsSandBox ? "SandBoxKey" : "Key");// 应用程序的 id
string Secret = AppSettingsHelper.GetContent("Eleme", IsSandBox ? "SandBoxSecret" : "Secret"); // 应用程序的秘钥
_elemeConfig = new Model.Entities.ElemeConfig(IsSandBox, Key, Secret);
}
#endregion
#region authrize
/// <summary>
/// 第一步: 获取授权URL
/// </summary>
/// <returns></returns>
public string GetAuthURL()
{
// oauthCodeUrl是根据是否是沙箱环境决定的,如果是沙箱环境用 https://open-api-sandbox.shop.ele.me/authorize
// callback是饿了么向你发送code 的地址,清自行到配置文件中配置或者写死也行
// state 自己用于判断code是哪个商户的,自己根据自己的业务决定,我这里用的是商户在数据库中的id
string url = _elemeConfig.oauthCodeUrl + "?response_type=code&client_id=" + _elemeConfig.appKey + "&redirect_uri=" + WebUtility.UrlEncode(AppSettingsHelper.GetContent("Eleme", "CallbackUrl")) + "&state=" + _user.StoreId + "&scope=all";
return url;
}
/// <summary>
/// 第二第三步 ,商户授权后,饿了么会向我们发送请求,我们从请求中拿到code和 state ,获取access_token,并将其保存在数据库中
/// </summary>
/// <param name="code"></param>
/// <param name="state"></param>
/// <returns></returns>
public async Task<AccessTokenResp> GetAccessToken(string code, string state)
{
AccessTokenResp accessTokenResp;
// 请求的URL
string getTokenURL = _elemeConfig.isSandbox ? "https://open-api-sandbox.shop.ele.me/token" : "https://open-api.shop.ele.me/token";
try
{
accessTokenResp = await getTokenURL
// 新建FlurlClient,并添加header
.WithHeader("Authorization", $"Basic {Base64Encode($"{_elemeConfig.appKey}:{_elemeConfig.appSecret}")}")
.WithHeader("Content-Type", "application/x-www-form-urlencoded")
// 接收所有状态码的json
.AllowAnyHttpStatus()
// 发送POST请求
.PostUrlEncodedAsync(new
{
grant_type = "authorization_code",
code,
redirect_uri = AppSettingsHelper.GetContent("Eleme", "CallbackUrl"),
client_id = _elemeConfig.appKey,
})
// 接收json并序列化为 自定义的VO
.ReceiveJson<AccessTokenResp>();
// 将获取到的token保存到数据库中
CoreCmsStore store = await _storeServices.QueryByIdAsync(state.ObjToInt());
if (store is not null)
{
store.eleToken = accessTokenResp.access_token;
store.eleRefreshToken = accessTokenResp.refresh_token;
bool isUpdated = await _dbManager.MasterDb.Updateable<CoreCmsStore>(store)
.ExecuteCommandHasChangeAsync();
ElemeApiResponse<GetUser> resp = await ElemeRPC<GetUser>("eleme.user.getUser", new Dictionary<string, object> { }, state.ObjToInt());
if (resp.result.authorizedShops.Any())
{
CoreCmsStore store2 = await _storeServices.QueryByIdAsync(state.ObjToInt());
store2.eleShopId = resp.result.authorizedShops[0].id;
store2.eleUserId = resp.result.userId;
await _dbManager.MasterDb.Updateable<CoreCmsStore>(store2)
.ExecuteCommandHasChangeAsync();
}
}
}
catch (FlurlHttpException fex)
{
accessTokenResp = JsonConvert.DeserializeObject<AccessTokenResp>(await fex.GetResponseStringAsync().ConfigureAwait(false));
}
return accessTokenResp;
}
/// <summary>
/// token 是有有效期的,可以使用hangfire 添加定时任务在每天的三更半夜进行刷新
/// </summary>
/// <param name="storeId"></param>
/// <returns></returns>
public async Task<AccessTokenResp> RefreshToken(int storeId)
{
CoreCmsStore store = await _storeServices.QueryByIdAsync(storeId);
AccessTokenResp accessTokenResp = null;
string getTokenURL = _elemeConfig.isSandbox ? "https://open-api-sandbox.shop.ele.me/token" : "https://open-api.shop.ele.me/token";
await getTokenURL
.WithHeader("Authorization", $"Basic {Base64Encode($"{_elemeConfig.appKey}:{_elemeConfig.appSecret}")}")
.WithHeader("Content-Type", "application/x-www-form-urlencoded")
.AllowAnyHttpStatus()
.PostUrlEncodedAsync(new
{
grant_type = "refresh_token",
refresh_token = store.eleRefreshToken,
})
.ReceiveJson<AccessTokenResp>();
if (accessTokenResp is { refresh_token: not null, access_token: not null, error: null, error_description: null })
{
store.eleRefreshToken = accessTokenResp.refresh_token;
store.eleToken = accessTokenResp.access_token;
await _dbManager.MasterDb.Updateable<CoreCmsStore>(store)
.ExecuteCommandHasChangeAsync();
}
return accessTokenResp;
}
#endregion
#region product 第四步,调用饿了么相关API ,由于所有api都是同一个,所以我们把它封装到ElemeRPC 这个函数,每次传参只需要传入 action和param即可
public async Task<ElemeApiResponse<Dictionary<string, object>>> UpdateCategory(UpdateCategoryReq req) => await ElemeRPC<Dictionary<string, object>>("eleme.product.category.updateCategoryV2", req.ToDictionary());
/// <summary>
/// Remove Food Category
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<ElemeApiResponse<Dictionary<string, object>>> RemoveCategory(long id) => await ElemeRPC<Dictionary<string, object>>("eleme.product.category.invalidCategory", new Dictionary<string, object> { { "categoryId", id } });
/// <summary>
/// get user and shop information
/// </summary>
public async Task<ElemeApiResponse<List<OCategory>>> GetShopCategories() => await ElemeRPC<List<OCategory>>("eleme.product.category.getShopCategories", new Dictionary<string, object> { { "shopId", (await _storeServices.QueryByIdAsync(_user.StoreId)).eleShopId } });
/// <summary>
/// get products by category id
/// </summary>
/// <param name="id">category id</param>
/// <returns></returns>
public async Task<AdminUiCallBack> GetItemsByCategoryId(long id)
{
ElemeApiResponse<Dictionary<string, OItem>> elemeApiResponse = await ElemeRPC<Dictionary<string, OItem>>("eleme.product.item.getItemsByCategoryId", new Dictionary<string, object> { { "categoryId", id } });
AdminUiCallBack jm = FormatElemeKVJSON(elemeApiResponse.DeserializeObject<ElemeApiResponse<Dictionary<string, object>>>());
if (
jm
is
{
data: not null
}
)
jm.data = await GetProductRelation(jm.data.DeserializeObject<List<OItem>>());
return jm;
}
#endregion
#region util
private string Base64Encode(string str) => Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
/// 共性寓于个性之中,封装通用的饿了么RPC
public async Task<ElemeApiResponse<TResult>> ElemeRPC<TResult>(string action, Dictionary<string, object> prams, int? storeId = null)
{
string apiURL = _elemeConfig.isSandbox ? "https://open-api-sandbox.shop.ele.me/api/v1" : "https://open-api.shop.ele.me/api/v1";
ElemeApiResponse<TResult> res;
try
{
res = await apiURL
.AllowAnyHttpStatus()
// 因为JOSN结构体都是一样的,只有action和 params不一样,我选择把 json数据也进行封装
.PostJsonAsync(await GenElemeJSON(action, prams, storeId))
.ReceiveJson<ElemeApiResponse<TResult>>();
}
catch (FlurlHttpException fex)
{
string responseString = await fex.GetResponseStringAsync().ConfigureAwait(false);
res = JsonConvert.DeserializeObject<ElemeApiResponse<TResult>>(responseString);
}
return res;
}
/// 生成饿了么通用RPC的JSON
public async Task<ElemeBaseReq> GenElemeJSON(string action, Dictionary<string, object> prams, int? storeId = null)
{
storeId ??= _user.StoreId;
CoreCmsStore store = await _storeServices.QueryByIdAsync(storeId);
ElemeBaseReq req = new()
{
action = action,
nop = "1.0.0",
@params = prams,
id = $"{Guid.NewGuid()}",
metas = new ElemeBaseReq.Metas
{
app_key = _elemeConfig.appKey,
timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
},
token = store.eleToken,
};
SortedDictionary<string, object> dic = new() {
{ "app_key", JsonConvert.SerializeObject(req.metas.app_key) },
{ "timestamp", req.metas.timestamp }
};
foreach (var item in req.@params as Dictionary<string, object>)
{
dic.Add($"{item.Key}", JsonConvert.SerializeObject(item.Value));
}
IEnumerable<string> list = dic.Select(i => $"{i.Key}={i.Value}");
string str = "";
foreach (var item in list)
{
str += item;
}
str = $"{req.action}{req.token}{str}{_elemeConfig.appSecret}";
using (var md5 = MD5.Create())
{
var sb = new StringBuilder();
foreach (var t in md5.ComputeHash(Encoding.UTF8.GetBytes(str)))
{
sb.Append(t.ToString("X2"));
}
req.signature = sb.ToString().ToUpper();
}
if (prams.Count == 0)
{
req.@params = new { };
}
return req;
}
#endregion
}
}