1 预约下单

1.1 需求分析

1.1.1 业务流程

首先明确本模块在核心业务流程的位置,下图是项目的核心业务流程:
在这里插入图片描述
大概界面原型如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1.2 订单状态

本项目订单状态共有7种,如下图:
在这里插入图片描述
待支付:订单的初始状态。
派单中:用户支付成功后订单的状态由待支付变为派单中。
待服务:服务人员或机构抢单成功订单的状态由派单中变为待服务。
服务中:服务人员开始服务,订单状态变为服务中。
订单完成:服务人员完成服务订单状态变为订单完成。
已取消:订单是待支付状态时用户取消订单,订单状态变为已取消。
已关闭:订单已支付状态下取消订单后订单状态变为已关闭。

1.2 系统设计

1.2.1 订单表设计

在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构,比如:在电商系统中,一个订单购买的多件不同的商品,设计订单表和订单明细表:
订单表:记录订单号、订单金额、下单人、订单状态等信息。
订单明细表:记录该订单购买商品的信息,包括:商品名称、商品价格、交易价格、购买商品数量等。

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

1.2.2 表结构的设置

除了订单号、订单金额、订单状态、下单人ID等字段外,订单表还存储哪些信息?
根据需求梳理预约下单提交的数据如下:
在这里插入图片描述
通过分析,订单表包括以下几部分:
订单基础信息:订单号、订单状态、排序字段、是否显示标记等。
价格信息:单价、购买数量、优惠金额、订单总金额等。
下单人信息:下单人ID、联系方式、位置信息(相当于收货地址)等。
服务(商品)相关信息:服务类型名称、服务项名称、服务单价、价格单位、购买数量等。
服务信息相当于商品,如果有订单明细表要在订单明细表中存储,本项目将服务相关信息存储在订单表。

Mysql表结构如下:

create table `jzo2o-orders`.orders
(
    id               bigint                             not null comment '订单id'
        constraint `PRIMARY`
        primary key,
    user_id          bigint                             not null comment '订单所属人',
    serve_type_id    bigint                             null comment '服务类型id',
    serve_type_name  varchar(50)                        null comment '服务类型名称',
    serve_item_id    bigint                             not null comment '服务项id',
    serve_item_name  varchar(50)                        null comment '服务项名称',
    serve_item_img   varchar(255)                       null comment '服务项图片',
    unit             int                                null comment '服务单位',
    serve_id         bigint                             not null comment '服务id',
    orders_status    int                                not null comment '订单状态,0:待支付,100:派单中,200:待服务,300:服务中,400:待评价,500:订单完成,600:已取消,700:已关闭',
    pay_status       int                                null comment '支付状态,2:待支付,4:支付成功',
    refund_status    int                                null comment '退款状态 1退款中 2退款成功 3退款失败',
    price            decimal(10, 2)                     not null comment '单价',
    pur_num          int      default 1                 not null comment '购买数量',
    total_amount     decimal(10, 2)                     not null comment '订单总金额',
    real_pay_amount  decimal(10, 2)                     not null comment '实际支付金额',
    discount_amount  decimal(10, 2)                     not null comment '优惠金额',
    city_code        varchar(20)                        not null comment '城市编码',
    serve_address    varchar(255)                       not null comment '服务详细地址',
    contacts_phone   varchar(20)                        not null comment '联系人手机号',
    contacts_name    varchar(255)                       not null comment '联系人姓名',
    serve_start_time datetime                           not null comment '服务开始时间',
    lon              double(10, 5)                      null comment '经度',
    lat              double(10, 5)                      null comment '纬度',
    pay_time         datetime                           null comment '支付时间',
    evaluation_time  datetime                           null comment '评价时间',
    trading_order_no bigint                             null comment '支付服务交易单号',
    transaction_id   varchar(50)                        null comment '第三方支付的交易号',
    refund_no        bigint                             null comment '支付服务退款单号',
    refund_id        varchar(50)                        null comment '第三方支付的退款单号',
    trading_channel  varchar(50)                        null comment '支付渠道',
    display          int      default 1                 null comment '用户端是否展示,1:展示,0:隐藏',
    sort_by          bigint                             null comment '排序字段,serve_start_time毫秒级时间戳+订单id后六位',
    create_time      datetime default CURRENT_TIMESTAMP not null,
    update_time      datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
)

数据来源分析:
其中serve_id,pur_num ,serve_start_time 是前端传过来的

1.3 开发远程调用接口

下单接口保存的数据较多,有一些数据需要远程调用来获取:

  1. 根据地址簿Id远程调用客户中心,查询我的地址簿信息。
  2. 根据服务Id远程调用运营基础服务,查询服务相关的信息。

1.3.0 复习下远程调用的开发

由于远程调用接口会被大量微服务所用,因此可以把接口抽取复用到一个API工程
之后,例如查询用户的远程调用,我们需要在API工程写一个接口,而在Customer微服务(远程调用中的服务端)进行实现接口即可,因此在开发中,一般先开发服务端,再开发远程调用的客户端,最后把API工程打包放入依赖仓库即可使用。

1.3.1 查询地址簿远程接口

微服务之间远程调用的接口统一定义在jzo2o-api工程。
查询地址簿远程接口是根据地址簿ID查询地址簿信息,接口定义如下:
接口路径:GET/customer/inner/address-book/{id}
请求数据类型 application/x-www-form-urlencoded

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

jzo2o-api工程定义接口
/**
 * 地址薄相关的远程调用接口
 */
