C# .NET 对接饿了么

	由于对接抖音使用了原生的HTTP Client ,为了简化操作 以及  让大家学习更多的开发技巧,这里使用Flurl.http进行 RPC的操作。

对接饿了么主要是三步:

  1. 生成授权URL ,这一步前端和后端做都可以。不过饿了么这一块是本人负责,因此由后端生成
  2. 商户点击生成的授权URL,进行授权
  3. 商户授权成功后,会向配置好的回调地址发送商户的code 以及自定义的state,有了code,我们就可以拿到该商户的access_token和refresh_token了
  4. 调用饿了么的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

    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值