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">
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 订单号生成规则
常见的订单号生成规则
- 自增数字序列
使用数据库的自增主键或者其他递增的数字序列(比如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 命令实现。
代码实现
@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 表设计
- 支付渠道表:
支付渠道表存储了第三方支付(微信、支付宝)的支付参数,如:商户号、证书序列号、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_no
unique (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_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);
//调用不同的支付渠道进行处理
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 接口分析
-
传入参数
本接口要调用支付服务的支付结果查询接口,根据支付服务的支付结果查询接口的传入参数分析本接口的传入参数。
支付服务的支付结果查询接口需要传入交易单号。交易单号在订单表已经保存所以前端传入订单号即可拿到交易单号。
所以传入参数:订单号
-
响应结果
本接口的目的是查询支付结果,所以响应结果中要有支付结果,其它的参数就是订单号、交易单号等相关参数。
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 需求分析
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 小熊家务帮day15-day17 预约下单模块(预约下单,熔断降级,支付功能,退款功能)
发表评论 取消回复