//contextId 指定FeignClient实例的上下文id,如果不设置默认为类名,value指定微服务的名称,path:指定接口地址
@FeignClient(contextId = "jzo2o-customer", value = "jzo2o-customer", path = "/customer/inner/address-book")
public interface AddressBookApi {

    @GetMapping("/{id}")
    AddressBookResDTO detail(@PathVariable("id") Long id);
}
Customer服务实现接口
/**
 * 地址薄远程调用
 */
@RestController
@RequestMapping("inner/address-book")
@Api(tags = "内部接口 - 地址薄相关接口")
public class InnerAddressBookController implements AddressBookApi {

    private IAddressBookService addressBookService;

    @Override
    @GetMapping("/{id}")
    @ApiOperation("地址薄详情")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "地址薄id", required = true, dataTypeClass = Long.class)
    })
    public AddressBookResDTO detail(@PathVariable("id") Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        return BeanUtil.toBean(addressBook, AddressBookResDTO.class);
    }

1.3.2 查询服务&服务项远程接口

jzo2o-api工程定义接口
@FeignClient(contextId = "jzo2o-foundations", value = "jzo2o-foundations", path = "/foundations/inner/serve")
public interface ServeApi {

    @GetMapping("/{id}")
    ServeAggregationResDTO findById(@PathVariable("id") Long id);

}
foundations服务实现接口

很明显这是个多表关联查询,不能用MP,因此先开发Mapper

Mapper开发
    /**
     * 根据id查询详情
     *
     * @param id 服务id
     * @return 服务详情
     */
    ServeAggregationResDTO findServeDetailById(@Param("id") Long id);
    <select id="findServeDetailById" resultType="com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO">
        SELECT
            serve.id,
            serve.city_code,
            serve.price,
            serve.is_hot,
            serve.hot_time_stamp,
            serve.sale_status,
            item.id AS serve_item_id,
            item.`name` AS serve_item_name,
            item.img AS serve_item_img,
            item.detail_img,
            item.serve_item_icon,
            item.unit,
            item.sort_num AS serve_item_sort_num,
            item.serve_type_id AS serve_type_id,
            type.`name` AS serve_type_name,
            type.img AS serve_type_img,
            type.serve_type_icon,
            type.sort_num AS serve_type_sort_num
        FROM
            serve
                inner JOIN serve_item AS item ON item.id = serve.serve_item_id
                inner JOIN serve_type AS type ON type.id = item.serve_type_id
        WHERE
            serve.id = #{id}
    </select>
Service层开发
    /**
     * 根据id查询详情
     *
     * @param id 服务id
     * @return 服务详情
     */
    @Override
    public ServeAggregationResDTO findServeDetailById(Long id) {
        return baseMapper.findServeDetailById(id);
    }
Controller开发
@RestController
@RequestMapping("/inner/serve")
@Api(tags = "内部接口 - 服务相关接口")
public class InnerServeController implements ServeApi {
    @Resource
    private IServeService serveService;

    @Override
    @GetMapping("/{id}")
    @ApiOperation("根据id查询服务")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "服务项id", required = true, dataTypeClass = Long.class)
    })
    public ServeAggregationResDTO findById(@NotNull(message = "id不能为空") @PathVariable("id") Long id) {
        return serveService.findServeDetailById(id);
    }
}

1.4 熔断降级

1.4.1 复习

什么是熔断降级?
在微服务架构一定要去预防微服务雪崩问题,微服务雪崩问题是指在微服务架构中,当一个服务出现故障时,由于服务之间的依赖关系,故障可能会传播到其他服务,导致大规模的服务失败,系统无法正常运行。这种情况就像雪崩一样,最初一个小问题最终引发了整个系统的崩溃。简单理解微服务雪崩就是微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。

常用的预防微服务雪崩的的方法:
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
熔断降级:当服务的异常数或异常比例超过了预设的阈值时,熔断器会进入开启状态,暂时中断对该服务的请求,此时走降级方法,能够快速响应,确保系统的基本功能能够继续运行。
限流:限制对服务的请求速率,避免短时间内大量的请求导致系统崩溃。
线程池隔离:给要请求的资源分配一个线程池,线程池去控制请求数量

1.4.2 使用sentinel实现熔断降级

本项目使用Sentinel实现限流、熔断等机制预防微服务雪崩。
熔断降级是微服务保护的一种方法,当使用Feign进行远程调用,在客户端通过熔断降级措施进行微服务保护。
如下图:
在这里插入图片描述
orders-manager订单服务请求customer查询地址簿,在进行feign远程调用过程出现异常将走降级方法,当异常比例或异常数达到一定的阈值将触发熔断,熔断期间将直接走降级逻辑快速响应。
当customer服务恢复后,熔断时间结束此时会再次尝试请求customer,如果成功请求将关闭熔断,恢复原来的链路。

根据上图可知,熔断、降级发生在客户端,下边在订单管理服务(调用customer的客户端)定义CustomerClient类用于请求customer服务。

1.4.3 客户端集成sentinel

这里是以服务提供者为单独定义远程调用Client类,如果要远程调用jzo2o-foundations服务则定义CustomerClient 类。

添加nacos配置文件shared-sentinel.yaml,如下:
在这里插入图片描述
在order项目中引入shared-sentinel.yaml配置文件:

  cloud:
    nacos:
      config:
        file-extension: yaml
        shared-configs: # 共享配置
          - data-id: shared-redis-cluster.yaml # 共享redis集群配置
            refresh: false
          - data-id: shared-xxl-job.yaml # xxl-job配置
            refresh: false
          - data-id: shared-rabbitmq.yaml # rabbitmq配置
            refresh: false
          - data-id: shared-es.yaml # es
            refresh: false
          - data-id: shared-mysql.yaml # mysql配置
            refresh: false
          - data-id: shared-sentinel.yaml # msentinel配置
            refresh: false  

