27、放款
27.1、需求介绍
27.1.1、平台放款
1、需求描述
标的募资时间到,平台会操作放款或撤标,如果达到放款条件则操作放款
说明:撤标过程与放款过程一致,处理业务相对简单,只是将出借金额返回给出借人即可,同学可以自行完成
2、相关数据库表
还款人还款记录:lend_return
投资人回款记录:lend_item_return
3、参考文档
参考《汇付宝商户账户技术文档》3.11满标放款
27.1.2、具体步骤
step1:放款
放款是一个同步接口,如果放款返回成功,那么我们要处理平台业务
step2:处理业务如下
(1)标的状态和标的平台收益
(2)给借款账号转入金额
(3)增加借款交易流水
(4)解冻并扣除投资人资金
(5)增加投资人交易流水
(6)生成借款人还款计划和出借人回款计划
27.2、管理平台放款
27.2.1、后端接口
1、Controller
AdminLendController
@ApiOperation("放款")
@GetMapping("/makeLoan/{id}")
public R makeLoan(
@ApiParam(value = "标的id", required = true)
@PathVariable("id") Long id) {
lendService.makeLoan(id);
return R.ok().message("放款成功");
}
2、满标放款
接口:LendService
/**
* 满标放款
* @param lendId
*/
void makeLoan(Long lendId);
实现:LendServiceImpl
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private UserAccountMapper userAccountMapper;
@Resource
private LendItemService lendItemService;
@Resource
private TransFlowService transFlowService;
@Transactional(rollbackFor = Exception.class)
@Override
public void makeLoan(Long lendId) {
//获取标的信息
Lend lend = baseMapper.selectById(lendId);
//放款接口调用
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("agentId", HfbConst.AGENT_ID);
paramMap.put("agentProjectCode", lend.getLendNo());//标的编号
String agentBillNo = LendNoUtils.getLoanNo();//放款编号
paramMap.put("agentBillNo", agentBillNo);
//平台收益,放款扣除,借款人借款实际金额=借款金额-平台收益
//月年化
BigDecimal monthRate = lend.getServiceRate().divide(new BigDecimal(12), 8, RoundingMode.DOWN);
//平台实际收益 = 已投金额 * 月年化 * 标的期数
BigDecimal realAmount = lend.getInvestAmount().multiply(monthRate).multiply(new BigDecimal(lend.getPeriod()));
paramMap.put("mchFee", realAmount); //商户手续费(平台实际收益)
paramMap.put("timestamp", RequestHelper.getTimestamp());
String sign = RequestHelper.getSign(paramMap);
paramMap.put("sign", sign);
log.info("放款参数:" + JSONObject.toJSONString(paramMap));
//发送同步远程调用
JSONObject result = RequestHelper.sendRequest(paramMap, HfbConst.MAKE_LOAD_URL);
log.info("放款结果:" + result.toJSONString());
//放款失败
if (!"0000".equals(result.getString("resultCode"))) {
throw new BusinessException(result.getString("resultMsg"));
}
//更新标的信息
lend.setRealAmount(realAmount);
lend.setStatus(LendStatusEnum.PAY_RUN.getStatus());
lend.setPaymentTime(LocalDateTime.now());
baseMapper.updateById(lend);
//获取借款人信息
Long userId = lend.getUserId();
UserInfo userInfo = userInfoMapper.selectById(userId);
String bindCode = userInfo.getBindCode();
//给借款账号转入金额
BigDecimal total = new BigDecimal(result.getString("voteAmt"));
userAccountMapper.updateAccount(bindCode, total, new BigDecimal(0));
//新增借款人交易流水
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
bindCode,
total,
TransTypeEnum.BORROW_BACK,
"借款放款到账,编号:" + lend.getLendNo());//项目编号
transFlowService.saveTransFlow(transFlowBO);
//获取投资列表信息
List<LendItem> lendItemList = lendItemService.selectByLendId(lendId, 1);
lendItemList.stream().forEach(item -> {
//获取投资人信息
Long investUserId = item.getInvestUserId();
UserInfo investUserInfo = userInfoMapper.selectById(investUserId);
String investBindCode = investUserInfo.getBindCode();
//投资人账号冻结金额转出
BigDecimal investAmount = item.getInvestAmount(); //投资金额
userAccountMapper.updateAccount(investBindCode, new BigDecimal(0), investAmount.negate());
//新增投资人交易流水
TransFlowBO investTransFlowBO = new TransFlowBO(
LendNoUtils.getTransNo(),
investBindCode,
investAmount,
TransTypeEnum.INVEST_UNLOCK,
"冻结资金转出,出借放款,编号:" + lend.getLendNo());//项目编号
transFlowService.saveTransFlow(investTransFlowBO);
});
//放款成功生成借款人还款计划和投资人回款计划
// TODO
}
3、根据lendId获取投资记录
接口:LendItemService
List<LendItem> selectByLendId(Long lendId, Integer status);
实现:LendItemServiceImpl
@Override
public List<LendItem> selectByLendId(Long lendId, Integer status) {
QueryWrapper<LendItem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("lend_id", lendId);
queryWrapper.eq("status", status);
return baseMapper.selectList(queryWrapper);
}
27.2.2、前端整合
srb-admin
1、定义api
创建 src/api/core/lend.js
makeLoan(id) {
return request({
url: `/admin/core/lend/makeLoan/${id}`,
method: 'get'
})
}
2、页面模板
src/views/core/lend/list.vue
<el-button v-if="scope.row.status == 1" type="warning" size="mini" @click="makeLoan(scope.row.id)">
放款
</el-button>
3、页面脚本
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: '已取消放款'
})
}
})
}
27.3、还款计划和回款计划
27.31、生成还款计划
LendServiceImpl
@Resource
private LendReturnService lendReturnService;
@Resource
private LendItemReturnService lendItemReturnService;
/**
* 生成还款计划
* @param lend 标的对象
*/
private void repaymentPlan(Lend lend) {
// 还款计划列表
List<LendReturn> lendReturnList = new ArrayList<>();
// 按还款时间生成还款计划
int len = lend.getPeriod();
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.setFee(new BigDecimal(0));
// 第二个月开始还款
lendReturn.setReturnDate(lend.getLendStartDate().plusMonths(i));
lendReturn.setOverdue(false);
// 最后一个月标识为最后一次还款
lendReturn.setLast(i == len);
lendReturn.setStatus(0);
lendReturnList.add(lendReturn);
}
// 批量保存
lendReturnService.saveBatch(lendReturnList);
// 获取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.selectByLendId(lend.getId(), 1);
for (LendItem lendItem : lendItemList) {
// 创建回款计划列表
List<LendItemReturn> lendItemReturnList = this.returnInvest(lendItem, lendReturnMap, lend);
lendItemReturnAllList.addAll(lendItemReturnList);
}
// 更新还款计划中的相关金额数据
for (LendReturn lendReturn : lendReturnList) {
BigDecimal sumPrincipal = lendItemReturnAllList.stream()
// 过滤条件:当回款计划中的还款计划id == 当前还款计划id的时候
.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 sumTotal = 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(sumTotal);
}
lendReturnService.updateBatchById(lendReturnList);
}
27.3.2、生成回款计划
LendServiceImpl
/**
* 生成回款记录
* @param lendItem 出借记录
* @param lendReturnMap 还款期数与还款计划ID对应Map
* @param lend 标的对象
* @return 回款记录集合
*/
public List<LendItemReturn> returnInvest(LendItem lendItem, Map<Integer, Long> lendReturnMap, Lend lend) {
// 投资金额
BigDecimal amount = lendItem.getInvestAmount();
// 年化利率
BigDecimal yearRate = lendItem.getLendYearRate();
// 投资期数
int totalMonth = lend.getPeriod();
// 还款期数 -> 利息
Map<Integer, BigDecimal> mapInterest;
// 还款期数 -> 本金
Map<Integer, BigDecimal> mapPrincipal;
// 根据还款方式计算本金和利息
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);
}
// 创建回款计划列表
List<LendItemReturn> lendItemReturnList = new ArrayList<>();
for (Map.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.setInvestUserId(lendItem.getInvestUserId());
lendItemReturn.setLendId(lendItem.getLendId());
lendItemReturn.setInvestAmount(lendItem.getInvestAmount());
lendItemReturn.setLendYearRate(lend.getLendYearRate());
lendItemReturn.setCurrentPeriod(currentPeriod);
lendItemReturn.setReturnMethod(lend.getReturnMethod());
// 最后一次本金,利息计算
if (lendItemReturnList.size() > 0 && 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.setPrincipal(lastPrincipal);
lendItemReturn.setInterest(lastInterest);
} 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;
}
27.3.3、在makeLoan中调用
LendServiceImpl
//放款成功生成借款人还款计划和投资人回款计划
this.repaymentPlan(lend);
28、投资列表展示
28.1、管理端显示投资记录
28.1.1、后端接口
1、Controller
创建 AdminLendItemController
package com.atguigu.srb.core.controller.admin;
@Api(tags = "标的的投资")
@RestController
@RequestMapping("/admin/core/lendItem")
@Slf4j
public class AdminLendItemController {
@Resource
private LendItemService lendItemService;
@ApiOperation("获取列表")
@GetMapping("/list/{lendId}")
public R list(
@ApiParam(value = "标的id", required = true)
@PathVariable Long lendId) {
List<LendItem> list = lendItemService.selectByLendId(lendId);
return R.ok().data("list", list);
}
}
2、Service
接口:LendItemService
List<LendItem> selectByLendId(Long lendId);
实现:LendItemServiceImpl
@Override
public List<LendItem> selectByLendId(Long lendId) {
QueryWrapper<LendItem> queryWrapper = new QueryWrapper();
queryWrapper.eq("lend_id", lendId);
List<LendItem> lendItemList = baseMapper.selectList(queryWrapper);
return lendItemList;
}
28.1.2、前端
1、创建api
api/core/lend-item.js
import request from '@/utils/request'
export default {
getList(lendId) {
return request({
url: `/admin/core/lendItem/list/` + lendId,
method: 'get'
})
}
}
2、页面脚本
views/core/lend/detail.vue
import lendItemApi from '@/api/core/lend-item'
data() {
return {
......,
lendItemList: [] //投资列表
}
},
created() {
if (this.$route.params.id) {
......
// 投资记录
this.fetchLendItemList()
}
},
methods
fetchLendItemList() {
lendItemApi.getList(this.$route.params.id).then(response => {
this.lendItemList = response.data.list
})
}
3、页面模板
views/core/lend/detail.vue
将投资记录放在借款人信息后面
<h4>投资记录</h4>
<el-table :data="lendItemList" stripe style="width: 100%" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="lendItemNo" label="投资编号" />
<el-table-column prop="investName" label="投资用户" />
<el-table-column prop="investAmount" label="投资金额" />
<el-table-column label="年化利率">
<template slot-scope="scope">
{{ scope.row.lendYearRate * 100 }}%
</template>
</el-table-column>
<el-table-column prop="investTime" label="投资时间" />
<el-table-column prop="lendStartDate" label="开始日期" />
<el-table-column prop="lendEndDate" label="结束日期" />
<el-table-column prop="expectAmount" label="预期收益" />
<el-table-column prop="investTime" label="投资时间" />
</el-table>
28.2、网站端显示投资记录
28.2.1、后端接口
Controller
LendItemController
@ApiOperation("获取列表")
@GetMapping("/list/{lendId}")
public R list(
@ApiParam(value = "标的id", required = true)
@PathVariable Long lendId) {
List<LendItem> list = lendItemService.selectByLendId(lendId);
return R.ok().data("list", list);
}
28.2.2、前端
页面脚本
pages/lend/_id.vue
async asyncData({ $axios, params }) {
......
//投资记录
let responseLendItemList = await $axios.$get(
'/api/core/lendItem/list/' + lendId
)
return {
......,
lendItemList: responseLendItemList.data.list, //投资记录
}
},
28.3、管理端显示还款计划
28.3.1、后端接口
1、Controller
创建AdminLendReturnController
package com.atguigu.srb.core.controller.admin;
@Api(tags = "还款记录")
@RestController
@RequestMapping("/admin/core/lendReturn")
@Slf4j
public class AdminLendReturnController {
@Resource
private LendReturnService lendReturnService;
@ApiOperation("获取列表")
@GetMapping("/list/{lendId}")
public R list(
@ApiParam(value = "标的id", required = true)
@PathVariable Long lendId) {
List<LendReturn> list = lendReturnService.selectByLendId(lendId);
return R.ok().data("list", list);
}
}
2、Service
接口:LendReturnService
List<LendReturn> selectByLendId(Long lendId);
实现:LendReturnServiceImpl
@Override
public List<LendReturn> selectByLendId(Long lendId) {
QueryWrapper<LendReturn> queryWrapper = new QueryWrapper();
queryWrapper.eq("lend_id", lendId);
List<LendReturn> lendReturnList = baseMapper.selectList(queryWrapper);
return lendReturnList;
}
28.3.2、前端
1、创建api
api/core/lend-return.js
import request from '@/utils/request'
export default {
getList(lendId) {
return request({
url: `/admin/core/lendReturn/list/` + lendId,
method: 'get'
})
}
}
2、页面脚本
views/core/lend/detail.vue
import lendReturnApi from '@/api/core/lend-return'
data() {
return {
......,
lendReturnList: [] //还款计划列表
}
},
created() {
if (this.$route.params.id) {
......
// 还款计划
this.fetchLendReturnList()
}
},
methods
fetchLendReturnList() {
lendReturnApi.getList(this.$route.params.id).then(response => {
this.lendReturnList = response.data.list
})
}
3、页面模板
views/core/lend/detail.vue
将还款计划放在投资记录后面
<h4>还款计划</h4>
<el-table :data="lendReturnList" stripe style="width: 100%" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="currentPeriod" label="当前的期数" />
<el-table-column prop="principal" label="本金" />
<el-table-column prop="interest" label="利息" />
<el-table-column prop="total" label="本息" />
<el-table-column prop="returnDate" label="还款日期" width="150" />
<el-table-column prop="realReturnTime" label="实际还款时间" />
<el-table-column label="是否逾期">
<template slot-scope="scope">
<span v-if="scope.row.overdue">
是(逾期金额:{{ scope.row.overdueTotal }}元)
</span>
<span v-else>否</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template slot-scope="scope">
{{ scope.row.status === 0 ? '未还款' : '已还款' }}
</template>
</el-table-column>
</el-table>
28.4、网站端显示还款计划
28.4.1、后端接口
Controller
创建 LendReturnController
package com.atguigu.srb.core.controller.api;
@Api(tags = "还款计划")
@RestController
@RequestMapping("/api/core/lendReturn")
@Slf4j
public class LendReturnController {
@Resource
private LendReturnService lendReturnService;
@ApiOperation("获取列表")
@GetMapping("/list/{lendId}")
public R list(
@ApiParam(value = "标的id", required = true)
@PathVariable Long lendId) {
List<LendReturn> list = lendReturnService.selectByLendId(lendId);
return R.ok().data("list", list);
}
}
28.4.2、前端
页面脚本
pages/lend/_id.vue
async asyncData({ $axios, params }) {
......
//还款计划
let responseLendReturnList = await $axios.$get(
'/api/core/lendReturn/list/' + lendId
)
return {
......,
lendReturnList: responseLendReturnList.data.list, //还款计划
}
},
28.5、网站端显示回款计划
28.5.1、后端接口
1、Controller
创建 LendItemReturnController
package com.atguigu.srb.core.controller.api;
@Api(tags = "回款计划")
@RestController
@RequestMapping("/api/core/lendItemReturn")
@Slf4j
public class LendItemReturnController {
@Resource
private LendItemReturnService lendItemReturnService;
@ApiOperation("获取列表")
@GetMapping("/auth/list/{lendId}")
public R list(
@ApiParam(value = "标的id", required = true)
@PathVariable Long lendId, HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
List<LendItemReturn> list = lendItemReturnService.selectByLendId(lendId, userId);
return R.ok().data("list", list);
}
}
2、Service
接口:LendItemReturnService
List<LendItemReturn> selectByLendId(Long lendId, Long userId);
实现:LendItemReturnServiceImpl
@Override
public List<LendItemReturn> selectByLendId(Long lendId, Long userId) {
QueryWrapper<LendItemReturn> queryWrapper = new QueryWrapper<>();
queryWrapper
.eq("lend_id", lendId)
.eq("invest_user_id", userId)
.orderByAsc("current_period");
return baseMapper.selectList(queryWrapper);
}
28.5.2、前端
1、页面脚本
pages/lend/_id.vue
data() {
return {
......,
lendItemReturnList: [], //回款计划
}
},
mounted() {
......
//回款计划
this.fetchLendItemReturnList()
},
methods
//回款计划
fetchLendItemReturnList() {
this.$axios
.$get('/api/core/lendItemReturn/auth/list/' + this.$route.params.id)
.then((response) => {
this.lendItemReturnList = response.data.list
})
},
2、页面模板
pages/lend/_id.vue
<!-- 回款计划 -->
<div class="item-detail-body clearfix mrt30 ui-tab">
<div class="ui-tab-nav hd">
<ul>
<li class="nav_li active">
<a href="javascript:;">回款计划</a>
</li>
</ul>
</div>
<div class="bd">
<div class="ui-tab-item active" style="display: block;">
<div class="repayment-list">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<thead>
<tr>
<th>期数</th>
<th>本金(元)</th>
<th>利息(元)</th>
<th>本息(元)</th>
<th>计划回款日期</th>
<th>实际回款日期</th>
<th>状态</th>
<th>是否逾期</th>
</tr>
</thead>
<tbody id="repayment_content">
<tr
v-for="lendItemReturn in lendItemReturnList"
:key="lendItemReturn.id"
>
<td>{{ lendItemReturn.currentPeriod }}</td>
<td class="c-orange">¥{{ lendItemReturn.principal }}</td>
<td class="c-orange">¥{{ lendItemReturn.interest }}</td>
<td class="c-orange">¥{{ lendItemReturn.total }}</td>
<td>{{ lendItemReturn.returnDate }}</td>
<td>{{ lendItemReturn.realReturnTime }}</td>
<td>
{{ lendItemReturn.status === 0 ? '未还款' : '已还款' }}
</td>
<td>
<span v-if="lendItemReturn.overdue">
是(逾期金额:{{ lendReturn.overdueTotal }}元)
</span>
<span v-else>否</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
本文章参考B站 尚硅谷《尚融宝》Java微服务分布式金融项目,仅供个人学习使用,部分内容为本人自己见解,与尚硅谷无关。