- 项目一:jo鸡尾酒
- 短信服务模块
- 多租户模块:
- 支付服务模块
- 1.为什么要设计支付服务?
- 2.支付设计难点以及使用什么技术来解决?
- 3.说出交易业务调用时序流程?
- 4.交易统一接口入参和出参分别是什么? 接口的作用是什么?
- 5.校验交易单完整性有哪些?
- 6.在用户支付之前为什么需要加分布式锁?
- 7.什么是交易适配路由? 有什么作用? 怎么实现的?
- 8.什么是交易处理接口? 作用是什么? 怎么实现的?
- 9.为什么还要有交易单?
- 10.支付查询结果如何保证一定能获取到?
- 11.实现支付配置功能如何设计?
- 12.商家入驻到平台之后需要做什么事情?
- 13.支付宝退款状态查询时间是怎么建议的?
- 14.支付的两个问题:重复支付和重新支付怎么解决?
- 15.交易单号是什么时候生成的?
- 16.你们是如何解决支付隔离的问题的?
- 17.你们项目中xxl-job是如何使用的?
- 18.支付功能怎么测试?
- 19.微信支付的二次封装是怎么封装的?
- 20.支付宝支付和微信支付的版本分别选择的是哪些?
- 商家的品牌管理模块:
- 非技术问题:
项目一:jo鸡尾酒
短信服务模块
1.为什么要设计SMS短信服务?
a.在大多数项目中都存在对接短信的需求且短信的执行流程都是一样的;
b.新设计的通用短信平台具有高可用、高可扩、集群容错、流量削峰、负载均衡、渠道路由等功能。
2.设计SMS短信服务的的困难点以及如何解决的呢?
困难点 | 解决方案 | |
---|---|---|
短信模板和签名如何申请? | 需要事先在三方短信平台(如:阿里云、腾讯云、百度云)开通短信账号,在平台中申请签名和模板,通过审核后才可使用 | |
在高并发情况下如何保证系统的稳定性? | 使用spring-cloud-stream-rabbitMQ来实现异步发送【流量削峰】来保证其发送稳定性 | |
如何提高后续对接新渠道的扩展性? | 需要使用工厂+代理模式构建易于扩展的平台 | |
如何做到渠道自动路由,路由中负载均衡如何实现? | 工厂+代理实现,轮询、随机、加权轮询、加权随机、一致hash |
4.商户在发送短信时,如何获取当前的短信配置?商户在发送短信时,如何获取当前的短信配置?
每个平台的短信配置都不一样,需要从各个平台的SDK文档查看。
a.**model-basic-producer**
模块在项目启动时会从mysql中加载所有短信配置到redis中:
1. **根据阿里云的短信标识,在数据库中查询到阿里云SMS配置信息;**
1. **根据业务前缀创建桶对象,把数据库中的aliyun短信配置存到桶对象中。**
b.用户发送短信请求,携带短信标识从redis中获得对应的短信配置,使用应用私钥对明文字符串进行加密;
代码实现(以阿里云为例):
/**
* @ClassName AlipayConfig.java
* @Description 支付宝配置类
*/
@Slf4j
@Configuration
public class AliyunSmsConfig {
@Autowired
ISmsChannelService smsChannelService;
@Autowired
RedissonClient redissonClient;
/***
* @description 初始化短信配置
*/
@PostConstruct
public void initSmsConfig() {
//查询阿里云SMS的配置信息
SmsChannel smsChannel = smsChannelService.findChannelByChannelLabel(SuperConstant.ALIYUN_SMS);
if (EmptyUtil.isNullOrEmpty(smsChannel)){
log.warn("阿里云SMS的未配置");
return;
}
RBucket<SmsChannel> aliyunSmsClient = redissonClient.getBucket("sms:aliyunSmsChannel");
aliyunSmsClient.set(smsChannel);
}
public Client createOrUpdateClient(SmsChannel smsChannel){
RBucket<SmsChannel> aliyunSmsClient = redissonClient.getBucket("sms:aliyunSmsChannel");
aliyunSmsClient.set(smsChannel);
Config config = new Config()
// 阿里云AccessKey ID
.setAccessKeyId(smsChannel.getAccessKeyId())
// 阿里云AccessKey Secret
.setAccessKeySecret(smsChannel.getAccessKeySecret());
// 访问的域名
config.endpoint = smsChannel.getDomain();
try {
return new Client(config);
} catch (Exception e) {
log.error("阿里云SMS的配置信息出错:{}", ExceptionsUtil.getStackTraceAsString(e));
return null;
}
}
/***
* @description 移除配置
* @return
*/
public void removeClient(){
RBucket<SmsChannel> aliyunSmsClient = redissonClient.getBucket("sms:aliyunSmsChannel");
aliyunSmsClient.delete();
}
/***
* @description 获得配置
* @return
*/
public Client queryClient(){
RBucket<SmsChannel> aliyunSmsClient = redissonClient.getBucket("sms:aliyunSmsChannel");
return this.createOrUpdateClient(aliyunSmsClient.get());
}
}
5.短信通道的功能需求及表结构设计?
表结构设计:
channel_name:通道名称
channel_label:通道唯一标记
channel_type:通道类型,1代表文字,2代表语音,3代表推送
domain:域名,各个三方短信平台的域名
access_key_id:秘钥id
access_key_secret:秘钥值
other_config:其他配置,这里采用了表中表的设计,就是把各个三方平台不一样的短信配置,都放到这个字段中,使用JSON格式存储起来。
level:优先级,数字越大代表优先级越高
remark:说明
代码实现简单的单表CRUD实现,需要注意的是,增删改操作一定要添加事务。
6.短信签名的功能需求及表结构设计?
表结构设计:
sign_name:签名名称
sign_code:三方签名码
sign_type:签名类型
document_type:证明类型
international:是否是港澳台/国际短信
sign_purpose:签名用途,0表示自用,1表示他用
remark:短信签名申请说明
accept_status:是否受理成功
accept_msg:受理返回的信息
audit_status:审核状态
audit_msg:审核信息
sign_no:应用签名编号:(多签名编号相同则认为是一个签名多个通道公用)
困难点:
客户端发送短信签名申请后在后端要同时发送给对应的三方短信平台,但是三方的短信平台接口是相互不兼容的,需要考虑的是如何把这些相互不兼容的接口给兼容。实现方案是采用适配器模式。
代码实现:
1. **短信签名dubbo接口实现类依赖短信签名适配器接口定义;**
1. **短信签名适配器接口实现类实现了短信签名适配器接口,在该实现类做了以下几件事:**
1. **创建一个Map集合的成员变量;**
1. **在一个静态内部类里,map集合调用put方法,key是短信渠道,value是三方短信平台的签名处理器接口的实现类存到Spring IOC容器的Bean的名称。**
1. **在申请签名/删除签名/修改签名/查询签名方法里直接使Map集合的对象调用get( )方法传入客户端给的短信渠道参数,得到对应的三方短信平台的签名处理器接口的实现类存到Spring IOC容器的Bean的名称;**
1. **根据这个名称在Spring IOC容器找到对应的三方短信平台签名处理器接口的实现类对象**
3. **三方短信平台的签名处理器接口的实现类依赖了三方短信平台的短信配置类,在该实现类有以下几个方法:(代码参考三方短信平台提供的SDK文档)**
1. **申请签名:**
1. **删除签名;**
1. **修改签名;**
1. **查询签名;**
7.短信模块的功能开发以及表结构设计
表结构设计:
channel_label :短信通道唯一标识
template_name:模板名称
sms_type:短信类型
template_no:应用模板编号:(多个模板编号相同则认为是一个签名多个通道公用)
template_code:三方应用模板code
content:模板内容
other_config:其他配置,这里采用了表中表的设计,就是把各个三方平台不一样的模板变量配置,都放到这个字段中,使用JSON格式存储起来。
international:是否国际/港澳台短信
remark:模板说明
accept_status:是否受理成功
accept_msg:受理返回信息
audit_status:审核状态
audit_msg:审核信息
实现细节参考短信签名**
8.短信发送流程是怎样的?怎么实现的?有什么困难点?
困难点:
a.流量削峰怎么设计?通过spring-cloud-stream rabbitmq组件来进行处理,最初发送的短信不是直接调用SmsSendAdapter进行处理,而是通过SmsSource接口推送到SmsSink中,通过监听SmsListen进行发送
b.负载均衡怎么设计?在SmsSendAdapter中进行短信发送时,优先通过registerBeanHandler到SendLoadBalancer的负载均衡实现,从而找到合适的渠道进行发送。
执行流程:
1.用户业务系统调用短信业务系统中;
具体实现:
a.用户服务在**sendLoginCode(String mobile)**
方法中,构建了SendMessageVo类的对象(包含了模板编号、签名编号、均衡算法、手机号、手机号码组),将此对象作为参数,调用短信服务接口的**sendSms(SendMessageVo sendMessageVo)**
方法。
2.短信业务系统把短信信息发送到rabbitmq(实现异步解耦和流量削峰功能);
具体实现:
在**SmsSendFaceImple**
发送短信实现类的**sendSms()**
方法中将用户服务发送来的**sendMessageVo**
对象变成一个字符串**sendMessageVoString**
,构建一个**MqMessage**
的对象并存储**sendMessageVoString**
,构建**Message**
的对象发送**MqMessage**
,调用**smsOutPut()**
方法,通过返回值确认消息是否发送成功(RabbitMQ的生产者确认机制)。
3.短信监听系统从rabbitmq拉取短信信息;
具体实现:
在**SmsListen**
短信监听类中,接收到消息后将消息由字符串解析成**SendMessageVo**
类的java对象,通过注入的**SmsSendAdapter**
发送短信适配器接口对象,继续调用**sendSms()**
方法并且把SendMessageVo的对象作为参数继续传递,最后调用**basicAck(deliveryTag,false)**
手动确认消息是否返回(消费者消息确认机制)。
4.过滤黑名单即判断前端发短信的号码是否在黑名单中;
具体实现:
在数据库查询出黑名单集合,smsBlacklistService.list()
,使用Stream流进行筛选,将在黑名单的手机号使用Set集合接收,调用removeAll()
方法移除。
5.通过负载均衡选择对应的通道;
a.负载均衡算法的具体实现:
创建**SendLoadBalancer**
短信发送负载均衡算法的接口且实现的对象被Spring的IOC容器管理;
1)**HashSend**
实现类(采用哈希算法);
a.通过短信模板查询出对应的渠道的Map集合(key是channelLabel【通道唯一标识】,value是level【优先级】);
通过stream流过滤出模板短信包通道唯一标识的集合
Set<String> channelLabelList = SmsTemplates.stream()
.map(SmsTemplate::getChannelLabel).collect(Collectors.toSet());
//通过集合查询模板对应的渠道集合
List<SmsChannel> smsChannels=smsChannelService.findChannelInChannelLabel(channelLabelList);
if (!EmptyUtil.isNullOrEmpty(smsChannels)){
return smsChannels.stream()
.collect(Collectors.toMap(SmsChannel::getChannelLabel, SmsChannel::getLevel));
}
b.取出Map集合的key,存入List集合;
c.计算mobile【手机号】的哈希值,使用这个哈希值对List集合的长度取余。
2)**RandomSend**
实现类(采用随机算法);
a.通过短信模板查询出对应的渠道的Map集合(key是channelLabel【通道唯一标识】,value是level【优先级】);
b.取出Map集合的key,存入List集合;
c.根据List的长度随机,**random.nextInt(keyList.size)**
。
3)**RoundRobbinSend**
实现类(轮询算法);(推荐)
a.通过短信模板查询出对应的渠道的Map集合(key是channelLabel【通道唯一标识】,value是level【优先级】);
b.取出Map集合的key,存入List集合;
c.定义一个静态变量Integer pos 初始值等于0,对这个静态添加synchronize(同步锁),如果pos大于等于List集合的长度重置为0,根据pos得到channelName
String channelName = null;
synchronized (pos) {
if (pos >= keySet.size())
pos = 0;
channelName = keyList.get(pos);
pos ++;
}
4)**WeightRandomSend**
实现类(加权随机算法);
a.通过短信模板查询出对应的渠道的Map集合(key是channelLabel【通道唯一标识】,value是level【优先级】);
b.Map集合调用keySet方法得到短信通道的Set集合,采用迭代器对Set集合进行遍历得到channel,再通过**Integer.valueOf(channelMap.get(channel))**
得到这个channel在Map集合出现的次数weight,对weight进行for循环将channel添加到List集合;
c.创建Random随机对象,随机数为List集合的长度。
5)**WeightRoundRobbinSend**
实现类(加权轮询算法);
a.通过短信模板查询出对应的渠道的Map集合(key是channelLabel【通道唯一标识】,value是level【优先级】);
b.Map集合调用keySet方法得到短信通道的Set集合,采用迭代器对Set集合进行遍历得到channel,再通过**Integer.valueOf(channelMap.get(channel))**
得到这个channel在Map集合出现的次数weight,对weight进行for循环将channel添加到List集合;
c.定义一个静态变量Integer pos 初始值等于0,对这个静态添加synchronize(同步锁),如果pos大于等于List集合的长度重置为0,根据pos得到channelName
b.通道选择:根据前端传入的负载均衡策略**loadBalancerType**
得到value(字符串),这个字符串可以通过Spring上下文对象getBean就可以获取到负载算法对应的实现类,通过实现类的**chooseChannel()**
方法获得通道的唯一标识**channelLabel**
;
6.查询短信渠道;
具体实现:
根据channnelLabel(),可以在数据库查询到短信渠道;
7.查询签名;
具体实现:
根据前端传入的signNo(签名编号)和channelLabel(通道唯一标识),在数据库查找对应的短信签名。
8.模板参数兑换;
具体实现:
使用**JSONArray.parseArray()**
将短信模板中变量配置的json格式转为OtherConfigVo类型的List集合,创建一个LinkedHashMap集合**otherConfigVoMap**
key是配置建,value是配置值。继续创建一个LinkedHashMap集合**templateParamHandler**
key是**otherConfigVoMap.get(entry.getkey)参数key是前端传递的templateParamMap集合的key**
value是templateParam集合的值。
9.把短信信息发送到三方短信平台;
具体实现:
通过通道唯一标识channelLabel得到**sendHandlers**
Map集合的value(字符串),这个字符串可以通过Spring上下文对象getBean方法就可以获取到短信发送的处理器具体的实现类对象,根据对象调用sendSms方法将短信发送到第三方平台。
10.轮询查询发送短信发送的结果;
具体实现:
通过通道唯一标识channelLabel得到**sendHandlers**
Map集合的value(字符串),这个字符串可以通过Spring上下文对象getBean方法就可以获取到短信发送的处理器接口具体的实现类对象,根据对象调用querySendSms方法主动在三方短信平台查询发送短信的结果。
11.将结果同步到短信业务系统;
具体实现:
在处理器接口的实现类的querySendSms()方法中,把发送短信的查询结果记录到SmsSendRecord类的对象中,然后调用smsSendRecordService服务将结果保存到数据库。
9.短信发送负载均衡策略的轮询算法优化?
优化一:
问题:目前在轮询算法中使用synchronize(同步锁)来解决在多线程环境下,数据不安全的问题,这带来一个问题,在高并发环境中synchronize的性能极差,所以需要进行优化。
解决方案:使用**java.util.concurrent.atomic**
包下的AtomicInteger来解决线程安全问题。**private static AtomicInterger pos = 0;**
优化二:
问题:虽然AtomicLong解决了线程安全问题,而且效率提升了许多,但是AtomicInteger是基于CAS实现的大量占用CPU。
解决方案:借鉴Spring Cloud Ribbon中的负载均衡轮询算法来进行进一步的优化。
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
* @param modulo 集合大小
* @return 获取集合中下标
*/
private int incrementAndGetModulo(int modulo) {
for (;;) { //自旋锁
int current = nextChannelCyclicCounter.get();
int next = (current + 1) % modulo; // 避免数组越界
if (nextChannelCyclicCounter.compareAndSet(current, next)) // CAS 思想
return next;
}
}
优化三:
问题:在负载均衡算法的实现类中都有**getChannelList()**
方法且内容重复,即出现大量重复代码。
解决方案:创建**BaseSendLoadBalancer**
基础实现类,在这个类中写**getChannelList()**
方法,其他五种算法实现类继承这个基础实现类。
多租户模块:
1.为什么要设计多租户模块?
**本项目是一个SAAS平台,各个商家共用运营商部署的服务器,来减少运维成本。如果不设计多租户,那么各个商家的经营数据在这个平台是共享的。这显然是错误的,虽然每个商家共用运营商部署的服务器,但是从商家的角度来说我的经营数据是不共享的。**
2.项目中是怎么实现多租户的?
多租户是一种软件架构技术,多出现在saas平台的系统中,主要的场景是在多用户的环境下,共用一套系统并且要注意数据之间的隔离性,其本质就是服务共享和数据隔离。
实现方案:项目中实现多租户采用的是应用层数据库层共享数据隔离,数据是逻辑隔离的:
应用程序和数据库只部署一套,所有租户共享即共同使用一个数据库和应用程序,而租户数据则使用表字段进行数据隔离。如,在表中增加TenantID多租户的数据字段,这是共享程度最高且隔离级别最低的模式。简单讲,每插入一条数据时都需要有一个租户的标识。这样才能在同一张表中区分出不同客户的数据,这也是项目中设计的【enterprise_id和store_id】。
3.有哪些难点?用到了哪些技术实现?
难点 | 技术点 | |
---|---|---|
隔离字段如何选择? | 【enterprise_id和store_id】商家id和门店id,商家之间的数据应该隔离各个门店之间的数据也是应该隔离的 | |
哪些表需要做隔离,判断的依据什么? | 1.先看数据库(每个库每张表进行查看);2.找到哪个服务使用这个库;3.当前执行的业务功能是什么;4.当前的表需不需要做数据隔离 | |
在项目中实现多租户的前提是什么? | 给每个商家设置一个域名,校验这个商家域名的合法性 | |
对于需要做数据隔离的数据库和表,隔离字段如何做SQL拼接? | 采用Mybatis-plus的 拦截器插件【TenantLineInterceptor】来对隔离字段进行动态SQL拼接,先对enterprise_id做拦截,再对store_id做拦截。Aop动态代理和Spring GateWay网关的过滤器 |
4.多租户方案Mybatis拦截器是怎么集成到项目中的?是怎么使用这个插件的?
1)插件**restkeeper-framework**
模块,这是核心组件模块,它是对框架的集成:Myabtis-plus、seata、jwt、redis等;
2)在上面的模块下创建子模块**framework-mybatis-plus**
,这是对MyBatis-plus组件的集成
a.创建**MyBatisPlusConfig**
配置类;
b.在配置类里创建自定义拦截器插件**MybatisPlusInterceptor**
对象并交由Spring的IOC容器管理,对象添加基于企业ID字段拦截器插件、基于门店ID字段拦截器插件和分页插件后返回给spring容器。
c.基于企业号ID字段拦截器的实现,实现**new TenantLineHandler() **
拦截处理器接口:
1)在重写的**getTenantId()**
的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的企业号信息,把企业号信息返回给企业号ID字段拦截器的对象。
2)在重写的**getTenantIdColumn()**
方法中,返回企业ID;
3)在重写的**ignoreTable()**
的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略),不在返回false(表示不忽略);
d.基于门店D字段拦截器的实现,实现**new TenantLineHandler() **
拦截处理器接口:
1)在重写的**getTenantId()**
的方法中,使用dubbo的RPC上下文对象,获取当前用户信息,从用户信息获取当前用户的门店ID,把门店ID返回给拦截器的对象。
2)在重写的**getTenantIdColumn()**
方法中,返回门店ID;
3)在重写的**ignoreTable()**
的方法中,获取需要忽略表名的集合,判断客户端的传递的表名是否在该集合中,在返回ture(表示忽略getTenantIdColumn),不在(表示不忽略getTenantIdColumn)且要企业ID字段不能为空后返回false;
e.创建对应的忽略表属性配置类**TenantProperties**
:
在这个配置类中,得到忽略企业ID和门店ID的集合。
f.在需要使用**framework-mybatis-plus**
时,只需要在**model-xxx-producer**
依赖**framework-mybtis-plus**
模块即可。
g.在**model-xxx-producer**
的applicaton.yml添加**mybatis-plus**
配置,主要有忽略商户号表和忽略门店号表【如果有使用nacos配置中心,则需要在配置中心中添加这个配置】。
5.运营平台、商家平台和用户App平台忽略表是怎么配置的?
a.运营平台调用了model-basic-producer
和model-security-producer
两个模块,在这两个模块下的application.yml配置文件中配置忽略商户号表和忽略门店号表。
b.商家平台调用了model-shop-producer
和model-shop-use
两个模块,在这两个模块下的application.yml配置文件中配置忽略商户号表和忽略门店号表。
b.用户App平台调用了model-shop-applet
模块,在该模块下的application.yml配置文件中配置忽略商户号表和忽略门店号表。
6.企业ID标识的是怎么传递的?
1)在客户端用户启用了认证服务后,在**JsonServerAuthenticationSuccessHandler**
认证服务转换器中,在构建用户信息时写入用户的企业ID和门店ID;
2)在用户认证成功后,把JwtToken封装到用户信息UserVo中(包含了用户的企业ID和门店ID),将UserVo返回给前端;
3)UserVo会传递给客户端(这一步代码由前端书写)后,以map的格式存储在客户端的localStorage中;
4)用户每次发起request请求时,通过拦截每次请求携带的JwtToken,认证该用户是否合法;
5)用户认证通过进入spring的gateway网关,在网关中使用【全局过滤器filter】拦截请求,解析请求头中的JwtToken中的user信息,解析完毕后使用http协议转发到web层,此时用户信息时存储在请求头中的;
具体实现:
a.在**AnalysisJwtTokenFilter**
类(这个类实现了**GlobalFilter**
全局过滤器接口)中解析JwtToken,将解析的结果转发至**framework-web**
模块下的TenantInterceptor类;
6)在web层请求被springMVC拦截器拦截,获取到请求头中的【用户信息】;
7)在**TenantIntercept**
类中从请求拿到用户信息,并放入dubbo的上下文对象即**RpcContext**
中;
Dubbo的隐式传参是如何实现的?
RpcContext是一个ThreadLocal的临时状态记录器,当接收到RPC请求或发起RPC请求时,RpcContext的状态都会变化。比如A服务调用B服务,B服务再调用C服务,则B服务机器上在B服务调用C服务之前,RpcContext记录的是A服务调用B服务的信息,在B服务调用C服务之后,RpcContext记录的是B服务调用C服务的信息。其原理:RpcContext内部有一个ThreadLocal变量,它是作为ThreaLocalMap的key,表明每个线程有一个RpcContext。
8)使用mybatis-plus多租户插件拦截,获取到用户的企业id后拼装到对应执行的SQL条件。
7.门店ID标识是如何传递的?
1.在客户端用户启用了认证服务后,在**JsonServerAuthenticationSuccessHandler**
认证服务转换器中,在构建用户信息时写入用户的门店ID;
2.在用户认证成功后,把企业ID初始化到JwtToken(包含了用户信息,用户的门店ID);
3.JwtToken会传递给客户端(这一步代码由前端书写),以map的格式存储在客户端的localStorage中,并且会单独创建一个key:store_id来存储门店id(值是前端从JwtToken中提取出来的);注意点:当用户点击切换门店后,在JwtToken中门店ID就会发生改变。
4.在用户发起request请求时,通过拦截每次请求携带的JwtToken,认证该用户是否合法;
5.用户认证通过进入spring的gateway网关,在网关中使用【全局过滤器filter】拦截请求,解析请求头中的JwtToken中的user信息,解析完毕后使用http协议转发到web层,此时用户信息时存储在请求头中的;
6.在web层请求被springMVC拦截器拦截,获取到请求头中的【用户信息】;
7.在**TenantIntercept**
类中从请求拿到用户信息,并放入dubbo的上下文对象即**RpcContext**
中;
8.使用mybatis-plus多租户插件拦截,获取到用户的门店id后拼装到对应执行的SQL条件。
8.场景题:
1、你们现在SAAS权限控制是怎么设计
2、你们的权限控制的时候区分商家吗?当前每个商家权限是不是一样的
项目只控制到 商家级别
3、如果我现在让你去设计一个不同商家哟不同的权限控制怎么设计?
在存储角色也区分商户号,结合权限控制,把角色管理的这个权限下放到商家系统
具体实现:在角色表添加商户id
切换门店怎么解决?
在客户端的local storage单独把门店id储存。
技术:网关全局过滤器-springmvc的拦截器+dubbo的上下文对象-mybatisplus的拦截器
结论:redis 定期+惰性删除 配合使用解决内存过期key问题
9.如果不使用MyBatis-Plus插件,如何来实现多租户的数据隔离?
可以自己编写一个拦截器,只要在SQL执行之前,将需要企业ID和门店ID拼接到SQL语句上就行了,通过模拟MyBatisPlus的拦截器就可以了。使用AOP的后置通知来做拦截,如何涉及到门店层面则是商户号+门店号共同来完成隔离。
如何对接多租户?a.数据库表结构加上多租户字段(首先考虑这张表是否要隔离,要就拼接SQL,不要就不拼接SQL);不使用Dubbo,可以使用Feign的隐式传参,Feign添加一个拦截器,重写这个请求
这张表要不要拼,拼的字段是什么?字段值是什么?不使用MP插件可以实现吗?不使用dubbo来传参,还有其他方式来传参吗?
SAAS数据使用的是逻辑隔离。
多租户的实现方案有哪些?应用程序和数据都隔离;应用程序共享数据库隔离;应用程序数据库都是共享的。前两种是物理隔离,后一种是逻辑隔离。
支付服务模块
1.为什么要设计支付服务?
我进了一个小公司,开发人员不多(5-10),技术比较陈旧,发现每一个项目很多同事,把上个项目的支付代码直接拷贝过来,直接沿用原来的代码来完成支付相关工作,好处是代码不需要写,不需要思考,直接使用;缺点:只适用于同类型的项目,局限性很大;代码维护成本比较大,优化代码改动量比较大;复用性较差。
通过设计一套通用的统一支付的基础服务,公司中将来所有的项目统一走我的基础服务。
2.支付设计难点以及使用什么技术来解决?
支付的交易单如何校验? | 在用户选择支付方式之后,正式支付之前对交易单的常规性和安全性进行校验 | |
---|---|---|
支付的安全性应该如何保证? | 根据订单编号在数据库查原始订单,原始订单和前台传过来的订单进行对比,如果数据不一样,说明数据被人已篡改,抛异常返回原因;前台对订单进行加密传输 | |
因为三方支付平台相互间不兼容的,如何整合到一起? | 通过设计模式中的适配器模式 | |
微信支付的SDK没有封装,又不能去拿三方写的SDK怎么办 | 参考支付宝的对支付的封装(得到市场认可了的),来对微信支付进行二次封装。 |
3.说出交易业务调用时序流程?
1)客户端收银员发起收银请求;
具体实现:
请求方式是Post,传入的参数是OrderVo(封装的是订单参数);
2)在订单服务中根据订单生成交易单信息;
具体实现:
a.有一个**tradingConvertor()**
方法,这个方法主要解决的是把订单转换成交易单,主要是根据订单的交易类型(交易类型【付款、退款、免单、挂账】)来进行转换的。
b.如果订单的交易类型是GZ在**CreditTradingStrategy**
策略中,先将数据库中的订单的支付渠道、支付类型、订单状态、收银id、收银人名称进行更改,然后采用构造者模式创建交易单后返回;
c.如果订单的交易类型是付款在**FkTradingStrategy**
策略中,将数据库中订单的实付金额、收银id、收银人名称、支付渠道、支付类型、订单状态(如果是现金结算改为YJS,是在线支付改为FKZ)进行修改,然后采用构造者模式创建交易单后返回;
d.如果订单的交易类型是MD在**FreeTradingStrategy**
策略中,将数据库中订单的实付金额、收银id、收银人名称、支付渠道、支付类型、订单状态(改为MD)进行修改,然后采用构造者模式创建交易单后返回;
e.如果订单的交易类型是TK在**TkTradingStrategy**
策略中,将数据库中订单的退款金额、退款行为进行修改,然后采用构造者模式创建交易单后返回;
*这里使用了设计模式中的策略模式,因为订单的交易类型有很多,所以产生很多的if/else判断,通过这个模式,就可以解决这个问题。定义一个`TradingStrategy`交易单抽象策略接口,不同的交易类型实现此接口。b、c、d、e这四种策略的对象都是交给Spring管理的。在`TradingStrategyImpl`类中将四种策略的对象注入,并且创建一个被final修饰的Map集合的成员变量,key是订单的交易类型,value是交易类型对应的策略对象。创建一个被`@PostConstruct`注解修饰的成员方法init,在此方法内调用map.put( ),还创建一个convert方法根据交易类型转换成交易单。**
3)在订单服务发起RPC(远程调用)支付服务的支付请求;
具体实现:
根据客户端传递的交易渠道来判断是远程调用**NativePayFace**
接口在线支付还是现金支付的**createDownLineTrading**
统一收单线下预创建方法。**NativePayFace**
接口,**NativePayFaceImpl**
类实现此接口,在此类注入**NativePayAdapter**
支付适配器接口,支付适配器接口的对象调用**createDownLineTrading()**
方法。
4)在支付服务中给支付中的订单加锁,选择交易适配路由(选择支付宝、微信、京东钱包等第三方支付系统),这里选择了设计模式中的适配器模式:根据交易渠道适配不同的三方支付系统的实现,注意:交易渠道必须由前端选择传递;
具体实现:
a.创建redis的key,key=业务前缀+业务系统的订单号;
b.通过redissonClient.getLock(key)获取锁对象后,给key加锁;
c.根据支付渠道,从IOC容器中找到**NativePayHandler**
接口的具体实现类的对象;
d.找到具体实现类对象之后,调用**AliNativePayHandler**
(支付的具体实现)类**createDownLineTrading()**
方法;
4.通过三方支付平台的回调结果,判断三方支付平台是否受理成功,返回true受理成功修改当前交易单,
e.
在这里因为三方支付平台的接口的相互不兼容,所以采取了设计模式中的适配器模式,使三方支付接口的相互兼容。NativePayAdapter
支付适配器接口,**NativePayAdapterImpl**
类实现了接口,在这个中创建了一个Map集合的成员变量(key是支付渠道,value是不同支付平台在Spring容器中的Bean的名称),且在静态代码块中通过map.put方法把支付渠道和对应的Bean名称加入Map集合中。
5)校验交易单的完整性,交易单应包含如下内容:订单编号、支付金额、交易渠道、商家信息、交易类型:付款/退款(FK/TK)、安全性校验:大额交易/常用支付地变更(IP)/频繁交易/密码变更/支付设备变更/收款方类型…..
具体实现:
在**BeforePayHandler**
接口的**BeforePayHandlerImpl**
实现类里**checkeCreateDownLineTrading()**
方法中,来校验交易单的完整性;
6)保证支付和三方系统的幂等性,支付服务的幂等性校验(如,订单编号orderNo),三方系统幂等性的保证(交易单编号-雪花算法)
具体实现:
在**BeforePayHandler**
接口的**BeforePayHandlerImpl**
实现类里**idempotentCreateDownLineTrading()**
方法中,根据业务系统的订单号在数据库查询该笔交单,判断交易单的状态是否为已结算、免单、付款中、挂账,如果是抛异常告诉客户端该笔订单的订单状态不能交易。
7)生成交易单号
具体实现:
如果不是则根据雪花算法生成新的交易单号,雪花算法是MyBatisPlus提供的一个接口,通过阿Autowired注入**IdentifierGenerator**
这个接口的对象,调用其中的**nextId()**
方法就可以生成唯一的交易单号了。
8)统一下单,通过http请求调用三方系统,注意:自己封装请求报文,必须【加密】处理报文;
具体实现:
通过企业ID,在对应的支付配置类获取商家入驻提交到数据库的支付配置,使用三方支付平台提供的Factory工具类使用支付配置,调用三方支付平台的支付API,传入交易单号和实付金额两个参数;
9)三方系统解密成功后,返回一个二维码链接给支付服务,这个二维码链接是第三方加密后的结果;
具体实现:
a.通过调用支付API,得到响应对象**precreateResponse**
(包含支付的二维码),通过**ResponseChecker.**_**success**_**(precreateResponse)**
得到受理结果,结果是一个布尔值,为true保存并修改当前交易单的参数(其中的交易状态修改为FKZ)并保存到数据库中;
10)支付服务将二维码的链接解密后返回给客户端
具体实现:
将响应的二维码的JSON格式转换为JAVA字符串后封装到Trading中;
11)买家通过扫码付款,将money直接转给第三方
12)通过第三方异步推送支付结果给支付服务;或者在支付服务设置定时任务,主动轮询三方系统,查询支付结果;
13)如果查询的结果是支付成功,修改订单和交易单状态;如果查询的结果是支付失败,状态修改为未支付;
4.交易统一接口入参和出参分别是什么? 接口的作用是什么?
交易入参(PayChannelVo交易渠道):
核心参数:
channel_name(渠道名称) channel_label(通道唯一标记) domian(域名)
app_id(商户id) public_key(公钥) merchant_private_key(商户私钥)
other_config(其他配置,这里是JSON格式) encrypt_key(AES混淆密钥)
交易出参(TradingVo交易单):
核心参数:
productOrderNo(业务系统订单号) tradingOrderNo(交易系统订单号[对于第三方来说: 商户订单])
tradingChannel(支付渠道[支付宝,微信,现金,免单挂账]) tradingType(交易类型[付款,退款,面单,挂账])
tradingState(交易单状态[DFK待付款,FKZ付款中,QXDD取消订单,YJS已结算,GZ挂账])
第三方交易返回的参数:
resultCode(第三方交易返回编码即最终确认交易结果) resultMsg(第三方交易返回提示信息即最终确认交易信息) resultJson(第三方交易返回信息json[分析交易最终信息])
支付服务返回给客户端的信息:
placeOrderCode(统一下单返回编码) placeOrderMsg(统一下单返回信息)
placeOrderJson(统一下单返回信息Json[用于生产二维码、Android、ios唤醒支付等])
5.校验交易单完整性有哪些?
1)常规校验(订单号、企业号、交易金额、支付渠道)
2)常用支付地异常
3)大额金额支付异常
4)支付人和银行保留信息不一致(身份证、手机号等)
6.在用户支付之前为什么需要加分布式锁?
是为了预防支付服务在统一收单交易预创建时出现并发问题,导致同一时间出现多张交易单,单个用户支付多次支付。
使用redis来实现分布式锁的,采用的是Hash结构,key是业务前缀 + 系统的订单号。
调用getFairLock()方法给订单加公平锁。公平锁:保证了当多个线程同时请求加锁,优先分配给先发出请求的线程。
7.什么是交易适配路由? 有什么作用? 怎么实现的?
在线支付方式(比如:支付宝,微信支付,JD钱包等第三方系统)的接口互相间是不兼容的,为了在项目始终使它们相互兼容能一起工作,所以使用了设计模式中的适配器模式。还有让项目的扩展性更高。
实现:
1)定义一个适配器的接口:NativePayAdapter接口;
2)定义NativePayAdapter接口的实现类NativePayAdapterImpl实现具体的接口方法,并且将该实现类的实例交由spring管理;
在适配器实现类有一个核心步骤:把NativePayHandler接口在适配器的实现类里,定义为Map集合的属性,Map的key是在线支付方式的支付渠道(由前端传过来的),value是NativePayHandler实现类在Spring IOC容器中bean的名称,内部类加上static要让支付接口的实现类,随着适配器实现类的加载而加载。通过前端传递的 支付渠道到map里面获取到对应的 value(字符串),然后通过spring 上下文对象 getBean就可以获取到对应实现类。
8.什么是交易处理接口? 作用是什么? 怎么实现的?
项目的交易处理接口是NativePayHandler,是定义在线支付方式的接口(具体的在线支付方式都得实现此接口),在改接口定义的如下的方法:
方法的功能查询支付的二维码、统一下单、交易查询、统一退款、退款查询的功能;
实现:
1)定义一个交易处理的接口:NativePayHandler接口;
2)native支付方式的实现类实现此接口,比如有AliNativePayHandler、WechatNativeHandler等:
在具体的实现类里,有交易单参数校验,对交易的订单进行幂等性处理(判断订单状态是否为已结算免单、付款中、取消订单或者其他状态一律抛异常,如果是首次支付则生成交易单号【使用雪花算法生成】);
9.为什么还要有交易单?
1、交易单是专门用于对接交易支付系统
2、订单和具体业务相关,交易单和业务无关, 解耦
3、交易系统是一个通用支付服务
10.支付查询结果如何保证一定能获取到?
支付结果查询在项目中采用了两种方式来实现:
1)第三方系统异步推送支付结果给餐掌柜的支付服务
*如果出现网络抖动或延迟咋办?这个问题是由第三方来解决的,就我了解到的第三方设计是:如果我们服务收不到回调通知请求,第三方会按照某个频率一直发送给我们的服务,直到我们获取到数据(这里会有一个幂等性操作)。
2)我们服务主动轮询(项目使用)
项目中的支付服务主动定时任务轮询查询订单的支付结果
这两种方式搭配使用的话,可以确保我们的系统一定能获取到用户的支付结果。
11.实现支付配置功能如何设计?
思考:
1、微信支付(appid,mchid、证书)
2、支付宝支付(appid、公钥、私钥、entry_key…)
3、对接jd(a,b,c,d)
问题:
- 数据很重要保存到数据库表中,表结构该怎么设计呢?
- 先去找共用的字段,设置到表结构
- 找不同的字段,不同的部分可以存到一个字段varchar(1000):JSON格式
{“a”,”1”,”b”,”2”} ——- 表中表
12.商家入驻到平台之后需要做什么事情?
首先在微信支付平台和支付宝支付平台注册账号,得到各种支付配置(appId,私钥、秘钥、公钥或证书),然后再商家平台的支付渠道中把这些支付配置传到后端,然后再保存到的数据库的tab_pay_channel表中。
13.支付宝退款状态查询时间是怎么建议的?
退款状态。枚举值:REFUND_SUCCESS 退款处理成功;未返回该字段表示退款请求未收到或者退款失败;
注:如果退款查询发起时间早于退款时间,或者间隔退款发起时间太短,可能出现退款查询时还没处理成功功,后面又处理成功的情况,第三方系统限流的情形,建议商户在退款发起后间隔10秒以上再发起退款查询请求
14.支付的两个问题:重复支付和重新支付怎么解决?
通过幂等性校验可以解决这个问题。在项目是通过判断订单状态是否为已结算免单、付款中、取消订单或者其他状态一律抛异常,如果是首次支付则生成交易单号【使用雪花算法生成】
15.交易单号是什么时候生成的?
是在交易单的幂等性校验之后,如果该笔订单为首次支付,则生成交易单号。
16.你们是如何解决支付隔离的问题的?
有两种实现一是选择商家ID;二是选择门店ID。项目选择的是商家ID,
将配置文件初始化,项目一加载就将商家的支付配置从数据库中缓存到redis中,在redis的结构是String结构,大key是业务前缀 + 商户ID,value是PayChannelVo对象。只要商家每次发起支付请求,在后端都会通过商家ID去redis中查询对应的商家支付配置。
支付渠道配置放在了 各个商家的管理后台,保存在【数据库】中
特征:
1、访问很频繁 — 热点
2、不经常改变 — 更新频率
3、数据很重要,应该持久化到数据库 — 数据重要
17.你们项目中xxl-job是如何使用的?
在支付结果和退款结果查询时有使用到定时任务(xxl-job)
- 在任务调度中心中配置执行器(名称)、配置任务
- 名称:支付状态同步执行器 AppName:model-trading-job-listen
- 名称:基础服务同步执行器 AppName:model-basic-job-listen
- 路由策略是怎么配置,你是怎么思考?
路由策略选择的是第一个,项目中使用定时任务是用来定时从第三方系统查询支付结果或者退款结果
即使执行器采用的集群部署, 我们只需要知道第一台执行器查询的结果即可。
- 阻塞处理策略,怎么思考?
单机串行:调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;在项目中支付本身就被加了分布式锁,即同一时刻只能有一笔订单进入支付状态,只有当该笔订单支付成功后下笔订单进入支付状态,所以使用单机串行也是贴合业务的。
- 定时任务怎么配置
采用每10秒(0/10 **?),主动从第三方系统查询支付结果或退款结果。**
- JobHandler和项目代码@XXljob(“”)
- 失败重试次数=0
不需要重试,10秒后又会再一次执行定时任务,相当于会将所有的未成功的请求再重新执行。
18.支付功能怎么测试?
1、从功能方面考虑:
1)正常完成支付的流程;
2)支付中断后继续支付的流程;
重新支付,沿用交易单重新发起支付。
3)支付中断后结束支付的流程;
直接修改订单的状态为取消订单
4)单订单支付的流程;
5)多订单合并支付的流程;
6)余额不足;
7)未绑定银行卡;8)、密码错误;
将交易单给删除
9)密码错误次数过多;
超时未支付,通过使用延时队列,比如设置15分钟,超过这个时间就把订单删除,
10)找人代付;(未实现)11)、弱网状态下,连续点击支付功能功能,会不会支付多次;12)、有优惠券、折扣、促销价进行结算是否正确;
支付失败之后怎么做
13)不同的支付方式:银行卡网银支付、支付宝支付、微信支付等;14)、支付失败后,再次支付。
使用支付宝和微信的沙箱来测试,测试通过之后修改appID、商户号、两个秘钥、支付宝的公钥和我们的私钥,改成正式的。微信支付和支付宝支付的jar包的版本,
2、从性能方面考虑:多个用户并发支付能否成功;支付的响应时间;
三方回调通知;
主动轮询第三方,项目设置时间为每隔8s
3、从安全性方面考虑使用Fiddler拦截订单信息,并修改订单金额,或者修改订单号,通过订单编号去查,校验订单的幂等性所以即便抓到也没问题(下两个订单A,B,付款时拦截订单B,并把订单B的订单号改为A订单的订单号)无法完成支付,
解决方案一:根据订单编号在数据库查原始订单,原始订单和前台传过来的订单进行对比,如果数据不一样,说明数据被人已篡改,抛异常返回原因;
解决方案二:前台对订单进行加密传输。
4、从用户体验方面考虑是否支持快捷键功能;点击付款按钮,是否有提示;取消付款,是否有提示;UI界面是否整洁;输入框是否对齐,大小是否适中等。5、兼容性BS架构:不同浏览器测试。APP:不同类型,不同分辨率,不同操作系统的手机上测试。
19.微信支付的二次封装是怎么封装的?
a.创建**WechatPayCondig**
配置类,在这个配置类中初始化所有商家的支付配置到redis中,定义在redis移除配置和获得配置的成员方法。
b.创建**Config**
类,这个参考了支付宝的Config,里面封装了微信支付的支付配置相关的内容(AppId、商户号、私钥、商户证书序列号、V3秘钥、微信支付的请求地址)
c.创建**Factory**
工具类,这个类是封装了微信支付多种支付方式的工厂(发起交易预创建、交易查询、退款、退款查询)
d.创建**WechatPayHttpClient**
工具类,主要负责构建与微信支付平台之间的请求,并且此类构建的**CloseableHttpClient**
还提供了在请求前后做了签名及验签等功能;
e.**PreCreate**
类,负责生成微信支付的下单,返回微信支付的支付二维码;
f.**BasicQuery**
类,对微信支付的支付结果查询的封装;
g.**Refund**
类,对微信支付申请退款,退款查询的封装;
20.支付宝支付和微信支付的版本分别选择的是哪些?
支付宝是2.2.0版本
微信支付是V3版本
商家的品牌管理模块:
1、负责这个模块的背景是什么?
2、遇到什么难点?怎么解决的?
不同的服务调用不同的数据源怎么解决事务问题? | 采用seata的分布式事务 | |
---|---|---|
表结构怎么设计? | 设计两张表,一张品牌表和文件表 | |
文件表会 |
非技术问题:
1.项目团队和成本?
a.项目的开发流程?
基于前后端分离的思路,首先先由产品定制产品需求并给开发进行产品宣讲。接着后端和前端同时动工。各自完成自己的开发任务。最后进行对接联调,同时进行提测。当测试通过之后,才进行上线。
b.项目的开发周期是怎样的?
为了缩短项目周期,快速占领市场,我们采用敏捷开发的思想。将整个项目分为三期来开发
第1期实现核心功能:需求与设计2个月,编码3个月,上线测试1个月。(主体功能,用户点餐下单支付)
第2期增强系统功能:需求与设计1个月,编码2个月,上线测试1个月。(比如优惠券、会员、打折)
第3期补充系统功能:需求与设计1个月,编码2个月,上线测试1个月。(修复系统的bug)
c.团队组成
团队人数:18人
产品经理:2人,负责产品推广与产品调研、确定客户需求。
UI: 2人,负责原型图设计
项目经理:1人
系统架构师:1人,主要负责需求调研、概要设计、详细设计。
资深开发工程师(后端):4人,主要负责项目核心功能开发。
程序员(后端):4人,主要负责项目后端代码开发。
前端开发工程师:2人,主要负责项目前端代码开发。
测试工程师:1人,主要负责项目测试。
运维人员:1人,主要负责上线部署。
2.你的项目并发量是多少?
日活量:比如10万用户,1万日活左右。
并发量(TPS)
PV(页面浏览量):用户每打开1个网站页面,记录1个PV。网站角度
UV(网站独立访客):1天内相同访客多次访问网站,只计算为1个独立访客。用户角度
IP(独立IP):拥有特定唯一IP的计算机访问网站的次数。赚钱的点
TPS(每秒事务数):
QPS(每秒查询率):一台服务器每秒能够响应的查询次数
具体计算方法:
1.系统用户数;2.同时在线用户数;3.业务并发用户数【均值】;4.最大并发访问数【峰值】
C=nL/T:C是平均的业务并发数、n是login session =的数量、L是login session的平均时长、T是考察的时间段长度。比如:login session的数量是500,平均时长是300s,考察时长是7200s。平均业务并发用户数就是22.83
可以说不知道,但是可以把这个公式给到面试官。
系统的吞吐量:UV*C
3.项目上线部署和排错相关?
项目已上线,具体的服务器不知道。服务和软件是独立部署在服务器上的,
错误定位,通过阿里的阿尔萨斯的工具来定位的
4.商家入驻平台的流程是什么?
1) 申请链接,填写表单,自主申请(适用于平台很大)
2) 业务员推广,业务员帮助商家填写入驻材料
3) 配置支付秘钥
4) 业务员帮助开通系统的操作权限,分配角色等
5) 配置系统的功能(门店、桌台、区域等管理) — 线下操作移到线上
6) 给出培训的手册