关于系统接入第三方应用之《顶层设计》
什么情况下会需要这样的代码: 传统的写法有什么优势: 这样的写法有什么优势: 实际案例:
接入第三方应用,多数是通过接口实现的,还有一部份是配合使用消息通知实现的。但,大多数还是API实现的。所以,接下来介绍的就是我的设计,两者都存在的方式。
首先就是调用接口,通过网关的时候都会有AK,SK,所以就会有对应的签名算法:

1 package com.rauto.sales.saicorder.outerapp; 2 3 import java.util.Map; 4 5 /** 6 * 请求签名算法 7 * 8 * @author Euler 9 * @date 2022 2022/5/23 10 */ 11 public interface Sign<T> { 12 13 /** 14 * 使用的签名算法 15 * 注:默认使用md5 16 * 17 * @return 18 */ 19 SignMethodEnum getSignMethod(); 20 21 /** 22 * 计算参数签名 23 * 24 * @param params 25 * @return 26 */ 27 String doSign(Map<String, String> params); 28 29 30 }

1 package com.rauto.sales.saicorder.outerapp; 2 3 /** 4 * 签名算法 5 * 6 * @author Euler 7 * @date 2022 2022/5/23 8 */ 9 public enum SignMethodEnum { 10 MD5("md5", "md5"), 11 HMAC("hmac", "hmac"), 12 HMAC_SHA256("hmac-sha256", "hmac-sha256"), 13 ; 14 15 private String value; 16 private String description; 17 18 SignMethodEnum(String value, String description) { 19 this.value = value; 20 this.description = description; 21 } 22 23 }
有了签名算法,有的系统调用接口用的是带有时效性的是accesstoken,并且这个accesstoken是全局通用的,这个时候你就得把这个accesstoken缓存并且全局共享的这种:

1 package com.rauto.sales.saicorder.outerapp; 2 3 /** 4 * accessToken 是最接近访问接口的动态令牌,拿到此令牌即可调用需要授权的接口 5 * 注意:在标准的授权登录中,是要先拿到用户授权的code,然后携code取accessToken信息 6 * T : 表示获取code的参数,通常是app_key和secret 7 * V : 表示用户授权,开放平台返回的结构 8 * Z : 表示获取令牌,开放平台返回的结构 9 * 10 * @author Euler 11 * @date 2022 2022/5/23 12 */ 13 public interface AccessToken<T, V, Z> { 14 15 16 /** 17 * 获取授权码 18 * <p> 19 * 用户授权后,开放平台返回的code。一般通过redirect方式返回 20 * 21 * @param params 22 * @return 23 */ 24 V getCode(T params); 25 26 27 /** 28 * 获取访问令牌 29 * <p> 30 * 通过code获取对应的accessToken 31 * 32 * @param codeResponse 授权码响应结果 33 * @return 34 */ 35 Z getAccessToken(V codeResponse); 36 37 38 }

1 package com.rauto.sales.saicorder.outerapp.tmall.accesstoken; 2 3 4 import com.rauto.sales.saicorder.outerapp.AccessToken; 5 import com.rauto.sales.saicorder.outerapp.tmall.req.CodeRequest; 6 import com.rauto.sales.saicorder.outerapp.tmall.resp.AccessTokenResponse; 7 import com.rauto.sales.saicorder.outerapp.tmall.resp.CodeResponse; 8 9 /** 10 * 通过阿里开放平台的接口,获取数据 11 * 12 * @author Euler 13 * @date 2022 2022/5/23 14 */ 15 public class BaseAccessToken implements AccessToken<CodeRequest, CodeResponse, AccessTokenResponse> { 16 /** 17 * 获取授权码 18 * <p> 19 * 用户授权后,开放平台返回的code。一般通过redirect方式返回 20 * 21 * @param params 22 * @return 23 */ 24 @Override 25 public CodeResponse getCode(CodeRequest params) { 26 return null; 27 } 28 29 /** 30 * 获取访问令牌 31 * <p> 32 * 通过code获取对应的accessToken 33 * 34 * @param codeResponse 授权码响应结果 35 * @return 36 */ 37 @Override 38 public AccessTokenResponse getAccessToken(CodeResponse codeResponse) { 39 return null; 40 } 41 }

