小熊家务帮day15-day18 预约下单模块(预约下单,熔断降级,支付功能,退款功能)
目录
- 1 预约下单
- 1.1 需求分析
- 1.1.1 业务流程
- 1.1.2 订单状态
- 1.2 系统设计
- 1.2.1 订单表设计
- 1.2.2 表结构的设置
- 1.3 开发远程调用接口
- 1.3.0 复习下远程调用的开发
- 1.3.1 查询地址簿远程接口
- jzo2o-api工程定义接口
- Customer服务实现接口
- 1.3.2 查询服务&服务项远程接口
- jzo2o-api工程定义接口
- foundations服务实现接口
- Mapper开发
- Service层开发
- Controller开发
- 1.4 熔断降级
- 1.4.1 复习
- 1.4.2 使用sentinel实现熔断降级
- 1.4.3 客户端集成sentinel
- 1.4.4 sentinel实现熔断降级代码
- 1.4.5 测试
- 1.5 下单接口设计
- 1.5.0 订单号生成规则
- 常见的订单号生成规则
- 本项目订单号生成规则
- 代码实现
- 1.5.1 接口分析
- 1.5.1 接口开发
- Controller层开发
- Service层开发
- 下单预约功能测试
- 2 支付功能
- 2.1 支付方式
- 2.2 支付服务的设计
- 2.2.1 表设计
- 2.2.2 数据流
- 2.2.3 支付/退款代码
- Controller
- Service
- 2.3 订单支付接口
- 2.3.0 需求
- 2.3.1 接口分析
- 2.3.2 接口开发
- Controller层开发
- Mapper层
- Service层开发
- 2.3.3 接口测试
- 2.4 对接查询支付结果
- 2.4.1 需求分析
- 2.4.2 接口分析
- 2.4.3 接口开发
- Controller层开发
- Mapper层开发
- Service层开发
- 2.4.4 接口测试
- 2.5 接收支付通知
- 2.5.1 需求分析
- 2.5.2 接受通知实现
- 2.5.3 定时查询支付状态并通知的实现
- 2.5.3 测试不点支付成功成功更新的业务
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 开发远程调用接口
下单接口保存的数据较多,有一些数据需要远程调用来获取:
- 根据地址簿Id远程调用客户中心,查询我的地址簿信息。
- 根据服务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">SELECTserve.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_numFROMserveinner JOIN serve_item AS item ON item.id = serve.serve_item_idinner JOIN serve_type AS type ON type.id = item.serve_type_idWHEREserve.id = #{id}</select>
Service层开发
/*** 根据id查询详情** @param id 服务id* @return 服务详情*/@Overridepublic ServeAggregationResDTO findServeDetailById(Long id) {return baseMapper.findServeDetailById(id);}
Controller开发
@RestController
@RequestMapping("/inner/serve")
@Api(tags = "内部接口 - 服务相关接口")
public class InnerServeController implements ServeApi {@Resourceprivate 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: yamlshared-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 # esrefresh: 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 {@Resourceprivate 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 {@Resourceprivate CustomerClient customerClient;/*** 下单服务* @param placeOrderReqDTO* @return*/@Overridepublic PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//地址簿idLong 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 订单号生成规则
常见的订单号生成规则
- 自增数字序列
使用数据库的自增主键或者其他递增的数字序列(比如redis的INCR命令)作为订单号的一部分。例如,订单号可以是"202310280001",其中"20231028"表示日期,"0001"是自增的订单序号。 - 时间戳+随机数
将年月日时分秒和一定范围内的随机数组合起来。例如,订单号可以是"20181028124523" + “1234”,其中"20181028124523"表示日期和时间,"1234"是随机生成的数字。
使用时间戳+随机数作为主键有重复的风险。 - 订单类型+日期+序号
将订单类型(例如"01"表示普通订单,“02"表示VIP订单等)、日期和序号组合起来。例如,订单号可以是"0101028100001”,其中"01"表示订单类型,"20181028"表示日期,"00001"是序号。
加上订单类型的好处是方便客户服务,根据订单号就可以知道订单的类型。 - 分布式唯一ID生成器
使用分布式唯一ID生成器(例如Snowflake雪花算法)生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。
Snowflake 算法根据机器ID、时间戳、序号等因素生成,保证全局唯一性,它的优势在于生成的 ID 具有趋势递增、唯一性、高效性等特点.
Snowflake 算法对系统时钟的依赖性较强,如果系统时钟发生回拨,可能会导致 ID 生成出现问题。因此,在使用 Snowflake 算法时,需要定时进行时钟同步,确保系统时钟的稳定性。
本项目订单号生成规则
19位:2位年+2位月+2位日+13位序号
例如:2406011000000000001
实现方案:
1、前6位通过当前时间获取。
2、后13位通过Redis的INCR 命令实现。
代码实现
@Autowiredprivate 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());//下单人idorders.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 表设计
- 支付渠道表:
支付渠道表存储了第三方支付(微信、支付宝)的支付参数,如:商户号、证书序列号、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 '更新时间'
)
- 交易单表
支付服务请求第三方支付下单成功向交易表写入一条记录
家政服务的一个订单可能对应支付服务的多条交易单,比如:用户用微信支付在交易单表生成一条交易单,如果微信支付失败再用支付宝支付时也会在交易单表中生成一条记录。
用户支付成功后支付服务更新交易单表的支付状态。
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_nounique (trading_order_no) comment '支付订单号'
)
- 退款记录表
用户申请退款在退款记录表写一条记录。
退款成功后支付服务更新退款状态。
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_nounique (refund_no)
)
2.2.2 数据流
支付接口:收到支付请求后请求第三方支付的下单接口,并向交易单表新增记录。
查询交易结果接口:请求第三方支付的查询支付结果并更新交易单表的支付状态。
接收第三方通过支付结果:更新交易单表的支付状态。
退款接口:新增退款记录
更新退款状态:请求第三方退款结果查询接口查询退款状态,并更新退款状态。
2.2.3 支付/退款代码
Controller
@Validated
@RestController("innerNativePayController")
@Api(tags = "内部接口 - Native支付")
@RequestMapping("/inner/native")
public class NativePayController implements NativePayApi {@Resourceprivate 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);//调用不同的支付渠道进行处理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();}}
其中微信接口:
@Overridepublic 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());}//指定统一下单codetradingEntity.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 {@Resourceprivate IOrdersCreateService ordersCreateService;@Resourceprivate 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*/@Overridepublic 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.ordersnativePayReqDTO.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 接口分析
-
传入参数
本接口要调用支付服务的支付结果查询接口,根据支付服务的支付结果查询接口的传入参数分析本接口的传入参数。
支付服务的支付结果查询接口需要传入交易单号。交易单号在订单表已经保存所以前端传入订单号即可拿到交易单号。
所以传入参数:订单号
-
响应结果
本接口的目的是查询支付结果,所以响应结果中要有支付结果,其它的参数就是订单号、交易单号等相关参数。
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*/@Overridepublic 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 需求分析
(尴尬的一点就是扫码支付,需要用户付完钱点击支付完成,那如果不点击,就不会更新支付状态,因此需要这个模块)
用户支付成功后支付服务 获取支付结果后会通知业务系统,业务系统收到支付结果只处理属于自己的支付通知,根据支付结果更新订单表的支付状态,现在来实现订单管理服务接收支付通知更新支付状态功能。
订单管理服务接收支付通知的交互流程如下:
支付服务将支付结果发送到MQ,订单管理服务监听MQ,收到支付结果,更新订单表的支付状态。
这里有个问题是:支付服务作为项目的公共支付服务,对接支付服务的可能不止家政服务订单还可能有其它收费订单,比如:年会员订单,购买优惠券等等,支付服务如何将属于每个收费订单的支付结果通知给它们呢?
- 首先在请求支付服务支付接口中需要传入product_app_id,它表示请求支付业务系统的应用标识
此应用标识会存储到交易单信息
- 支付服务通知支付结果时将交易单中的product_app_id一起发给各个监听MQ的微服务。
具体的方法是如下:
支付服务向jzo2o.exchange.topic.trade交换机发送消息,Routing Key=UPDATE_STATUS
绑定此交换机的有多个队列,每个队列是不同的收费订单支付通知队列,如下图:
当支付服务向jzo2o.exchange.topic.trade交换机发送一条支付通知消息,所有绑定此交换机的队列且Routing Key=UPDATE_STATUS都会收到支付通知。
业务系统收到支付结果后解析出product_app_id,判断是否属于自己的支付结果通知,如果是再进行处理。
整体交互流程如下:
2.5.2 接受通知实现
@Resourceprivate IOrdersCreateService ordersCreateService;/*** 更新支付结果* 支付成功** @param msg 消息*/@RabbitListener(bindings = @QueueBinding(value = @Queue(name = MqConstants.Queues.ORDERS_TRADE_UPDATE_STATUS),exchange = @Exchange(name = MqConstants.Exchanges.TRADE, type = ExchangeTypes.TOPIC),key = MqConstants.RoutingKeys.TRADE_UPDATE_STATUS))public void listenTradeUpdatePayStatusMsg(String msg) {//解析消息 消息内容是List<TradeStatusMsg>的json格式List<TradeStatusMsg> tradeStatusMsgs = JSON.parseArray(msg, TradeStatusMsg.class);//判断是不是自己的 根据productAppId判断//还需要只取出支付成功的结果List<TradeStatusMsg> collect = tradeStatusMsgs.stream().filter(item -> item.getProductAppId().equals("jzo2o.orders") && item.getStatusCode() == 4).collect(Collectors.toList());//调用方法更新数据库订单表的支付状态和订单状态for (TradeStatusMsg tradeStatusMsg : collect) {ordersCreateService.paySuccess(tradeStatusMsg);}}
2.5.3 定时查询支付状态并通知的实现
首先分为两类:付款中和其他
对于付款中的订单,计算是否超时
对于其他类型的订单(例如付款成功,付款失败等等),直接发到交换机,让相关业务去处理,相关业务会将付款成功的写入数据库
* 分片广播方式查询支付状态* 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理*/@XxlJob("tradingJob")public void tradingJob() {// 分片参数int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);List<Trading> list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);if (CollUtil.isEmpty(list)) {XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);return;}//定义消息通知列表,只要是状态不为【付款中】就需要通知其他系统List<TradeStatusMsg> tradeMsgList = new ArrayList<>();for (Trading trading : list) {if (trading.getTradingOrderNo() % shardTotal != shardIndex) {continue;}try {//查询交易单TradingDTO tradingDTO = this.basicPayService.queryTradingResult(trading.getTradingOrderNo());if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder().tradingOrderNo(trading.getTradingOrderNo()).productOrderNo(trading.getProductOrderNo()).productAppId(trading.getProductAppId()).transactionId(tradingDTO.getTransactionId()).tradingChannel(tradingDTO.getTradingChannel()).statusCode(tradingDTO.getTradingState().getCode()).statusName(tradingDTO.getTradingState().name()).info(tradingDTO.getMemo())//备注信息.build();tradeMsgList.add(tradeStatusMsg);}else{//如果是未支付,需要判断下时间,超过20分钟未支付的订单需要关闭订单以及设置状态为QXDDlong between = LocalDateTimeUtil.between(trading.getCreateTime(), LocalDateTimeUtil.now(), ChronoUnit.MINUTES);if (between >= 20) {try {basicPayService.closeTrading(trading.getTradingOrderNo());} catch (Exception e) {log.error("超过20分钟未支付自动关单出现异常,交易单号:{}",trading.getTradingOrderNo());}}}} catch (Exception e) {XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e);}}if (CollUtil.isEmpty(tradeMsgList)) {return;}//发送消息通知其他系统String msg = JSONUtil.toJsonStr(tradeMsgList);rabbitClient.sendMsg(MqConstants.Exchanges.TRADE, MqConstants.RoutingKeys.TRADE_UPDATE_STATUS, msg);}
之后在xxl-job中配置定时任务
2.5.3 测试不点支付成功成功更新的业务
下个订单,支付了但是不点击支付完成:
之后看一看订单会不会自动更新状态
测试成功
相关文章:
小熊家务帮day15-day18 预约下单模块(预约下单,熔断降级,支付功能,退款功能)
目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务&服务项远程接口jzo2o-ap…...
[word] word悬挂缩进怎么设置? #经验分享#职场发展#经验分享
word悬挂缩进怎么设置? 在编辑Word的时候上方会有个Word标尺,相信很多伙伴都没使用过。其实它隐藏着很多好用的功能,今天就给大家分享下利用这个word标尺的悬挂缩进怎么设置,一起来看看吧! 1、悬挂缩进 选中全文&…...
6-Maven的使用
6-Maven的使用 常用maven命令 //常用maven命令 mvn -v //查看版本 mvn archetype:create //创建 Maven 项目 mvn compile //编译源代码 mvn test-compile //编译测试代码 mvn test //运行应用程序中的单元测试 mvn site //生成项目相关信息的网站 mvn package //依据项目生成 …...
WPF真入门教程32--WPF数字大屏项目实干
1、项目背景 WPF (Windows Presentation Foundation) 是微软的一个框架,用于构建桌面客户端应用程序,它支持富互联网应用程序(RIA)的开发。在数字大屏应用中,WPF可以用来构建复杂的用户界面,展示庞大的数据…...
数据可视化Python实现超详解【数据分析】
各位大佬好 ,这里是阿川的博客,祝您变得更强 个人主页:在线OJ的阿川 大佬的支持和鼓励,将是我成长路上最大的动力 阿川水平有限,如有错误,欢迎大佬指正 Python 初阶 Python–语言基础与由来介绍 Python–…...
Maxkb玩转大语言模型
Maxkb玩转大语言模型 随着国外大语言模型llama3的发布,搭建本地个人免费“人工智能”变得越来越简单,今天博主分享使用Max搭建本地的个人聊天式对话及个人本地知识域的搭建。 1.安装Maxkb开源应用 github docker快速安装 docker run -d --namemaxkb -p 8…...
React Hooks 封装可粘贴图片的输入框组件(wangeditor)
需求是需要一个文本框 但是可以支持右键或者ctrlv粘贴图片,原生js很麻烦,那不如用插件来实现吧~我这里用的wangeditor插件,初次写初次用,可能不太好,但目前是可以达到实现需求的一个效果啦!后面再改进吧~ …...
Wireshark TS | 应用传输丢包问题
问题背景 仍然是来自于朋友分享的一个案例,实际案例不难,原因也就是互联网线路丢包产生的重传问题。但从一开始只看到数据包截图的判断结果,和最后拿到实际数据包的分析结果,却不是一个结论,方向有点跑偏,…...
架构设计-web项目中跨域问题涉及到的后端和前端配置
WEB软件项目中经常会遇到跨域问题,解决方案早已是业内的共识,简要记录主流的处理方式: 跨域感知session需要解决两个问题: 1. 跨域问题 2. 跨域cookie传输问题 跨域问题 解决跨域问题有很多种方式,如使用springboot…...
==Redis淘汰策略(内存满了触发)==
好的,面试官。这个问题我需要从三个方面来回答。第一个方面: 当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据配置的内存淘汰策略。 把访问频率不高的 key 从内存中移除。maxmemory 默认情况是当前服务器的最大内存…...
2024年高考作文考人工智能,人工智能写作文能否得高分
前言 众所周知,今年全国一卷考的是人工智能,那么,我们来测试一下,国内几家厉害的人工智能他们的作答情况,以及能取得多少高分呢。由于篇幅有限,我这里只测试一个高考真题,我们这里用百度的文心…...
Vue3学习记录第三天
Vue3学习记录第三天 背景说明学习记录Vue3中shallowReactive()和shallowRef()Vue3中toRaw()和markRaw()前端...语法Vue3中readonly()和shallowReadonly()函数前端的防抖 背景 之前把Vue2的基础学了, 这个课程的后面有简单介绍Vue3的部分. 学习知识容易忘, 这里仅简答做一个记录…...
数仓建模中的一些问题
在数仓建设的过程中,由于未能完全按照规范操作, 从而导致数据仓库建设比较混乱,常见有以下问题: 数仓常见问题 ● 数仓分层不清晰:数仓的分层没有明确的逻辑,难以管理和维护。 ● 数据域划分不明确…...
spring整合kafka
原文链接:spring整合kafka_spring集成kafka-CSDN博客 1、导入依赖 <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId><version>2.5.10.RELEASE</version> </depende…...
【web前端】CSS样式
CSS应用方式 在标签 <h2 style"color: aquamarine">hello world!</h2> 在head标签中写style标签 <head><meta charset"UTF-8"><title>Title</title><style>.c1{height: 100px;}.c2{height: 200px;color: aqua;…...
【ARM Cache 与 MMU 系列文章 7.7 – ARMv8/v9 MMU Table 表分配原理及其代码实现 1】
请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】 及【嵌入式开发学习必备专栏】 文章目录 MMU Table 表分配原理及其代码实现虚拟地址空间 Region的配置系统物理地址位宽获取汇编代码实现MMU Table 表分配原理及其代码实现 假设当前系统中需要映射多个region,其中第一个要映…...
AIGC之MetaHuman:HeyGen(基于AI驱动的视频生成平台+数字人)的简介、安装和使用方法、案例应用之详细攻略
AIGC之MetaHuman:HeyGen(基于AI驱动的视频生成平台数字人)的简介、安装和使用方法、案例应用之详细攻略 目录 HeyGen的简介 1、HeyGen是一款AI视频生成平台,它提供以下关键功能: HeyGen的安装和使用方法 1、使用方法 01创建或选择一个头…...
6.7-6.10作业
1. /*1.使用switch实现银行系统,默认用户为A,密码为1234,余额2000 如果登录失败,则直接结束 如果登录成功,则显示银行页面 1.查询余额 2.取钱 3.存钱 如果是1,则打印余额 如果是2,则输入取钱金…...
【Redis】Redis经典问题:缓存穿透、缓存击穿、缓存雪崩
目录 缓存的处理流程缓存穿透解释产生原因解决方案1.针对不存在的数据也进行缓存2.设置合适的缓存过期时间3. 对缓存访问进行限流和降级4. 接口层增加校验5. 布隆过滤器原理优点缺点关于扩容其他使用场景SpringBoot 整合 布隆过滤器 缓存击穿产生原因解决方案1.设置热点数据永不…...
从GPU到ASIC,博通和Marvell成赢家
ASIC市场上,博通预计今年AI收入将达到110亿美元以上,主要来自与Google和Meta的合作;Marvell预计2028年AI收入将达到70亿至80亿美元,主要来自与Amazon和Google的合作。 随着芯片设计和系统复杂性的增加,科技大厂将更多地…...
【java问答小知识6】一些Java基础的知识,用于想学习Java的小伙伴们建立一些简单的认知以及已经有经验的小伙伴的复习知识点
请解释Java中的双亲委派模型是什么? 回答:双亲委派模型是Java类加载机制的核心原则,它确保所有类加载器在尝试加载一个类之前,都会委托给它的父类加载器。 Java中的类路径(Classpath)是什么? 回…...
数学建模笔记
数学建模 定义角度 数学模型是针对参照某种事物系统的特征或数量依存关系,采用数学语言,概括地或近似地表述出的一种数学结构,这种数学结构是借助于数学符号刻画出来的某种系统的纯关系结构。从广义理解,数学模型包括数学中的各…...
shell编程(三)—— 控制语句
程序的运行除了顺序运行外,还可以通过控制语句来改变执行顺序。本文介绍bash的控制语句用法。 一、条件语句 Bash 中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在[[ ]]里的表达式。 bash中的检测命令由[[]]包起来,用于检测一个条…...
反射学习记
Java 中的反射是什么意思?有哪些应用场景? 每个类都有⼀个 Class 对象,包含了与类有关的信息。当编译⼀个新类时,会产生一个同名的 .class 文件,该⽂件 内容保存着 Class 对象。类加载相当于 Class 对象的加载&a…...
使用Python操作Redis
大家好,在当今的互联网时代,随着数据量和用户量的爆发式增长,对于数据存储和处理的需求也日益增加。Redis作为一种高性能的键值存储数据库,以其快速的读写速度、丰富的数据结构支持和灵活的应用场景而备受青睐。本文将介绍Redis数…...
Vue-CountUp-V2 数字滚动动画库
安装: $ npm install --save countup.js vue-countup-v2示例如下: <template><div class"iCountUp"><ICountUp:delay"delay":endVal"endVal":options"options"ready"onReady"/>&…...
C语言详解(文件操作)1
Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 💥💥个人主页:奋斗的小羊 💥💥所属专栏:C语言 🚀本系列文章为个人学习…...
Python Requests库详解
大家好,在现代网络开发中,与Web服务器进行通信是一项至关重要的任务。Python作为一种多才多艺的编程语言,提供了各种工具和库来简化这一过程。其中,Requests库作为Python中最受欢迎的HTTP库之一,为开发人员提供了简单而…...
Kafka 详解:全面解析分布式流处理平台
Kafka 详解:全面解析分布式流处理平台 Apache Kafka 是一个分布式流处理平台,主要用于构建实时数据管道和流式应用。它具有高吞吐量、低延迟、高可用性和高可靠性的特点,广泛应用于日志收集、数据流处理、消息系统、实时分析等场景。 &…...
RabbitMQ系列-rabbitmq无法重新加入集群,启动失败的问题
当前存在3个节点:rabbitmq5672、rabbitmq5673、rabbitmq5674 当rabbitmq5673节点掉线之后,重启失败 重启的时候5672节点报错如下: 解决方案 在集群中取消失败节点 rabbitmqctl forget_cluster_node rabbitrabbitmq5673删除失败节点5673的…...
wordpress符号插件/今天国内新闻
2019独角兽企业重金招聘Python工程师标准>>> MapReduce 简介 说明: 通过由普通机器组成的集群对大量数据集进行并行处理可依靠的容错软件框架。 MapReduce作业可以将数据集分割为Map任务并行处理的数据块,框架对对Map过程产生的数据进行排序&…...
免费php网站系统/外贸网站建设推广
class People:def __init__(self, name, age):self.name nameself.age ageself._protect_var 10 # 受保护的成员,使用一个下划线_,它仅仅是提示成员受保护,但可以被更改self.__private_var 10 # 使用双下划线__可以定义私有属性def sayhi(self):p…...
做网站如何安全 博客/山西网络营销外包
一、什么是检查点 LoadRunner中检查点是用来判断脚本是否执行成功的。如果不加检查点,只要服务器返回的HTTP状态码是200,VuGen就认为脚本执行通过了。但是很多情况下服务器返回200并不代表脚本执行通过了,比如:登录操作࿰…...
利用对象储存做网站/百度收录查询方法
有导式:给出输入和输出 无导式:只给输入,输出分类 >> doc compet >> doc midpoint 用于权值初始化 >> doc initcon 用于阈值初始化 死神经元 Competitive Learning 权值调整,学习率 样本 learnk 权值…...
河北 石家庄 网站建设/百度推广有效果吗
作者:老齐与本文相关的图书推荐:《跟老齐学Python:轻松入门》《Python正则表达式》这个系列,已经完成了两篇,本文是第三篇,请继续阅读。如果错过了前两篇,请关注微信公众号:老齐教室…...
找网站建设公司哪家最好/网络服务器搭建
SQL 关于row_number()over() 今天用到了row_number()over(),现在就以知识点的形式总结一下,以便于以后的回顾。 -------------------参考资料MSDN------------------- 语法: ROW_NUMBER ( ) OVER ( [ PARTITION BY value_expression , ... [ …...