项目代码中添加依赖

<dependency>
    <groupId>com.jzo2o</groupId>
    <artifactId>jzo2o-sentinel</artifactId>
</dependency>

问:为什么这样呢?
答:我在fremwork中定义了全部依赖,只需要在需要这个sentinel的地方导入这一个依赖就可以全部导入所有相关依赖

1.4.4 sentinel实现熔断降级代码

问:为什么不在api定义这个熔断处理器
答:因为每个客户端的业务需求不一样

@SentinelResource注解的属性说明:
value: 用于定义资源的名称,即 Sentinel 会对该资源进行流量控制和熔断降级。
fallback :非限流、熔断等导致的异常执行的降级方法
blockHandler :触发限流、熔断时执行的降级方法

测试:

/**
 * 调用customer的客户端类
 */
@Component
@Slf4j
public class CustomerClient {
    @Resource
    private AddressBookApi addressBookApi;

    /**
     * 客户端定义自己的降级逻辑
     * @param id
     * @return
     */
    //value 资源名称 将来在sentinel可以查到
    //fallback   定义降级逻辑
    //blockHandler 定义降级逻辑
    @SentinelResource(value = "getAddressBookDetail", fallback = "detailFallback", blockHandler = "detailBlockHandler")
    public AddressBookResDTO getDetail(Long id){
        AddressBookResDTO detail = addressBookApi.detail(id);
        return detail;
    }

    //getDetail执行异常走这个方法
    public AddressBookResDTO detailFallback(Long id, Throwable throwable) {
        log.error("非限流、熔断等导致的异常执行的降级方法,id:{},throwable:", id, throwable);
        return null;
    }


    //熔断后的降级逻辑
    public AddressBookResDTO detailBlockHandler(Long id, BlockException blockException) {
        log.error("触发限流、熔断时执行的降级方法,id:{},blockException:", id, blockException);
        return null;
    }
}

下边在下单方法中通过CustomerClient 调用customer:

@Slf4j
@Service
public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersCreateService {
    @Resource
    private CustomerClient customerClient;

    /**
     * 下单服务
     * @param placeOrderReqDTO
     * @return
     */
    @Override
    public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {

        //地址簿id
        Long addressBookId = placeOrderReqDTO.getAddressBookId();

        //下单人信息,获取地址簿,调用jzo2o-customer服务获取
        AddressBookResDTO detail = customerClient.getDetail(addressBookId);

        //服务相关信息,调用jzo2o-foundations获取

        //生成订单号

        //计算价格

        //组装订单信息,插入数据库订单表
        
        return null;
    }

}

使用项目使用@EnableFeignClients扫描Feign接口,生成代理对象。
具体代码在jzo2o-api工程:

