管理员在后台对标的进行放款
还款人还款记录:lend_return
投资人回款记录:lend_item_return
参考《汇付宝商户账户技术文档》3.11满标放款
AdminLendController.java
@ApiOperation("放款")
@GetMapping("/makeLoan/{id}")
public R makeLoan(
@ApiParam(value = "标的id",required = true)
@PathVariable Long id){
lendService.makeLoan(id);
return R.ok().setMessage("放款成功");
}
LendService.java
void makeLoan(Long id);
LendServiceImpl.java
@Resource
private UserAccountMapper userAccountMapper;
@Resource
private UserBindService userBindService;
@Resource
private TransFlowService transFlowService;
@Resource
private LendItemService lendItemService;
@Override
@Transactional(rollbackFor = Exception.class)
public void makeLoan(Long id) {
Lend lend = baseMapper.selectById(id);
String agentBillNo = LendNoUtils.getLoanNo();
Map<String, Object> map = new HashMap<>();
map.put("agentId", HfbConst.AGENT_ID);
map.put("agentProjectCode", lend.getLendNo());
map.put("agentBillNo", agentBillNo);
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());
map.put("sign", RequestHelper.getSign(map));
// 放款是一个同步接口,如果放款返回成功,那么我们要处理平台业务
JSONObject result = RequestHelper.sendRequest(map, HfbConst.MAKE_LOAN_URL);
BigDecimal voteAmt = result.getBigDecimal("voteAmt"); // 投资金额
if (!"0000".equals(result.get("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、更新借款人账户
String benefitBindCode = userBindService.getBindCodeByUserId(lend.getUserId());
userAccountMapper.updateAccount(benefitBindCode, voteAmt, new BigDecimal(0));
// 3、添加交易流水
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
benefitBindCode,
TransTypeEnum.BORROW_BACK,
voteAmt,
"放款到账,项目编号:" + lend.getLendNo()
);
transFlowService.saveTransFlow(transFlowBO);
// 4、解冻并扣除投资人资金
List<LendItem> lendItemList = lendItemService.listByIdAndStatus(lend.getId(), LendItemEnum.PAY_OK.getStatus());
lendItemList.forEach(lendItem -> {
String voteBindCode = userBindService.getBindCodeByUserId(lendItem.getInvestUserId());
userAccountMapper.updateAccount(voteBindCode, new BigDecimal(0), lendItem.getInvestAmount().negate());
// 5、增加投资人交易流水
TransFlowBO investTransFlow = new TransFlowBO(
LendNoUtils.getLendNo(),
voteBindCode,
TransTypeEnum.INVEST_UNLOCK,
lendItem.getInvestAmount(),
"冻结资金转出放款,项目编号:" + lend.getLendNo() + "项目名称:" + lend.getTitle()
);
transFlowService.saveTransFlow(investTransFlow);
});
// 6、生成借款人还款计划、投资人回款计划
repaymentPlan(lend);
}
@Resource
private LendReturnService lendReturnService;
/**
* 生成借款人还款计划
*
* @param lend 标的
*/
private void repaymentPlan(Lend lend) {
// 一、创建还款计划列表
List<LendReturn> lendReturnList = new ArrayList<>();
// 根据标的还款期限(period)生成还款计划
// 你借多少期,我就给你生成多少条还款计划
int period = lend.getPeriod().intValue();
// 遍历还款期限,从第1期开始
for (int i = 1; i <= period; i++) {
// 创建还款计划
LendReturn lendReturn = new LendReturn();
// 填充属性
lendReturn.setLendId(lend.getId());
lendReturn.setBorrowInfoId(lend.getBorrowInfoId());
lendReturn.setReturnNo(LendNoUtils.getReturnNo());
lendReturn.setUserId(lend.getUserId());
lendReturn.setAmount(lend.getAmount());
lendReturn.setBaseAmount(lend.getInvestAmount());// 计算利息所需要用到的本金:根据已投金额,得出利息
lendReturn.setCurrentPeriod(i);
lendReturn.setLendYearRate(lend.getLendYearRate());
lendReturn.setReturnMethod(lend.getReturnMethod());
lendReturn.setFee(BigDecimal.ZERO);
lendReturn.setReturnDate(lend.getLendStartDate().plusMonths(i)); // 开始日期往后i个月开始还款
lendReturn.setOverdue(false); // 不逾期
lendReturn.setLast(i == period); // 是否为最后一期
lendReturn.setStatus(0); // 设置还款状态
lendReturnList.add(lendReturn); // 将还款计划加入还款计划列表
}
// 批量保存还款计划
lendReturnService.saveBatch(lendReturnList);
// 将还款计划列表的还款期数、还款计划id提取出来,转换成一个还款计划map
Map<Integer, Long> lendReturnMap = lendReturnList.stream().collect(
Collectors.toMap(LendReturn::getCurrentPeriod, LendReturn::getId)
);
// ------------------------------------------------------------------
// ---------------------------获取回款计划----------------------------
// ------------------------------------------------------------------
// 创建当前标的下所有投资人的回款计划列表
List<LendItemReturn> lendItemReturnAllList = new ArrayList<>();
// 获取当前标的下所有已支付的投资记录
List<LendItem> lendItemList = lendItemService.listByIdAndStatus(lend.getId(), LendItemEnum.PAY_OK.getStatus());
// 遍历投资记录
for (LendItem lendItem : lendItemList) {
// 三、根据投资记录、还款计划map、标的,当前投资记录的所有回款计划
List<LendItemReturn> lendItemReturnList = this.returnInvest(lendItem, lendReturnMap, lend);
// 将每个投资记录的回款计划添加至回款计划列表中
lendItemReturnAllList.addAll(lendItemReturnList);
}
// 四、遍历还款计划列表
for (LendReturn lendReturn : lendReturnList) {
// 计算总利息、总本金、总本息
// 把回款计划中同一期所有的利息/本金/本息加起来
// 计算出总利息、总本金、总本息
BigDecimal principal = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getPrincipal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal interest = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getInterest)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = lendItemReturnAllList.stream()
.filter(item -> item.getLendReturnId().longValue() == lendReturn.getId().longValue())
.map(LendItemReturn::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
lendReturn.setPrincipal(principal);
lendReturn.setInterest(interest);
lendReturn.setTotal(total);
}
// 批量更新
lendReturnService.updateBatchById(lendReturnList);
}
@Resource
private LendItemReturnService lendItemReturnService;
/**
* 投资人还款计划
*
* @param lend
* @param lendReturnMap
* @param lendItem
* @return
*/
private List<LendItemReturn> returnInvest(LendItem lendItem, Map<Integer, Long> lendReturnMap, Lend lend) {
// 投资金额
BigDecimal investAmount = lendItem.getInvestAmount();
// 年化利率
BigDecimal lendYearRate = lendItem.getLendYearRate();
// 投资期数
Integer period = lend.getPeriod();
// 创建还款期数、利息的map
Map<Integer, BigDecimal> mapInterest;
// 创建还款期数、本金的map
Map<Integer, BigDecimal> mapPrincipal;
// 二、根据还款方式计算本金和利息
switch (ReturnMethodEnum.getByValue(lend.getReturnMethod().intValue())) {
case ONE:
mapInterest = Amount1Helper.getPerMonthInterest(investAmount, lendYearRate, period);
mapPrincipal = Amount1Helper.getPerMonthPrincipal(investAmount, lendYearRate, period);
break;
case TWO:
mapInterest = Amount2Helper.getPerMonthInterest(investAmount, lendYearRate, period);
mapPrincipal = Amount2Helper.getPerMonthPrincipal(investAmount, lendYearRate, period);
break;
case THREE:
mapInterest = Amount3Helper.getPerMonthInterest(investAmount, lendYearRate, period);
mapPrincipal = Amount3Helper.getPerMonthPrincipal(investAmount, lendYearRate, period);
break;
default:
mapInterest = Amount4Helper.getPerMonthInterest(investAmount, lendYearRate, period);
mapPrincipal = Amount4Helper.getPerMonthPrincipal(investAmount, lendYearRate, period);
break;
}
// 创建回款计划列表
List<LendItemReturn> lendItemReturnList = new ArrayList<>();
// 遍历还款期数、利息map
for (Entry<Integer, BigDecimal> entry : mapInterest.entrySet()) {
Integer currentPeriod = entry.getKey(); // 当前期数
// 通过当前期数获取到还款计划的id
Long lendReturnId = lendReturnMap.get(currentPeriod);
// 创建回款计划
LendItemReturn lendItemReturn = new LendItemReturn();
// 填充数据
lendItemReturn.setLendReturnId(lendReturnId);
lendItemReturn.setLendItemId(lendItem.getId());
lendItemReturn.setLendId(lendItem.getLendId());
lendItemReturn.setInvestUserId(lendItem.getInvestUserId());
lendItemReturn.setInvestAmount(lendItem.getInvestAmount());
lendItemReturn.setCurrentPeriod(currentPeriod);
lendItemReturn.setLendYearRate(lend.getLendYearRate());
lendItemReturn.setReturnMethod(lend.getReturnMethod());
// filter做过滤 map取属性 reduce做处理
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);
} else {
// 非最后一期,直接从map中获取对应当前期数的本金/利息
lendItemReturn.setPrincipal(mapPrincipal.get(currentPeriod));
lendItemReturn.setInterest(mapInterest.get(currentPeriod));
}
// 回款总金额
lendItemReturn.setTotal(lendItemReturn.getPrincipal().add(lendItemReturn.getInterest()));
// 设置其他属性
lendItemReturn.setFee(BigDecimal.ZERO);
lendItemReturn.setReturnDate(lend.getLendStartDate().plusMonths(currentPeriod));
lendItemReturn.setOverdue(false);
lendItemReturn.setStatus(0);
// 添加到回款计划列表中
lendItemReturnList.add(lendItemReturn);
}
// 批量保存
lendItemReturnService.saveBatch(lendItemReturnList);
// 返回回款计划列表
return lendItemReturnList;
}
LendItemService.java
// 根据标的id和状态获取投资人列表
List<LendItem> listByIdAndStatus(Long lendId, Integer status)
LendItemServiceImpl.java
@Override
public List<LendItem> listByIdAndStatus(Long lendId, Integer status) {
QueryWrapper<LendItem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("lend_id",lendId).eq("status",status);
return baseMapper.selectList(queryWrapper);
}
创建 src/api/core/lend.js
makeLoan(id) {
return request({
url: `/admin/core/lend/makeLoan/${
id}`,
method: 'get'
})
}
src/views/core/lend/list.vue
methods:
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: '已取消放款'
})
}
})
}
还款方式为等额本息,借3个月,标的金额为95000,实际募集2000,有两个人分别投资了1000元
测试时,注意多个表之间的数据关系,尤其是汇付宝的数据,一旦测试失败,需要重新投资的话,需要清空user_invest
表。
lend
user_account
trans_flow
lend_item_return
user_account