苍穹外卖—订单模块
该模块分为地址表的增删改查、用户下单、订单支付三个部分。
第一部分地址表的增删改查无非就是对于单表的增删改查,较基础,因此直接导入代码。
地址表
一个用户可以有多个地址,同时有一个地址为默认地址。用户还可为地址添加例如"公司"、"学校"、"家"之类的标签。项目中的address_book表就包含了这些信息,其中红色字体为重要信息。
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
user_id | bigint | 用户id | 逻辑外键 |
consignee | varchar(50) | 收货人 | |
sex | varchar(2) | 性别 | |
phone | varchar(11) | 手机号 | |
province_code | varchar(12) | 省份编码 | |
province_name | varchar(32) | 省份名称 | |
city_code | varchar(12) | 城市编码 | |
city_name | varchar(32) | 城市名称 | |
district_code | varchar(12) | 区县编码 | |
district_name | varchar(32) | 区县名称 | |
detail | varchar(200) | 详细地址信息 | 具体到门牌号 |
label | varchar(100) | 标签 | 公司、家、学校等 |
is_default | tinyint(1) | 是否默认地址 | 1是0否 |
新增地址
请求路径为/user/addressBook,请求方法为Post,以json格式提交请求参数。
// Controller———————————————————
@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端地址簿接口")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
@PostMapping
@ApiOperation("新增地址")
public Result save(@RequestBody AddressBook addressBook) {
// 调用服务层方法保存地址信息
addressBookService.save(addressBook);
// 返回成功结果
return Result.success();
}
}
// Service———————————————————————
public interface AddressBookService {
void save(AddressBook addressBook);
}
// ServiceImpl———————————————————
@Service
@Slf4j
public class AddressBookServiceImpl implements AddressBookService {
@Autowired
private AddressBookMapper addressBookMapper;
public void save(AddressBook addressBook) {
// 设置当前用户的ID
addressBook.setUserId(BaseContext.getCurrentId());
// 设置地址簿是否为默认地址,默认为0(不是默认地址)
addressBook.setIsDefault(0);
// 调用映射器插入地址簿信息
addressBookMapper.insert(addressBook);
}
}
// Mapper———————————————————————
@Mapper
public interface AddressBookMapper {
@Insert("insert into address_book" +
" (user_id, consignee, phone, sex, province_code, province_name, city_code, city_name, district_code," +
" district_name, detail, label, is_default)" +
" values (#{userId}, #{consignee}, #{phone}, #{sex}, #{provinceCode}, #{provinceName}, #{cityCode}, #{cityName}," +
" #{districtCode}, #{districtName}, #{detail}, #{label}, #{isDefault})")
void insert(AddressBook addressBook);
}
查询地址列表
请求路径为/user/addressBook/list,请求方法为Get,无需传参,后端通过Token中的userId获取地址信息并返回。
// Controller———————————————————
@GetMapping("/list")
@ApiOperation("查询当前登录用户的所有地址信息") // API操作说明
public Result<List<AddressBook>> list() {
// 创建AddressBook对象并设置当前登录用户的ID
AddressBook addressBook = new AddressBook();
addressBook.setUserId(BaseContext.getCurrentId());
// 调用服务层方法获取地址列表
List<AddressBook> list = addressBookService.list(addressBook);
// 返回成功结果和地址列表
return Result.success(list);
}
// Service———————————————————————
List<AddressBook> list(AddressBook addressBook);
// ServiceImpl———————————————————
public List<AddressBook> list(AddressBook addressBook) {
return addressBookMapper.list(addressBook);
}
// Mapper———————————————————————
List<AddressBook> list(AddressBook addressBook);
<mapper namespace="com.sky.mapper.AddressBookMapper">
<select id="list" parameterType="AddressBook" resultType="AddressBook">
select * from address_book
<where>
<if test="userId != null">and user_id = #{userId}</if>
<if test="phone != null">and phone = #{phone}</if>
<if test="isDefault != null">and is_default = #{isDefault}</if>
</where>
</select>
</mapper>
查询默认地址
请求路径为/user/addressBook/default,请求方法为Get,后端通过Token中的userId获取地址信息并返回。
// Controller———————————————————
@GetMapping("default")
@ApiOperation("查询默认地址") // API操作说明,用于描述查询默认地址的接口
public Result<AddressBook> getDefault() {
// 创建AddressBook对象并设置查询条件:当前登录用户的ID和默认地址标志
AddressBook addressBook = new AddressBook();
addressBook.setIsDefault(1); // 设置默认地址标志为1
addressBook.setUserId(BaseContext.getCurrentId()); // 设置当前登录用户的ID
// 调用服务层方法查询符合条件的地址列表
List<AddressBook> list = addressBookService.list(addressBook);
// 检查查询结果,如果存在且只有一个默认地址,则返回该地址
if (list != null && list.size() == 1) {
return Result.success(list.get(0));
}
// 如果没有查询到默认地址,则返回错误信息
return Result.error("没有查询到默认地址");
}
修改地址
同样分为查询回显和修改地址两个接口。
根据Id查询地址
请求路径为/user/addressBook/{id},请求方法为Delete。
// Controller———————————————————
@GetMapping("/{id}")
@ApiOperation("根据id查询地址")
public Result<AddressBook> getById(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
return Result.success(addressBook);
}
// Service———————————————————————
AddressBook getById(Long id);
// ServiceImpl———————————————————
public AddressBook getById(Long id) {
AddressBook addressBook = addressBookMapper.getById(id);
return addressBook;
}
// Mapper———————————————————————
@Select("select * from address_book where id = #{id}")
AddressBook getById(Long id);
修改地址
请求路径为/user/addressBook,请求方法为Put,以json格式提交请求参数(老师提供的xml文件有误,无法修改省份城市和区域)。
// Controller———————————————————
@PutMapping
@ApiOperation("根据id修改地址")
public Result update(@RequestBody AddressBook addressBook) {
addressBookService.update(addressBook);
return Result.success();
}
// Service———————————————————————
void update(AddressBook addressBook);
// ServiceImpl———————————————————
public void update(AddressBook addressBook) {
addressBookMapper.update(addressBook);
}
// Mapper———————————————————————
void update(AddressBook addressBook);
<update id="update" parameterType="addressBook">
update address_book
<set>
<if test="consignee != null">consignee = #{consignee},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="provinceCode != null">province_code = #{provinceCode},</if>
<if test="provinceName != null">province_name = #{provinceName},</if>
<if test="cityCode != null">city_code = #{cityCode},</if>
<if test="cityName != null">city_name = #{cityName},</if>
<if test="districtCode != null">district_code = #{districtCode},</if>
<if test="districtName != null">district_name = #{districtName},</if>
<if test="detail != null">detail = #{detail},</if>
<if test="label != null">label = #{label},</if>
<if test="isDefault != null">is_default = #{isDefault},</if>
</set>
where id = #{id}
</update>
删除地址
请求路径为/user/addressBook,请求方法为Delete,以Query格式提交id。
// Controller———————————————————
@DeleteMapping
@ApiOperation("根据id删除地址")
public Result deleteById(Long id) {
addressBookService.deleteById(id);
return Result.success();
}
// Service———————————————————————
void deleteById(Long id);
// ServiceImpl———————————————————
public void deleteById(Long id) {
addressBookMapper.deleteById(id);
}
// Mapper———————————————————————
@Delete("delete from address_book where id = #{id}")
void deleteById(Long id);
设置默认地址
其本质上是一个修改操作:将该地址is_default修改为1(默认)。
请求路径为/user/addressBook/default,请求方法为Put,以json格式提交请求。
// Controller———————————————————
@PutMapping("/default")
@ApiOperation("设置默认地址")
public Result setDefault(@RequestBody AddressBook addressBook) {
addressBookService.setDefault(addressBook);
return Result.success();
}
// Service———————————————————————
void setDefault(AddressBook addressBook);
// ServiceImpl———————————————————
@Transactional
public void setDefault(AddressBook addressBook) {
//1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ?
addressBook.setIsDefault(0);
addressBook.setUserId(BaseContext.getCurrentId());
addressBookMapper.updateIsDefaultByUserId(addressBook);
//2、将当前地址改为默认地址 update address_book set is_default = ? where id = ?
addressBook.setIsDefault(1);
addressBookMapper.update(addressBook);
}
// Mapper———————————————————————
@Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")
void updateIsDefaultByUserId(AddressBook addressBook);
代码完成后重新运行项目并前往小程序,点击个人中心—地址管理—新增收货地址—填写相关信息,然后测试各模块是否能实现预期目标
用户下单
接口设计
在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。用户下单后会产生订单相关数据,订单数据需要能够体现出如购买的商品、每个商品数量、订单总金额、收货地址、用户名、用户手机号等信息。
在下单界面,显示的收货地址并不需要提交,只需提交该地址在地址簿中的id即可。配送状态默认为立即送出,也可自选配送时间。购买的商品同理,后端会读取购物车表中的数据,只需传入购物车id即可。总金额包含商品金额+打包费+配送费,本项目的配送费统一为6元,打包费一个商品1元,在小程序端便已计算,传入的为总打包费和总金额。备注和餐具数量为必须传入的参数。
用户下单本质上是新增操作,也就是将下单后产生的订单数据插入到数据库中。因此请求方式选择Post,请求路径为/user/order/submit。
提交的数据包括地址簿id、总金额、配送状态、预计送达时间(下单时间+1小时,小程序端会自行计算)、打包费、付款方式(目前只有微信支付一种方式,但为方便以后区分不同的支付方式也需保留)、还有备注和餐具数量。
数据库设计
因为前端传入的参数较多,我们可以将信息分为订单表和订单明细表来分开存储,一个订单包含多个明细,属于一对多的关系。
订单表orders:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
number | varchar(50) | 订单号 | |
status | int | 订单状态 | 1待付款2待接单3已接单 4派送中5已完成6已取消 |
user_id | bigint | 用户id | 逻辑外键 |
address_book_id | bigint | 地址id | 逻辑外键 |
order_time | datetime | 下单时间 | |
checkout_time | datetime | 付款时间 | |
pay_method | int | 支付方式 | 1微信支付2支付宝支付 |
pay_status | tinyint | 支付状态 | 0未支付1已支付2退款 |
amount | decimal(10,2) | 订单金额 | |
remark | varchar(100) | 备注信息 | |
phone | varchar(11) | 手机号 | 冗余字段 |
address | varchar(255) | 详细地址信息 | 冗余字段 |
consignee | varchar(32) | 收货人 | |
cancel_reason | varchar(255) | 订单取消原因 | |
rejection_reason | varchar(255) | 拒单原因 | |
cancel_time | datetime | 订单取消时间 | |
estimated_delivery_time | datetime | 预计送达时间 | |
delivery_status | tinyint | 配送状态 | 1立即送出0选择具体时间 |
delivery_time | datetime | 送达时间 | |
pack_amount | int | 打包费 | |
tableware_number | int | 餐具数量 | |
tableware_status | tinyint | 餐具数量状态 | 1按餐量提供0选择具体数量 |
订单明细表order_detail:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 商品名称 | 冗余字段 |
image | varchar(255) | 商品图片路径 | 冗余字段 |
order_id | bigint | 订单id | 逻辑外键 |
dish_id | bigint | 菜品id | 逻辑外键 |
setmeal_id | bigint | 套餐id | 逻辑外键 |
dish_flavor | varchar(50) | 菜品口味 | |
number | int | 商品数量 | |
amount | decimal(10,2) | 商品单价 |
下单后等待支付的页面包含支付倒计时,默认为15分钟,后端只需返回下单时间即可,小程序端会自行计算。订单总金额和订单号也许返回。同时因为订单支付需以订单id来区分,因此也需返回。
功能实现
后端可用OrdersSubmitDTO类来接收参数。返回的为OrdersSubmitDTO类对象。请求路径为/user/order/submit,请求方式为Post。因为订单模块是用户端的功能,日后在管理端也需开发同类名的接口,为防止程序冲突,需指定其生成的bean的名字。
// Controller———————————————————
@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户端订单相关接口")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> Submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("用户下单,参数为:{}", ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submit(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
}
// Service———————————————————————
public interface OrderService {
OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO);
}
// ServiceImpl———————————————————
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Override
@Transactional // 事务注解,确保方法内所有操作在同一个事务中
public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) {
//一、处理各种业务异常(地址为空、购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
// 抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
Long userId = BaseContext.getCurrentId(); // 获取当前用户ID
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList == null || shoppingCartList.size() == 0) {
// 抛出业务异常
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
// 构造订单数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO, orders); // 复制属性
orders.setOrderTime(LocalDateTime.now()); // 设置订单时间
orders.setPayStatus(Orders.UN_PAID); // 设置支付状态
orders.setStatus(Orders.PENDING_PAYMENT); // 设置订单状态
orders.setNumber(String.valueOf(System.currentTimeMillis())); // 设置订单编号
orders.setPhone(addressBook.getPhone()); // 设置联系电话
orders.setConsignee(addressBook.getConsignee()); // 设置收货人
orders.setUserId(userId); // 设置用户ID
orders.setAddress(addressBook.getDetail()); // 设置地址
//二、向订单表插入一条数据
orderMapper.insert(orders);
// 向订单明细表插入多条数据
List<OrderDetail> orderDetailList = new ArrayList<>();
for (ShoppingCart cart : shoppingCartList) {
OrderDetail orderDetail = new OrderDetail(); // 创建订单明细对象
BeanUtils.copyProperties(cart, orderDetail); // 复制属性
orderDetail.setOrderId(orders.getId()); // 设置当前订单明细关联的订单ID
orderDetailList.add(orderDetail); // 添加到订单明细列表
}
//三、向订单明细表批量插入数据
orderDetailMapper.insertBatch(orderDetailList);
//四、清空当前用户的购物车数据
shoppingCartMapper.cleanByUserId(userId);
//五、封装对象并返回结果
OrderSubmitVO submitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
return submitVO;
}
}
// Mapper———————————————————————
@Mapper
public interface OrderMapper {
void insert(Orders orders);
}
@Mapper
public interface OrderDetailMapper {
void insertBatch(List<OrderDetail> orderDetailList);
}
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into orders (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status,
amount, remark, phone, address, user_name, consignee, cancel_reason, rejection_reason, cancel_time,
estimated_delivery_time, delivery_status, delivery_time, pack_amount, tableware_number, tableware_status)
values (
#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod}, #{payStatus},
#{amount}, #{remark}, #{phone}, #{address}, #{userName}, #{consignee}, #{cancelReason}, #{rejectionReason}, #{cancelTime},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{deliveryTime}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus}
)
</insert>
</mapper>
<mapper namespace="com.sky.mapper.OrderDetailMapper">
<insert id="insertBatch">
insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, number, amount)
values
<foreach collection="orderDetailList" item="od" separator=",">
(#{od.name}, #{od.image}, #{od.orderId}, #{od.dishId}, #{od.setmealId}, #{od.dishFlavor}, #{od.number},#{od.amount})
</foreach>
</insert>
</mapper>
测试成功页面:
订单支付
功能介绍
本项目采用微信支付,但因为小程序的支付功能必须是商户注册才能开通,如果在注册小程序时使用的是个人注册是无法实现该功能的,因此我们只学习实现流程,但并不真正的实现支付功能。
接入微信支付共需三步:
一、提交资料
在线提交营业执照、身份证、银行账户等基本信息,并按指引完成账户验证
二、签署协议
微信支付团队会在1-2个工作日内完成审核,审核通过后请在线签约,即可体验各项产品能力
三、绑定场景
如需自行开发完成收款,需将商户号与APPID进行绑定,或开通微信收款商业版(免开发)完成收款
这些一般由相关人员完成,我们了解即可。目前微信支付支持的支付产品有多种,如:付款码支付、JSAPI支付、小程序支付、Native支付、APP支付、刷脸支付、刷掌支付。
因为我们目前开发的是小程序支付,因此主要介绍这一种。下图为小程序支付时序图:
首先是微信用户下单—商户系统返回订单号等信息—小程序向后端申请微信支付—后端调用微信下单接口(即发起请求)。但此时只是发起了订单,并未支付。微信方会返回预交易标识字符串,后端为了安全需再次对该数据进行处理并签名, 再将处理后的数据返回到小程序端。
此时小程序端会弹出支付界面,点击确认支付并输入密码后会调用wx.requestPayment方法并将刚刚获取的数据及其他数据返回给微信方,微信方再返回支付结果,小程序端再予以显示支付结果。
此时支付已完成,但后端并无数据,微信方还会推送支付结果到后端,后端收到后予以处理并更新订单相关数据。
重要的有三步:后端调用微信下单、小程序端调起微信支付、微信端推送支付结果。我们依次来看。
一、后端调用微信JSAPI下单接口在微信支付服务台生成预支付交易单。该接口请求URL: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi,请求方式: POST,提交规定的的json格式数据:
{
// 商户注册所得的商户号
"mchid": "1900006XXX",
// 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。
"out_trade_no": "1217752501201407033233368318",
// 应用ID
"appid": "wxdace645e0bc2cXXX",
// 商品简单描述,该字段请按照以下规则填写:商品名称示例:腾讯充值中心-QQ会员充值
"description": "Image形象店-深圳腾大-QQ公仔",
// 接收微信支付异步通知回调地址,通知url必须为直接可访问的URL,不能携带参数。
"notify_url": "https://www.weixin.qq.com/wxpay/pay.php",
// 订单金额信息
"amount": {
// 总金额,单位为分
"total": 1,
// 货币类型
"currency": "CNY"
},
// 支付人信息
"payer": {
// 用户在直连商户appid下的唯一标识。
"openid": "04GgauInH_RCEdvrrNGrntXDuxXX"
}
}
微信端会返回预支付交易会话标识prepay_id,有效期为两小时:
{
"prepay_id" : "wx201410272009395522657a690389285100"
}
二、通过小程序下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法wx.requestPayment(OBJECT)调起微信支付。以下为Object请求参数。
其内部还包含了三个回调函数,分别代表不同的结果:
wx.requestPayment({
// 时间戳,从1970年1月1日00:00:00至今的秒数,即当前的时间
"timeStamp": "1414561699",
// 随机字符串,不长于32位
"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
// 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
"package": "prepay_id=wx201410272009395522657a690389285100",
// 签名方式,默认为MD5,支持HMAC-SHA256和RSA
"signType": "RSA",
// 签名,具体签名方案参见小程序支付接口文档
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
// 接口调用成功的回调函数
"success": function(res) {
// 成功处理逻辑
},
// 接口调用失败的回调函数
"fail": function(res) {
// 失败处理逻辑
},
// 接口调用结束的回调函数(调用成功、失败都会执行)
"complete": function(res) {
// 完成处理逻辑
}
})
三、第三步由导入的PayNotifyController类完成,后文会介绍
准备工作
在编写代码前,我们还需做些准备工作。
首先是后端与微信端交互时涉及到了大量的数据交互,我们需要对其进行一定的处理以确保数据的安全。
其次我们之前访问后端都是直接访问localhost本地服务器,这就导致了外部的设备无法与本地服务器进行交互,因此我们需要"内网穿透"功能来获取临时IP。
首先是数据处理,我们需要从微信商户平台下载两文件:
- 获取微信支付平台证书apiclient_key.pem
- 商户私钥文件:wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
后续会使用到这两文件。
然后是实现内网穿透,我们需要借助工具cpolar,先前往官网注册并下载软件:
cpolar下载地址https://dashboard.cpolar.com/get-startedhttps://dashboard.cpolar.com/get-startedhttps://dashboard.cpolar.com/get-started 然后回到网站,点击左侧的验证,并复制Authtoken
然后回到安装目录并执行cmd,输入cpolar.exe authtoken 刚刚复制的token,回车,系统会在指定目录下生成一yml文件(该步骤只需执行一次,即生成文件后便不需要再次这样操作):
//cmd窗口弹出生成的yml文件存储位置
Authtoken saved to configuration file: C:\Users\chn/.cpolar/cpolar.yml
执行完上述步骤后,回到cmd窗口,输入cpolar.exe http 8080(此处的8080为后端接口,需与自己的后端接口对应),我们就可以启动服务获取临时ip地址。cmd窗口弹出:
cpolar by @bestexpresser (Ctrl+C to quit)
Tunnel Status online
Account aaaa (Plan: Free)
Version 2.86.16/3.18
Web Interface 127.0.0.1:4042
Forwarding https://12cfe0d9.r9.cpolar.top -> http://localhost:8080
Forwarding http://12cfe0d9.r9.cpolar.top -> http://localhost:8080
# Conn 0
Avg Conn Time 0.00ms
根据弹出信息我们就可以得知,我们可以通过http://12cfe0d9.r9.cpolar.top来取代http://localhost:8080,例如外网可以通过http://12cfe0d9.r9.cpolar.top/doc.html来访问该项目的接口文档(第一次访问较慢,等待即可)。
注意因为我们目前仍处于学习阶段,电脑大部分时间都处于局域网之内并无公网ip,因此需要这样获取临时ip,但实际开发中项目上线后一般都会有公网ip,我们直接使用即可。
代码导入
因为微信支付的代码较为固定,因此我们直接导入即可。
首先配置微信支付所需的配置项:
//application.yml——————————————————————————
sky:
......
wechat:
# 小程序的appid
appid: ${sky.wechat.appid}
# 小程序的秘钥
secret: ${sky.wechat.secret}
# 商户号
mchId: ${sky.wechat.mchId}
# 商户API证书的证书序列号
mchSerialNo: ${sky.wechat.mchSerialNo}
# 商户私钥文件路径
privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
# 证书解密的密钥
apiV3Key: ${sky.wechat.apiV3Key}
# 平台证书路径
weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
# 支付成功的回调地址
notifyUrl: ${sky.wechat.notifyUrl}
# 退款成功的回调地址
refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
//application-dev.yml——————————————————————————————————————————
sky:
......
wechat:
# 微信公众号或小程序的AppID
appid: wxe8b6f903deb8566b
# 微信公众号或小程序的AppSecret
secret: 23d7d1bc0eed6b49ef7e58bc0cc6a296
# 微信支付分配的商户号
mchid: 1561414331
# 商户API证书的证书序列号
mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
# 商户私钥文件的路径
privateKeyFilePath: D:\pay\apiclient_key.pem
# APIv3密钥,用于签名和解密
apiV3Key: CZBK51236435wxpay435434323FFDuv3
# 微信支付平台证书文件的路径
weChatPayCertFilePath: D:\pay\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
# 支付成功的回调地址
notifyUrl: https://6619cf50.r6.cpolar.top/notify/paySuccess
# 退款成功的回调地址
refundNotifyUrl: https://6619cf50.r6.cpolar.top/notify/refundSuccess
回到server模块的controller包user包OrderController中添加相关方法
// OrderController———————————————————
@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
log.info("订单支付:{}", ordersPaymentDTO);
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
log.info("生成预支付交易单:{}", orderPaymentVO);
return Result.success(orderPaymentVO);
}
// OrderService———————————————————————
OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;
void paySuccess(String outTradeNo);
}
// OrderServiceImpl———————————————————
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
//调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
return vo;
}
public void paySuccess(String outTradeNo) {
// 根据订单号查询订单
Orders ordersDB = orderMapper.getByNumber(outTradeNo);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
}
// OrderMapper———————————————————————
@Select("select * from orders where number = #{orderNumber}")
Orders getByNumber(String orderNumber);
@Select("select * from user where id = #{id}")
User getById(Long Id);
void update(Orders orders);
<!--OrderMapper-->
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' "> cancel_reason=#{cancelReason}, </if>
<if test="rejectionReason != null and rejectionReason!='' "> rejection_reason=#{rejectionReason}, </if>
<if test="cancelTime != null"> cancel_time=#{cancelTime}, </if>
<if test="payStatus != null"> pay_status=#{payStatus}, </if>
<if test="payMethod != null"> pay_method=#{payMethod}, </if>
<if test="checkoutTime != null"> checkout_time=#{checkoutTime}, </if>
<if test="status != null"> status = #{status}, </if>
<if test="deliveryTime != null"> delivery_time = #{deliveryTime} </if>
</set>
where id = #{id}
</update>
然后在controller包下新建notify包,并将PayNotifyController类复制进去(该类用于接收微信端推送的支付结果):
//支付回调相关接口
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
@Autowired
private OrderService orderService;
@Autowired
private WeChatProperties weChatProperties;
// 支付成功回调
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//读取数据
String body = readData(request);
log.info("支付成功回调:{}", body);
//数据解密
String plainText = decryptData(body);
log.info("解密后的文本:{}", plainText);
JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
String transactionId = jsonObject.getString("transaction_id");//微信支付交易号
log.info("商户平台订单号:{}", outTradeNo);
log.info("微信支付交易号:{}", transactionId);
//业务处理,修改订单状态、来单提醒
orderService.paySuccess(outTradeNo);
//给微信响应
responseToWeixin(response);
}
// 读取数据
private String readData(HttpServletRequest request) throws Exception {
BufferedReader reader = request.getReader();
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
}
//数据解密
private String decryptData(String body) throws Exception {
JSONObject resultObject = JSON.parseObject(body);
JSONObject resource = resultObject.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String nonce = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//密文解密
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
return plainText;
}
// 给微信响应
private void responseToWeixin(HttpServletResponse response) throws Exception{
response.setStatus(200);
HashMap<Object, Object> map = new HashMap<>();
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
}
跳过支付
因为我们以个体注册的小程序无法进行支付,所以需修改代码跳过微信支付这一步,具体方法可参考该文章:
跳过微信支付https://blog.csdn.net/2301_79693537/article/details/140846695
用户端订单操作
查询历史订单
请求路径为/user/order/historyOrders,请求方法为get,Query传入三个参数page、pageSize、status分别代表页面、每页记录数、订单状态。
// Controller———————————————————
@GetMapping("/historyOrders")
@ApiOperation("历史订单查询")
public Result<PageResult> page(int page, int pageSize, Integer status) {
PageResult pageResult = orderService.pageQuery4User(page, pageSize, status);
return Result.success(pageResult);
}
// Service———————————————————————
PageResult pageQuery4User(int page, int pageSize, Integer status);
// ServiceImpl———————————————————
@Override
public PageResult pageQuery4User(int page, int pageSize, Integer status) {
//需要在查询功能之前开启分页功能:当前页的页码 每页显示的条数
PageHelper.startPage(page, pageSize);
//封装所需的请求参数为DTO对象
OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
ordersPageQueryDTO.setStatus(status);
// 分页条件查询
Page<Orders> ordersPage = orderMapper.pageQuery(ordersPageQueryDTO);
//由接口可知需要封装为orderVO类型:订单菜品信息orderDishes,订单详情orderDetailList
List<OrderVO> list = new ArrayList();
// 查询出订单明细,并封装入OrderVO进行响应
if (ordersPage != null && ordersPage.getTotal() > 0) { //有订单才有必要接着查询订单详情信息
for (Orders orders : ordersPage) {
Long orderId = orders.getId();// 订单id
// 根据订单id,查询订单明细
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetails);
list.add(orderVO);
}
}
return new PageResult(ordersPage.getTotal(), list);
}
// Mapper———————————————————————
public interface OrderDetailMapper {
@Select("select * from order_detail where order_id=#{orderId}")
List<OrderDetail> getByOrderId(Long orderId);
}
public interface OrderMapper {
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);
}
查询订单详情
请求路径为/user/order/orderDetail/{id},请求方法为get,Path传入参数id,意为订单id。
// Controller———————————————————
@GetMapping("/orderDetail/{id}")
@ApiOperation("根据订单ID查看订单详情")
public Result<OrderVO> OrderDetailById(@PathVariable Long id) {
OrderVO orderVO = orderService.OrderDetailById(id);
return Result.success(orderVO);
}
// Service———————————————————————
OrderVO OrderDetailById(Long id);
// ServiceImpl———————————————————
@Override
public OrderVO OrderDetailById(Long id) {
//根据id查询订单,OrderVO要用
Orders orders=orderMapper.getById(id);
//根据订单查询订单详情
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(id);
// 将结果封装到OrderVO并返回
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetails);
return orderVO;
}
// Mapper———————————————————————
@Select("select * from orders where id =#{id}")
Orders getById(Long id);
取消订单
请求路径为/user/order/cancel/{id},请求方法为put,Path传入参数id,意为订单id。
待支付和待接单状态下,用户可直接取消订单(status为1或2),其他状态下则抛出异常。如果在待接单状态下取消订单,需要给用户退款,因为无法实现微信接口的退款,本项目以控制台输出XX订单已退款来代替微信退款。取消订单后需要将订单状态修改为“已取消”。
// Controller———————————————————
@PutMapping("/cancel/{id}")
@ApiOperation("根据订单ID取消订单")
public Result<OrderVO> cancelOrderById(@PathVariable Long id) throws Exception {
orderService.cancelOrderById(id);
return Result.success();
}
// Service———————————————————————
void cancelOrderById(Long id) throws Exception;
// ServiceImpl———————————————————
@Override
public void cancelOrderById(Long id) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
if (ordersDB.getStatus() > 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//以上验证都通过后,此时订单处于待支付和待接单状态下
Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 订单处于待接单状态下取消,需要进行退款
if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
//调用微信支付退款接口,因为无法调用,所以仅做示范
/*
weChatPayUtil.refund(
ordersDB.getNumber(), //商户订单号
ordersDB.getNumber(), //商户退款单号
new BigDecimal(0.01),//退款金额,单位 元
new BigDecimal(0.01));//原订单金额
*/
log.info("订单{}已退款",ordersDB.getId());
//支付状态修改为 退款
orders.setPayStatus(Orders.REFUND);
}
// 更新订单状态、取消原因、取消时间
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("用户取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
再来一单
请求路径为/user/order/repetition/{id},请求方法为Post,Path传入参数id,意为订单id。再来一单意为将原订单中的商品重新加入到购物车中。
小程序会先发起清空购物车的请求,然后再发起再来一单的请求,后台响应请求后,小程序再跳转到点餐页并读取购物车中的数据。
// Controller———————————————————
@PostMapping("/repetition/{id}")
@ApiOperation("再来一单")
public Result oneMore(@PathVariable Long id){
orderService.oneMore(id);
return Result.success();
}
// Service———————————————————————
void oneMore(Long id);
// ServiceImpl———————————————————
public void oneMore(Long id) {
// 获取当前用户的ID
Long userId = BaseContext.getCurrentId();
// 根据提供的订单ID查询订单详情列表
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
// 将每个订单详情转换为购物车项
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(orderDetail -> {
// 创建新的购物车对象
ShoppingCart shoppingCart = new ShoppingCart();
// 复制订单详情到购物车对象,排除ID属性
BeanUtils.copyProperties(orderDetail, shoppingCart, "id");
// 设置购物车项的用户ID为当前用户ID
shoppingCart.setUserId(userId);
// 设置购物车项的创建时间为当前时间
shoppingCart.setCreateTime(LocalDateTime.now());
// 返回转换后的购物车对象
return shoppingCart;
}).collect(Collectors.toList()); // 收集转换后的购物车对象列表
// 批量插入购物车项到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}
// Mapper———————————————————————
public interface ShoppingCartMapper {
void insertBatch(List<ShoppingCart> shoppingCartList);
}
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>
商家端订单操作
订单查询
请求路径为/admin/order/conditionSearch,请求方法为get,请求方式为Query,传参里page和pagesize为必须,beginTime、endTime、number、phone 和 status 则是可选。
使用OrdersPageQueryDTO类来接收,因为返回数据中有一项orderDishes为菜品信息,其在Orders中并不存在,需查询菜品详情表后返回对应的数据(例如:宫保鸡丁*3;),因此因此在Impl中查询的返回类型为orderVO的集合orderVOList。
回到controller包的admin部分创建OrderController并编写代码:
// Controller———————————————————
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {
@Autowired
private OrderService orderService;
//订单查询
@GetMapping("/conditionSearch")
@ApiOperation("订单查询")
public Result<PageResult> OrderQuery(OrdersPageQueryDTO ordersPageQueryDTO) {
PageResult pageResult = orderService.OrderQuery(ordersPageQueryDTO);
return Result.success(pageResult);
}
}
// Service———————————————————————
PageResult OrderQuery(OrdersPageQueryDTO ordersPageQueryDTO);
// ServiceImpl———————————————————
@Override
public PageResult OrderQuery(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO,调用自定义方法
List<OrderVO> orderVOList = getOrderVOList(page);
return new PageResult(page.getTotal(), orderVOList);
}
//将的Orders对象转换为OrderVO对象列表。
private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();
List<Orders> ordersList = page.getResult();
//CollectionUtils工具类,用于判断ordersList集合是否为空
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
//调用自定义方法
String orderDishes = getOrderDishesStr(orders);
// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}
//根据订单id获取菜品信息字符串
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());
// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}
各个状态的订单数量统计
即红点中的数字,当有新的未处理的订单时,会通过红点来提醒管理者。
请求路径为/admin/order/statistics,请求方法为get,无请求参数。返回的数据为OrderStatisticsVO包含三个变量:confirmed、deliveryInProgress、toBeConfirmed分别意为待派送数量、派送中数量、待接单数量。
// Controller———————————————————
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数量统计")
public Result<OrderStatisticsVO> statistics() {
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}
// Service———————————————————————
OrderStatisticsVO statistics();
// ServiceImpl———————————————————
public OrderStatisticsVO statistics() {
// 根据状态,分别查询出待接单、待派送、派送中的订单数量
Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);
// 将查询出的数据封装到orderStatisticsVO中响应
OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
orderStatisticsVO.setConfirmed(confirmed);
orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
//也可简写为
// OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO(
// orderMapper.countStatus(Orders.TO_BE_CONFIRMED),
// orderMapper.countStatus(Orders.CONFIRMED),
// orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS));
return orderStatisticsVO;
}
// Mapper———————————————————————
@Select("select count(id) from orders where status = #{status}")
Integer countStatus(Integer status);
Impl中获取值并赋给OrderStatisticsVO对象的语句可直接使用构造函数代替,但因为OrderStatisticsVO中并无构造函数,若补充注解@AllArgsConstructor其又会覆盖掉无参构造,还需添加注解@NoArgsConstructor,代码变动较多,我们作为初学者便不再尝试。
查询订单详情
请求路径为/admin/order/details/{id},请求方法为get,Path传入参数id,意为订单id。
该功能之前已实现,直接在controller层调用orderService.OrderDetailById(id);并返回结果即可。
// Controller———————————————————
@GetMapping("/details/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> getDetailsById(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.OrderDetailById(id);//已实现
return Result.success(orderVO);
}
接单
就是将订单的状态修改为3(已接单)。
请求路径为/admin/order/confirm,请求方法为Put,以json格式提交id,后端使用OrdersConfirmDTO类来接收。(这里就一个id参数,为什么不使用Path格式传参?好怪)
// Controller———————————————————
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}
// Service———————————————————————
void confirm(OrdersConfirmDTO ordersConfirmDTO);
// ServiceImpl———————————————————
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
Orders orders = Orders.builder()
.id(ordersConfirmDTO.getId())
.status(Orders.CONFIRMED)
.build();
orderMapper.update(orders);
}
拒单
与接单同理,就是将订单状态修改为6(已取消),不过多了些业务逻辑:只有订单处于“待接单”状态时可以执行拒单操作、商家拒单时需要指定拒单原因、商家拒单时,如果用户已经完成了支付,需要为用户退款。
请求路径为/admin/order/rejection,请求方法为Put,以json格式提交id和rejectionReason,后端使用OrdersRejectionDTO类来接收。
// Controller———————————————————
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}
// Service———————————————————————
void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;
// ServiceImpl———————————————————
@Override
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());
// 订单只有存在且状态为2(待接单)才可以拒单
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (Objects.equals(payStatus, Orders.PAID)) {
// //用户已支付,需要退款(微信支付跳过)
// String refund = weChatPayUtil.refund(
// ordersDB.getNumber(),
// ordersDB.getNumber(),
// new BigDecimal(0.01),
// new BigDecimal(0.01));
// log.info("申请退款:{}", refund);
log.info("{}申请退款", BaseContext.getCurrentId());
}
// 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
Orders orders = new Orders();
orders.setId(ordersDB.getId());
orders.setStatus(Orders.CANCELLED);
orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
取消订单
同理,取消订单也是将订单状态修改为6(已取消),但业务规则不一样:商家取消订单时需要指定取消原因、商家取消订单时,如果用户已经完成了支付,需要为用户退款。
请求路径为/admin/order/cancel,请求方法为put,以json格式提交id和cancelReason,后端使用OrdersRejectionDTO类来接收。
// Controller———————————————————
@PutMapping("/cancel")
@ApiOperation("取消订单")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
orderService.cancel(ordersCancelDTO);
return Result.success();
}
// Service———————————————————————
void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;
// ServiceImpl———————————————————
@Override
public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());
//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == 1) {
// //用户已支付,需要退款(微信支付跳过)
// String refund = weChatPayUtil.refund(
// ordersDB.getNumber(),
// ordersDB.getNumber(),
// new BigDecimal(0.01),
// new BigDecimal(0.01));
// log.info("申请退款:{}", refund);
log.info("{}申请退款", BaseContext.getCurrentId());
}
// 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
Orders orders = new Orders();
orders.setId(ordersCancelDTO.getId());
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason(ordersCancelDTO.getCancelReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
派送订单
将订单状态修改为4(派送中),只有状态为“待派送”的订单可以执行派送订单操作,即status为3。
请求路径为/admin/order/delivery/{id},请求方法为put,Path传入参数id,意为订单id。
// Controller———————————————————
@PutMapping("/delivery/{id}")
@ApiOperation("派送订单")
public Result onTheWay(@PathVariable Long id) {
orderService.onTheWay(id);
return Result.success();
}
// Service———————————————————————
void onTheWay(Long id);
// ServiceImpl———————————————————
@Override
public void onTheWay(Long id) {
Orders orderDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为3
if (orderDB == null || !orderDB.getStatus().equals(Orders.CONFIRMED)) {
//抛出异常:订单状态错误
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(id);
// 更新订单状态,状态转为4(派送中)
orders.setStatus(Orders.DELIVERY_IN_PROGRESS);
orderMapper.update(orders);
}
完成订单
将订单状态修改为5(已完成),只有状态为“派送中”(即status为3)的订单可以执行派送订单操作。
请求路径为/admin/order/complete/{id},请求方法为put,Path传入参数id,意为订单id。
// Controller———————————————————
@PutMapping("/complete/{id}")
@ApiOperation("完成订单")
public Result complete(@PathVariable("id") Long id) {
orderService.complete(id);
return Result.success();
}
// Service———————————————————————
void complete(Long id);
// ServiceImpl———————————————————
@Override
public void complete(Long id) {
Orders orderDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为4
if (orderDB == null || !orderDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
//抛出异常:订单状态错误
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(id);
// 更新订单状态,状态转为5(已完成)
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
下单功能优化
提示:完成该模块很麻烦,且不实现该功能也不影响,推荐了解即可。
优化用户下单功能,加入校验逻辑,如果用户的收货地址距离商家门店超出配送范围(配送范围为5公里内),则下单失败。
进入百度地图开放平台并登陆账号、完善相关信息:
百度地图开放平台https://lbsyun.baidu.com/ 进入控制台,创建应用,获取AK,应用类型选择服务端。ip白名单尽量写0.0.0.0/0即不对ip做任何限制。
百度地图开发平台的AK(Access Key)是一种用于识别用户身份并控制访问权限的密钥。它类似于一个“通行证”,用于在调用百度地图开放平台提供的各种API服务时进行身份验证,确保只有经过授权的用户才能使用这些服务。
回到项目配置相关信息:
//application.yml——————————————————————————
sky:
......
shop:
address: ${sky.shop.address}
baidu:
ak: ${sky.baidu.ak}
//application-dev.yml——————————————————————
sky:
......
shop:
address: 北京市海淀区上地十街10号
baidu:
ak: 刚刚获取的ak
然后回到OrderServiceImpl中注入上面的配置项,并编写校验方法:
public class OrderServiceImpl implements OrderService {
......
@Value("${sky.shop.address}")
private String shopAddress;
@Value("${sky.baidu.ak}")
private String ak;
......
/**
* 检查客户的收货地址是否超出配送范围
* @param address
*/
private void checkOutOfRange(String address) {
Map map = new HashMap();
map.put("address",shopAddress);
map.put("output","json");
map.put("ak",ak);
//获取店铺的经纬度坐标
String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
JSONObject jsonObject = JSON.parseObject(shopCoordinate);
if(!jsonObject.getString("status").equals("0")){
throw new OrderBusinessException("店铺地址解析失败");
}
//数据解析
JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
String lat = location.getString("lat");
String lng = location.getString("lng");
//店铺经纬度坐标
String shopLngLat = lat + "," + lng;
map.put("address",address);
//获取用户收货地址的经纬度坐标
String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
jsonObject = JSON.parseObject(userCoordinate);
if(!jsonObject.getString("status").equals("0")){
throw new OrderBusinessException("收货地址解析失败");
}
//数据解析
location = jsonObject.getJSONObject("result").getJSONObject("location");
lat = location.getString("lat");
lng = location.getString("lng");
//用户收货地址经纬度坐标
String userLngLat = lat + "," + lng;
map.put("origin",shopLngLat);
map.put("destination",userLngLat);
map.put("steps_info","0");
//路线规划
String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);
jsonObject = JSON.parseObject(json);
if(!jsonObject.getString("status").equals("0")){
throw new OrderBusinessException("配送路线规划失败");
}
//数据解析
JSONObject result = jsonObject.getJSONObject("result");
JSONArray jsonArray = (JSONArray) result.get("routes");
Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");
if(distance > 5000){
//配送距离超过5000米
throw new OrderBusinessException("超出配送范围");
}
}
在负责处理用户下单请求的submit方法中,各种业务异常处理之后,构造订单数据之前添加条件判断语句:
@Override
@Transactional // 事务注解,确保方法内所有操作在同一个事务中
public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) {
//各种业务异常处理
......
//检查用户的收货地址是否超出配送范围
checkOutOfRange(address:addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());
......
// 构造订单数据
}
此时如果距离过远会报错,距离足够则正常下单。但小程序端因为代码问题不会出提示,如果我们想要实现微信小程序的距离提醒功能可以参考该博客:
苍穹外卖超出配送范围前端不提示问题解决方法https://blog.csdn.net/qq_65993561/article/details/143636095 总之就是非常麻烦,晚安,好梦。一篇文章写了四万字,浏览器都开始卡了。