@Slf4j
@Configuration
@EnableFeignClients(basePackages = "com.jzo2o.api")
@Import({com.jzo2o.utils.MyQueryMapEncoder.class})
@ConditionalOnProperty(prefix = "feign", name = "enable", havingValue = "true")
public class ClientScanConfiguration {
				........

在CustomerClient 中注入了Feign接口的代理对象,通过Feign进行远程调用。

1.4.5 测试

1、通过接口文档测试下单接口,触发customerClient.getDetail(addressBookId);
五秒内俩异常就测完了,关闭customer服务发两次请求就行了
在这里插入图片描述
2、在sentinel中配置熔断规则
在这里插入图片描述
5秒以内最少请求2次,有1次异常则进行熔断。熔断时长为30秒。

3、测试:
一次异常后熔断:
在这里插入图片描述
后面再发请求就是熔断降级方法:
在这里插入图片描述

1.5 下单接口设计

整个订单模块包括:订单管理、抢单、派单、历史订单四个小模块,对应的工程如下:
在这里插入图片描述

1.5.0 订单号生成规则

常见的订单号生成规则
  1. 自增数字序列
    使用数据库的自增主键或者其他递增的数字序列(比如redis的INCR命令)作为订单号的一部分。例如,订单号可以是"202310280001",其中"20231028"表示日期,"0001"是自增的订单序号。
  2. 时间戳+随机数
    将年月日时分秒和一定范围内的随机数组合起来。例如,订单号可以是"20181028124523" + “1234”,其中"20181028124523"表示日期和时间,"1234"是随机生成的数字。
    使用时间戳+随机数作为主键有重复的风险。
  3. 订单类型+日期+序号
    将订单类型(例如"01"表示普通订单,“02"表示VIP订单等)、日期和序号组合起来。例如,订单号可以是"0101028100001”,其中"01"表示订单类型,"20181028"表示日期,"00001"是序号。
    加上订单类型的好处是方便客户服务,根据订单号就可以知道订单的类型。
  4. 分布式唯一ID生成器
    使用分布式唯一ID生成器(例如Snowflake雪花算法)生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。
    Snowflake 算法根据机器ID、时间戳、序号等因素生成,保证全局唯一性,它的优势在于生成的 ID 具有趋势递增、唯一性、高效性等特点.
    Snowflake 算法对系统时钟的依赖性较强,如果系统时钟发生回拨,可能会导致 ID 生成出现问题。因此,在使用 Snowflake 算法时,需要定时进行时钟同步,确保系统时钟的稳定性。
本项目订单号生成规则

19位:2位年+2位月+2位日+13位序号
例如:2406011000000000001
实现方案:
1、前6位通过当前时间获取。
2、后13位通过Redis的INCR 命令实现。

代码实现
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 生成订单号id 格式:yymmdd+13位id
     * @return
     */
    private Long generateOrderId(){
        //调用redis生成自增序号
        Long id = redisTemplate.opsForValue().increment(RedisConstants.Lock.ORDERS_SHARD_KEY_ID_GENERATOR, 1);

        long orderId = DateUtils.getFormatDate(LocalDateTime.now(), "yyMMdd") * 10000000000000L + id;
        
        return orderId;
    }

1.5.1 接口分析

除了serve_id、pur_num、serve_start_time 由前端传入以外还需要传入以下参数:
优惠券ID:用户选择优惠券,系统根据优惠券的信息计算优惠金额,需要前端传入优惠券的Id。
我的地址簿ID:用户从我的地址簿中选择地址,前端传入我的地址簿Id,系统从我的地址簿中查询服务地址及具体的经纬度坐标。
其中服务和服务项的信息需要远程调用Foundations服务,而关于客户的信息需要远程调用Costumer服务
接口定义如下:
接口名称:下单接口
接口功能:普通用户创建订单
接口路径:POST/orders-manager/consumer/orders/place
请求数据类型 application/json
在这里插入图片描述
在这里插入图片描述

1.5.1 接口开发

Controller层开发
    @ApiOperation("下单接口")
    @PostMapping("/place")
    public PlaceOrderResDTO place(@RequestBody PlaceOrderReqDTO placeOrderReqDTO) {
        return ordersCreateService.placeOrder(placeOrderReqDTO);
    }
Service层开发
    /**
     * 下单服务
     * @param placeOrderReqDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {

        //下单人信息,获取地址簿,调用jzo2o-customer服务获取
        Long addressBookId = placeOrderReqDTO.getAddressBookId();
        AddressBookResDTO detail = customerClient.getDetail(addressBookId);

        //服务相关信息,调用jzo2o-foundations获取
        ServeAggregationResDTO serveAggregationResDTO = serveApi.findById(placeOrderReqDTO.getServeId());

        //准备组装数据
        Orders orders = new Orders();

        //生成订单号
        Long orderId = generateOrderId();
        orders.setId(orderId);

        //服务类型组装
        orders.setServeTypeId(serveAggregationResDTO.getServeTypeId());
        orders.setServeTypeName(serveAggregationResDTO.getServeTypeName());
        orders.setServeItemId(serveAggregationResDTO.getServeItemId());
        orders.setServeItemName(serveAggregationResDTO.getServeItemName());
        orders.setServeItemImg(serveAggregationResDTO.getServeItemImg());
        orders.setUnit(serveAggregationResDTO.getUnit());
        orders.setServeId(placeOrderReqDTO.getServeId());

        //下单人id
        orders.setUserId(UserContext.currentUserId());

        //订单状态默认待支付
        orders.setOrdersStatus(OrderStatusEnum.NO_PAY.getStatus());
        //支付状态
        orders.setPayStatus(OrderPayStatusEnum.NO_PAY.getStatus());

        //价格
        orders.setPrice(serveAggregationResDTO.getPrice());
        orders.setPurNum(placeOrderReqDTO.getPurNum());
        orders.setTotalAmount(serveAggregationResDTO.getPrice().multiply(new BigDecimal(placeOrderReqDTO.getPurNum())));

        //优惠价格
        orders.setDiscountAmount(BigDecimal.ZERO);
        //实际价格
        orders.setRealPayAmount(NumberUtils.sub(orders.getTotalAmount(),orders.getDiscountAmount()));

        //地址薄方面属性组装
        orders.setCityCode(serveAggregationResDTO.getCityCode());
        String ServeAddress = new StringBuffer(detail.getProvince())
                .append(detail.getCity())
                .append(detail.getCounty())
                .append(detail.getAddress())
                .toString();
        orders.setServeAddress(ServeAddress);
        orders.setContactsPhone(detail.getPhone());
        orders.setContactsName(detail.getName());
        orders.setServeStartTime(placeOrderReqDTO.getServeStartTime());
        orders.setLon(detail.getLon());
        orders.setLat(detail.getLat());

        //排序字段,根据服务开始时间转为毫秒时间戳+订单后5位
        long sortBy = DateUtils.toEpochMilli(orders.getServeStartTime()) + orders.getId() % 100000;
        orders.setSortBy(sortBy);

        //插入数据库订单表
        boolean save = this.save(orders);

        if (!save) {
            throw new DbRuntimeException("下单失败");
        }

        return new PlaceOrderResDTO(orders.getId());

    }
下单预约功能测试

在这里插入图片描述
输入信息点击立即预约:
在这里插入图片描述
在这里插入图片描述

查一下数据库:

在这里插入图片描述
预约功能实现完成

2 支付功能

2.1 支付方式

小程序调起支付这里,微信会校验小程序的APPID与微信支付商户的ID是否绑定,微信支付商户的ID怎么获取呢?是需要注册商户上传企业资料及法人资料,微信审核通过后方可 注册成功,所以注册成为一个普通商户对项目测试有限制。
为了简便使用,支付接口使用扫码支付支付,此接口不存在小程序端调起支付的限制,也就是使用Native支付接口
在这里插入图片描述
具体使用:https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html

2.2 支付服务的设计

2.2.1 表设计

  1. 支付渠道表:
    支付渠道表存储了第三方支付(微信、支付宝)的支付参数,如:商户号、证书序列号、api私钥等信息
create table `jzo2o-trade`.pay_channel
(
    id                   bigint                             not null comment '主键'
        constraint `PRIMARY`
        primary key,
    channel_name         varchar(32)                        null comment '通道名称',
    channel_label        varchar(32)                        null comment '通道唯一标记',
    domain               varchar(255)                       null comment '域名',
    app_id               varchar(32) collate utf8_bin       not null comment '商户appid',
    public_key           varchar(2000) collate utf8_bin     not null comment '支付公钥',
    merchant_private_key varchar(2000) collate utf8_bin     not null comment '商户私钥',
    other_config         varchar(1000)                      null comment '其他配置',
    encrypt_key          varchar(255) charset utf8mb4       null comment 'AES混淆密钥',
    remark               varchar(400)                       null comment '说明',
    notify_url           varchar(255)                       null comment '回调地址',
    enable_flag          varchar(10)                        null comment '是否有效',
    enterprise_id        bigint                             null comment '商户ID【系统内部识别使用】',
    create_time          datetime default CURRENT_TIMESTAMP null comment '创建时间',
    update_time          datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
  1. 交易单表
    支付服务请求第三方支付下单成功向交易表写入一条记录
    家政服务的一个订单可能对应支付服务的多条交易单,比如:用户用微信支付在交易单表生成一条交易单,如果微信支付失败再用支付宝支付时也会在交易单表中生成一条记录。
    用户支付成功后支付服务更新交易单表的支付状态。
create table `jzo2o-trade`.trading
(
    id               bigint                                 not null comment '主键'
        constraint `PRIMARY`
        primary key,
    product_order_no bigint                                 not null comment '业务系统订单号',
    trading_order_no bigint                                 not null comment '交易系统订单号【对于三方来说:商户订单】',
    transaction_id   varchar(50)                            null comment '第三方支付交易号',
    trading_channel  varchar(32) charset utf8mb4            not null comment '支付渠道【支付宝、微信、现金、免单挂账】',
    trading_type     varchar(22)                            not null comment '交易类型【付款、退款、免单、挂账】',
    trading_state    int                                    not null comment '交易单状态【2-付款中,3-付款失败,4-已结算,5-取消订单,6-免单,7-挂账】',
    payee_name       varchar(50)                            null comment '收款人姓名',
    payee_id         bigint                                 null comment '收款人账户ID',
    payer_name       varchar(50)                            null comment '付款人姓名',
    payer_id         bigint                                 null comment '付款人Id',
    trading_amount   decimal(22, 2)                         not null comment '交易金额,单位:元',
    refund           decimal(12, 2)                         null comment '退款金额【付款后,单位:元',
    is_refund        varchar(32) charset utf8mb4            null comment '是否有退款:YES,NO',
    result_code      varchar(80)                            null comment '第三方交易返回编码【最终确认交易结果】',
    result_msg       varchar(255)                           null comment '第三方交易返回提示消息【最终确认交易信息】',
    result_json      varchar(2000)                          null comment '第三方交易返回信息json【分析交易最终信息】',
    place_order_code varchar(80)                            null comment '统一下单返回编码',
    place_order_msg  varchar(255)                           null comment '统一下单返回信息',
    place_order_json text                                   null comment '统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】',
    enterprise_id    bigint                                 not null comment '商户号',
    memo             varchar(150)                           null comment '备注【订单门店,桌台信息】',
    qr_code          text                                   null comment '二维码base64数据',
    open_id          varchar(36) collate utf8mb4_unicode_ci null comment 'open_id标识',
    enable_flag      varchar(10)                            null comment '是否有效',
    create_time      datetime default CURRENT_TIMESTAMP     null comment '创建时间',
    update_time      datetime default CURRENT_TIMESTAMP     null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint trading_order_no
        unique (trading_order_no) comment '支付订单号'
)
  1. 退款记录表
    用户申请退款在退款记录表写一条记录。
    退款成功后支付服务更新退款状态。
create table `jzo2o-trade`.refund_record
(
    id               bigint                             not null comment '主键'
        constraint `PRIMARY`
        primary key,
    trading_order_no bigint                             not null comment '交易系统订单号【对于三方来说:商户订单】',
    product_order_no bigint                             not null comment '业务系统订单号',
    refund_no        bigint                             not null comment '本次退款订单号',
    refund_id        varchar(50)                        null comment '第三方支付的退款单号',
    enterprise_id    bigint                             not null comment '商户号',
    trading_channel  varchar(32) charset utf8mb4        not null comment '退款渠道【支付宝、微信、现金】',
    refund_status    int                                not null comment '退款状态:0-发起退款,1-退款中,2-成功, 3-失败',
    refund_code      varchar(80) charset utf8           null comment '返回编码',
    refund_msg       text charset utf8                  null comment '返回信息',
    memo             varchar(150) charset utf8          null comment '备注【订单门店,桌台信息】',
    refund_amount    decimal(12, 2)                     not null comment '本次退款金额',
    total            decimal(12, 2)                     not null comment '原订单金额',
    create_time      datetime default CURRENT_TIMESTAMP null comment '创建时间',
    update_time      datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint refund_no
        unique (refund_no)
)

2.2.2 数据流

支付接口:收到支付请求后请求第三方支付的下单接口,并向交易单表新增记录。
查询交易结果接口:请求第三方支付的查询支付结果并更新交易单表的支付状态。
接收第三方通过支付结果:更新交易单表的支付状态。
退款接口:新增退款记录
更新退款状态:请求第三方退款结果查询接口查询退款状态,并更新退款状态。
在这里插入图片描述

2.2.3 支付/退款代码

Controller
@Validated
@RestController("innerNativePayController")
@Api(tags = "内部接口 - Native支付")
@RequestMapping("/inner/native")
public class NativePayController implements NativePayApi {

    @Resource
    private NativePayService nativePayService;

    /***
     * 扫码支付,收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。
     *
     * @param nativePayDTO 扫码支付提交参数
     * @return 扫码支付响应数据,其中包含二维码路径
     */
    @Override
    @PostMapping
    @ApiOperation(value = "统一收单线下交易", notes = "统一收单线下交易")
    @ApiImplicitParam(name = "nativePayDTO", value = "扫码支付提交参数", required = true)
    public NativePayResDTO createDownLineTrading(@RequestBody NativePayReqDTO nativePayDTO) {
        Trading tradingEntity = BeanUtil.toBean(nativePayDTO, Trading.class);
        Trading trading = this.nativePayService.createDownLineTrading(nativePayDTO.isChangeChannel(),tradingEntity);
        return BeanUtil.toBean(trading, NativePayResDTO.class);
    }

}
Service
    public Trading createDownLineTrading(boolean changeChannel,Trading tradingEntity) {
        //获取付款中的记录
        Trading trading = tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel());
        //如果切换二维码需要查询其它支付渠道付款中的交易单进行退款操作
        if(changeChannel){
            changeChannelAndCloseTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(),tradingEntity.getTradingChannel());
        }
        //付款中的记录直接返回无需生成二维码
        if (ObjectUtil.isNotNull(trading)){
            return trading;
        }
        //交易前置处理:检测交易单参数
        beforePayHandler.checkCreateTrading(tradingEntity);

        tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK);
        tradingEntity.setEnableFlag(Constants.YES);

        //对交易订单加锁
        Long productOrderNo = tradingEntity.getProductOrderNo();
        String key = TradingCacheConstant.CREATE_PAY + productOrderNo;
        RLock lock = redissonClient.getFairLock(key);
        try {
            //获取锁
            if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
                //交易前置处理:幂等性处理
//                this.beforePayHandler.idempotentCreateTrading(tradingEntity);

                //&#x8C03;&#x7528;&#x4E0D;&#x540C;&#x7684;&#x652F;&#x4ED8;&#x6E20;&#x9053;&#x8FDB;&#x884C;&#x5904;&#x7406;
                PayChannelEnum payChannel = PayChannelEnum.valueOf(tradingEntity.getTradingChannel());
                NativePayHandler nativePayHandler = HandlerFactory.get(payChannel, NativePayHandler.class);
                nativePayHandler.createDownLineTrading(tradingEntity);

                //生成统一收款二维码
                String placeOrderMsg = tradingEntity.getPlaceOrderMsg();
                String qrCode = this.qrCodeService.generate(placeOrderMsg, payChannel);
                tradingEntity.setQrCode(qrCode);
                //指定交易状态为付款中
                tradingEntity.setTradingState(TradingStateEnum.FKZ);
                //新增交易数据
                boolean flag = this.tradingService.save(tradingEntity);
                if (!flag) {
                    throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.SAVE_OR_UPDATE_FAIL.getValue());
                }

                return tradingEntity;
            }
            throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
        } catch (CommonException e) {
            throw e;
        } catch (Exception e) {
            log.error("统一收单线下交易预创建异常:{}", ExceptionUtil.stacktraceToString(e));
            throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
        } finally {
            lock.unlock();
        }
    }