1 package com.rauto.sales.saicorder.outerapp.tmall.accesstoken; 2 3 4 import com.rauto.sales.saicorder.outerapp.AccessToken; 5 import com.rauto.sales.saicorder.outerapp.tmall.req.CodeRequest; 6 import com.rauto.sales.saicorder.outerapp.tmall.resp.AccessTokenResponse; 7 import com.rauto.sales.saicorder.outerapp.tmall.resp.CodeResponse; 8 9 /** 10 * 基于缓存的accessToken的实现 11 * <p> 12 * 如果多个系统共同使用一个accessToken,可以将其通过redis共享。 13 * 默认实现是基于进程内的内存共享 14 * 15 * @author Euler 16 * @date 2022 2022/5/23 17 */ 18 public class CacheWrapperAccessToken implements AccessToken<CodeRequest, CodeResponse, AccessTokenResponse> { 19 20 /** 21 * 基于请求天猫接口的实现 22 */ 23 private AccessToken accessToken; 24 25 public CacheWrapperAccessToken(AccessToken accessToken) { 26 this.accessToken = accessToken; 27 } 28 29 /** 30 * 获取授权码 31 * <p> 32 * 用户授权后,开放平台返回的code。一般通过redirect方式返回 33 * 34 * @param params 35 * @return 36 */ 37 @Override 38 public CodeResponse getCode(CodeRequest params) { 39 return null; 40 } 41 42 /** 43 * 获取访问令牌 44 * <p> 45 * 通过code获取对应的accessToken 46 * 47 * @param codeResponse 授权码响应结果 48 * @return 49 */ 50 @Override 51 public AccessTokenResponse getAccessToken(CodeResponse codeResponse) { 52 return null; 53 } 54 }
有了accesstoken你就可以调用API,API接口设计要具备单一职责、开闭原则,至于实现代码怎么写,只要没有bug都没问题:



vvv
然后就是有的系统是通过MQ接入进来的:

1 package com.rauto.sales.saicorder.outerapp; 2 3 import com.rauto.sales.saicorder.outerapp.tmall.dto.MessageDTO; 4 import com.rauto.sales.saicorder.outerapp.tmall.dto.MessageDealResultDTO; 5 import com.taobao.api.ApiException; 6 7 /** 8 * 消息处理方案 9 */ 10 abstract public class MessageObserver { 11 12 /** 13 * 后面紧接着要处理的业务 14 * 这里通常是将一个业务线上的很长的执行步骤,按照某一个维度进行垂直分割以后,形成的一个chain. 15 * 执行的时候是按照业务线依次执行的。如果可以一步完成,就没有next processor 16 */ 17 private MessageObserver nextProcessor; 18 19 public MessageObserver(MessageObserver nextProcessor) { 20 this.nextProcessor = nextProcessor; 21 } 22 23 /** 24 * 没有next processor的可以直接无参构造 25 */ 26 public MessageObserver() { 27 } 28 29 /** 30 * 处理逻辑 31 * 32 * @param messageData 33 * @param contextObject 34 * @return 35 */ 36 public abstract MessageDealResultDTO dealWithMessage(MessageDTO messageData, MessageDealResultDTO contextObject) throws ApiException; 37 38 39 public MessageDealResultDTO doProcessor(MessageDTO messageData, MessageDealResultDTO contextObject) throws ApiException { 40 dealWithMessage(messageData, contextObject); 41 if (nextProcessor != null) { 42 nextProcessor.doProcessor(messageData, contextObject); 43 } 44 return contextObject; 45 } 46 47 48 }
有消息进来,就少不消息过滤:

1 package com.rauto.sales.saicorder.outerapp.tmall.message; 2 3 import com.rauto.sales.saicorder.mybatis.entity.TbSyncThirdOrder; 4 import com.rauto.sales.saicorder.outerapp.tmall.enums.FilterGroupEnum; 5 import com.taobao.api.ApiException; 6 7 /** 8 * 消息过滤 9 * 10 * @author Euler 11 * @date 2022 2022/6/7 12 */ 13 public interface MessageFilter<T> { 14 15 /** 16 * 过滤执行 17 * 18 * @param tbSyncThirdOrder 入参 19 * @return 如果符合条件,就返回true 20 */ 21 boolean doFilter(T tbSyncThirdOrder) throws ApiException; 22 23 /** 24 * 如果有多个过滤条件,可以指定优先级 25 * 26 * @return 27 */ 28 int getSort(); 29 30 /** 31 * 获取当前的filter是属于哪个分组的 32 * 33 * @return 34 */ 35 FilterGroupEnum getGroup(); 36 37 }
有消息就会有消息状态流转,这里使用访问者模式,配合双分派,完美解决:

1 package com.rauto.sales.saicorder.outerapp.tmall.message.visitor; 2 3 import com.rauto.sales.saicorder.outerapp.tmall.enums.BusinessStatusEnum; 4 import com.rauto.sales.saicorder.outerapp.tmall.enums.MessageDealWithResultEnum; 5 6 /** 7 * @author Euler 8 * @date 2022 2022/11/22 9 */ 10 public abstract class DbStatus { 11 /** 12 * 处理结果 13 */ 14 private Integer businessStatus; 15 16 public DbStatus(Integer businessStatus) { 17 this.businessStatus = businessStatus; 18 } 19 20 private DbStatus() { 21 } 22 23 /** 24 * 接收天猫状态的visitor 25 * @param tmallVisitorStatus 26 * @return 27 */ 28 public abstract MessageDealWithResultEnum acceptTmallVisitor(TmallVisitorStatus tmallVisitorStatus); 29 30 31 /** 32 * 将DB记录的处理结果转成BusinessStatusEnum 33 * @return 34 */ 35 protected BusinessStatusEnum getBusinessStatus(){ 36 return BusinessStatusEnum.valueOfCode(businessStatus); 37 } 38 }

1 package com.rauto.sales.saicorder.outerapp.tmall.message.visitor; 2 3 import com.rauto.sales.saicorder.outerapp.tmall.enums.BusinessStatusEnum; 4 import com.rauto.sales.saicorder.outerapp.tmall.enums.MessageDealWithResultEnum; 5 6 /** 7 * 天猫visitor 8 * 9 * @author Euler 10 * @date 2022 2022/11/22 11 */ 12 public interface TmallVisitorStatus { 13 14 /** 15 * 判断数据库记录是否处理成功 16 * 17 * @param statusEnum 18 * 19 * @return 20 */ 21 default boolean dbRecordIsFail(BusinessStatusEnum statusEnum) { 22 return BusinessStatusEnum.ABNORMAL.equals(statusEnum) || BusinessStatusEnum.CREATE_ORDER_ABNORMAL.equals(statusEnum); 23 } 24 25 26 /** 27 * 访问Db 已支付状态 28 * 29 * @param dbPayedStatus 30 * 31 * @return 32 */ 33 MessageDealWithResultEnum visitDbStatus(DbPayedStatus dbPayedStatus); 34 35 36 /** 37 * 访问Db已核销状态 38 * 39 * @param dbChargeOffStatus 40 * 41 * @return 42 */ 43 MessageDealWithResultEnum visitDbStatus(DbChargeOffStatus dbChargeOffStatus); 44 45 46 /** 47 * 访问Db 已退款状态 48 * 49 * @param dbCloseStatus 50 * 51 * @return 52 */ 53 MessageDealWithResultEnum visitDbStatus(DbCloseStatus dbCloseStatus); 54 55 56 }
有了以上的抽象层设计,就可以随意扩展了。同时还要做的一个是限流、日志记录、数据的最终一致性少不了重试。方便排查,方便甩锅。
本文来自博客园,作者:Eular,转载请注明原文链接:https://www.cnblogs.com/euler-blog/p/18598072
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