核心业务7:放款实现
1.放款实现流程
-------------------未完成生成借款人还款计划和投资人回款计划--------------
2.数据库表
3.前端流程
4.汇付宝流程
5.尚融宝后端流程
-------------------未完成生成借款人还款计划和投资人回款计划--------------
------------------生成投资人回款计划和借款人还款计划----------------
6.数据库表
7.还款计划生成
8.回款计划生成
9.对应问题
------------------生成投资人回款计划和借款人还款计划----------------
核心业务7:放款实现
1.放款实现流程
①管理员点击放款
②后端开始放款(前端传入标的id)
- 尚融宝调用汇付宝放款接口,汇付宝放款成功后,尚融宝开始更新表
- 更改标的的状态和平台的受益
- 对借款人
给借款人账号转入金额 - 增加借款交易流水
- 对投款人(多个投资人)
解除投款人的冻结资金 - 增加投资人交易流水(多个投资人)
- 生成借款人还款计划和投资人回款计划TODO
2.数据库表
①汇付宝表
- user_account
- user_invest
②两表绑定
- 一个user_invest的协议号绑定两个账户
- 故当放款时,user_invest的状态都改为1即投资成功未还款,user_account中投资人删去冻结金额,借款人收到借款金额(平台服务费减去)
③尚融宝数据库
- lend
- lend_item
- user_account
- trans_flow
④尚融宝表关联
- 在放款时
- 修改lend表的标的状态和平台实际收益
- 修改user_account借款人的获取金额
- 增加一条交易流水trans_flow
- 解除投资人的冻结金额user_account
- 增加一条交易流水trans_flow
- 生成借款人还款计划和出借人回款计划TODO
3.前端流程
①前端按钮
②前端路由
③前端代码
- 路由
srb-admin\src\api\core\lend.js
//满标放款
makeLoan(id) {
return request({
url: `/admin/core/lend/makeLoan/${id}`,
method: 'get',
})
},
- 页面脚本
srb-admin\src\views\core\lend\list.vue
//放款
makeLoan(id) {
this.$confirm('确定放款吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
return lendApi.makeLoan(id)
})
.then((response) => {
//放款成功则重新获取数据列表
this.fetchData()
this.$message({
type: 'success',
message: response.message,
})
})
.catch((error) => {
console.log('取消', error)
if (error === 'cancel') {
this.$message({
type: 'info',
message: '已取消放款',
})
}
})
},
4.汇付宝流程
①汇付宝接受数据
②汇付宝返回数据
5.尚融宝后端流程
①接口
②代码
- controller
com/atguigu/srb/core/controller/admin/AdminLendController.java
@ApiOperation("放款")
@GetMapping("/makeLoan/{id}")
public R makeLoan(
@ApiParam(value = "标的id", required = true)
@PathVariable("id") Long id) {
lendService.makeLoan(id);
return R.ok().message("放款成功");
}
- serviceTODO
com/atguigu/srb/core/service/LendService.java
void makeLoan(Long id);
/**
* @param id:
* @return void
* @author Likejin
* @description 标的放款
* @date 2023/4/18 20:05
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void makeLoan(Long id) {
//获取标的信息
Lend lend = baseMapper.selectById(id);
//调用汇付宝放款接口
HashMap<String, Object> map = new HashMap<>();
map.put("agentId", HfbConst.AGENT_ID);
map.put("agentProjectCode",lend.getLendNo());
map.put("agentBillNo",LendNoUtils.getLoanNo());
//计算月年化
BigDecimal monthRate = lend.getServiceRate().divide(new BigDecimal(12),8,BigDecimal.ROUND_DOWN);
//已投金额*月年化*投资时长
BigDecimal realAmount = lend.getInvestAmount().multiply(monthRate).multiply(new BigDecimal(lend.getPeriod()));
//平台服务费
map.put("mchFee",realAmount);
map.put("timestamp", RequestHelper.getTimestamp());
String sign = RequestHelper.getSign(map);
map.put("sign", sign);
//提交远程请求
//汇付宝变化
//借款人的账户增加钱(自动扣除平台手续费),投资人解除冻结资金
JSONObject result = RequestHelper.sendRequest(map, HfbConst.MAKE_LOAD_URL);
log.info("放款结果",result.toJSONString());
//判断放款是否成功
//对放款失败的处理
if(!"0000".equals(result.getString("resultCode"))){
throw new BusinessException(result.getString("resultMsg"));
}
//放款成功
//1.标的状态和标的平台收益:更新标的相关信息
lend.setRealAmount(realAmount);//平台收益
lend.setStatus(LendStatusEnum.PAY_RUN.getStatus());
lend.setPaymentTime(LocalDateTime.now());
baseMapper.updateById(lend);
//2.给借款人账号转入金额
//获取借款人bindCode
Long userId = lend.getUserId();
UserInfo userInfo = userInfoMapper.selectById(userId);
String bindCode = userInfo.getBindCode();
BigDecimal voteAmt = new BigDecimal(result.getString("voteAmt"));
//具体给借款人转账
userAccountMapper.updateAccount(bindCode,voteAmt,new BigDecimal(0));
//3.增加借款交易流水
TransFlowBO transFlowBo = new TransFlowBO(
result.getString("agentBillNo"),
bindCode,
voteAmt,
TransTypeEnum.BORROW_BACK,
"项目放款,项目编号:" + lend.getLendNo() + "项目名称"+lend.getTitle()
);
transFlowService.saveTransFlow(transFlowBo);
//4.解冻并扣除投资人资金
//获取标的下的投资列表
List<LendItem> lendItemList = lendItemService.selectByLendId(id,1);
lendItemList.stream().forEach(item ->{
Long investUserId = item.getInvestUserId();
UserInfo investUserInfo = userInfoMapper.selectById(investUserId);
String investBindCode = investUserInfo.getBindCode();
userAccountMapper.updateAccount(investBindCode,new BigDecimal(0),item.getInvestAmount().negate());
//5.增加投资人交易流水
//(此时不能再用汇付宝发回来的流水编号,该编号已用在增加借款交易流水上)
//(此时也不能用lendItem中的投资编号,该编号用在投资时冻结流水编号)
TransFlowBO investTransFlowBo = new TransFlowBO(
LendNoUtils.getTransNo(),
investBindCode,
item.getInvestAmount(),
TransTypeEnum.INVEST_UNLOCK,
"项目放款,冻结资金转出,项目编号:" + lend.getLendNo() + "项目名称"+lend.getTitle()
);
transFlowService.saveTransFlow(investTransFlowBo);
});
//6.生成借款人还款计划和出借人回款计划
this.repaymentPlan(lend);
}
//还款计划
/**
* 还款计划
*
* @param lend
*/
private void repaymentPlan(Lend lend) {
//传进去lend对象
//3期10个投资人
//还三期,每期还款拆分成十份
//还款计划列表
}
//回款计划
/**
* 回款计划
*
* @param lendItemId
* @param lendReturnMap 还款期数与还款计划id对应map
* @param lend
* @return
*/
public List<LendItemReturn> returnInvest(Long lendItemId, Map<Integer, Long> lendReturnMap, Lend lend) {
//回款针对某个投资,标的,投资人的id
return null;
}
- 其他service
com/atguigu/srb/core/service/LendItemService.java
List<LendItem> selectByLendId(Long lendId, Integer status);
/**
* @param lendId:
* @param status:
* @return List<LendItem>
* @author Likejin
* @description 根据标的id获取在该标的下的所有投资人(根据状态)
* @date 2023/4/18 20:55
*/
@Override
public List<LendItem> selectByLendId(Long lendId, Integer status) {
QueryWrapper<LendItem> lendItemQueryWrapper = new QueryWrapper<>();
lendItemQueryWrapper
.eq("status",status)
.eq("lend_id",lendId);
return baseMapper.selectList(lendItemQueryWrapper);
}
③代码逻辑
- 前端传来数据封装了标的id
- a 封装汇付宝请求更新数据
利用id查到标的对象lend即可完成封装 - b 更改标的的状态和标的中的平台收益
利用lend对象即可完成 - c 给借款人账号转入金额
由于封装过方法需要传入借款人的bind_code,则根据lend获取到借款人的user_id然后获取到bind_code传入,根据汇付宝返回实际的放款金额更新 - d 生成借款人交易流水
利用汇付宝返回的流水号作为流水号,其他的都可获得 - e 给出借人减去对应的冻结资金
根据标的id获取到所有投资该借款的lend_item,根据每个lend_item获取到投资人user_id进而获取到bind_code更新冻结紫荆(方法同c) - f 生成解冻交易流水
注意:此时的流水号不能为前面,已经用过。需要再次生成。
6.数据库表
①数据库表
- lend标的表
- lend_item投资表
- lend_return 借款人还款表
- lend_item_return投资人回款表
②数据库表生成逻辑
- 借款人借款生成标的lend
- 投资人投款生成投资人投款记录lend_item
- 管理员放款
借款人生成还款表(借款编号,当前期数)lend_return
投资人生成回款表(借款编号,当前期数)
即一张还款表对应多张回款表(一个借款对应多个投资)
7.还款计划生成
①还款计划流程
- 传入参数为标的lend
- 创建还款计划列表List<LendReturn>
- 根据还款时间生成还款列表
- 循环每一期填充基本属性
- 还款计划中的本金,利息,金额需要根据回款总额相加获得(先不填充)Principal,Interestset,Total
- 设置最后一期的状态(是或者不是)last
- 批量保存还款计划列表
- 封装生成回款计划的参数(lendItem,lend,lendReturnMap)
- 键值对(期数:还款id)Map<Integer, Long> lendReturnMap
- 创建所有投资的所有回款记录列表List<LendItemReturn>
- 获取lend_item中所有的已支付的投资并且遍历
- 调用回款计划保存回款列表并且返回一笔投资对应的所有回款计划(调用回款计划的方法)
- 将每一笔投资的所有回款计划保存回款记录列表
- 遍历还款记录列表
- 遍历所有的回款记录列表
- 根据还款记录的id来判断当前期数回款是不是属于当前还款
- 将当前期数的所有回款相加则是当前期数还款的基本信息
- 保存对应的期数的lendReturn的利息,本金,总额的数据
- 遍历所有的回款记录列表
- 批量更新还款计划列表
②还款计划代码
com/atguigu/srb/core/service/impl/LendServiceImpl.java
/**
* 还款计划
*
* @param lend
*/
private void repaymentPlan(Lend lend) {
//简介:
//3期10个投资人
//还三期,每期还款拆分成十份(获得每个投资人的投资钱和投资user_id,投资钱加起来等于还款人还的钱)
//最后一个月的还钱(总共借的钱减去所有投资人实际投的钱)
//计算:一个投资人的所有回款,所有投资人的所有回款,计算借款人的还款
//lend_item_return 中的lend_return_id对应的lend_return的id
//还款计划1:回款计划是n(回款计划有还款计划的id)
//取数据:把所有的投资者的所有的汇款记录取出
//遍历:遍历还款列表
//过滤:把和当前这期还款对象所有的回款记录找出
//找数据:找出需要处理的属性
//做数据处理:将找出的数据相加
//总体步骤
//创建还款计划列表
ArrayList<LendReturn> lendReturnList = new ArrayList<>();
//根据还款时间生成还款计划(for period)
int len = lend.getPeriod().intValue();
for(int i =1;i <= len;i++) {
//{
// 创建还款计划对象
LendReturn lendReturn = new LendReturn();
// 填充基本属性
//创建还款计划对象
lendReturn.setReturnNo(LendNoUtils.getReturnNo());
lendReturn.setLendId(lend.getId());
lendReturn.setBorrowInfoId(lend.getBorrowInfoId());
lendReturn.setUserId(lend.getUserId());
lendReturn.setAmount(lend.getAmount());
lendReturn.setBaseAmount(lend.getInvestAmount());
lendReturn.setLendYearRate(lend.getLendYearRate());
lendReturn.setCurrentPeriod(i);//当前期数
lendReturn.setReturnMethod(lend.getReturnMethod());
//说明:还款计划中的这三项 = 回款计划中对应的这三项和:因此需要先生成对应的回款计划
// lendReturn.setPrincipal();
// lendReturn.setInterest();
// lendReturn.setTotal();
lendReturn.setFee(new BigDecimal(0));
lendReturn.setReturnDate(lend.getLendStartDate().plusMonths(i)); //第二个月开始还款
lendReturn.setOverdue(false);
//判断是否是最后一期还款
if(i == len){
lendReturn.setLast(true);
}else{
lendReturn.setLast(false);
}
//设置还款状态
lendReturn.setStatus(0);
//将还款对象加入还款计划列表
lendReturnList.add(lendReturn);
//}
}
//批量保存还款计划
lendReturnService.saveBatch(lendReturnList);
///
//生成期数和还款记录的id对应的键值对集合
Map<Integer, Long> lendReturnMap = lendReturnList.stream().collect(
Collectors.toMap(LendReturn::getCurrentPeriod, LendReturn::getId)
);
//创建所有投资的所有回款记录列表
List<LendItemReturn> lendItemReturnAllList = new ArrayList<>();
//获取当前标的下的所有已支付的投资
List<LendItem> lendItemList = lendItemService.selectByLendId(lend.getId(), 1);
//遍历投资列表
for(LendItem lendItem : lendItemList) {
// 根据投资记录的id调用回款计划生成的方法,得到当前这笔投资的回款计划列表
List<LendItemReturn> lendItemReturnList = this.returnInvest(lendItem.getId(), lendReturnMap, lend);
// debug这里出错 lendItem.getId 而不是lendItem.getLendid
// 将当前这笔投资计划的回款计划列表,放入所有投资所有回款记录列表
lendItemReturnAllList.addAll(lendItemReturnList);
// }
}
//
//遍历还款记录列表{
for(LendReturn lendReturn : lendReturnList) {
//通过filter,map,reduce将相关期数的回款数据过滤出来
//将当前期数的所有投资人的数据相加,就是当前期数的所有投资人的回款数据(本金利息总金额)
BigDecimal sumPrincipal = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getPrincipal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumInterest = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getInterest)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumTotall = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
lendReturn.setPrincipal(sumPrincipal);
lendReturn.setInterest(sumInterest);
lendReturn.setTotal(sumTotall);
//将计算出的数据填充入还款计划列表
// }
}
//批量更新还款计划列表
lendReturnService.updateBatchById(lendReturnList);
}
8.回款计划生成
①流程
- 传入参数为lendItem,lend,lendReturnMap
- 获取当前投资lend_item的基本信息
- 调用工具类根据lend_item获取到所有的期数对应的款项(本金,利息),键值对(期数:本金|利息)
- 创建回款计划列表(一笔投资对应多个回款(期数))
- 遍历lendReturnMap(封装了期数:还款id)
- 创建回款计划表
- 封装期数和还款id到回款计划表
- 封装回款计划表的其他参数
- 注意最后一个月的计算:需要用总的减去前面所有月
- 将回款记录保存到回款计划列表
- 批量保存回款计划列表
- 返回该回款计划列表
②代码
com/atguigu/srb/core/service/impl/LendServiceImpl.java
/**
* 回款计划(针对某一笔投资的回款计划)
* @param lendItemId
* @param lendReturnMap 还款期数与还款计划id对应map
* @param lend
* @return
*/
//一个投资人的所有回款
public List<LendItemReturn> returnInvest(Long lendItemId, Map<Integer, Long> lendReturnMap, Lend lend) {
//回款针对某个投资,标的,投资人的id
//总体流程
//获取当前投资记录信息
LendItem lendItem = lendItemService.getById(lendItemId);
//调用工具类计算本金和利息,存储为集合{期数:本金|利息}
BigDecimal amount = lendItem.getInvestAmount();
BigDecimal yearRate = lendItem.getLendYearRate();
Integer totalMonth = lend.getPeriod();
Map<Integer, BigDecimal> mapInterest = null; //还款期数 -> 利息
Map<Integer, BigDecimal> mapPrincipal = null; //还款期数 -> 本金
//根据还款方式计算本金和利息
if (lend.getReturnMethod().intValue() == ReturnMethodEnum.ONE.getMethod()) {
//利息
mapInterest = Amount1Helper.getPerMonthInterest(amount, yearRate, totalMonth);
//本金
mapPrincipal = Amount1Helper.getPerMonthPrincipal(amount, yearRate, totalMonth);
} else if (lend.getReturnMethod().intValue() == ReturnMethodEnum.TWO.getMethod()) {
mapInterest = Amount2Helper.getPerMonthInterest(amount, yearRate, totalMonth);
mapPrincipal = Amount2Helper.getPerMonthPrincipal(amount, yearRate, totalMonth);
} else if (lend.getReturnMethod().intValue() == ReturnMethodEnum.THREE.getMethod()) {
mapInterest = Amount3Helper.getPerMonthInterest(amount, yearRate, totalMonth);
mapPrincipal = Amount3Helper.getPerMonthPrincipal(amount, yearRate, totalMonth);
} else {
mapInterest = Amount4Helper.getPerMonthInterest(amount, yearRate, totalMonth);
mapPrincipal = Amount4Helper.getPerMonthPrincipal(amount, yearRate, totalMonth);
}
//一笔投资生成一系列的回款计划{
ArrayList<LendItemReturn> lendItemReturnList = new ArrayList<>();
for (Map.Entry<Integer, BigDecimal> entry : mapInterest.entrySet()) {
//获取当前期数
Integer currentPeriod = entry.getKey();
Long lendReturnId = lendReturnMap.get(currentPeriod);
//创建回款计划表
LendItemReturn lendItemReturn = new LendItemReturn();
//根据当前期数,获取还款计划的id,将还款记录关联到汇款记录
lendItemReturn.setLendReturnId(lendReturnId);
//设置回款记录的基本属性
lendItemReturn.setLendItemId(lendItemId);
lendItemReturn.setInvestUserId(lendItem.getInvestUserId());
lendItemReturn.setLendId(lendItem.getLendId());
lendItemReturn.setInvestAmount(lendItem.getInvestAmount());
lendItemReturn.setLendYearRate(lend.getLendYearRate());
//设置当前期数
lendItemReturn.setCurrentPeriod(currentPeriod);
lendItemReturn.setReturnMethod(lend.getReturnMethod());
//计算回款本金,利息和总额(注意最后一个月的计算)
if(currentPeriod.intValue() == lend.getPeriod().intValue()){
//最后一期
//获取前面所有的本金
BigDecimal sumPrincipal = lendItemReturnList.stream()
.map(LendItemReturn::getPrincipal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal lastPrincipal = lendItem.getInvestAmount().subtract(sumPrincipal);
lendItemReturn.setPrincipal(lastPrincipal);
//利息
BigDecimal sumInterest = lendItemReturnList.stream()
.map(LendItemReturn::getInterest)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal lastInterest = lendItem.getExpectAmount().subtract(sumInterest);
lendItemReturn.setInterest(lastInterest);
//debug修改setPrincipal为setInterest
}else{
//非最后一期
//本机
lendItemReturn.setPrincipal(mapPrincipal.get(currentPeriod));
//利息
lendItemReturn.setInterest(mapInterest.get(currentPeriod));
}
//回款总金额
lendItemReturn.setTotal(lendItemReturn.getPrincipal().add(lendItemReturn.getInterest()));
//其他属性的设置
lendItemReturn.setFee(new BigDecimal("0"));
lendItemReturn.setReturnDate(lend.getLendStartDate().plusMonths(currentPeriod));
//是否逾期,默认未逾期
lendItemReturn.setOverdue(false);
lendItemReturn.setStatus(0);
//将汇款记录放入回款列表
lendItemReturnList.add(lendItemReturn);
// }
}
//批量保存
lendItemReturnService.saveBatch(lendItemReturnList);
return lendItemReturnList;
}
9.对应问题/h1>
①为什么要在还款计划中调用后回款计划
- 因为还款计划的利息本金需要用回款计划的之和来计算比较精确
②为什么调用时要传入map期数对应还款id
- 方便回款计划的生成
- 回款计划直接用期数和id即可
- 回款计划也可自己再查多少期对应多少id,不符合实际逻辑(实际上应该是还款多少期,回款就有多少期,然后回款需要多少金额,还款就还多少)
③为什么要对最后一期特殊处理
- 最后一期需要总的减去保证和相等,否则用户体验差
④对应逻辑
- 还款还多少期,回款就收多少期(回款生成依赖还款期数和id)
- 回款需要多少钱,还款就还多少钱(还款的金额生成依赖回款的金额)