其中微信接口:

    @Override
    public void createDownLineTrading(Trading tradingEntity) throws CommonException {
        // 查询配置
        WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId());
        //请求地址
        String apiPath = "/v3/pay/transactions/native";

        //请求参数
        Map<String, Object> params = MapUtil.<String, Object>builder()
                .put("mchid", client.getMchId())
                .put("appid", client.getAppId())
                .put("description", tradingEntity.getMemo())
                .put("notify_url", client.getNotifyUrl())
                .put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo()))
                .put("amount", MapUtil.<String, Object>builder()
                        .put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分
                        .put("currency", "CNY") //人民币
                        .build())
                .build();

        try {
            WeChatResponse response = client.doPost(apiPath, params);
            if (!response.isOk()) {
                //下单失败
                throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
            }
            //指定统一下单code
            tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus()));
            //二维码需要展现的信息
            tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url"));
            //指定统一下单json字符串
            tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));

        } catch (Exception e) {
            throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue());
        }
    }

2.3 订单支付接口

2.3.0 需求

用户点击支付按钮,前端请求订单服务,订单服务请求支付服务,最后支付服务请求微信的下单接口。
流程如下:
在这里插入图片描述
通过交互流程可知:
订单管理服务请求支付服务生成支付二维码,拿到交易单号将其和支付渠道更新到订单表。
最后订单管理服务将交易单信息及二维码返回给前端。

