• 【金融项目】尚融宝项目(十三)


    25、充值

    25.1、需求介绍

    25.1.1、投资人充值

    **1、需求描述 **

    标的产生后,平台展示标的,投资人就可以在平台投资标的,获取收益;投资人投资标的必须满足以下条件:

    在这里插入图片描述

    充值过程与绑定过程一致,也是在平台发送充值请求,跳转到资金托管平台,在资金托管平台完成充值,然后同步或异步返回或通知平台

    2、相关数据库表

    在这里插入图片描述

    3、参考文档

    参考《汇付宝商户账户技术文档》3.9用户充值

    25.1.2、具体步骤

    step1:用户在个人中心点击 “充值”

    step2:尚融宝展示账户充值页面

    在这里插入图片描述

    step3:用户填写充值金额,点击“充值”按钮

    step4:跳转到汇付宝页面(资金托管接口调用)

    在这里插入图片描述

    step5:汇付宝验证用户交易密码

    在这里插入图片描述

    step6:汇付修改账号资金余额(更新user_account记录中的amount的值)

    step7:异步回调(1)账户金额更改(2)添加交易流水

    step8:用户点击“返回平台”,返回尚融宝

    在这里插入图片描述

    25.2、充值

    25.2.1、后端接口实现

    1、Controller

    UserAccountController

    package com.atguigu.srb.core.controller.api;
    
    @Api(tags = "会员账户")
    @RestController
    @RequestMapping("/api/core/userAccount")
    @Slf4j
    public class UserAccountController {
        @Resource
        private UserAccountService userAccountService;
        @ApiOperation("充值")
        @PostMapping("/auth/commitCharge/{chargeAmt}")
        public R commitCharge(
                @ApiParam(value = "充值金额", required = true)
                @PathVariable BigDecimal chargeAmt, HttpServletRequest request) {
            String token = request.getHeader("token");
            Long userId = JwtUtils.getUserId(token);
            String formStr = userAccountService.commitCharge(chargeAmt, userId);
            return R.ok().data("formStr", formStr);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2、Service

    接口:UserAccountService

    String commitCharge(BigDecimal chargeAmt, Long userId);
    
    • 1

    实现:UserAccountServiceImpl

    @Resource
    private UserInfoMapper userInfoMapper;
    
    @Override
    public String commitCharge(BigDecimal chargeAmt, Long userId) {
        UserInfo userInfo = userInfoMapper.selectById(userId);
        String bindCode = userInfo.getBindCode();
        //判断账户绑定状态
        Assert.notEmpty(bindCode, ResponseEnum.USER_NO_BIND_ERROR);
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("agentId", HfbConst.AGENT_ID);
        paramMap.put("agentBillNo", LendNoUtils.getChargeNo());
        paramMap.put("bindCode", bindCode);
        paramMap.put("chargeAmt", chargeAmt);
        paramMap.put("feeAmt", new BigDecimal("0"));
        paramMap.put("notifyUrl", HfbConst.RECHARGE_NOTIFY_URL);
        paramMap.put("returnUrl", HfbConst.RECHARGE_RETURN_URL);
        paramMap.put("timestamp", RequestHelper.getTimestamp());
        String sign = RequestHelper.getSign(paramMap);
        paramMap.put("sign", sign);
        //构建充值自动提交表单
        String formStr = FormHelper.buildForm(HfbConst.RECHARGE_URL, paramMap);
        return formStr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    25.2.2、前端整合

    页面脚本

    pages/user/recharge.vue

    methods: {
        commitCharge() {
          this.$alert(
            '
    您即将前往汇付宝充值
    '
    , '前往汇付宝资金托管平台', { dangerouslyUseHTMLString: true, confirmButtonText: '立即前往', callback: (action) => { if (action === 'confirm') { this.$axios .$post( '/api/core/userAccount/auth/commitCharge/' + this.chargeAmt ) .then((response) => { document.write(response.data.formStr) }) } }, } ) }, },
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    25.3、回调接口

    25.3.1**、定义回调接口**

    1、controller

    UserAccountController中创建回调方法

    @ApiOperation(value = "用户充值异步回调")
    @PostMapping("/notify")
    public String notify(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"))) {
                return userAccountService.notify(paramMap);
            } else {
                log.info("用户充值异步回调充值失败:" + JSON.toJSONString(paramMap));
                return "success";
            }
        } else {
            log.info("用户充值异步回调签名错误:" + JSON.toJSONString(paramMap));
            return "fail";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、Service

    接口:UserAccountService

    String notify(Map<String, Object> paramMap);
    
    • 1

    实现:UserAccountServiceImpl

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String notify(Map<String, Object> paramMap) {
        log.info("充值成功:" + JSONObject.toJSONString(paramMap));
        String bindCode = (String)paramMap.get("bindCode"); //充值人绑定协议号
        String chargeAmt = (String)paramMap.get("chargeAmt"); //充值金额
        //优化
        baseMapper.updateAccount(bindCode, new BigDecimal(chargeAmt), new BigDecimal(0));
        
        //增加交易流水
        //TODO
        
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3、创建Mapper方法

    接口:UserAccountMapper

    void updateAccount(
        @Param("bindCode")String bindCode,
        @Param("amount")BigDecimal amount,
        @Param("freezeAmount")BigDecimal freezeAmount);
    
    • 1
    • 2
    • 3
    • 4

    XML:UserAccountMapper.xml

    <update id="updateAccount">
        update
        user_account
        set
        amount = amount + #{amount},
        freeze_amount = freeze_amount + #{freezeAmount}
        where
        user_id = (select id from user_info where bind_code = #{bindCode})
    update>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    25.3.2、增加交易流水

    1、枚举

    TransTypeEnum

    CHARGE(1,"充值"),
    INVEST_LOCK(2,"投标锁定"),
    INVEST_UNLOCK(3,"放款解锁"),
    CANCEL_LEND(4,"撤标"),
    BORROW_BACK(5,"放款到账"),
    RETURN_DOWN(6,"还款扣减"),
    INVEST_BACK(7,"出借回款"),
    WITHDRAW(8,"提现"),
    ;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2、创建BO对象

    package com.atguigu.srb.core.pojo.bo;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class TransFlowBO {
        private String agentBillNo;
        private String bindCode;
        private BigDecimal amount;
        private TransTypeEnum transTypeEnum;
        private String memo;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3、保存交易流水业务

    接口:TransFlowService

    void saveTransFlow(TransFlowBO transFlowBO);
    
    • 1

    实现:TransFlowServiceImpl

    @Resource
    private UserInfoMapper userInfoMapper;
    
    @Override
    public void saveTransFlow(TransFlowBO transFlowBO) {
        //获取用户基本信息 user_info
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
        userInfoQueryWrapper.eq("bind_code", transFlowBO.getBindCode());
        UserInfo userInfo = userInfoMapper.selectOne(userInfoQueryWrapper);
        //存储交易流水数据
        TransFlow transFlow = new TransFlow();
        transFlow.setUserId(userInfo.getId());
        transFlow.setUserName(userInfo.getName());
        transFlow.setTransNo(transFlowBO.getAgentBillNo());
        transFlow.setTransType(transFlowBO.getTransTypeEnum().getTransType());
        transFlow.setTransTypeName(transFlowBO.getTransTypeEnum().getTransTypeName());
        transFlow.setTransAmount(transFlowBO.getAmount());
        transFlow.setMemo(transFlowBO.getMemo());
        baseMapper.insert(transFlow);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4、notify中调用

    UserAccountServiceImpl

    @Resource
    private TransFlowService transFlowService;
    
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String notify(Map<String, Object> paramMap) {
        ......
        //增加交易流水
        String agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
        TransFlowBO transFlowBO = new TransFlowBO(
            agentBillNo,
            bindCode,
            new BigDecimal(chargeAmt),
            TransTypeEnum.RECHARGE,
            "充值");
        transFlowService.saveTransFlow(transFlowBO);
        
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    25.4、接口调用的幂等性

    25.4.1**、接口幂等性原则**

    1、什么是接口幂等性

    接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次调用而产生了副作用。

    举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性

    2、回调重试

    汇付宝向尚融宝发起回调,如果没有收到正确的响应 “success”,则尚融宝会发起重试

    汇付宝中的相关代码如下:

    package com.heepay.config;
    
    @Slf4j
    public class NotifyThread implements Runnable {
        private int count = 1;
        private String notifyUrl;
        private Map<String, Object> paramMap;
        public NotifyThread(){}
        public NotifyThread(String notifyUrl, Map<String, Object> paramMap) {
            this.notifyUrl = notifyUrl;
            this.paramMap = paramMap;
        }
        @Override
        public void run() {
            task();
        }
        private void task() {
            String result = SignUtil.sendRequest(paramMap,notifyUrl);
            log.info(notifyUrl + ":" + result + " count:" + count);
            if(!"success".equals(result)) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //ScheduledTask.queue.offer(new NotifyVo(notifyUrl, paramMap));
                count++;
                if(count <= 5) {
                    task();
                    log.info("失败重试:" + JSON.toJSONString(this));
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3、存在的问题

    当回调重试时,金额和流水会重复增加

    在这里插入图片描述

    25.4.2、解决方案

    1、设置唯一索引

    设置了唯一索引后,即使回调重复执行,遇到唯一索引,就会抛出异常,从而使事务回滚。

    在这里插入图片描述

    2、判断流水是否存在

    判断流水如果存在,则从业务方法中直接退出

    接口:TransFlowService

    boolean isSaveTransFlow(String agentBillNo);
    
    • 1

    实现:TransFlowServiceImpl

    @Override
    public boolean isSaveTransFlow(String agentBillNo) {
        QueryWrapper<TransFlow> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("trans_no", agentBillNo);
        int count = baseMapper.selectCount(queryWrapper);
        return count > 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    调用 :UserAccountServiceImpl

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void notify(Map<String, Object> paramMap) {
        log.info("充值成功:" + JSONObject.toJSONString(paramMap));
        //判断交易流水是否存在
        String agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
        boolean isSave = transFlowService.isSaveTransFlow(agentBillNo);
        if (isSave) {
            log.warn("幂等性返回");
            return "success";
        }
       ......
           
        //增加交易流水
        //agentBillNo = (String)paramMap.get("agentBillNo"); //商户充值订单号
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    26、投标

    26.1、需求介绍

    26.1.1、投资人投标

    1、需求描述

    平台发布标的,出借人充值就可以投资标的

    在这里插入图片描述

    2、相关数据库表

    在这里插入图片描述

    3、参考文档

    参考《汇付宝商户账户技术文档》3.10满标投资,投资过程与账户绑定、用户充值过程一致

    在这里插入图片描述

    26.1.2、具体步骤

    step1:点击标的,进入标的详情页面

    在这里插入图片描述

    step2:输入投资金额,计算获得收益

    step3:同意协议,点击立即投资

    在这里插入图片描述

    step4:跳转到汇付宝页面(资金托管接口调用)

    step5:汇付宝验证用户交易密码

    step6:汇付宝修改账号资金余额

    (更新user_account记录中的amount的值和freeze_amount的值)

    汇付宝新增投资记录(新增user_invest记录)

    step7:异步回调

    (1)账户金额更改(剩余金额和冻结金额)

    (2)修改投资状态(lend_item表中的status)

    (3)更新标的信息(lend表中的投资人数和已投金额)

    (4)添加交易流水

    step8:用户点击“返回平台”,返回尚融宝

    26.2、标的详情

    26.2.1、需求

    展示信息:

    1、标的基本信息(标的表 lend)

    2、借款人信息(借款人表 borrower)

    3、账户余额信息(会员账户表 user_account)

    4、根据投资金额计算收益(根据四种还款方式计算)

    5、投资记录(投资记录表 lend_item,后续完善)

    6、还款记录(还款记录表 lend_return,后续完善)

    投标条件:

    1、已经登录的会员

    2、只有投资人可以投标,借款人不可以投标

    3、投标金额必须是100整数倍

    4、账号可用余额充足

    5、同意投标协议

    26.2.2、标的和借款人信息接口

    LendController

    @ApiOperation("获取标的信息")
    @GetMapping("/show/{id}")
    public R show(
        @ApiParam(value = "标的id", required = true)
        @PathVariable Long id) {
        Map<String, Object> lendDetail = lendService.getLendDetail(id);
        return R.ok().data("lendDetail", lendDetail);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    26.2.3、账户余额信息接口

    1、UserAccountController

    @ApiOperation("查询账户余额")
    @GetMapping("/auth/getAccount")
    public R getAccount(HttpServletRequest request){
        String token = request.getHeader("token");
        Long userId = JwtUtils.getUserId(token);
        BigDecimal account = userAccountService.getAccount(userId);
        return R.ok().data("account", account);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2、Service

    接口:UserAccountService

    BigDecimal getAccount(Long userId);
    
    • 1

    实现:UserAccountServiceImpl

    @Override
    public BigDecimal getAccount(Long userId) {
        //根据userId查找用户账户
        QueryWrapper<UserAccount> userAccountQueryWrapper = new QueryWrapper<>();
        userAccountQueryWrapper.eq("user_id", userId);
        UserAccount userAccount = baseMapper.selectOne(userAccountQueryWrapper);
        return userAccount.getAmount();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    26.2.4、前端整合

    pages/lend/_id.vue

    1、获取标的详情信息

      async asyncData({ $axios, params }) {
        let lendId = params.id //通过路由参数获取标的id
        //通过lendId获取标的详情信息
        let response = await $axios.$get('/api/core/lend/show/' + lendId)
        return {
          lend: response.data.lendDetail.lend, //标的详情
          borrower: response.data.lendDetail.borrower, //借款人信息
        }
      },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2、查询账户余额

        //查询账户余额
        fetchAccount() {
          let userInfo = cookie.get('userInfo')
          if (userInfo) {
            this.$axios
              .$get('/api/core/userAccount/auth/getAccount')
              .then((response) => {
                this.account = response.data.account
              })
          }
        },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3、获取登录人的用户类型

    //获取登录人的用户类型
    fetchUserType() {
          let userInfo = cookie.get('userInfo')
          if (userInfo) {
            userInfo = JSON.parse(userInfo)
            this.userType = userInfo.userType
          }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    26.3、计算收益

    26.3.1、还款方式

    1、等额本息

    等额本息法最重要的一个特点是每月的还款额相同,从本质上来说是本金所占比例逐月递增,利息所占比例逐月递减,月还款数不变。

    即在月供“本金与利息”的分配比例中,前半段时期所还的利息比例大、本金比例小,还款期限过半后逐步转为本金比例大、利息比例小。

    计算公式为:

    每月利息 = 剩余本金 x 贷款月利率

    每月还本付息金额 = 还款总额 / 贷款月数

    每月本金 = 每月还本付息金额 - 每月利息

    **注意:**在等额本息法中,银行一般先收剩余本金利息,后收本金,所以利息在月供款中的比例会随本金的减少而降低,本金在月供款中的比例因而升高,但月供总额保持不变。

    2、等额本金

    等额本金法最大的特点是每月的还款额不同,呈现逐月递减的状态;它是将贷款本金按还款的总月数均分,再加上上期剩余本金的利息,这样就形成月还款额,所以等额本金法第一个月的还款额最多 ,然后逐月减少,越还越少。

    计算公式为:

    每月利息 = 剩余本金 x 贷款月利率

    每月本金 = 贷款额 / 贷款月数

    每月还本付息金额 = 每月本金 + 每月利息

    **注意:**在等额本金法中,人们每月归还的本金额始终不变,利息随剩余本金的减少而减少,因而其每月还款额逐渐减少。

    3、按期付息到期还本

    按期付息到期还本是借款人在贷款到期日一次性归还贷款本金,利息按期归还

    计算公式为:

    每月利息 = 贷款额 x 贷款月利率

    总利息 = 每月利息 x 贷款月数

    4、一次还本付息

    一次还本付息是贷款到期后一次性归还本金和利息

    计算公式为:

    还款金额 = 贷款额 + 贷款额 x 月利率 x 贷款月数

    26.3.2、后端接口

    1、还款方式工具类

    根据我们的表设计,出借人要能知道每月回款的本金与利息,借款人也一样,他也要知道每月的还款本金与利息,还有我们需要计算投资人的投资收益等数据。

    因此我们将四种还款方式工具类设计如下:

    在这里插入图片描述

    **说明:**还款方式计算复杂,尽做了解,有兴趣的同学可以深入理解,这里不做详细介绍

    2、定义枚举

    ReturnMethodEnum

    ONE(1, "等额本息"),
    TWO(2, "等额本金"),
    THREE(3, "每月还息一次还本"),
    FOUR(4, "一次还本还息"),
    ;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3、Controller

    LendController

    @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().data("interestCount", interestCount);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4、Service

    接口:LendService

    BigDecimal getInterestCount(BigDecimal invest, BigDecimal yearRate, Integer totalmonth, Integer returnMethod);
    
    • 1

    实现:LendServiceImpl

    @Override
    public BigDecimal getInterestCount(BigDecimal invest, BigDecimal yearRate, Integer totalmonth, Integer returnMethod) {
        BigDecimal interestCount;
        //计算总利息
        if (returnMethod.intValue() == ReturnMethodEnum.ONE.getMethod()) {
            interestCount = Amount1Helper.getInterestCount(invest, yearRate, totalmonth);
        } else if (returnMethod.intValue() == ReturnMethodEnum.TWO.getMethod()) {
            interestCount = Amount2Helper.getInterestCount(invest, yearRate, totalmonth);
        } else if(returnMethod.intValue() == ReturnMethodEnum.THREE.getMethod()) {
            interestCount = Amount3Helper.getInterestCount(invest, yearRate, totalmonth);
        } else {
            interestCount = Amount4Helper.getInterestCount(invest, yearRate, totalmonth);
        }
        return interestCount;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    26.3.3、前端整合

    计算收益

    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
        })
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    26.4、投标

    26.4.1、后端接口

    1、实现思路

    投标时要在服务器端校验数据:

    • 标的状态必须为募资中
    • 标的不能超卖
    • 账户可用余额充足

    2、创建VO

    创建InvestVO

    package com.atguigu.srb.core.pojo.vo;
    
    @Data
    @ApiModel(description = "投标信息")
    public class InvestVO {
        private Long lendId;
        //投标金额
        private String investAmount;
        //用户id
        private Long investUserId;
        //用户姓名
        private String investName;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、Controller

    LendItemController

    package com.atguigu.srb.core.controller.api;
    
    @Api(tags = "标的的投资")
    @RestController
    @RequestMapping("/api/core/lendItem")
    @Slf4j
    public class LendItemController {
        
        @Resource
        LendItemService lendItemService;
        @ApiOperation("会员投资提交数据")
        @PostMapping("/auth/commitInvest")
        public R commitInvest(@RequestBody InvestVO investVO, HttpServletRequest request) {
            String token = request.getHeader("token");
            Long userId = JwtUtils.getUserId(token);
            String userName = JwtUtils.getUserName(token);
            investVO.setInvestUserId(userId);
            investVO.setInvestName(userName);
            //构建充值自动提交表单
            String formStr = lendItemService.commitInvest(investVO);
            return R.ok().data("formStr", formStr);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4、Service

    接口:LendItemService

    String commitInvest(InvestVO investVO);
    
    • 1

    实现:LendItemServiceImpl

    @Resource
    private LendMapper lendMapper;
    @Resource
    private LendService lendService;
    @Resource
    private UserAccountService userAccountService;
    @Resource
    private UserBindService userBindService;
    
    @Override
    public String commitInvest(InvestVO investVO) {
        //输入校验==========================================
        Long lendId = investVO.getLendId();
        //获取标的信息
        Lend lend = lendMapper.selectById(lendId);
        //标的状态必须为募资中
        Assert.isTrue(
            lend.getStatus().intValue() == LendStatusEnum.INVEST_RUN.getStatus().intValue(),
            ResponseEnum.LEND_INVEST_ERROR);
        //标的不能超卖:(已投金额 + 本次投资金额 )>=标的金额(超卖)
        BigDecimal sum = lend.getInvestAmount().add(new BigDecimal(investVO.getInvestAmount()));
        Assert.isTrue(sum.doubleValue() <= lend.getAmount().doubleValue(),
                      ResponseEnum.LEND_FULL_SCALE_ERROR);
        //账户可用余额充足:当前用户的余额 >= 当前用户的投资金额(可以投资)
        Long investUserId = investVO.getInvestUserId();
        BigDecimal amount = userAccountService.getAccount(investUserId);//获取当前用户的账户余额
        Assert.isTrue(amount.doubleValue() >= Double.parseDouble(investVO.getInvestAmount()),
                      ResponseEnum.NOT_SUFFICIENT_FUNDS_ERROR);
        //在商户平台中生成投资信息==========================================
        //标的下的投资信息
        LendItem lendItem = new LendItem();
        lendItem.setInvestUserId(investUserId);//投资人id
        lendItem.setInvestName(investVO.getInvestName());//投资人名字
        String lendItemNo = LendNoUtils.getLendItemNo();
        lendItem.setLendItemNo(lendItemNo); //投资条目编号(一个Lend对应一个或多个LendItem)
        lendItem.setLendId(investVO.getLendId());//对应的标的id
        lendItem.setInvestAmount(new BigDecimal(investVO.getInvestAmount())); //此笔投资金额
        lendItem.setLendYearRate(lend.getLendYearRate());//年化
        lendItem.setInvestTime(LocalDateTime.now()); //投资时间
        lendItem.setLendStartDate(lend.getLendStartDate()); //开始时间
        lendItem.setLendEndDate(lend.getLendEndDate()); //结束时间
        //预期收益
        BigDecimal expectAmount = lendService.getInterestCount(
            lendItem.getInvestAmount(),
            lendItem.getLendYearRate(),
            lend.getPeriod(),
            lend.getReturnMethod());
        lendItem.setExpectAmount(expectAmount);
        //实际收益
        lendItem.setRealAmount(new BigDecimal(0));
        lendItem.setStatus(0);//默认状态:刚刚创建
        baseMapper.insert(lendItem);
        //组装投资相关的参数,提交到汇付宝资金托管平台==========================================
        //在托管平台同步用户的投资信息,修改用户的账户资金信息==========================================
        //获取投资人的绑定协议号
        String bindCode = userBindService.getBindCodeByUserId(investUserId);
        //获取借款人的绑定协议号
        String benefitBindCode = userBindService.getBindCodeByUserId(lend.getUserId());
        //封装提交至汇付宝的参数
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("agentId", HfbConst.AGENT_ID);
        paramMap.put("voteBindCode", bindCode);
        paramMap.put("benefitBindCode",benefitBindCode);
        paramMap.put("agentProjectCode", lend.getLendNo());//项目标号
        paramMap.put("agentProjectName", lend.getTitle());
        //在资金托管平台上的投资订单的唯一编号,要和lendItemNo保持一致。
        paramMap.put("agentBillNo", lendItemNo);//订单编号
        paramMap.put("voteAmt", investVO.getInvestAmount());
        paramMap.put("votePrizeAmt", "0");
        paramMap.put("voteFeeAmt", "0");
        paramMap.put("projectAmt", lend.getAmount()); //标的总金额
        paramMap.put("note", "");
        paramMap.put("notifyUrl", HfbConst.INVEST_NOTIFY_URL); //检查常量是否正确
        paramMap.put("returnUrl", HfbConst.INVEST_RETURN_URL);
        paramMap.put("timestamp", RequestHelper.getTimestamp());
        String sign = RequestHelper.getSign(paramMap);
        paramMap.put("sign", sign);
        //构建充值自动提交表单
        String formStr = FormHelper.buildForm(HfbConst.INVEST_URL, paramMap);
        return formStr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    5、UserBindService

    创建一个通用的Service方法,以方便调用,根据userId获取用户绑定账号

    接口:UserBindService

    String getBindCodeByUserId(Long userId);
    
    • 1

    实现:UserBindServiceImpl

    @Override
    public String getBindCodeByUserId(Long userId){
        QueryWrapper<UserBind> userBindQueryWrapper = new QueryWrapper<>();
        userBindQueryWrapper.eq("user_id", userId);
        UserBind userBind = baseMapper.selectOne(userBindQueryWrapper);
        String bindCode = userBind.getBindCode();
        return bindCode;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    26.4.2、前端整合

    投资

    pages/lend/_id.vue

        //投资
        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.account) {
            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/commitInvest', this.invest) .then((response) => { // console.log(response.data.formStr) // debugger document.write(response.data.formStr) }) } }, } ) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    26.5、回调接口

    26.5.1、定义回调接口

    1、controller

    LendItemController中创建回调方法

    @ApiOperation("会员投资异步回调")
    @PostMapping("/notify")
    public String notify(HttpServletRequest request) {
        Map<String, Object> paramMap = RequestHelper.switchMap(request.getParameterMap());
        log.info("用户投资异步回调:" + JSON.toJSONString(paramMap));
        //校验签名 P2pInvestNotifyVo
        if(RequestHelper.isSignEquals(paramMap)) {
            if("0001".equals(paramMap.get("resultCode"))) {
                lendItemService.notify(paramMap);
            } else {
                log.info("用户投资异步回调失败:" + JSON.toJSONString(paramMap));
                return "fail";
            }
        } else {
            log.info("用户投资异步回调签名错误:" + JSON.toJSONString(paramMap));
            return "fail";
        }
        return "success";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、Service

    实现回调的业务

    接口:LendItemService

    void notify(Map<String, Object> paramMap);
    
    • 1

    实现:LendItemServiceImpl

    @Resource
    private TransFlowService transFlowService;
    @Resource
    private UserAccountMapper userAccountMapper;
    
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void notify(Map<String, Object> paramMap) {
        log.info("投标成功");
        //获取投资编号
        String agentBillNo = (String)paramMap.get("agentBillNo");
        boolean result = transFlowService.isSaveTransFlow(agentBillNo);
        if(result){
            log.warn("幂等性返回");
            return;
        }
        //获取用户的绑定协议号
        String bindCode = (String)paramMap.get("voteBindCode");
        String voteAmt = (String)paramMap.get("voteAmt");
        //修改商户系统中的用户账户金额:余额、冻结金额
        userAccountMapper.updateAccount(bindCode, new BigDecimal("-" + voteAmt), new BigDecimal(voteAmt));
        //修改投资记录的投资状态改为已支付
        LendItem lendItem = this.getByLendItemNo(agentBillNo);
        lendItem.setStatus(1);//已支付
        baseMapper.updateById(lendItem);
        //修改标的信息:投资人数、已投金额
        Long lendId = lendItem.getLendId();
        Lend lend = lendMapper.selectById(lendId);
        lend.setInvestNum(lend.getInvestNum() + 1);
        lend.setInvestAmount(lend.getInvestAmount().add(lendItem.getInvestAmount()));
        lendMapper.updateById(lend);
        //新增交易流水
        TransFlowBO transFlowBO = new TransFlowBO(
            agentBillNo,
            bindCode,
            new BigDecimal(voteAmt),
            TransTypeEnum.INVEST_LOCK,
            "投资项目编号:" + lend.getLendNo() + ",项目名称:" + lend.getTitle());
        transFlowService.saveTransFlow(transFlowBO);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    3、辅助方法

    LendItemServiceImpl

    private LendItem getByLendItemNo(String lendItemNo) {
        QueryWrapper<LendItem> queryWrapper = new QueryWrapper();
        queryWrapper.eq("lend_item_no", lendItemNo);
        return baseMapper.selectOne(queryWrapper);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    本文章参考B站 尚硅谷《尚融宝》Java微服务分布式金融项目,仅供个人学习使用,部分内容为本人自己见解,与尚硅谷无关。

  • 相关阅读:
    微信小程序如何分包
    安装nginx-ingress
    谷粒商城 (五) --------- 人人开源搭建后台系统
    基于Servlet+jsp+mysql开发javaWeb学生管理系统(学生信息、学生选课、学生成绩、学生签到考勤)
    二维码制作教程:如何制作一个文件二维码?
    linux相关笔记
    three.js 第一节 - 场景、相机、渲染器
    数据采集:智能制造的基础环节
    C# 读写TXT文件
    VsCode配置c/c++环境
  • 原文地址:https://blog.csdn.net/Chovy_pyc/article/details/127894239