核心业务9:还款业务实现
1.还款业务前端
2.还款业务流程图
3.汇付宝接口调用
4.尚融宝接口调用
5.首页信息展示的完善
6.资金记录展示
核心业务9:还款业务实现
1.还款业务前端
- 按钮
pages/lend/_id.vue
<td>
<a href="javascript:" @click="commitReturn(lendReturn.id)">
{{ lendReturn.status === 0 ? '还款' : '' }}
</a>
</td>
- 页面脚本
pages/lend/_id.vue
//绑定的方法
commitReturn(lendReturnId) {
this.$alert(
'<div style="size: 18px;color: red;">您即将前往汇付宝确认还款</div>',
'前往汇付宝资金托管平台',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '立即前往',
callback: (action) => {
if (action === 'confirm') {
this.$axios
.$post('/api/core/lendReturn/auth/commitReturn/' + lendReturnId)
.then((response) => {
document.write(response.data.formStr)
})
}
},
}
)
}
2.还款业务流程图
①还款业务流程图
3.汇付宝接口调用
①提交表单
②返回数据
③汇付宝数据库
-
user_item_return
-
user_return
-
用户提交表单后
对对应借款人账户和投资人账户进行扣钱和加钱。
生成还款流水和借款流水。
4.尚融宝接口调用
①接口
②业务代码
- controller
com/atguigu/srb/core/controller/api/LendReturnController.java
@ApiOperation("用户还款")
@PostMapping("/auth/commitReturn/{lendReturnId}")
public R commitReturn(
@ApiParam(value = "还款计划id", required = true)
@PathVariable Long lendReturnId, HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
String formStr = lendReturnService.commitReturn(lendReturnId, userId);
return R.ok().data("formStr", formStr);
}
@ApiOperation("还款异步回调")
@PostMapping("/notifyUrl")
public String notifyUrl(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"))) {
lendReturnService.notify(paramMap);
} else {
log.info("还款异步回调失败:" + JSON.toJSONString(paramMap));
return "fail";
}
} else {
log.info("还款异步回调签名错误:" + JSON.toJSONString(paramMap));
return "fail";
}
return "success";
}
- service
com/atguigu/srb/core/service/LendReturnService.java
String commitReturn(Long lendReturnId, Long userId);
void notify(Map<String, Object> paramMap);
/**
* @param lendReturnId:
* @param userId:
* @return String
* @author Likejin
* @description 还款操作
* @date 2023/4/19 15:51
*/
@Override
public String commitReturn(Long lendReturnId, Long userId) {
//还款记录
LendReturn lendReturn = baseMapper.selectById(lendReturnId);
//校验账户余额是否够
BigDecimal account = userAccountService.getAccount(userId);
Assert.isTrue(account.doubleValue()>=lendReturn.getTotal().doubleValue(), ResponseEnum.NOT_SUFFICIENT_FUNDS_ERROR);
//标的记录
Lend lend = lendMapper.selectById(lendReturn.getLendId());
String bindCode = userBindService.getBindCodeByUserId(userId);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("agentId", HfbConst.AGENT_ID);
//商户商品名称
paramMap.put("agentGoodsName", lend.getTitle());
//批次号
paramMap.put("agentBatchNo",lendReturn.getReturnNo());
//还款人绑定协议号
paramMap.put("fromBindCode", bindCode);
//还款总额
paramMap.put("totalAmt", lendReturn.getTotal());
paramMap.put("note", "");
//还款明细
List<Map<String, Object>> lendItemReturnDetailList = lendItemReturnService.addReturnDetail(lendReturnId);
//封装为json对象
paramMap.put("data", JSONObject.toJSONString(lendItemReturnDetailList));
paramMap.put("voteFeeAmt", new BigDecimal(0));
paramMap.put("notifyUrl", HfbConst.BORROW_RETURN_NOTIFY_URL);
paramMap.put("returnUrl", HfbConst.BORROW_RETURN_RETURN_URL);
paramMap.put("timestamp", RequestHelper.getTimestamp());
String sign = RequestHelper.getSign(paramMap);
paramMap.put("sign", sign);
//构建自动提交表单
String formStr = FormHelper.buildForm(HfbConst.BORROW_RETURN_URL, paramMap);
return formStr;
}
/**
* @param paramMap:
* @return void
* @author Likejin
* @description 还款回调
* @date 2023/4/19 16:25
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void notify(Map<String, Object> paramMap) {
log.info("还款成功");
//幂等性判断
//还款编号
String agentBatchNo = (String)paramMap.get("agentBatchNo");
boolean result = transFlowService.isSaveTransFlow(agentBatchNo);
if(result){
log.warn("幂等性返回");
return;
}
//获取还款记录
QueryWrapper<LendReturn> lendReturnQueryWrapper = new QueryWrapper<>();
lendReturnQueryWrapper.eq("return_no",agentBatchNo);
LendReturn lendReturn = baseMapper.selectOne(lendReturnQueryWrapper);
//更新还款状态
lendReturn.setStatus(1);
String voteFeeAmt = (String)paramMap.get("voteFeeAmt");
lendReturn.setFee(new BigDecimal(voteFeeAmt));
lendReturn.setRealReturnTime(LocalDateTime.now());
baseMapper.updateById(lendReturn);
//更新标的信息
Lend lend = lendMapper.selectById(lendReturn.getLendId());
//最后一次还款更新标的
if(lendReturn.getLast()){
lend.setStatus(LendStatusEnum.PAY_OK.getStatus());
lendMapper.updateById(lend);
}
//借款账号转出金额
BigDecimal totalAmt = new BigDecimal((String)paramMap.get("totalAmt"));
String bindCode = userBindService.getBindCodeByUserId(lendReturn.getUserId());
userAccountMapper.updateAccount(bindCode,totalAmt.negate(),new BigDecimal(0));
//借款转出流水
//借款人交易流水
TransFlowBO transFlowBO = new TransFlowBO(
agentBatchNo,
bindCode,
totalAmt,
TransTypeEnum.RETURN_DOWN,
"借款人还款扣减,项目编号:" + lend.getLendNo() + ",项目名称:" + lend.getTitle());
transFlowService.saveTransFlow(transFlowBO);
//获取的回款明细
List<LendItemReturn> lendItemReturnList = lendItemReturnService.selectLendItemReturnList(lendReturn.getId());
//获取回款信息{
lendItemReturnList.forEach(item ->{
// 更新回款信息
item.setStatus(1);
lendItemReturnMapper.updateById(item);
// 更新出借信息(投资人实际收益)
LendItem lendItem = lendItemMapper.selectById(item.getLendItemId());
lendItem.setRealAmount(lendItem.getRealAmount().add(item.getInterest()));
lendItemMapper.updateById(lendItem);
// 投资账号转入金额
String investBindCode = userBindService.getBindCodeByUserId(item.getInvestUserId());
userAccountMapper.updateAccount(investBindCode,item.getTotal(),new BigDecimal(0));
// 投资转入流水
TransFlowBO investTransFlowBO = new TransFlowBO(
LendNoUtils.getReturnItemNo(),
investBindCode,
item.getTotal(),
TransTypeEnum.INVEST_BACK,
"还款到账,项目编号:" + lend.getLendNo() + ",项目名称:" + lend.getTitle());
transFlowService.saveTransFlow(investTransFlowBO);
});
// }
}
- 其他service
com/atguigu/srb/core/service/impl/LendItemReturnServiceImpl.java
List<Map<String,Object>> addReturnDetail(Long lendReturnId);
/**
* @param lendReturnId:
* @return List<LendItemReturn>
* @author Likejin
* @description 根据还款记录的id查询对应的回款记录
* @date 2023/4/19 16:07
*/
List<LendItemReturn> selectLendItemReturnList(long lendReturnId);
/**
* @param lendReturnId:
* @return List<Map<String,Object>>
* @author Likejin
* @description 组装汇付宝需要的还款对象(回款)
* @date 2023/4/19 16:03
*/
@Override
public List<Map<String, Object>> addReturnDetail(Long lendReturnId) {
//通过还款id找到对应的回款计划数据,组装data参数需要的listmap
//获取标的
//获取还款记录
LendReturn lendReturn = lendReturnMapper.selectById(lendReturnId);
//获取标的
Lend lend = lendMapper.selectById(lendReturn.getLendId());
//根据还款id拿到回款列表
List<LendItemReturn> lendItemReturnList = this.selectLendItemReturnList(lendReturnId);
List<Map<String,Object>> lendItemReturnDetailList = new ArrayList<>();
for(LendItemReturn lendItemReturn:lendItemReturnList){
//获取投资记录
Long lendItemId = lendItemReturn.getLendItemId();
LendItem lendItem = lendItemMapper.selectById(lendItemId);
//获取投资人id
Long investUserId = lendItem.getInvestUserId();
String bindCode = userBindService.getBindCodeByUserId(investUserId);
HashMap<String, Object> map = new HashMap<>();
//项目编号
map.put("agentProjectCode", lend.getLendNo());
//出借编号
map.put("voteBillNo", lendItem.getLendItemNo());
//收款人(出借人)
map.put("toBindCode", bindCode);
//还款金额
map.put("transitAmt", lendItemReturn.getTotal());
//还款本金
map.put("baseAmt", lendItemReturn.getPrincipal());
//还款利息
map.put("benifitAmt", lendItemReturn.getInterest());
//商户手续费
map.put("feeAmt", new BigDecimal("0"));
lendItemReturnDetailList.add(map);
}
return lendItemReturnDetailList;
}
/**
* @param lendReturnId:
* @return List<LendItemReturn>
* @author Likejin
* @description 根据还款id查询对应的回款列表
* @date 2023/4/19 16:08
*/
@Override
public List<LendItemReturn> selectLendItemReturnList(long lendReturnId) {
QueryWrapper<LendItemReturn> lendItemReturnQueryWrapper = new QueryWrapper<>();
lendItemReturnQueryWrapper.eq("lend_return_id",lendReturnId);
return baseMapper.selectList(lendItemReturnQueryWrapper);
}
③接口逻辑1 组装表单
- 前端传来还款计划的id lend_return表的id
- 通过校验token获取user_id
- 组装表单
- 校验账户余额,根据user_id和lend_return的还款金额校验
- 组装表单中其他参数
- 组装表单中需要的data(还款对应的所有回款记录)
④接口逻辑20 异步回调
- 校验签名,判断结果是否正确
- 幂等性返回检验
- 更新当前还款记录 状态为1
- 如果是最后一次还款则更新标的信息
- 借款账号转出金额
- 借款流水保存
- 更新所有对应的回款信息
- 更新回款状态
- 更新借款信息的实际收益(lend_item),累加
- 更新回款对应的账号的金额
- 回款金额流水
5.首页信息展示的完善
①前端
srb-site\pages\user\index.vue
<template>
<div class="personal-main">
<div class="pmain-profile">
<div class="pmain-welcome">
<span class="fr">上次登录时间: {{ userIndexVO.lastLoginTime }} </span>
</div>
<div class="pmain-user">
<div class="user-head">
<span class="head-img">
<span>
<img
:src="userIndexVO.headImg"
style="width:88px;height:88px;z-index:0;"
/>
<i class="headframe" style="z-index:0;"></i>
</span>
</span>
</div>
<div class="user-info">
<ul>
<li>
用户名<span>{{ userIndexVO.name }}</span>
<NuxtLink
v-if="
userIndexVO.userType === 2 && userIndexVO.bindStatus === 1
"
to="/user/borrower"
>
立即借款
</NuxtLink>
</li>
<li v-if="userIndexVO.bindStatus !== 1">
您还未开通第三方支付账户,请
<NuxtLink to="/user/bind">立即开通</NuxtLink>
以确保您的正常使用和资金安全。
</li>
</ul>
</div>
</div>
<div v-if="userIndexVO.bindStatus === 1" class="pmain-money">
<ul>
<li class="none">
<span>
<em>账户余额</em>
<i class="markicon"></i>
</span>
<span class="truemoney">
<i class="f26 fb">{{ userIndexVO.amount }}</i> 元
</span>
</li>
<li>
<span>
<em>冻结金额</em>
<i class="markicon"></i>
</span>
<span class="truemoney">
<i class="f26 fb">{{ userIndexVO.freezeAmount }}</i
>元
</span>
</li>
<li>
<NuxtLink to="/user/recharge">
<el-button size="mini" type="danger">充值</el-button>
</NuxtLink>
<NuxtLink to="/user/withdraw">
<el-button size="mini" type="success">提现</el-button>
</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
userIndexVO: {},
}
},
created() {
this.fetchUserData()
},
methods: {
fetchUserData() {
this.$axios
.$get('/api/core/userInfo/auth/getIndexUserInfo')
.then((response) => {
this.userIndexVO = response.data.userIndexVO
})
},
},
}
</script>
②后端
- vo
com/atguigu/srb/core/pojo/vo/UserIndexVO.java
package com.atguigu.srb.core.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel(description = "首页用户信息")
public class UserIndexVO {
@ApiModelProperty(value = "用户id")
private Long userId;
@ApiModelProperty(value = "用户姓名")
private String name;
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "1:出借人 2:借款人")
private Integer userType;
@ApiModelProperty(value = "用户头像")
private String headImg;
@ApiModelProperty(value = "绑定状态(0:未绑定,1:绑定成功 -1:绑定失败)")
private Integer bindStatus;
@ApiModelProperty(value = "帐户可用余额")
private BigDecimal amount;
@ApiModelProperty(value = "冻结金额")
private BigDecimal freezeAmount;
@ApiModelProperty(value = "上次登录时间")
private LocalDateTime lastLoginTime;
}
- controller
com/atguigu/srb/core/controller/api/UserInfoController.java
@ApiOperation("获取个人空间用户信息")
@GetMapping("/auth/getIndexUserInfo")
public R getIndexUserInfo(HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
UserIndexVO userIndexVO = userInfoService.getIndexUserInfo(userId);
return R.ok().data("userIndexVO", userIndexVO);
}
- service
UserIndexVO getIndexUserInfo(Long userId);
/**
* @param userId:
* @return UserIndexVO
* @author Likejin
* @description 返回页面的展示结果
* @date 2023/4/19 17:38
*/
@Override
public UserIndexVO getIndexUserInfo(Long userId) {
//用户信息
UserInfo userInfo = baseMapper.selectById(userId);
//账户信息
QueryWrapper<UserAccount> userAccountQueryWrapper = new QueryWrapper<>();
userAccountQueryWrapper.eq("user_id", userId);
UserAccount userAccount = userAccountMapper.selectOne(userAccountQueryWrapper);
//登录日志
QueryWrapper<UserLoginRecord> userLoginRecordQueryWrapper = new QueryWrapper<>();
userLoginRecordQueryWrapper
.eq("user_id", userId)
.orderByDesc("id")
.last("limit 1");
UserLoginRecord userLoginRecord = userLoginRecordMapper.selectOne(userLoginRecordQueryWrapper);
//组装结果数据
UserIndexVO userIndexVO = new UserIndexVO();
userIndexVO.setUserId(userInfo.getId());
userIndexVO.setUserType(userInfo.getUserType());
userIndexVO.setName(userInfo.getName());
userIndexVO.setNickName(userInfo.getNickName());
userIndexVO.setHeadImg(userInfo.getHeadImg());
userIndexVO.setBindStatus(userInfo.getBindStatus());
userIndexVO.setAmount(userAccount.getAmount());
userIndexVO.setFreezeAmount(userAccount.getFreezeAmount());
userIndexVO.setLastLoginTime(userLoginRecord.getCreateTime());
return userIndexVO;
}
6.资金记录展示
①前端
- pages/user/fund.vue
fetchTransFlowList() {
this.$axios.$get('/api/core/transFlow/list').then((response) => {
this.transFlowList = response.data.list
})
},
②后端
- controller
package com.atguigu.srb.core.controller.api;
import com.atguigu.common.result.R;
import com.atguigu.srb.base.util.JwtUtils;
import com.atguigu.srb.core.pojo.entity.TransFlow;
import com.atguigu.srb.core.service.TransFlowService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Api(tags = "资金记录")
@RestController
@RequestMapping("/api/core/transFlow")
@Slf4j
public class TransFlowController {
@Resource
private TransFlowService transFlowService;
@ApiOperation("获取列表")
@GetMapping("/list")
public R list(HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = JwtUtils.getUserId(token);
List<TransFlow> list = transFlowService.selectByUserId(userId);
return R.ok().data("list", list);
}
}
- service
com/atguigu/srb/core/service/TransFlowService.java
List<TransFlow> selectByUserId(Long userId);
/**
* @param userId:
* @return List<TransFlow>
* @author Likejin
* @description 获取流水列表
* @date 2023/4/19 17:36
*/
@Override
public List<TransFlow> selectByUserId(Long userId) {
QueryWrapper<TransFlow> queryWrapper = new QueryWrapper<>();
queryWrapper
.eq("user_id", userId)
.orderByDesc("id");
return baseMapper.selectList(queryWrapper);
}