2.3.1 接口分析

要生成二维码需要由订单管理服务请求支付服务的支付接口,支付服务的支付接口如下:
接口路径:POST/trade/inner/native
请求数据类型 application/json
在这里插入图片描述
在这里插入图片描述
因此,支付接口需要:

productOrderNo是业务系统的订单号,本项目就是家政服务的订单号。
tradingAmount:支付金额
enterpriseId:商户号,进入微信或支付宝商户平台获取。
memo: 备注信息
tradingChannel:微信支付传入WECHAT_PAY,支付宝支付传入ALI_PAY
changeChannel:当用户先微信支付,然后又进行支付宝支付表示切换了支付渠道,此时传入true

我们需要根据支付服务的支付接口的参数分析这些参数的数据来源
productOrderNo:即订单号由前端传入
tradingAmount:根据订单号查询订单信息即可拿到金额
enterpriseId:在nacos配置好
memo:程序拼装
tradingChannel:支付渠道,前端传入
changeChannel:根据订单号查询订单表的trading_channel字段来判断。第一次支付后将第一次支付的支付渠道更新至订单表,第二次如果切换了支付渠道通过trading_channel字段可以知道是否切换支付渠道。

所以综上分析,前端请求订单管理服务提供的支付接口需要传入:
productOrderNo: 订单id
tradingChannel:支付渠道。

