一、订单

1、登录页面配置

     前面使用的都是采用 Postman 实现登录,现在实现 oauth 自定义登录。
先 将登录相关的静态资源导入到 changgou-user-oauth 中:
在这里插入图片描述
导入 thymeleaf 模板引擎依赖:

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

提供控制器 com.changgou.oauth.controller.LoginRedirect,实现登录页跳转:

@Controller
@RequestMapping(value = "/oauth")
public class LoginRedirect {

    /***
     * 跳转到登录页面
     * @return
     */
    @GetMapping(value = "/login")
    public String login(){
        return "login";
    }
}

针对静态资源和登录页面,我们需要实现忽略安全配置,并且要指定登录页面。修改com.changgou.oauth.config.WebSecurityConfig 的 2 个 configure 方法:
在这里插入图片描述

第 2 个 configure 配置:
在这里插入图片描述
测试:
在这里插入图片描述

点击登录按钮,访问之前的登录方法实现登录,需要对登录页做一下调整。
首先,修改 login.html,引入 thymeleaf 命名空间:

1562915939313
点击登录按钮,使用 vue+axios 实现登录,需要定义脚本,访问后台登录方法。
先添加 vue 入口标签,修改 login.html,添加id=“app” :
在这里插入图片描述
引入 js:
在这里插入图片描述

登录脚本实现:
在这里插入图片描述
修改表单:
在这里插入图片描述
测试:输入用户名和密码都是 changgou,点击”登录“按钮,页面显示 ”正在登录“后跳转至:
在这里插入图片描述
用户未登录的时候,并清除 Cookie ,直接访问购物车:
在这里插入图片描述
     可以看到,返回的只是个错误状态码,不方便测试,可以设置为重定向到登录页面,让用户登录。需要修改网关的头文件,让用户每次没登录的时候,都跳转到登录页面。
    
修改 changgou-gateway-web 的 com.changgou.filter.AuthorizeFilter,代码如下:
在这里插入图片描述
在这里插入图片描述
    此时再测试,未登录时访问购物车页面,就可以跳转到登录页面了。当然,在实际应用中,这里不能直接跳转到登录页,应该提示状态给页面,让页面根据判断跳转,这里只是为了方便测试。

    现在还有个问题,如果未登录访问购物车,会跳转到登录页面,但是登录成功后,却并没有再返回到要访问的购物车页面。可以将用户要访问的页面作为参数传递给登录控制器,登录控制器记录下来,每次登录成功后,再跳转记录访问路劲参数指定的页面。

    先修改网关,携带当前 URI。修改 changgou-gateway-web 的 com.changgou.filter.AuthorizeFilter,在之前的 URL 后面添加 FROM 参数以及 FROM 参数的值为 request.getURI(),代码如下:
1565298329794
再修改 changgou-user-oauth 的 com.changgou.oauth.controller.LoginRedirect 记录访问来源页,使 认证服务器获取 FROM 参数:

@GetMapping(value = "/login")
    public String login(@RequestParam(value = "FROM",required = false,defaultValue = "")
                                    String from, Model model){
        model.addAttribute("from",from);
        return "login";
    }

修改页面,获取来源页信息,并存到 from 变量中,登录成功后跳转到该地址。
在这里插入图片描述
这里的计时跳转逻辑是有问题的,到时间并没能跳转,改成:
在这里插入图片描述
    此时再测试,就可以识别未登录用户,跳转到登录页,然后根据登录状态,如果登录成功,则跳转到来源页。

2、用户收件地址查询

订单的前端页面是这样的:
在这里插入图片描述
这里,“收件人信息”,是从 user 工程中获取到的,在数据库中对应 tb_address 表:
在这里插入图片描述
对应表结构:

CREATE TABLE `tb_address` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `provinceid` varchar(20) DEFAULT NULL COMMENT '省',
  `cityid` varchar(20) DEFAULT NULL COMMENT '市',
  `areaid` varchar(20) DEFAULT NULL COMMENT '县/区',
  `phone` varchar(20) DEFAULT NULL COMMENT '电话',
  `address` varchar(200) DEFAULT NULL COMMENT '详细地址',
  `contact` varchar(50) DEFAULT NULL COMMENT '联系人',
  `is_default` varchar(1) DEFAULT NULL COMMENT '是否是默认 1默认 0否',
  `alias` varchar(50) DEFAULT NULL COMMENT '别名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8;

     需要在 user 工程中提供根据用户名查询收件地址的方法。
