参考《汇付宝商户账户技术文档》3.10满标投资,投资过程与账户绑定、用户充值过程一致
step1:点击标的,进入标的详情页面
LendController.java
@ApiOperation("标的详情")
@GetMapping("/getLendDetail/{id}")
public R getLendDetail(@PathVariable Long id) {
Map<String, Object> lendDetail = lendService.getLendDetail(id);
return R.ok().setData("lendDetail", lendDetail);
}
LendService.java
Map<String, Object> getLendDetail(Long id);
LendServiceImpl.java
@Resource
private BorrowerService borrowerService;
@Override
public Map<String, Object> getLendDetail(Long id) {
// 获取表的信息
Lend lend = baseMapper.selectById(id);
setExtensionLend(lend);
QueryWrapper<Borrower> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", lend.getUserId());
Borrower borrower = borrowerService.getBaseMapper().selectOne(queryWrapper);
// 获取借款人信息
BorrowerDetailVO borrowerDetailVO = borrowerService.getBorrowerDetailVO(borrower.getId());
Map<String, Object> map = new HashMap<>();
map.put("lend", lend);
map.put("borrower", borrowerDetailVO);
return map;
}
pages/lend/_id.vue
以后也会上传到码云…
pages/lend/_id.vue
// 获取标的详情
// 希望搜索引擎能收录,所以使用异步查询的方式
async asyncData({
$axios, params }) {
let lendId = params.id
let response = await $axios.$get('/api/core/lend/getLendDetail/' + lendId)
return {
lend: response.data.lendDetail.lend,
borrower: response.data.lendDetail.borrower,
}
},
//此时方法在客户端的浏览器中执行,可以获取到cookie
mounted() {
//查询账户余额
this.fetchAmount()
//判断登录人的用户类型
this.fetchUserType()
},
methods: {
//查询账户余额
fetchAmount() {
let userInfo = cookie.get('userInfo')
if (userInfo) {
this.$axios
.$get('/api/core/userAccount/auth/getAmount')
.then((response) => {
this.amount = response.data.amount
})
}
},
//获取登录人的用户类型
fetchUserType() {
let userInfo = cookie.get('userInfo')
if (userInfo) {
userInfo = JSON.parse(userInfo)
this.userType = userInfo.userType
}
},
},
step2:输入投资金额,计算获得收益
等额本息法最重要的一个特点是每月的还款额相同,从本质上来说是本金所占比例逐月递增,利息所占比例逐月递减,月还款数不变。
即在月供“本金与利息”的分配比例中,前半段时期所还的利息比例大、本金比例小,还款期限过半后逐步转为本金比例大、利息比例小。
计算公式为:
每月利息 = 剩余本金 x 贷款月利率
每月还本付息金额 = 还款总额 / 贷款月数
每月本金 = 每月还本付息金额 - 每月利息
注意:在等额本息法中,银行一般先收剩余本金利息,后收本金,所以利息在月供款中的比例会随本金的减少而降低,本金在月供款中的比例因而升高,但月供总额保持不变。
等额本金法最大的特点是每月的还款额不同,呈现逐月递减的状态;它是将贷款本金按还款的总月数均分,再加上上期剩余本金的利息,这样就形成月还款额,所以等额本金法第一个月的还款额最多 ,然后逐月减少,越还越少。
计算公式为:
每月利息 = 剩余本金 x 贷款月利率
每月本金 = 贷款额 / 贷款月数
每月还本付息金额 = 每月本金 + 每月利息
注意:在等额本金法中,人们每月归还的本金额始终不变,利息随剩余本金的减少而减少,因而其每月还款额逐渐减少。
按期付息到期还本是借款人在贷款到期日一次性归还贷款本金,利息按期归还
计算公式为:
每月利息 = 贷款额 x 贷款月利率
总利息 = 每月利息 x 贷款月数
一次还本付息是贷款到期后一次性归还本金和利息
计算公式为:
还款金额 = 贷款额 + 贷款额 x 月利率 x 贷款月数
LendController.java
@ApiOperation("计算投资收益")
@GetMapping("/getInterestCount/{invest}/{yearRate}/{totalMonth}/{returnMethod}")
public R getInterestCount(
@ApiParam(value = "投资金额", required = true)
@PathVariable("invest") BigDecimal invest,
@ApiParam(value = "年化收益", required = true)
@PathVariable("yearRate") BigDecimal yearRate,
@ApiParam(value = "期数", required = true)
@PathVariable("totalMonth") Integer totalMonth,
@ApiParam(value = "还款方式", required = true)
@PathVariable("returnMethod") Integer returnMethod){
BigDecimal interestCount = lendService.getInterestCount(invest, yearRate,totalMonth,returnMethod);
return R.ok().setData("interestCount",interestCount);
}
ReturnMethodEnum.java
public static ReturnMethodEnum getByValue(Integer value){
for(ReturnMethodEnum transactType : values()){
if (transactType.getMethod() == value) {
return transactType;
}
}
return null;
}
根据我们的表设计,出借人需要知道每月回款的本金与利息,借款人也一样,他也要知道每月的还款本金与利息,还有我们需要计算投资人的投资收益等数据。
因此我们将四种还款方式工具类设计如下:
说明:还款方式计算复杂,仅做了解,这里不做详细介绍
后续会上传码云,有急需的可以私信
LendService.java
// 根据用户的还款方式,来计算不同的收益
BigDecimal getInterestCount(BigDecimal invest, BigDecimal yearRate, Integer totalMonth, Integer returnMethod);
LendServiceImpl.java
@Override
public BigDecimal getInterestCount(BigDecimal invest, BigDecimal yearRate, Integer totalMonth, Integer returnMethod) {
BigDecimal interestCount;
switch (ReturnMethodEnum.getByValue(returnMethod.intValue())) {
case ONE:
interestCount = Amount1Helper.getInterestCount(invest, yearRate, totalMonth);
break;
case TWO:
interestCount = Amount2Helper.getInterestCount(invest, yearRate, totalMonth);
break;
case THREE:
interestCount = Amount3Helper.getInterestCount(invest, yearRate, totalMonth);
break;
default:
interestCount = Amount4Helper.getInterestCount(invest, yearRate, totalMonth);
break;
}
return interestCount;
}
pages/lend/_id.vue
//计算收益
getInterestCount() {
this.$axios
.$get(
`/api/core/lend/getInterestCount/${
this.invest.investAmount}/${
this.lend.lendYearRate}/${
this.lend.period}/${
this.lend.returnMethod}`
)
.then((response) => {
this.interestCount = response.data.interestCount
})
},
step3:同意协议,点击立即投资
step5:汇付宝验证用户交易密码
step6:汇付宝修改账号资金余额(更新user_account记录中的amount的值和freeze_amount的值)
汇付宝新增投资记录(新增user_invest记录)
InvestVO.java
@Data
public class InvestVO {
private Long lendId; // 标的id
private Long investUserId; // 投资人id
private String investUserName; // 投资人姓名
private BigDecimal investAmount; // 投资金额
}
LendItemController.java
@Api("标的项")
@RestController
@RequestMapping("/api/core/lendItem")
@Slf4j
public class LendItemController {
@Resource
private LendItemService lendItemService;
@ApiOperation("我要投资")
@PostMapping("/auth/invest")
public R invest(@RequestBody InvestVO investVO, HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
String userName = JwtUtils.getUserName(token);
investVO.setInvestUserId(userId);
investVO.setInvestUserName(userName);
String formStr = lendItemService.invest(investVO);
return R.ok().setData("formStr", formStr);
}
}
UserTypeEnum.java
@AllArgsConstructor
@Getter
public enum UserTypeEnum {
INVESTOR(1, "投资人"),
BORROWER(2, "借款人");
private Integer status;
private String msg;
}
LendItemEnum.java
@Getter
@AllArgsConstructor
public enum LendItemEnum {
NO_PAY(0, "未支付"),
PAY_OK(1, "已支付");
Integer status;
String msg;
}
LendItemService.java
String invest(InvestVO investVO);
LendItemServiceImpl.java
@Resource
private LendService lendService;
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private LendMapper lendMapper;
@Resource
private UserAccountService userAccountService;
@Resource
private UserBindService userBindService;
@Override
public String invest(InvestVO investVO) {
// 标的状态必须是募资中
Lend lend = lendMapper.selectById(investVO.getLendId());
Assert.isTrue(lend.getStatus() == LendStatusEnum.INVEST_RUN.getStatus(), ResponseEnum.LEND_INVEST_ERROR);
// 用户必须是投资人
UserInfo userInfo = userInfoMapper.selectById(investVO.getInvestUserId());
Assert.isTrue(userInfo.getUserType() == UserTypeEnum.INVESTOR.getStatus(), ResponseEnum.LEND_USER_TYPE_ERROR);
// 账户余额必须大于等于投资金额
BigDecimal amount = userAccountService.getAmount(investVO.getInvestUserId());
Assert.isTrue(amount.doubleValue() >= investVO.getInvestAmount().doubleValue(), ResponseEnum.NOT_SUFFICIENT_FUNDS_ERROR);
// 投资金额必须是100的正整数倍
Assert.isTrue(
investVO.getInvestAmount().remainder(new BigDecimal(100)).equals(new BigDecimal(0)) && investVO.getInvestAmount().doubleValue() > 0,
ResponseEnum.LEND_INVEST_AMOUNT_ERROR
);
// 投资金额 + 已投金额 <= 募资金额
BigDecimal sum = investVO.getInvestAmount().add(lend.getInvestAmount());
Assert.isTrue(sum.doubleValue() <= lend.getAmount().doubleValue(), ResponseEnum.LEND_FULL_SCALE_ERROR);
// 生成投资项对应的投资信息
LendItem lendItem = new LendItem();
lendItem.setLendItemNo(LendNoUtils.getLendItemNo());
lendItem.setLendId(investVO.getLendId());
lendItem.setInvestUserId(investVO.getInvestUserId());
lendItem.setInvestName(investVO.getInvestUserName());
lendItem.setInvestAmount(investVO.getInvestAmount());
lendItem.setLendYearRate(lend.getLendYearRate());
lendItem.setInvestTime(LocalDateTime.now());
lendItem.setLendStartDate(lend.getLendStartDate());
lendItem.setLendEndDate(lend.getLendEndDate());
BigDecimal exceptAmount = lendService.getInterestCount(
investVO.getInvestAmount(),
lend.getLendYearRate(),
lend.getPeriod(),
lend.getReturnMethod()
);
lendItem.setExpectAmount(exceptAmount);
BigDecimal realAmount = lendService.getInterestCount(
investVO.getInvestAmount(),
lend.getServiceRate(),
lend.getPeriod(),
lend.getReturnMethod()
);
lendItem.setRealAmount(realAmount);
lendItem.setStatus(LendItemEnum.NO_PAY.getStatus());
baseMapper.insert(lendItem);
// 组装汇付宝参数
Map<String, Object> map = new HashMap<>();
String voteBindCode = userBindService.getBindCodeByUserId(lendItem.getInvestUserId());
String benefitBindCode = userBindService.getBindCodeByUserId(lend.getUserId());
map.put("agentId", HfbConst.AGENT_ID);
map.put("voteBindCode", voteBindCode);
map.put("benefitBindCode", benefitBindCode);
map.put("agentProjectCode", lend.getLendNo());
map.put("agentProjectName", lend.getTitle());
map.put("agentBillNo", lendItem.getLendItemNo());
map.put("voteAmt", lendItem.getInvestAmount());
map.put("votePrizeAmt", "0");
map.put("voteFeeAmt", "0");
map.put("projectAmt", lend.getAmount());
map.put("note", "");
map.put("notifyUrl", HfbConst.INVEST_NOTIFY_URL);
map.put("returnUrl", HfbConst.INVEST_RETURN_URL);
map.put("timestamp", RequestHelper.getTimestamp());
map.put("sign", RequestHelper.getSign(map));
String formStr = FormHelper.buildForm(HfbConst.INVEST_URL, map);
return formStr;
}
创建一个通用的Service方法,以方便调用,根据userId获取用户绑定账号
UserBindService.java
String getBindCodeByUserId(Long userId);
UserBindServiceImpl.java
@Override
public String getBindCodeByUserId(Long userId) {
QueryWrapper<UserBind> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id",userId);
UserBind userBind = baseMapper.selectOne(queryWrapper);
return userBind.getBindCode();
}
//投资
commitInvest() {
//校验用户是否登录
let userInfo = cookie.get('userInfo')
// console.log(typeof userInfo)
// console.log(!userInfo) //true
if (!userInfo) {
window.location.href = '/login'
return
}
//校验当前用户是否是投资人
let userInfoObj = JSON.parse(userInfo)
if (userInfoObj.userType == 2) {
//借款人
this.$message.error('借款人无法投资')
return
}
console.log(this.lend.investAmount)
console.log(this.invest.investAmount)
console.log(this.lend.amount)
//判断标的是否超卖:标的已投金额 + 本次投资金额 > 标的总金额
if (
this.lend.investAmount + Number(this.invest.investAmount) >
this.lend.amount
) {
this.$message.error('标的可投资金额不足')
return
}
//是否是100的整数倍
// console.log(this.invest.investAmount)
// console.log(Number(this.invest.investAmount))
// console.log(typeof Number(this.invest.investAmount))
// return
if (
Number(this.invest.investAmount) <= 0 ||
this.invest.investAmount % this.lend.lowestAmount != 0
) {
this.$message.error(`投资金额必须是${
this.lend.lowestAmount}的整数倍`)
return
}
//余额的判断
if (this.invest.investAmount > this.amount) {
this.$message.error('余额不足,请充值')
return
}
//数据提交
this.$alert(
'您即将前往汇付宝确认标的',
'前往汇付宝资金托管平台',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '立即前往',
callback: (action) => {
console.log('action', action)
if (action === 'confirm') {
this.invest.lendId = this.lend.id
this.$axios
.$post('/api/core/lendItem/auth/invest', this.invest)
.then((response) => {
// console.log(response.data.formStr)
// debugger
document.write(response.data.formStr)
})
}
},
}
)
},
},
step7:异步回调
(1)账户金额更改(剩余金额和冻结金额)
(2)修改投资状态(lend_item表中的status)
(3)更新标的信息(lend表中的投资人数和已投金额)
(4)添加交易流水
step8:用户点击“返回平台”,返回尚融宝
LendItemController.java
@ApiOperation("投资回调")
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
Map<String, Object> map = RequestHelper.switchMap(request.getParameterMap());
// 校验签名
if (RequestHelper.isSignEquals(map)) {
if("0001".equals(map.get("resultCode"))){
lendItemService.notify(map);
}else{
log.info("用户投资异步回调失败:" + JSON.toJSONString(map));
return "fail";
}
} else {
log.info("用户投资异步回调签名错误:" + JSON.toJSONString(map));
return "fail";
}
return "success";
}
LendItemService.java
void notify(Map<String, Object> map);
LendItemServiceImpl.java
@Resource
private UserAccountMapper userAccountMapper;
@Resource
private TransFlowService transFlowService;
@Override
public void notify(Map<String, Object> map) {
String agentBillNo = (String) map.get("agentBillNo");
if (transFlowService.haveTransFlow(agentBillNo)) {
log.warn("幂等性返回");
return;
}
String bindCode = (String) map.get("voteBindCode");
BigDecimal amount = new BigDecimal((String) map.get("voteAmt"));
// 更新账户余额、冻结金额
userAccountMapper.updateAccount(bindCode, new BigDecimal("-" + map.get("voteAmt")), amount);
// 更新标的项状态
LendItem lendItem = this.getLendItem(agentBillNo);
lendItem.setStatus(LendItemEnum.PAY_OK.getStatus());
baseMapper.updateById(lendItem);
// 更新标的人数、已筹金额
Lend lend = lendMapper.selectById(lendItem.getLendId());
lend.setInvestAmount(lend.getInvestAmount().add(amount));
lend.setInvestNum(lend.getInvestNum() + 1);
lendMapper.updateById(lend);
// 添加流水
TransFlowBO transFlowBO = new TransFlowBO(
agentBillNo,
bindCode,
TransTypeEnum.INVEST_LOCK,
amount,
"项目编号:"+lend.getLendNo()+",项目名称:"+lend.getTitle()
);
transFlowService.saveTransFlow(transFlowBO);
}
/**
* 先提取出来,以后需要的话再公开
* @param agentBillNo
* @return
*/
private LendItem getLendItem(String agentBillNo) {
QueryWrapper<LendItem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("lend_item_no", agentBillNo);
return baseMapper.selectOne(queryWrapper);
}