接口定义如下:
在这里插入图片描述
在这里插入图片描述

2.3.2 接口开发

Controller层开发
@RestController("consumerOrdersController")
@Api(tags = "用户端-订单相关接口")
@RequestMapping("/consumer/orders")
public class ConsumerOrdersController {

    @Resource
    private IOrdersCreateService ordersCreateService;
    @Resource
    private IOrdersManagerService ordersManagerService;



    @PutMapping("/pay/{id}")
    @ApiOperation("订单支付")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "订单id", required = true, dataTypeClass = Long.class)
    })
    public OrdersPayResDTO pay(@PathVariable("id") Long id, @RequestBody OrdersPayReqDTO ordersPayReqDTO) {
        return null;
    }
Mapper层

根据接口分析,请求支付服务生成支付二维码成功将交易单号和支付渠道更新到订单表中。
单表操作不需要写了

Service层开发

其中我们需要将nacos配置文件的商户号拿出来,因本次写一个配置文件类去进行注入

@Data
@Component
@ConfigurationProperties(prefix = "jzo2o.trade")
public class TradeProperties {

    /**
     * 支付宝商户id
     */
    private Long aliEnterpriseId;

    /**
     * 微信支付商户id
     */
    private Long wechatEnterpriseId;
}

之后正式进行Service层开发,其中远程调用还是使用API工程进行调用支付微服务,(暂时不写熔断降级业务流程了)

    /**
     * 订单支付实现
     * @param id 订单id
     * @param ordersPayReqDTO 支付类型请求体
     * @return
     */
    @Override
    public OrdersPayResDTO pay(Long id, OrdersPayReqDTO ordersPayReqDTO) {
        //看看订单支付了吗或者存在吗
        Orders orders = this.getById(id);
        if(ObjectUtils.isNull(orders)){
            throw new CommonException("订单不存在");
        }else if(orders.getPayStatus().equals(OrderPayStatusEnum.PAY_SUCCESS)){
            OrdersPayResDTO ordersPayResDTO = BeanUtils.copyBean(orders,OrdersPayResDTO.class);
            ordersPayResDTO.setProductOrderNo(orders.getId());
            return ordersPayResDTO;
        }else{
            //生成支付二维码
            NativePayResDTO nativePayResDTO = generateQrCode(orders,ordersPayReqDTO.getTradingChannel());
            OrdersPayResDTO ordersPayResDTO = BeanUtil.toBean(nativePayResDTO, OrdersPayResDTO.class);
            return ordersPayResDTO;
        }
    }

    /**
     * 请求支付服务,生成二维码
     * @param orders 订单对象
     * @param tradingChannel 请求渠道
     */
    private NativePayResDTO generateQrCode(Orders orders, PayChannelEnum tradingChannel) {

        //封装请求支付服务的参数
        NativePayReqDTO nativePayReqDTO = new NativePayReqDTO();

        //判断请求渠道微信还是支付宝,从而取出相应商户号
        Long enterpriseId = ObjectUtils.equal(tradingChannel, PayChannelEnum.WECHAT_PAY) ? tradeProperties.getWechatEnterpriseId() : tradeProperties.getAliEnterpriseId();
        nativePayReqDTO.setEnterpriseId(enterpriseId);

        //家政订单号
        nativePayReqDTO.setProductOrderNo(orders.getId());

        //金额
        nativePayReqDTO.setTradingAmount(orders.getRealPayAmount());

        //业务系统标识 统一为jzo2o.orders
        nativePayReqDTO.setProductAppId("jzo2o.orders");

        //请求的支付渠道
        nativePayReqDTO.setTradingChannel(tradingChannel);

        //是否切换支付渠道
        //首先拿到当前orders的支付渠道,和现在的比一下
        if(ObjectUtils.isNotEmpty(orders.getTradingChannel()) && ObjectUtils.notEqual(orders.getTradingChannel(),tradingChannel.getValue())){
            //表示切换渠道了
            nativePayReqDTO.setChangeChannel(true);
        }else{
            nativePayReqDTO.setChangeChannel(false);
        }

        //设置备注
        nativePayReqDTO.setMemo(orders.getServeItemName());

        //远程调用请求支付服务生成支付二维码
        NativePayResDTO downLineTrading = nativePayApi.createDownLineTrading(nativePayReqDTO);

        //二维码生成成功,拿到支付服务返回的交易单号,更新数据库中
        if(ObjectUtils.isNotNull(downLineTrading)){
            log.info("订单:{}请求支付,生成二维码:{}",orders.getId(),downLineTrading.toString());
            //更新订单表的支付订单号和交易渠道
            boolean update = lambdaUpdate()
                    .eq(Orders::getId, downLineTrading.getProductOrderNo())
                    .set(Orders::getTradingOrderNo, downLineTrading.getTradingOrderNo())
                    .set(Orders::getTradingChannel, downLineTrading.getTradingChannel())
                    .update();
            if(!update){
                throw new CommonException("订单:"+orders.getId()+"请求支付更新交易单号失败");
            }
        }else{
            throw new CommonException("请求支付服务生成二维码失败");
        }
        return downLineTrading;
    }