在 AddressService 中提供方法:

 List<Address> list(String username);

实现:

   @Override
    public List<Address> list(String username) {
        Address address = new Address();
        address.setUsername(username);
        return addressMapper.select(address);
    }

控制层:

@GetMapping("/user/list")
public Result<List<Address>> list(){

    // 获取用户登录名
    String username = tokenDecode.getUserInfo().get("username");
    
    List<Address> address=addressService.list(username);
    return new Result<>(true,StatusCode.OK,"查询成功",address);
}

运行结果:
在这里插入图片描述

     送货清单其实就是购物车列表,直接查询之前的购物车列表即可。
    

3、 下单

在这里插入图片描述
    点击结算页的时候,会立即创建订单数据,创建订单数据会将数据存入到 2 张表中,分别是 订单表 和 订单明细表,此处还需要修改商品对应的库存数量。
    而且提交订单后,需要把商品详情从用户的购物车中删除;需要进行价格校验,检查价格是否变化,以当前数据库中的价格为准,以防出现 “异价” 的问题;需要检查库存,特别是并非库存,以防出现 “超卖” 的问题。
    

(1)表结构介绍

订单表:

CREATE TABLE `tb_order` (
  `id` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '订单id',
  `total_num` int(11) DEFAULT NULL COMMENT '数量合计',
  `total_money` int(11) DEFAULT NULL COMMENT '金额合计',
  `pre_money` int(11) DEFAULT NULL COMMENT '优惠金额',
  `post_fee` int(11) DEFAULT NULL COMMENT '邮费',
  `pay_money` int(11) DEFAULT NULL COMMENT '实付金额',
  `pay_type` varchar(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付类型,1、在线支付、0 货到付款',
  `create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '订单更新时间',
  `pay_time` datetime DEFAULT NULL COMMENT '付款时间',
  `consign_time` datetime DEFAULT NULL COMMENT '发货时间',
  `end_time` datetime DEFAULT NULL COMMENT '交易完成时间',
  `close_time` datetime DEFAULT NULL COMMENT '交易关闭时间',
  `shipping_name` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流名称',
  `shipping_code` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流单号',
  `username` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '用户名称',
  `buyer_message` varchar(1000) COLLATE utf8_bin DEFAULT NULL COMMENT '买家留言',
  `buyer_rate` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否评价',
  `receiver_contact` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',
  `receiver_mobile` varchar(12) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人手机',
  `receiver_address` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人地址',
  `source_type` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '订单来源:1:web,2:app,3:微信公众号,4:微信小程序  5 H5手机页面',
  `transaction_id` varchar(30) COLLATE utf8_bin DEFAULT NULL COMMENT '交易流水号',
  `order_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '订单状态,0:未完成,1:已完成,2:已退货',
  `pay_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付状态,0:未支付,1:已支付,2:支付失败',
  `consign_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '发货状态,0:未发货,1:已发货,2:已收货',
  `is_delete` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否删除',
  PRIMARY KEY (`id`),
  KEY `create_time` (`create_time`),
  KEY `status` (`order_status`),
  KEY `payment_type` (`pay_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

订单明细表:

CREATE TABLE `tb_order_item` (
  `id` varchar(50) COLLATE utf8_bin NOT NULL COMMENT 'ID',
  `category_id1` int(11) DEFAULT NULL COMMENT '1级分类',
  `category_id2` int(11) DEFAULT NULL COMMENT '2级分类',
  `category_id3` int(11) DEFAULT NULL COMMENT '3级分类',
  `spu_id` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT 'SPU_ID',
  `sku_id` bigint(20) NOT NULL COMMENT 'SKU_ID',
  `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  `name` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名称',
  `price` int(20) DEFAULT NULL COMMENT '单价',
  `num` int(10) DEFAULT NULL COMMENT '数量',
  `money` int(20) DEFAULT NULL COMMENT '总金额',
  `pay_money` int(11) DEFAULT NULL COMMENT '实付金额',
  `image` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '图片地址',
  `weight` int(11) DEFAULT NULL COMMENT '重量',
  `post_fee` int(11) DEFAULT NULL COMMENT '运费',
  `is_return` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否退货,0:未退货,1:已退货',
  PRIMARY KEY (`id`),
  KEY `item_id` (`sku_id`),
  KEY `order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

    两个表通过外键 orde_id 进行连结。

(2)下单实现

    下单的时候,先添加订单,向 tb_order 表中增加数据,再添加订单明细,向 tb_order_item 表中增加数据。
     需要修改 changgou-service-order 微服务,实现下单操作,这里会生成订单号,我们首先需要在 启动类 中创建一个 IdWorker 对象( 为什么不使用自增 ID 呢?因为可能存在重复 和 数值超出范围 问题):

@Bean
public IdWorker idWorker(){
    return new IdWorker(1,1);
}
  • 业务层
    修改 changgou-service-order 微服务,修改com.changgou.order.service.impl.OrderServiceImpl:
   void addOrder(Order order);

实现:

 @Override
    public void addOrder(Order order) {
        // 订单主键
        order.setId(String.valueOf(idWorker.nextId()));

        // 总数量
        int totolNum = 0;

        // 总金额
        int totalMoney = 0;

        // 获取订单(购物车)明细
        List<OrderItem> orderItem = redisTemplate.boundHashOps("Cart_" + order.getUsername()).values();
    
        for (OrderItem item : orderItem) {

            // 价格校验
            int price = orderItemMapper.selectByPrimaryKey(item.getId()).getPrice();
            if (price == item.getMoney()) {
                // 总金额
                totalMoney += item.getMoney();
            } else totalMoney += price;

            // 总数量
            totolNum += item.getNum();
        }

        // 订单商品总数目 = 每个商品数量之和
        order.setTotalNum(totolNum);

        // 订单总金额 = 每个商品金额之和
        order.setTotalMoney(totalMoney);

        // 订单实付金额 = 每个商品实付金额之和
        order.setPayMoney(totalMoney);

        // 订单优惠金额 = 总金额 - 实付金额,暂时为 0
        order.setPreMoney(0);

        // 订单创建时间
        order.setCreateTime(new Date());

        // 订单修改时间
        order.setUpdateTime(order.getCreateTime());

        // 订单评价状态,0 表示未评价,1 表示已评价
        order.setBuyerRate("0");

        // 订单来源,1 表示 Web
        order.setSourceType("1");

        // 订单状态,0 表示未完成,1 表示已完成,2 表示已退货
        order.setOrderStatus("0");

        // 订单支付状态,0 表示未支付,1 表示已支付,2 表示支付失败
        order.setPayStatus("0");

        // 订单删除状态,0 表示未删除
        order.setIsDelete("0");

        // 订单发货状态,0 表示未发货,1 表示已发货,2 表示已收货
        order.setConsignStatus("0");

        // 先添加订单信息
        orderMapper.insertSelective(order);

        // 再添加订单明细信息
        for (OrderItem item : orderItem) {
            item.setId(String.valueOf(idWorker.nextId()));
            // 是否退货,0 表示未退货,1 表示已退货
            item.setIsReturn("0");
            item.setOrderId(order.getId());
            // 退货状态,0 表示未退货
            item.setIsReturn("0");
            orderItemMapper.insertSelective(item);
        }

        // 下单后需要把商品从购物车中移除
        redisTemplate.delete("Cart_" + order.getUsername());
    }

     可以看到,逻辑是传来 order 对象, 从 Redis 中查到 “Cart_xxx” 为命名空间的记录对应的 value,也就是 xxx 用户购物车/订单的所有记录,遍历,求出总金额和数量;再为 order 对象设置属性,将 order 对象持久化到 order 表中,再遍历一次 Redis 中的记录,把这些记录都持久化到 order_item 表中,下单后还需要把 xxx 用户购物车中的所有记录移除。
     这里,通用 Mapper 使用的是 insertSelective 方法。 如果使用的是 insert ,那么所有的字段都会添加一遍,即使有的字段没有值;如果使用 inserSelective 就会只给有值的字段赋值(会对传进来的值做非空判断)。还有要注意的是,在启动类用 @MapperScan 注解,导入的包应该是 tk 的,而不是 spring mybatis 的。

( RedisTemplate 中的命名空间、key、value 的关系示意图:
在这里插入图片描述 可以看到,用用户名作命名空间,商品的 sku ID 作 key,商品详情的 orderItem 作 value,这设计挺巧妙的 🤗。)

  • 控制层
        修改 com.changgou.order.controller.OrderController 类:
@Autowired
private TokenDecode tokenDecode;
  @PostMapping
    public Result add(@RequestBody Order order) {

        // 获取用户名
        Map<String, String> userInfo = tokenDecode.getUserInfo();

        String userName = userInfo.get("username");

        order.setUsername(userName);
        orderService.addOrder(order);

        return new Result(true, StatusCode.OK, "添加成功");
    }

保存订单测试,可以看到 Redis 中用户对应的购物记录被删除,而且 MySQL 2 个表数据添加成功。
    现在还有一个问题,下单并不是对购物车所有的商品进行选择,是需要勾选商品的,先在 Order 类中添加一个字段,用于记录当前订单勾选的商品 ID:

private List<Long> skuIds;

修改业务层逻辑:
在这里插入图片描述
    这样后续都是对勾选的商品进行的操作。

(3)库存变更

    上面操作只实现了下单操作,但对应的库存还没跟着一起减少,在下单之后,应该 调用商品微服务,将下单的商品库存减少,销量增加
先实现库存减少的逻辑:
在这里插入图片描述
    可以看到,多线程操作时,无法保证数据的 原子性,出现了 “超卖” 问题。
    解决方法:采用数据库的行级锁控制超卖现象,存储引擎是 InnoDB,默认支持 行级锁 ,只允许一个事务修改记录,只有等该事务结束后,其他事务才能操作;当语句执行结果返回 0,即 受影响行数为 0 时,回滚事务。

在 SkuMapper 中提供方法:

public interface SkuMapper extends Mapper<Sku> {

    @Update("update tb_sku set num=num-#{num} where id=#{id} and num>=#{num}")
    int decrCount(@Param(value = "id") Long id, @Param(value = "num") Integer num);
}

业务层,先在 SkuService 接口中提供 库存递减 的方法:

void decrCount(Map<Long, Integer> decrMap);

实现:

@Override
    public void decrCount(Map<Long, Integer> decrMap) {
        for (Map.Entry<Long, Integer> entry : decrMap.entrySet()) {
            // 商品 ID
            Long id = entry.getKey();

            // 数量
            Integer num = entry.getValue();

            /**
             * 使用行级锁防止超卖,通过数据库的事务特性保证数据原子性
             */
            int row = skuMapper.decrCount(id, num);
            if (row <= 0) {
                throw new RuntimeException("库存不足,请回滚。");
            }
        }
    }

(回滚还未实现。)
控制层:

 @GetMapping(value = "/decr")
    public Result decrCount(@RequestParam Map<Long,Integer> decrMap){

        skuService.decrCount(decrMap);
        return new Result(true,StatusCode.OK,"库存递减成功");
    }

相应地,在 SkuFeign 中提供:

@GetMapping(value = "/decr")
    public Result decrCount(@RequestParam Map<Long,Integer> decrMap);

在 OrderServiceImpl 中调用:
在这里插入图片描述
     这里要注意,Feign 调用的 map 的 key 必须是 String 类型 ,而不能写成 Long,否则会报错 IllegalStateException: QueryMap key must be a String: .Long,因为 Spring Cloud 借助 Feign 调用微服务,是基于 Http 请求的。
    测试一下效果,现在访问 http://localhost:8001/api/cart/list,用户名为 changgou,购物车有 3 个商品记录,对其中一个商品进行下单操作:
在这里插入图片描述
可以看到,响应数据是 500 状态码,Feign 调用出现了问题,再看控制台的报错:
在这里插入图片描述
主要原因是 java.lang.String cannot be cast to java.lang.Integer,但是这个 decrMap 中 ,key 改成 String 类型了,而 value 确实是 Integer 。

在这里插入图片描述

进行debug, 发现 在业务层调用 feign 之前,参数还是 Integer 的:
在这里插入图片描述

    但是用 Feign 调用后,就都变成 String 了(想想也是,毕竟 Feign 走的是 Http 协议,参数是 String 才是符合常理的,能识别到 Map 类型就不错了,Map 的 Value 应当都是按照 String 处理的)
    所以,得到教训:涉及 Feign 调用,Map 的 key 和 value 都建议使用 String 类型,到了具体的方法中再转化成数值类型。
在这里插入图片描述
在这里插入图片描述
这时对下单功能在进行测试:
在这里插入图片描述
     因为会调用 skuMapper 的 decrCount 方法,所以只需要保证 下单http://localhost:18090/cart/add?num=xxx&id=xxx 时,num 的值大于等于数据库 tb_sku 中对应记录的 num 值,SQL 语句就会被执行,数据库中 num 值减少了订单中的量,即 实现了库存变更的功能否则会进行回滚。而且可以看到数据库中 tb_order 订单表有新增数据。
    

(4)增加积分

     除了库存变更,还需要给用户增加积分。实现的步骤是相似的。
    比如每次下单完成之后,给用户增加 10 个积分,这个积分只是用来表示用户活跃度。

  • user 表结构中有积分字段:
    在这里插入图片描述
        对于积分,也是需要保证它的 原子性 的。
  • dao 层
    修改 changgou-service-user 微服务的 com.changgou.user.dao.UserMapper 接口,增加用户积分方法:
public interface UserMapper extends Mapper<User> {

    /**
     * 增加用户积分
     * @param username
     * @param points
     */
    @Update("update tb_user set points=points+#{points} where username=#{username}")
    void addPoints(@Param(value = "username") String username, @Param(value = "points") Integer points);
}
  • 业务层

修改 com.changgou.user.service.UserService 接口:

   void addPoints(String username,Integer points);

实现:

  @Override
    public void addPoints(String username, Integer points) {
        userMapper.addPoints(username,points);
    }
  • 控制层

修改 UserController,添加增加用户积分方法:

@Autowired
private TokenDecode tokenDecode;

/***
 * 增加用户积分
 * @param points
 * @return
 */
@GetMapping(value = "/points/add")
public Result addPoints(@RequestParam Integer points){
    String username = tokenDecode.getUserInfo().get("username");
    userService.addPoints(username,points);
    return new Result(true,StatusCode.OK,"积分成功");
}
  • 提供 Feign

在 changgou-service-user-api 工程 中提供 com.changgou.user.feign.UserFeign:

 @GetMapping(value = "/points/add")
    public Result addPoints(@RequestParam Integer points);

在 order 中要调用 Feign,所以需要添加 changgou-service-user-api 的依赖:

<!--user api 依赖-->
<dependency>
    <groupId>com.changgou</groupId>
    <artifactId>changgou-service-user-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

相应的,order 的启动类上需要声明 Feign 包:
在这里插入图片描述

在下单的 addOrder 方法中,除了更改库存,还要添加积分:
在这里插入图片描述
    接下来,goods 工程 需要像上一篇 order 工程那样,定义 feign 拦截器( 逻辑是:获取当前的 Http Headers ,并放到 Http Headers 中进后续的请求,相当于 Http Headers 的传递。各大微服务之间的认证,其实就是令牌的传递过程。令牌传递起来了,就可以获取其中的用户信息并解析,比如,TokenDecode 类中有 getUserInfo 方法,可以获取用户名 username)。同样地,applicatin.ym 不能使用 hystrix 的 多线程模式:

#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE

二、 支付流程分析

在这里插入图片描述 骤分析如下:

  • 用户下单之后,订单数据会存入到 MySQL 中,同时会将订单对应的支付日志存入到 Redis ,以队列的方式存储。 (创建延时队列)
  • 用户下单后,进入支付页面,支付页面调用支付系统,从 微信支付服务器 获取二维码数据,并在页面生成支付二维码。
  • 用户扫码支付后,微信支付服务器会通调用前预留的回调地址,并携带支付状态信息到支付系统。
  • 支付系统接到支付状态信息后,将支付状态信息发送给 RabbitMQ 。
  • 订单系统监听 RabbitMQ 中的消息获取支付状态,并根据支付状态修改订单状态。
  • 为了防止网络问题导致 notifyurl 没有接到对应数据,定时任务定时获取 Redis 中队列数据去微信支付接口查询状态,并定时更新对应状态。

(✨ 对于可能出现付款失败的情况,我们采用延时 MQ,比如 30 min,系统会监听队列,如果还未支付,说明超时,需要回滚库存,取消订单。)

1、 二维码创建

    简单说下利用 qrious 制作二维码插件。
    qrious 是一款基于 HTML5 Canvas 的纯 JS 二维码生成插件。通过 qrious.js 可以快速生成各种二维码,可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64 编码。

qrious.js 二维码插件的可用配置参数如下:

参数	    类型	默认值		描述
background	String	"white"		二维码的背景颜色。
foreground	String	"black"		二维码的前景颜色。
level		String	"L"			二维码的误差校正级别(L, M, Q, H)。
mime		String	"image/png"	二维码输出为图片时的MIME类型。
size		Number	100			二维码的尺寸,单位像素。
value		String	""			需要编码为二维码的值

下面的代码即可生成一张二维码:

<html>
<head>
    <title>二维码入门小demo</title>
    <!--1.引入js  2. 创建一个img标签 用来存储显示二维码的图片 3.创建js对象 4.设置js对象的配置项-->

    <script src="qrious.js"></script>

</head>
<body>


<img id="myqrious">

</body>

<script>
    var qrious = new QRious({
        element: document.getElementById("myqrious"),// 指定的是图片所在的DOM对象
        size: 250,//指定图片的像素大小
        level: 'H',//指定二维码的容错级别(H:可以恢复30%的数据)
        value: 'weixin://wxpay/bizpayurl?pr=Hujo6p6'//指定二维码图片代表的真正的值
    })

</script>
</html>

还需要在同一个文件夹下导入 qrious:
在这里插入图片描述
运行效果:
在这里插入图片描述

2、微信扫码支付简介
(1)微信扫码支付申请

     微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信 “扫一扫” 完成支付的模式。该模式适用于 PC 网站支付、实体店单品或订单支付、媒体广告支付等场景。

申请步骤:

  • 第一步:注册公众号(类型须为:服务号)
        请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型。
  • 第二步:认证公众号
         公众号认证后才可申请微信支付,认证费:300元/次。
  • 第三步:提交资料申请微信支付
        登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
  • 第四步:开户成功,登录商户平台进行验证
        资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
  • 第五步:在线签署协议
         本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。

    本系统使用已经提供好的 “传智播客”的微信支付账号。

(2) 开发文档

微信支付接口调用的整体思路:
    按 API 要求组装参数,以 XML 方式发送(POST )给微信支付接口(URL),微信支付接口也是以 XML 方式给予响应。程序根据返回的结果(其中包括支付 URL )生成二维码或判断订单状态。

在线微信支付开发文档:

https://pay.weixin.qq.com/wiki/doc/api/index.html

    如果不能联网,可以查阅讲义配套资源 (资源\配套软件\微信扫码支付\开发文档)。
    
    接下来会讲到 ”统一下单” 和 ”查询订单” 两组 API。

1. appid:微信公众账号或开放平台 APP 的唯一标识
2. mch_id:商户号  (配置文件中的 partner)
3. partnerkey:商户密钥
4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
(3)微信支付模式介绍

模式二:(之前有区分 模式一、模式二,实际上现在文档里只有这个模式了 2021.04.14 )
在这里插入图片描述
业务流程说明:

  1. 商户后台系统根据用户选购的商品生成订单。
  2. 用户确认支付后调用微信支付【统一下单API】生成预支付交易;
  3. 微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
  4. 商户后台系统根据返回的code_url生成二维码。
  5. 用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
  6. 微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
  7. 用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
  8. 微信支付系统根据用户授权完成支付交易。
  9. 微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
  10. 微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
  11. 未收到支付通知的情况,商户后台系统调用【查询订单API】。
  12. 商户确认订单已支付后给用户发货。
         二维码只能用一次,扫描之后就不能再扫啦,用于线上支付。

之前的模式一:
在这里插入图片描述

  1. 商户后台系统根据微信支付规定格式生成二维码(规则见下文),展示给用户扫码。
  2. 用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
  3. 微信支付系统收到客户端请求,发起对商户后台系统支付回调URL的调用。调用请求将带productid 和用户的 openid 等参数,并要求商户系统返回交数据包,详细请见"本节3.1回调数据输入参数"
  4. 商户后台系统收到微信支付系统的回调请求,根据productid生成商户系统的订单。
  5. 商户系统调用微信支付【统一下单API】请求下单,获取交易会话标识(prepay_id)
  6. 微信支付系统根据商户系统的请求生成预支付交易,并返回交易会话标识(prepay_id)。
  7. 商户后台系统得到交易会话标识 prepay_id(2小时内有效)。
  8. 商户后台系统将 prepay_id 返回给微信支付系统。返回数据见"本节3.2回调数据输出参数"
  9. 微信支付系统根据交易会话标识,发起用户端授权支付流程。
  10. 用户在微信客户端输入密码,确认支付后,微信客户端提交支付授权。
  11. 微信支付系统验证后扣款,完成支付交易。
  12. 微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
  13. 微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
  14. 未收到支付通知的情况,商户后台系统调用【查询订单API】。
  15. 商户确认订单已支付后给用户发货。
         只是二维码生成的逻辑有差异。

三、总结

(1)实现登录页面,需要静态资源与后端代码配合,先引入 thymeleaf 模板引擎依赖,
提供 /oauth/login 路径对应的控制类和方法,方法返回值对应 thymeleaf 模板前端页面 login.html。
     在继承了 WebSecurityConfigurerAdapter 的配置类的 configure(WebSecurity web)configure(HttpSecurity http) 方法中,分别声明放行地址
和 自定义登录地址(也就是 /oauth/login )。
     还实现了 如果未登录成功,会携带这时的路径访问登录地址,比如,先访问的 http://localhost:8001/api/cart/list 查看购物车,但是此时没有携带 bearer token,登录失败,此时路径会变成 http://localhost:9001/oauth/login?FROM=http://localhost:8001/api/cart/list,到登录页面,如果此时登录成功,会跳转到 FROM 里的路径,也就是 http://localhost:8001/api/cart/list。
    
(2)用户收件地址查询是通过 username 对 tb_address 表的查询。
    
(3)下单操作,对应的路径是 http://localhost:8001/api/order,使用 IdWorker 对象生成订单 、订单明细 ID,这样可以避免自增 ID 带来的重复、数值超出范围的问题。到 Redis 中,以 cart_xxx(用户名 username)为 namespace ,查询记录,得到的就是用户所有勾选了的下单商品对应的 orderitems,简略来说,就像{{商品1,数量 2,价格 3,},{商品 4,数量 5 ,价格 6}}这样就可以求得总数量 和 总金额,再为订单设置时间、状态等属性,先把订单记录插入到 tb_order 表中,再遍历一次 orderitems,为订单明细设置状态等属性,再把订单明细记录到 tb_order_item 表中。
     下单后,还需要从 Redis 中删除记录;递减库存,增加用户积分(活跃度)。
     递减库存和增加活跃度都需要保证变量的原子性,
     采用数据库的行级锁控制超卖现象,存储引擎是 InnoDB,默认支持行级锁 ,只允许一个事务修改记录,只有等该事务结束后,其他事务才能操作;当语句执行结果返回 0,即 受影响行数为 0 时,回滚事务。
     在相应的 mapper 里提供方法,使用 @update 注解执行 SQL 语句。
     库存对应 tb_sku 表中的 num,活跃度对应 tb_user 表中的 points,所以需要提供相应的 feign。递减库存,传订单明细们的 skuId 和 num,可以封装成 Map,不过要注意,因为是 Feign 调用,所以 Map 的 key 和 value 应该都是 String,到具体方法再转化成其他类型;增加积分,传 username 和 points 即可。
    
(4)goods 工程 需要像上一篇 order 工程那样,定义 feign 拦截器( 逻辑是:获取当前的 Http Headers ,并放到 Http Headers 中进后续的请求,相当于 Http Headers 的传递。各大微服务之间的认证,其实就是令牌的传递过程。令牌传递起来了,就可以获取其中的用户信息并解析,比如,TokenDecode 类中有 getUserInfo 方法,可以获取用户名 username)。同样地,applicatin.ym 不能使用 hystrix 的 多线程模式,需要使用 SEMAPHORE 模式。
    
(5)本系统支付采用微信支付接口,按照相应规范进行开发。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