目录
一、展示投资列表
(一)需求
(二)后端
(三)前端
二、充值功能
(一)需求
1、需求描述
2、流程
(二)充值
1、后端
2、前端
(三)回调接口
1、定义回调接口
2、增加交易流水
(四)接口调用幂等性
1、接口幂等性原则
2、解决方案
一、展示投资列表
(一)需求
标的形成后,客户可在客户端看到标的列表,投资者可以通过点击标的了解详情并投资心仪标的
(二)后端
LendController中创建list方法
@Api(tags = "标的")
@RestController
@RequestMapping("/api/core/lend")
@Slf4j
public class LendController {
@Resource
private LendService lendService;
@ApiOperation("获取标的列表")
@GetMapping("/list")
public R list() {
List<Lend> lendList = lendService.selectList();
return R.ok().data("lendList", lendList);
}
}
(三)前端
pages/lend/index.vue
脚本
此处使用了服务器端渲染
<template>
<!--列表-->
<div class="page-filter wrap">
<div class="breadcrumbs">
<a href="index.html">首页</a>><span class="cur">散标投资列表</span>
</div>
<div class="invest-filter" data-target="sideMenu">
<div class="filter-inner clearfix">
<div class="filter-item">
<div class="hd">
<h3>筛选投资项目</h3>
<label>
<input id="filterMulti" name="multiple_choice" type="checkbox" />
多选</label
>
</div>
<div class="bd">
<dl>
<dt>项目类型</dt>
<dd>
<ul>
<li class="n1">
<a
href="javascript:url('post_type','');"
id="post_type_"
class="active"
>不限</a
>
</li>
<li class="n2">
<a
href="javascript:url('post_type','car');"
id="post_type_car"
>车易贷</a
>
</li>
<li class="n3">
<a
href="javascript:url('post_type','house');"
id="post_type_house"
>房易贷</a
>
</li>
<li class="n4">
<a
href="javascript:url('post_type','bridge');"
id="post_type_bridge"
>赎楼贷</a
>
</li>
<li class="n5">
<a
href="javascript:url('post_type','worth');"
id="post_type_worth"
>债权贷</a
>
</li>
</ul>
</dd>
</dl>
<dl>
<dt>年利率</dt>
<dd>
<ul>
<li class="n1">
<a
href="javascript:url('borrow_interestrate','');"
id="borrow_interestrate_"
class="active"
>不限</a
>
</li>
<li class="n2">
<a
id="borrow_interestrate_1"
href="javascript:url('borrow_interestrate','1');"
>12%以下</a
>
</li>
<li class="n3">
<a
id="borrow_interestrate_2"
href="javascript:url('borrow_interestrate','2');"
>12%-14%</a
>
</li>
<li class="n4">
<a
id="borrow_interestrate_3"
href="javascript:url('borrow_interestrate','3');"
>14%-16%</a
>
</li>
<li class="n5">
<a
id="borrow_interestrate_4"
href="javascript:url('borrow_interestrate','4');"
>16%及以上</a
>
</li>
</ul>
</dd>
</dl>
<dl>
<dt>期限</dt>
<dd>
<ul>
<li class="n1">
<a
href="javascript:url('spread_month','');"
id="spread_month_"
class="active"
>不限</a
>
</li>
<li class="n2">
<a
id="spread_month_1"
href="javascript:url('spread_month','1');"
>1月以下</a
>
</li>
<li class="n3">
<a
id="spread_month_2"
href="javascript:url('spread_month','2');"
>1-3月</a
>
</li>
<li class="n4">
<a
id="spread_month_3"
href="javascript:url('spread_month','3');"
>3-6月</a
>
</li>
<li class="n5">
<a
id="spread_month_4"
href="javascript:url('spread_month','4');"
>6-12月</a
>
</li>
<li class="n6">
<a
id="spread_month_5"
href="javascript:url('spread_month','5');"
>12月及以上</a
>
</li>
</ul>
</dd>
</dl>
<dl class="repayment">
<dt>还款方式</dt>
<dd>
<ul>
<li class="n1">
<a
href="javascript:url('repay_style','');"
id="repay_style_"
class="active"
>不限</a
>
</li>
<li class="n2">
<a
id="repay_style_end"
href="javascript:url('repay_style','end');"
>到期还本付息</a
>
</li>
<li class="n2">
<a
id="repay_style_endmonth"
href="javascript:url('repay_style','endmonth');"
>按月付息,到期还本</a
>
</li>
<li class="n2">
<a
id="repay_style_month"
href="javascript:url('repay_style','month');"
>等额本息</a
>
</li>
</ul>
</dd>
</dl>
</div>
</div>
<div class="common-problem">
<h3>常见问题</h3>
<ul>
<li><a href="#">什么是债权贷?</a></li>
<li><a href="#">关于"债权贷"产品的说明</a></li>
<li><a href="#">金融理财收费标准</a></li>
<li><a href="#">债权贷和房易贷、车易贷有什么区别?</a></li>
</ul>
</div>
</div>
</div>
<div class="invest-list mrt30 clearfix">
<div class="hd">
<h3>投资列表</h3>
<div class="count">
<ul>
<li class="line">
散标投资交易金额 <span class="f20 bold"
>73.54亿元</span
>
</li>
<li>
累计赚取收益 <span class="f20 bold">2.52亿元</span>
</li>
</ul>
</div>
</div>
<div class="bd">
<div class="invest-table clearfix">
<div class="title clearfix">
<ul>
<li class="col-330">借款标题</li>
<li class="col-180">
<a href="javascript:url('order','account_up');" class=""
>借款金额</a
>
</li>
<li class="col-110">
<a href="javascript:url('order','apr_up');" class="">年利率</a>
</li>
<li class="col-150">
<a href="javascript:url('order','period_up');" class=""
>借款期限</a
>
</li>
<li class="col-150">还款方式</li>
<li class="col-120">
<a href="javascript:url('order','scale_up');" class=""
>借款进度</a
>
</li>
<li class="col-120-t">操作</li>
</ul>
</div>
<!------------投资列表-------------->
<div class="item" v-for="lend in lendList" :key="lend.id">
<ul>
<li class="col-330 col-t">
<NuxtLink :to="'/lend/' + lend.id" target="_blank">
<i class="icon icon-zhai"></i>
</NuxtLink>
<NuxtLink
class="f18"
:to="'/lend/' + lend.id"
:title="lend.title"
target="_blank"
>
{{ lend.title }}
</NuxtLink>
</li>
<li class="col-180">
<span class="f20 c-333"> {{ lend.amount }}元 </span>
</li>
<li class="col-110 relative">
<span class="f20 c-orange">
{{ lend.lendYearRate * 100 }}%
</span>
</li>
<li class="col-150">
<span class="f20 c-333">{{ lend.period }}个月</span>
</li>
<li class="col-150">{{ lend.params.returnMethod }}</li>
<li class="col-120">
<div class="circle">
<div class="left progress-bar">
<!-- <div
:class="
'progress-bgPic progress-bfb' +
Math.floor((lend.investAmount / lend.amount) * 10)
"
> -->
<div
:class="
'progress-bgPic progress-bfb' +
Math.floor((lend.investAmount / lend.amount) * 10)
"
>
<div class="show-bar">
{{ (lend.investAmount / lend.amount) * 100 }}%
</div>
</div>
</div>
</div>
</li>
<li class="col-120-2">
<NuxtLink
class="ui-btn btn-gray"
:to="'/lend/' + lend.id"
target="_blank"
>
{{ lend.params.status }}
</NuxtLink>
</li>
</ul>
</div>
<!------------投资列表-------------->
</div>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/index.css'
import '~/assets/css/detail.css'
export default {
async asyncData({ $axios }) {
console.log('服务器端获取远程数据。。。。。。。。。。。。。。。。')
let response = await $axios.$get('/api/core/lend/list')
return {
lendList: response.data.lendList,
}
},
}
</script>
二、充值功能
(一)需求
1、需求描述
标的产生后,平台展示标的,投资人就可以在平台投资标的,获取收益;投资人投资标的必须满足以下条件:
充值过程与绑定过程一致,也是在平台发送充值请求,跳转到资金托管平台(汇付宝),在资金托管平台(汇付宝)完成充值,然后同步或异步返回或通知平台
操作表:user_account(用户账户表) ====> trans_flow(交易流水表)
用户输入充值后,点击确认,此时我们需要有一个方法返回一个表单且需自动提交(携带汇付宝需要的参数且不能让客户看到表单,因此需要自动提交),然后汇付宝会调用我们的回调方法,在回调方法中,我们需要更新用户账户表并在交易流水表中产生一个记录
汇付宝携带参数说明
2、流程
step1:用户在个人中心点击 “充值”
step2:尚融宝展示账户充值页面
step3:用户填写充值金额,点击“充值”按钮
step4:跳转到汇付宝页面(资金托管接口调用)
step5:汇付宝验证用户交易密码
step6:汇付宝修改账号资金余额(更新user_account记录中的amount的值,这一步汇付宝做)
step7:异步回调
(1)账户金额更改
(2)添加交易流水
step8:用户点击“返回平台”,返回尚融宝
(二)充值
1、后端
controller
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) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
String formStr = userAccountService.commitCharge(chargeAmt, userId);
return R.ok().data("formStr", formStr);
}
}
service
接口:UserAccountService
String commitCharge(BigDecimal chargeAmt, Long userId);
实现:UserAccountServiceImpl
@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> paramMap = new HashMap<>();
paramMap.put("agentId", HfbConst.AGENT_ID);
paramMap.put("agentBillNo", LendNoUtils.getNo());
paramMap.put("bindCode", bindCode);
paramMap.put("chargeAmt", chargeAmt);
paramMap.put("feeAmt", new BigDecimal("0"));
paramMap.put("notifyUrl", HfbConst.RECHARGE_NOTIFY_URL);//检查常量是否正确
paramMap.put("returnUrl", HfbConst.RECHARGE_RETURN_URL);
paramMap.put("timestamp", RequestHelper.getTimestamp());
String sign = RequestHelper.getSign(paramMap);
paramMap.put("sign", sign);
//构建充值自动提交表单
String formStr = FormHelper.buildForm(HfbConst.RECHARGE_URL, paramMap);
return formStr;
}
2、前端
pages/user/recharge.vue
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)
})
}
},
}
)
},
},
(三)回调接口
1、定义回调接口
controller
UserAccountController中创建回调方法
@ApiOperation(value = "用户充值异步回调")
@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));
return "success";
}
} else {
log.info("用户充值异步回调签名错误:" + JSON.toJSONString(paramMap));
return "fail";
}
}
service
接口:UserAccountService
String notify(Map<String, Object> paramMap);
实现:UserAccountServiceImpl
@Transactional(rollbackFor = Exception.class)
@Override
public String notify(Map<String, Object> paramMap) {
log.info("充值成功:" + JSONObject.toJSONString(paramMap));
String bindCode = (String)paramMap.get("bindCode"); //充值人绑定协议号
String chargeAmt = (String)paramMap.get("chargeAmt"); //充值金额
//优化
baseMapper.updateAccount(bindCode, new BigDecimal(chargeAmt), new BigDecimal(0));
//增加交易流水
//TODO
return "success";
}
mapper
接口:UserAccountMapper
void updateAccount(
@Param("bindCode")String bindCode,
@Param("amount")BigDecimal amount,
@Param("freezeAmount")BigDecimal freezeAmount);
XML:UserAccountMapper.xml
<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>
2、增加交易流水
增加枚举TransTypeEnum
@AllArgsConstructor
@Getter
public enum TransTypeEnum {
RECHARGE(1,"充值"),
INVEST_LOCK(2,"投标锁定"),
INVEST_UNLOCK(3,"放款解锁"),
CANCEL_LEND(4,"撤标"),
BORROW_BACK(5,"放款到账"),
RETURN_DOWN(6,"还款扣减"),
INVEST_BACK(7,"出借回款"),
WITHDRAW(8,"提现"),
;
private Integer transType ;
private String transTypeName;
public static String getTransTypeName(int transType) {
for (TransTypeEnum obj : TransTypeEnum.values()) {
if (transType == obj.getTransType().intValue()) {
return obj.getTransTypeName();
}
}
return "";
}
}
创建BO对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransFlowBO {
private String agentBillNo;
private String bindCode;
private BigDecimal amount;
private TransTypeEnum transTypeEnum;
private String memo;
}
service
接口:TransFlowService
void saveTransFlow(TransFlowBO transFlowBO);
实现:TransFlowServiceImpl
@Resource
private UserInfoMapper userInfoMapper;
@Override
public void saveTransFlow(TransFlowBO transFlowBO) {
//获取用户基本信息 user_info
QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
userInfoQueryWrapper.eq("bind_code", transFlowBO.getBindCode());
UserInfo userInfo = userInfoMapper.selectOne(userInfoQueryWrapper);
//存储交易流水数据
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);
}
完整的回调函数notify
@Resource
private TransFlowService transFlowService;
@Transactional(rollbackFor = Exception.class)
@Override
public String notify(Map<String, Object> paramMap) {
log.info("充值成功:" + JSONObject.toJSONString(paramMap));
String bindCode = (String)paramMap.get("bindCode"); //充值人绑定协议号
String chargeAmt = (String)paramMap.get("chargeAmt"); //充值金额
//优化
baseMapper.updateAccount(bindCode, new BigDecimal(chargeAmt), new BigDecimal(0));
//增加交易流水
String agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
bindCode,
new BigDecimal(chargeAmt),
TransTypeEnum.RECHARGE,
"充值");
transFlowService.saveTransFlow(transFlowBO);
return "success";
}
(四)接口调用幂等性
1、接口幂等性原则
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次调用而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性
回调重试
汇付宝向尚融宝发起回调,如果没有收到正确的响应 "success",则尚融宝会发起重试
汇付宝中的相关代码如下:
@Slf4j
public class NotifyThread implements Runnable {
private int count = 1;
private String notifyUrl;
private Map<String, Object> paramMap;
public NotifyThread(){}
public NotifyThread(String notifyUrl, Map<String, Object> paramMap) {
this.notifyUrl = notifyUrl;
this.paramMap = paramMap;
}
@Override
public void run() {
task();
}
private void task() {
String result = SignUtil.sendRequest(paramMap,notifyUrl);
log.info(notifyUrl + ":" + result + " count:" + count);
if(!"success".equals(result)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//ScheduledTask.queue.offer(new NotifyVo(notifyUrl, paramMap));
count++;
if(count <= 5) {
task();
log.info("失败重试:" + JSON.toJSONString(this));
}
}
}
}
存在的问题:当回调重试时,金额和流水会重复增加
2、解决方案
① 设置唯一索引
设置了唯一索引后,即使回调重复执行,遇到唯一索引,就会抛出异常,从而使事务回滚。
但很显然,我们不能光靠报错回滚数据库事务来防止接口幂等性
② 判断流水是否存在
判断流水如果存在,则从业务方法中直接退出
接口:TransFlowService
boolean isSaveTransFlow(String agentBillNo);
实现:TransFlowServiceImpl
@Override
public boolean isSaveTransFlow(String agentBillNo) {
QueryWrapper<TransFlow> queryWrapper = new QueryWrapper();
queryWrapper.eq("trans_no", agentBillNo);
int count = baseMapper.selectCount(queryWrapper);
if(count > 0) {
return true;
}
return false;
}
调用 :UserAccountServiceImpl
@Transactional(rollbackFor = Exception.class)
@Override
public void notify(Map<String, Object> paramMap) {
log.info("充值成功:" + JSONObject.toJSONString(paramMap));
//判断交易流水是否存在
String agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
boolean isSave = transFlowService.isSaveTransFlow(agentBillNo);
if(isSave){
log.warn("幂等性返回");
return;
}
......
//增加交易流水
//agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
}