2.3.3 接口测试

点击预约之后:
在这里插入图片描述
之后付钱查看数据库:
在这里插入图片描述

2.4 对接查询支付结果

2.4.1 需求分析

在用户支付后用户点击“完成支付”此时前端请求订单服务的查询支付结果接口,如果支付成功则跳转到支付成功界面。
交互流程如下:
在这里插入图片描述
此接口对于支付中的订单最终由支付服务调用微信查询支付结果。
订单管理服务查询到支付结果后更新订单的支付状态。

2.4.2 接口分析

  1. 传入参数
    本接口要调用支付服务的支付结果查询接口,根据支付服务的支付结果查询接口的传入参数分析本接口的传入参数。
    支付服务的支付结果查询接口需要传入交易单号。交易单号在订单表已经保存所以前端传入订单号即可拿到交易单号。
    所以传入参数:订单号
    在这里插入图片描述

  2. 响应结果
    本接口的目的是查询支付结果,所以响应结果中要有支付结果,其它的参数就是订单号、交易单号等相关参数。
    在这里插入图片描述

2.4.3 接口开发

Controller层开发
    @GetMapping("/pay/{id}/result")
    @ApiOperation("查询订单支付结果")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "订单id", required = true, dataTypeClass = Long.class)
    })
    public OrdersPayResDTO payResult(@PathVariable("id") Long id) {
        return ordersCreateService.getPayResultFromTradServer(id);
    }
Mapper层开发

拿到支付结果更新订单表的支付结果。不需要写,直接mp即可

Service层开发

根据接口分析,查询支付结果需要做两部分的工作:
调用支付服务的查询支付结果接口获取支付结果。
将支付结果更新至订单表的支付状态字段。

    /**
     * 请求支付服务获取支付结果
     * @param id
     * @return
     */
    @Override
    public OrdersPayResDTO getPayResultFromTradServer(Long id) {
        //根据订单号查询订单信息,拿到交易单号
        Orders orders = this.getById(id);
        //校验存在性
        if(ObjectUtils.isNull(orders)){
            throw new CommonException("订单不存在");
        }

        //未支付且已存在支付服务的交易单号此时远程调用支付服务查询支付结果
        if (OrderPayStatusEnum.NO_PAY.getStatus() == orders.getPayStatus()
                && ObjectUtil.isNotEmpty(orders.getTradingOrderNo())) {

            //根据交易单号请求支付服务查询支付结果
            TradingResDTO tradResultByTradingOrderNo = tradingApi.findTradResultByTradingOrderNo(orders.getTradingOrderNo());

            //如果支付成功,更新数据库
            if(ObjectUtils.equal((tradResultByTradingOrderNo.getTradingState()), TradingStateEnum.YJS)){
                TradeStatusMsg tradeStatusMsg = new TradeStatusMsg();
                tradeStatusMsg.setProductOrderNo(id);
                tradeStatusMsg.setTradingChannel(tradResultByTradingOrderNo.getTradingChannel());
                tradeStatusMsg.setTradingOrderNo(tradResultByTradingOrderNo.getTradingOrderNo());
                tradeStatusMsg.setTransactionId(tradResultByTradingOrderNo.getTransactionId());
                tradeStatusMsg.setStatusCode(OrderPayStatusEnum.PAY_SUCCESS.getStatus());
                tradeStatusMsg.setStatusName(OrderPayStatusEnum.PAY_SUCCESS.name());
                owner.paySuccess(tradeStatusMsg);

                //构造返回数据
                OrdersPayResDTO ordersPayResDTO = BeanUtils.toBean(tradeStatusMsg,OrdersPayResDTO.class);
                ordersPayResDTO.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());
                return ordersPayResDTO;
            }
        }
        
        //支付未成功 或者 早就支付了
        OrdersPayResDTO ordersPayResDTO = new OrdersPayResDTO();
        ordersPayResDTO.setPayStatus(orders.getPayStatus());
        ordersPayResDTO.setProductOrderNo(orders.getId());
        ordersPayResDTO.setTradingOrderNo(orders.getTradingOrderNo());
        ordersPayResDTO.setTradingChannel(orders.getTradingChannel());
        return ordersPayResDTO;
    }
    /**
     * 支付成功, 更新数据库的订单表及其他信息
     *
     * @param tradeStatusMsg 交易状态消息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void paySuccess(TradeStatusMsg tradeStatusMsg){


        boolean update = lambdaUpdate().eq(Orders::getId, tradeStatusMsg.getProductOrderNo())
                .eq(Orders::getOrdersStatus,0) //订单状态只能由待支付 才可以变为派单中
                .set(Orders::getPayStatus, OrderPayStatusEnum.PAY_SUCCESS.getStatus())
                .set(Orders::getTransactionId, tradeStatusMsg.getTransactionId())
                .set(Orders::getOrdersStatus, OrderStatusEnum.DISPATCHING.getStatus()) //订单状态变为派单中
                .set(Orders::getPayTime, LocalDateTime.now())
                .update();

        if(!update){
            throw new CommonException("支付成功,更新"+tradeStatusMsg.getProductOrderNo()+"订单状态为派单中失败");
        }
    }

2.4.4 接口测试

支付完成后点击支付完成
在这里插入图片描述
之后可以查看订单状态
在这里插入图片描述
在这里插入图片描述

2.5 接收支付通知

2.5.1 需求分析

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部