核心业务5:我要充值
1.充值业务流程图
2.充值业务流程逻辑
3.数据库表
4.前端逻辑代码
5.汇付宝代码逻辑
6.尚融宝代码逻辑
7.幂等性判断原理和解决方案
8.代码规范和原理了解
核心业务5:我要充值
1.充值业务流程图
①在用户注册之后
会保存数据到对应数据库(user_info和user_account)
②用户绑定了汇付宝之后(有bind_code)
③用户点击充值页面
④输入充值金额后点击充值跳转到尚融宝中封装数据后返回给浏览器(自动表单提交到汇付宝)
⑤汇付宝显示表单数据用户填写提交
⑥汇付宝
- 更新汇付宝数据库的user_account表的余额
- 同步返回数据给前端页面控制跳转
- 异步返回数据给尚融宝同步user_account数据和trans_flow数据
2.充值业务流程逻辑
①用户填写金额后提交
- 前端提交到尚融宝
- 尚融宝返回表单到浏览器(自动提交给汇付宝)
- 汇付宝回显页面
②用户填写汇付宝回显页面后提交
- 汇付宝更改user_account返回提交成功页给浏览器(同步)
- 汇付宝异步调用尚融宝接口让其动态更新其user_account和trans_flow表
- 尚融宝返回success给汇付宝
3.数据库表
①尚融宝
- user_account
- trans_flow
②尚融宝数据表关联
- user_account和trans_flow都是用user_id来绑定,并且与user_info也用user_id绑定
③汇付宝
- user_account
④汇付宝数据关联
- user_account通过user_code和user_bind的bind_code表绑定
⑤尚融宝和汇付宝互相绑定
- 通过秘钥检验身份(双方互相)
- 两者的所有表通过唯一bind_code 和唯一user_id进行绑定
- 即两表的user_info和user_bind通过user_id和bind_code唯一确定。
- 两表的user_account通过唯一bind_code确定
4.前端逻辑代码
①前端对象
②前端调用接口
③前端代码
srb-site\pages\user\recharge.vue
<template>
<div class="personal-main">
<div class="personal-pay">
<h3><i>充值</i></h3>
<div class="quick-pay-wrap">
<h4>
<span class="quick-tit pay-cur"><em>汇付宝充值</em></span>
</h4>
<form id="form" name="form" method="post" action="">
<div class="quick-main">
<div class="fl quick-info">
<div class="info-1">
<span class="info-tit">充值金额</span>
<span class="info1-input">
<input
type="text"
class="pay-money-txt"
maxlength="10"
v-model="chargeAmt"
/>
<em>元</em>
</span>
</div>
<div class="bank-check" id="bank-check2">
<b class="selected" id="bankProtocol1"></b>
<span class="bank-agree">
我同意并接受
<a href="#" target="_blank">
《尚融宝投资咨询与管理服务电子协议》
</a>
</span>
</div>
<input
type="button"
value="充值"
class="btn-paycz"
@click="commitCharge()"
/>
</div>
<div class="pay-tipcon" style="height: 110px;">
<b>温馨提示:</b><br />
1、为了您的资金安全,您的账户资金由第三方汇付宝进行托管。<br />
2、充值前请注意您的银行卡充值限额,以免造成不便。<br />
3、为了您的资金安全,建议充值前进行实名认证。<br />
4、如果充值遇到任何问题,请联系客服:4006-001-999。
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
chargeAmt: 0,
}
},
methods: {
commitCharge() {
let _this = this
this.$alert(
//标题
'<div style="size: 18px;color: red;">您即将前往汇付宝充值</div>',
'前往汇付宝资金托管平台',
{
//配了此则能够渲染出html页面(不加上面还有div)
dangerouslyUseHTMLString: true,
//按钮
confirmButtonText: '立即前往',
//回调函数
callback: (action) => {
//点击后如果action为'confirm',action也可能为close
if (action === 'confirm') {
//当前场景this和外面this不一样修改this为前面的_this
_this.$axios
.$post(
'/api/core/userAccount/auth/commitCharge/' + _this.chargeAmt
)
.then((response) => {
//直接替换当前浏览器的内容
document.write(response.data.formStr)
})
}
},
}
)
},
},
}
</script>
④前端代码逻辑
- 通过alert弹框向后端发起ajax请求
- 通过document.write将后端返回的字符串渲染为表单(自动提交)
5.汇付宝代码逻辑
①汇付宝的接受参数
- 提交表单参数
- 异步调用尚融宝请求对象参数
②接受表单的提交返回表单回显页
表单提交地址
③点击提交后保存数据,同步返回,异步调用
- 调用的方法
-
账户充值
-
异步调用
-
同步跳转
-
注意异步调用逻辑
进行异步调用,如果异步调用超时或者返回不为success,则进行五次失败重试。
如果返回的是success,那么异步调用结束,
6.尚融宝代码逻辑
①封装对象
- 封装汇付宝异步调用请求的数据
②开发接口
③充值接口代码逻辑
- controller
com.atguigu.srb.core.controller.api.UserAccountController
@Api(tags = "会员账户")
@RestController
@RequestMapping("/api/core/userAccount")
@Slf4j
public class UserAccountController {
@Resource
private UserAccountService userAccountService;
@ApiOperation("充值")
@PostMapping("/auth/commitCharge/{chargeAmt}")
public R commitCharge(
@ApiParam(value = "充值金额", required = true)
@PathVariable BigDecimal chargeAmt, HttpServletRequest request) {
//request获取充值用户id(不能直接传id不安全,必须从token中解析id)
//充值金额
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
//表单提交(组装表单字符串用于远程提交数据到汇付宝)
String formStr = userAccountService.commitCharge(chargeAmt, userId);
return R.ok().data("formStr", formStr);
}
}
- service
package com.atguigu.srb.core.service;
import com.atguigu.srb.core.pojo.entity.UserAccount;
import com.baomidou.mybatisplus.extension.service.IService;
import java.math.BigDecimal;
import java.util.Map;
/**
* <p>
* 用户账户 服务类
* </p>
*
* @author Likejin
* @since 2023-04-09
*/
public interface UserAccountService extends IService<UserAccount> {
String commitCharge(BigDecimal chargeAmt, Long userId);
}
package com.atguigu.srb.core.service.impl;
import com.atguigu.srb.core.enums.TransTypeEnum;
import com.atguigu.srb.core.hfb.FormHelper;
import com.atguigu.srb.core.hfb.HfbConst;
import com.atguigu.srb.core.hfb.RequestHelper;
import com.atguigu.srb.core.mapper.UserAccountMapper;
import com.atguigu.srb.core.mapper.UserInfoMapper;
import com.atguigu.srb.core.pojo.bo.TransFlowBO;
import com.atguigu.srb.core.pojo.entity.UserAccount;
import com.atguigu.srb.core.pojo.entity.UserInfo;
import com.atguigu.srb.core.service.TransFlowService;
import com.atguigu.srb.core.service.UserAccountService;
import com.atguigu.srb.core.util.LendNoUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 用户账户 服务实现类
* </p>
*
* @author Likejin
* @since 2023-04-09
*/
@Service
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private TransFlowService transFlowService;
/**
* @param chargeAmt:
* @param userId:
* @return String
* @author Likejin
* @description 组装表单字符串返回前段自动提交
* @date 2023/4/17 11:13
*/
@Override
public String commitCharge(BigDecimal chargeAmt, Long userId) {
//获取充值人绑定协议号
UserInfo userInfo = userInfoMapper.selectById(userId);
String bindCode = userInfo.getBindCode();
Map<String, Object> paramMap = new HashMap<>();
//唯一商户号
paramMap.put("agentId", HfbConst.AGENT_ID); //商户号
paramMap.put("agentBillNo", LendNoUtils.getChargeNo()); //充值账单号
paramMap.put("bindCode",bindCode);//用户绑定协议号
paramMap.put("chargeAmt",chargeAmt);//充值金额
paramMap.put("feeAmt",new BigDecimal(0));//充值手续费为0
paramMap.put("notifyUrl",HfbConst.RECHARGE_NOTIFY_URL);//异步返回url
paramMap.put("returnUrl",HfbConst.RECHARGE_RETURN_URL);//同步返回url
paramMap.put("timestamp", RequestHelper.getTimestamp());//时间戳
paramMap.put("sign",RequestHelper.getSign(paramMap));//签名
//构件表单(表单提交的地址)
String formStr = FormHelper.buildForm(HfbConst.RECHARGE_URL, paramMap);
return formStr;
}
}
④异步回调接口实现
- controller
com.atguigu.srb.core.controller.api.UserAccountController
@ApiOperation(value = "用户充值异步回调")
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
//把返回的request数据提取数据
Map<String, Object> paramMap = RequestHelper.switchMap(request.getParameterMap());
log.info("用户充值异步回调:" + JSON.toJSONString(paramMap));
//验签
if (RequestHelper.isSignEquals(paramMap)) {
//判断业务是否成功
if("0001".equals(paramMap.get("resultCode"))){
//同步账户数据
return userAccountService.notify(paramMap);
}else{
log.info("用户充值异步回调充值失败:" + JSON.toJSONString(paramMap));
return "success";//汇付宝重试的原因就是返回非success,不希望汇付宝重试,所以返回success
}
}else{
log.info("用户充值异步回调签名错误:" + JSON.toJSONString(paramMap));
return "fail";
}
}
- service
com.atguigu.srb.core.service.UserAccountService
String notify(Map<String, Object> paramMap);
/**
* @param paramMap:
* @return void
* @author Likejin
* @description 汇付宝发来的响应让尚融宝做账户同步处理
* @date 2023/4/17 13:02
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String notify(Map<String, Object> paramMap) {
String agentBillNo = (String)paramMap.get("agentBillNo");
//先幂等性判断?判断标准(是否调用过该接口(判断交易流水账单号是否存在))
boolean isSave = transFlowService.isSaveTransFlow(agentBillNo);
if(isSave){
log.warn("幂等性返回");
return "success";
}
//账户同步user_account(充值处理)
//希望能够灵活修改余额和冻结金额写sql
String bindCode =(String) paramMap.get("bindCode");
String chargeAmt =(String) paramMap.get("chargeAmt");
baseMapper.updateAccount(bindCode,new BigDecimal(chargeAmt),new BigDecimal(0));
//记录账户流水(汇付宝发来了账户流水账单号)
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
bindCode,
new BigDecimal(chargeAmt),
TransTypeEnum.RECHARGE,
"充值啦");
transFlowService.saveTransFlow(transFlowBO);
return "success";
}
- 其他service
package com.atguigu.srb.core.service;
import com.atguigu.srb.core.pojo.bo.TransFlowBO;
import com.atguigu.srb.core.pojo.entity.TransFlow;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 交易流水表 服务类
* </p>
*
* @author Likejin
* @since 2023-04-09
*/
public interface TransFlowService extends IService<TransFlow> {
void saveTransFlow(TransFlowBO transFlowbo);
boolean isSaveTransFlow(String agentBillNo);
}
package com.atguigu.srb.core.service.impl;
import com.atguigu.srb.core.mapper.UserInfoMapper;
import com.atguigu.srb.core.pojo.bo.TransFlowBO;
import com.atguigu.srb.core.pojo.entity.TransFlow;
import com.atguigu.srb.core.mapper.TransFlowMapper;
import com.atguigu.srb.core.pojo.entity.UserInfo;
import com.atguigu.srb.core.service.TransFlowService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* <p>
* 交易流水表 服务实现类
* </p>
*
* @author Likejin
* @since 2023-04-09
*/
@Service
public class TransFlowServiceImpl extends ServiceImpl<TransFlowMapper, TransFlow> implements TransFlowService {
@Resource
private UserInfoMapper userInfoMapper;
/**
* @param transFlowbo:
* @return void
* @author Likejin
* @description 保存交易流水
* @date 2023/4/17 13:21
*/
@Override
public void saveTransFlow(TransFlowBO transFlowbo) {
TransFlow transFlow = new TransFlow();
//封装user_account需要的数据
//通过bind_code获取user_id
String bindCode = transFlowbo.getBindCode();
QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
userInfoQueryWrapper.eq("bind_code",bindCode);
UserInfo userInfo = userInfoMapper.selectOne(userInfoQueryWrapper);
transFlow.setTransAmount(transFlowbo.getAmount());
transFlow.setMemo(transFlowbo.getMemo());
transFlow.setTransTypeName(transFlowbo.getTransTypeEnum().getTransTypeName());
transFlow.setTransType(transFlowbo.getTransTypeEnum().getTransType());
transFlow.setTransNo(transFlowbo.getAgentBillNo());
transFlow.setUserId(userInfo.getId());
transFlow.setUserName(userInfo.getName());
baseMapper.insert(transFlow);
}
/**
*
* @param agentBillNo
* @return 根据流水号查询流水号是否存在(幂等性)
*/
@Override
public boolean isSaveTransFlow(String agentBillNo) {
QueryWrapper<TransFlow> transFlowQueryWrapper = new QueryWrapper<>();
transFlowQueryWrapper.eq("trans_no",agentBillNo);
Integer count = baseMapper.selectCount(transFlowQueryWrapper);
return count > 0;
}
}
- mapper
package com.atguigu.srb.core.mapper;
import com.atguigu.srb.core.pojo.entity.UserAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
/**
* <p>
* 用户账户 Mapper 接口
* </p>
*
* @author Likejin
* @since 2023-04-09
*/
public interface UserAccountMapper extends BaseMapper<UserAccount> {
void updateAccount(
@Param("bindCode")String bindCode,
@Param("amount")BigDecimal amount,
@Param("freezeAmount")BigDecimal freezeAmount);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.srb.core.mapper.UserAccountMapper">
<update id="updateAccount">
update user_account
set
amount = amount + #{amount},
freeze_amount = freeze_amount + #{freezeAmount}
where user_id = (select id from user_info where bind_code = #{bindCode} )
</update>
</mapper>
⑤业务逻辑
-
充值接口逻辑
前端传来充值金额和token。目的返回表单
利用token获得user_id,根据user_id来封装所需参数,注意sign的获得(通过封装的所有参数和秘钥生成sign,另一端校验通过统一的方式生成sign对比是否相等) -
异步回调接口
汇付宝传来响应结果。目的更新user_account表和更新trans_flow表,返回succes。
验证签名是否正确sign,失败则返回fail(错误发生在汇付宝端)。TODO:此时重试也无用。不希望重试。为什么返回fail?
成功则开始检验结果是否成功(响应码是否为001),如果不是则返回success(错误发生在汇付宝内部,重试响应码结果仍然不为001无需重试。)
如果是则账户同步数据。幂等性判断(判断交易流水账单号是否存在)。账户同步user_account。同步trans_flow -
问题1:TODO
-
问题2:修改用户账单user_account为了灵活修改余额和冻结金额,自己写sql。
即可以复用,如果不传入数据则是0不修改,灵活修改。 -
问题3:封装TransFlowBO修改trans_flow
汇付宝传来的数据封装为TransFlowBO,包含了当前充值的详细信息(除了user_id和user_name) -
注意trans_flow流水的状态
7.幂等性判断原理和解决方案
①什么是幂等性
- 用户对于同一次请求或者多次请求的结果是一致的
②为什么会发生幂等性
- 重试机制
比如业务中的汇付宝没有重试机制,那么网络断掉,有可能尚融宝未更新数据但是汇付宝更新数据,用户看到的和实际的不同。 - 重试机制的问题
如果汇付宝有重试机制,那么尚融宝接受回调后已经增加了钱,返回响应网络断调,汇付宝得不到响应进行重试,尚融宝又增加了一遍,出现问题
③如何解决幂等性
- 在数据库中设置唯一字段流水号,设置事务
如果第二次汇付宝再次调用,由于无法插入trans_flow,因为流水号策略为唯一则插入失败,回滚 - 业务首先判断是否为多次请求调用?用流水号查看数据库中是否有这条数据。
8.代码规范和原理了解
①对象的包名
- VO:前端
- entity:数据库
- dto:execel
- bo:业务对象
②远程调用接口的方法
-
ajax:有跨域的限制
-
表单提交远程调用:不收跨域限制,组装表单
-
JAVA代码远程调用:底层调用