整体流程
标的产生后,平台展示标的,投资人就可以在平台投资标的,获取收益;投资人投资标的必须满足以下条件:
充值过程与绑定过程一致,也是在平台发送充值请求,跳转到资金托管平台,在资金托管平台完成充值,然后同步或异步返回或通知平台
相关数据库表
参考文档
参考《汇付宝商户账户技术文档》3.9用户充值
充值
需求
step1:用户在个人中心点击 “充值”
step2:尚融宝展示账户充值页面
step3:用户填写充值金额,点击“充值”按钮
step4:跳转到汇付宝页面(资金托管接口调用)
step5:汇付宝验证用户交易密码
后端
controller
UserAccountController.java
package com.indi.srb.core.controller.api;
@Api(tags = "用户账户")
@RestController
@RequestMapping("/api/core/userAccount")
@Slf4j
public class UserAccountController {
@Resource
private UserAccountService userAccountService;
@ApiOperation("账户充值")
@PostMapping("/auth/commitCharge/{chargeAmt}")
public R recharge(
@PathVariable BigDecimal chargeAmt,
HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
String formStr = userAccountService.commitCharge(chargeAmt, userId);
return R.ok().setData("formStr", formStr);
}
}
service
UserAccountService.java
String commitCharge(BigDecimal chargeAmt, Long userId);
UserAccountServiceImpl.java
@Resource
private UserInfoMapper userInfoMapper;
@Override
public String commitCharge(BigDecimal chargeAmt, Long userId) {
// 判断用户绑没绑定
UserInfo userInfo = userInfoMapper.selectById(userId);
String bindCode = userInfo.getBindCode();
Assert.notEmpty(bindCode, ResponseEnum.USER_NO_BIND_ERROR);
// 组装表单
Map<String, Object> map = new HashMap<>();
map.put("agentId", HfbConst.AGENT_ID);
map.put("agentBillNo", LendNoUtils.getChargeNo());
map.put("bindCode", bindCode);
map.put("chargeAmt", chargeAmt);
map.put("feeAmt", new BigDecimal(0));
map.put("notifyUrl", HfbConst.RECHARGE_NOTIFY_URL);
map.put("returnUrl", HfbConst.RECHARGE_RETURN_URL);
map.put("timestamp", RequestHelper.getTimestamp());
map.put("sign", RequestHelper.getSign(map));
String formStr = FormHelper.buildForm(HfbConst.RECHARGE_URL, map);
return formStr;
}
前端
template
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
pages/user/recharge.vue
ajax远程调用:远程服务器必修开放跨域访问权限
form表单远程调用:不受跨域控制、缺点需要组装表单
<script>
export default {
data() {
return {
chargeAmt: 0,
}
},
methods: {
commitCharge() {
this.$alert(
'<div style="size: 18px;color: red;">您即将前往汇付宝充值</div>',
'前往汇付宝资金托管平台',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '立即前往',
callback: (action) => {
if (action === 'confirm') {
this.$axios
.$post(
'/api/core/userAccount/auth/commitCharge/' + this.chargeAmt
)
.then((response) => {
document.write(response.data.formStr)
})
}
},
}
)
},
},
}
</script>
充值回调
需求
step7:异步回调(1)账户金额更改(2)添加交易流水
step8:用户点击“返回平台”,返回尚融宝
接口幂等性原则
什么是接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次调用而产生副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性
存在的问题
当回调重试时,金额和流水会重复增加
解决方案
1、设置唯一索引
设置了唯一索引后,即使回调重复执行,遇到唯一索引,就会抛出异常,从而使事务回滚,这是保底方案,我们要尽量避免程序进入到这一步。
2、判断流水是否存在
判断流水如果存在,则从业务方法中直接退出
TransFlowService.java
boolean isSaveTransFlow(String agentBillNo);
后端UserAccount
controller
UserAccountController.java
@ApiOperation("充值回调")
@PostMapping("/notify")
public String notify(HttpServletRequest 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));
// 如果想让汇付宝发起失败重试的话,则返回非success字段,如果不需要重试,则返回success
// 此处返回success,是因为汇付宝那边已经充值失败了,所以根本就不需要重试,再重试也是失败,所以直接给用户返回错误信息即可。
return "success";
}
}else{
// 此处可能是因为服务器签名错误、或者是请求超时,
// 验证签名的时候,汇付宝那边会发过一个时间戳过来,这个时间戳一般都是实时的,这边接收之后发现是好几分钟之前的,
// 这就可能是请求被别人拦截了,然后模拟了一个请求,给发过来了,时间花费较长,可能就会导致请求超时
// 所以需要发起汇付宝重试
log.info("充值签名错误:"+JSON.toJSONString(paramMap));
return "fail";
}
}
service
UserAccountService.java
String notify(Map<String, Object> paramMap);
UserAccountServiceImpl.java
@Resource
private TransFlowService transFlowService;
@Transactional(rollbackFor = Exception.class)
@Override
public String notify(Map<String, Object> paramMap) {
// 能进入到这,就说明充值成功了
log.info("充值成功:" + JSONObject.toJSONString(paramMap));
String agentBillNo = (String) paramMap.get("agentBillNo");
if (transFlowService.haveTransFlow(agentBillNo)) {
log.warn("幂等性返回");
return "success";
}
String bindCode = (String) paramMap.get("bindCode");
String chargeAmt = (String) paramMap.get("chargeAmt");
// 根据绑定协议查到用户id,然后更新账户余额、冻结金额、
baseMapper.updateAccount(bindCode, new BigDecimal(chargeAmt), new BigDecimal(0));
// 增加交易流水
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
bindCode,
new BigDecimal(chargeAmt),
TransTypeEnum.RECHARGE,
TransTypeEnum.RECHARGE.getTransTypeName()
);
transFlowService.saveTransFlow(transFlowBO);
return "success";
}
mapper
UserAccountMapper.java
void updateAccount(@Param("bindCode") String bindCode,
@Param("amount") BigDecimal amount,
@Param("freezeAmount") BigDecimal freezeAmount);
UserAccountMapper.xml
<?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.indi.srb.core.mapper.UserAccountMapper">
<update id="updateAccount">
update user_account
set
amount = amount + #{amount},
freeze_amount = freeze_amount + #{freezeAmount}
where
user_id = (select user_id from user_bind where bind_code = #{bindCode})
</update>
</mapper>
后端TransFlow
bo
TransFlowBO.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TransFlowBO {
private String agentBillNo; // 商户充值订单号
private String bindCode; // 绑定协议
private BigDecimal amount; // 充值金额
private TransTypeEnum transTypeEnum; // 交易类型
private String memo; // 备注
}
service
TransFlowService.java
void saveTransFlow(TransFlowBO transFlowBO);
boolean isSaveTransFlow(String agentBillNo);
TransFlowServiceImpl.java
@Resource
private UserInfoMapper userInfoMapper;
@Override
public void saveTransFlow(TransFlowBO transFlowBO) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("bind_code", transFlowBO.getBindCode());
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
TransFlow transFlow = new TransFlow();
transFlow.setUserId(userInfo.getId());
transFlow.setUserName(userInfo.getName());
transFlow.setTransNo(transFlowBO.getAgentBillNo());
transFlow.setTransType(transFlowBO.getTransTypeEnum().getTransType());
transFlow.setTransTypeName(transFlowBO.getTransTypeEnum().getTransTypeName());
transFlow.setTransAmount(transFlowBO.getAmount());
transFlow.setMemo(transFlowBO.getMemo());
baseMapper.insert(transFlow);
}
@Override
public boolean isSaveTransFlow(String agentBillNo) {
QueryWrapper<TransFlow> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("trans_No", agentBillNo);
Integer count = baseMapper.selectCount(queryWrapper);
return count > 0;
}