当前位置: 首页 > article >正文

苍穹外卖—订单模块

        该模块分为地址表的增删改查、用户下单、订单支付三个部分。
        第一部分地址表的增删改查无非就是对于单表的增删改查,较基础,因此直接导入代码。

地址表

        一个用户可以有多个地址,同时有一个地址为默认地址。用户还可为地址添加例如"公司"、"学校"、"家"之类的标签。项目中的address_book表就包含了这些信息,其中红色字体为重要信息。

字段名数据类型说明备注
idbigint主键自增
user_idbigint用户id逻辑外键
consigneevarchar(50)收货人
sexvarchar(2)性别
phonevarchar(11)手机号
province_codevarchar(12)省份编码
province_namevarchar(32)省份名称
city_codevarchar(12)城市编码
city_namevarchar(32)城市名称
district_codevarchar(12)区县编码
district_namevarchar(32)区县名称
detailvarchar(200)详细地址信息具体到门牌号
labelvarchar(100)标签公司、家、学校等
is_defaulttinyint(1)是否默认地址1是0否

新增地址

        请求路径为/user/addressBook,请求方法为Post,以json格式提交请求参数。

// Controller———————————————————
@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端地址簿接口")
public class AddressBookController {@Autowiredprivate 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 {@Autowiredprivate AddressBookMapper addressBookMapper;public void save(AddressBook addressBook) {// 设置当前用户的IDaddressBook.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对象并设置当前登录用户的IDAddressBook 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); // 设置默认地址标志为1addressBook.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———————————————————@Transactionalpublic 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:

字段名数据类型说明备注
idbigint主键自增
numbervarchar(50)订单号
statusint订单状态

1待付款2待接单3已接单

4派送中5已完成6已取消

user_idbigint用户id逻辑外键
address_book_idbigint地址id逻辑外键
order_timedatetime下单时间
checkout_timedatetime付款时间
pay_methodint支付方式1微信支付2支付宝支付
pay_statustinyint支付状态0未支付1已支付2退款
amountdecimal(10,2)订单金额
remarkvarchar(100)备注信息
phonevarchar(11)手机号冗余字段
addressvarchar(255)详细地址信息冗余字段
consigneevarchar(32)收货人
cancel_reasonvarchar(255)订单取消原因
rejection_reasonvarchar(255)拒单原因
cancel_timedatetime订单取消时间
estimated_delivery_timedatetime预计送达时间
delivery_statustinyint配送状态1立即送出0选择具体时间
delivery_timedatetime送达时间
pack_amountint打包费
tableware_numberint餐具数量
tableware_statustinyint餐具数量状态1按餐量提供0选择具体数量

        订单明细表order_detail: 

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)商品名称冗余字段
imagevarchar(255)商品图片路径冗余字段
order_idbigint订单id逻辑外键
dish_idbigint菜品id逻辑外键
setmeal_idbigint套餐id逻辑外键
dish_flavorvarchar(50)菜品口味
numberint商品数量
amountdecimal(10,2)商品单价

        下单后等待支付的页面包含支付倒计时,默认为15分钟,后端只需返回下单时间即可,小程序端会自行计算。订单总金额和订单号也许返回。同时因为订单支付需以订单id来区分,因此也需返回。 

功能实现 

        后端可用OrdersSubmitDTO类来接收参数。返回的为OrdersSubmitDTO类对象。请求路径为/user/order/submit,请求方式为Post。因为订单模块是用户端的功能,日后在管理端也需开发同类名的接口,为防止程序冲突,需指定其生成的bean的名字。

// Controller———————————————————
@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户端订单相关接口")
@Slf4j
public class OrderController {@Autowiredprivate 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderDetailMapper orderDetailMapper;@Autowiredprivate AddressBookMapper addressBookMapper;@Autowiredprivate 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(); // 获取当前用户IDShoppingCart 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); // 设置用户IDorders.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()); // 设置当前订单明细关联的订单IDorderDetailList.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:# 小程序的appidappid: ${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:# 微信公众号或小程序的AppIDappid: wxe8b6f903deb8566b# 微信公众号或小程序的AppSecretsecret: 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———————————————————@Autowiredprivate UserMapper userMapper;@Autowiredprivate WeChatPayUtil weChatPayUtil;public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {// 当前登录用户idLong 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 {@Autowiredprivate OrderService orderService;@Autowiredprivate 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———————————————————@Overridepublic 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,订单详情orderDetailListList<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———————————————————@Overridepublic 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———————————————————@Overridepublic 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) {// 获取当前用户的IDLong 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为当前用户IDshoppingCart.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 {@Autowiredprivate 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———————————————————@Overridepublic 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) {// 将共同字段复制到OrderVOOrderVO orderVO = new OrderVO();BeanUtils.copyProperties(orders, orderVO);//调用自定义方法String orderDishes = getOrderDishesStr(orders);// 将订单菜品信息封装到orderVO中,并添加到orderVOListorderVO.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———————————————————@Overridepublic 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———————————————————@Overridepublic 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———————————————————@Overridepublic void onTheWay(Long id) {Orders orderDB = orderMapper.getById(id);// 校验订单是否存在,并且状态为3if (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———————————————————@Overridepublic void complete(Long id) {Orders orderDB = orderMapper.getById(id);// 校验订单是否存在,并且状态为4if (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        总之就是非常麻烦,晚安,好梦。一篇文章写了四万字,浏览器都开始卡了。

相关文章:

苍穹外卖—订单模块

该模块分为地址表的增删改查、用户下单、订单支付三个部分。 第一部分地址表的增删改查无非就是对于单表的增删改查&#xff0c;较基础&#xff0c;因此直接导入代码。 地址表 一个用户可以有多个地址&#xff0c;同时有一个地址为默认地址。用户还可为地址添加例如&q…...

「 机器人 」扑翼飞行器的数据驱动建模核心方法

前言 数据驱动建模可充分利用扑翼飞行器的已有运行数据,改进动力学模型与控制策略,并对未建模动态做出更精确的预测。在复杂的非线性飞行环境中,该方法能有效弥补传统解析建模的不足,具有较高的研究与应用价值。以下针对主要研究方向和实现步骤进行整理与阐述。 1. 数据驱动…...

openeuler 22.03 lts sp4 使用 cri-o 和 静态 pod 的方式部署 k8s-v1.32.0 高可用集群

前情提要 整篇文章会非常的长…可以选择性阅读,另外,这篇文章是自己学习使用的,用于生产,还请三思和斟酌 静态 pod 的部署方式和二进制部署的方式是差不多的,区别在于 master 组件的管理方式是 kubectl 还是 systemctl有 kubeadm 工具,为什么还要用静态 pod 的方式部署?…...

Helm Chart 实战指南

Helm 是 Kubernetes 的包管理工具,而 Helm Chart 是 Helm 的核心概念,用于定义、安装和升级 Kubernetes 应用。本文将带你从零开始,通过实战演练,掌握 Helm Chart 的创建、配置和部署,帮助你高效管理 Kubernetes 应用。 1. 环境准备 在开始之前,确保你已经具备以下环境:…...

【数据结构】_顺序表经典算法OJ(力扣版)

目录 1. 移除元素 1.1 题目描述及链接 1.2 解题思路 1.3 程序 2. 合并两个有序数组 1.1 原题链接及题目描述 1.2 解题思路 1.3 程序 1. 移除元素 1.1 题目描述及链接 原题链接&#xff1a;27. 移除元素 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a…...

目前市场主流的AI PC对于大模型本地部署的支持情况分析-Deepseek

以下是目前市场主流AI PC对**大模型本地部署支持情况**的综合分析&#xff0c;结合硬件能力、软件生态及厂商动态进行总结&#xff1a; --- ### **一、硬件配置与算力支持** 1. **核心处理器架构** - **异构计算方案&#xff08;CPUGPUNPU&#xff09;**&#xff1a;主流…...

Vue3 v-bind 和 v-model 对比

1. 基本概念 1.1 v-bind 单向数据绑定从父组件向子组件传递数据简写形式为 : 1.2 v-model 双向数据绑定父子组件数据同步本质是 v-bind 和 v-on 的语法糖 2. 基础用法对比 2.1 表单元素绑定 <!-- v-bind 示例 --> <template><input :value"text&quo…...

MySQL分表自动化创建的实现方案(存储过程、事件调度器)

《MySQL 新年度自动分表创建项目方案》 一、项目目的 在数据库应用场景中&#xff0c;随着数据量的不断增长&#xff0c;单表存储数据可能会面临性能瓶颈&#xff0c;例如查询、插入、更新等操作的效率会逐渐降低。分表是一种有效的优化策略&#xff0c;它将数据分散存储在多…...

接口技术-第6次作业

目录 作业内容 解答 1.假设在一个系统中&#xff0c;8255A的端口地址为184H-187H&#xff0c;A口工作于方式1输出&#xff0c;B口工作于方式1输入&#xff0c;禁止中断&#xff0c;C口剩余的两根线PC5&#xff0c;PC4位输入&#xff0c;如下图所示&#xff0c;试编写初始化…...

计算机网络之计算机网络体系结构

一、定义与概述 计算机网络体系结构是计算机网络及其部件所应该完成功能的精确定义&#xff0c;这些功能由何种硬件或软件完成是遵循这种体系结构的。体系结构是抽象的&#xff0c;实现是具体的&#xff0c;是运行在计算机软件和硬件之上的。 二、主流模型 目前&#xff0c;…...

(1)Linux高级命令简介

Linux高级命令简介 在安装好linux环境以后第一件事情就是去学习一些linux的基本指令&#xff0c;我在这里用的是CentOS7作演示。 首先在VirtualBox上装好Linux以后&#xff0c;启动我们的linux&#xff0c;输入账号密码以后学习第一个指令 简介 Linux高级命令简介ip addrtou…...

网络直播时代的营销新策略:基于受众分析与开源AI智能名片2+1链动模式S2B2C商城小程序源码的探索

摘要&#xff1a;随着互联网技术的飞速发展&#xff0c;网络直播作为一种新兴的、极具影响力的媒体形式&#xff0c;正逐渐改变着人们的娱乐方式、消费习惯乃至社交模式。据中国互联网络信息中心数据显示&#xff0c;网络直播用户规模已达到3.25亿&#xff0c;占网民总数的45.8…...

CSS(快速入门)

欢迎大家来到我的博客~欢迎大家对我的博客提出指导&#xff0c;有错误的地方会改进的哦~点击这里了解更多内容 目录 一、什么是CSS?二、基本语法规范三、CSS选择器3.1 标签选择器3.2 id选择器3.3 class选择器3.4 通配符选择器3.5 复合选择器 四、常用CSS样式4.1 color4.2 font…...

waitpid使用

waitpid 是 Unix/Linux 系统中用于等待子进程状态变化的系统调用。它允许父进程挂起执行&#xff0c;直到指定的子进程终止或者发生了其他指定的状态变化。 waitpid 的语法 pid_t waitpid(pid_t pid, int *status, int options); pid: 要等待的子进程的进程 ID&#xff0c;特殊…...

对顾客行为的数据分析:融入2+1链动模式、AI智能名片与S2B2C商城小程序的新视角

摘要&#xff1a;随着互联网技术的飞速发展&#xff0c;企业与顾客之间的交互方式变得日益多样化&#xff0c;移动设备、社交媒体、门店、电子商务网站等交互点应运而生。这些交互点不仅为顾客提供了便捷的服务体验&#xff0c;同时也为企业积累了大量的顾客行为数据。本文旨在…...

MySQL查询优化(三):深度解读 MySQL客户端和服务端协议

如果需要从 MySQL 服务端获得很高的性能&#xff0c;最佳的方式就是花时间研究 MySQL 优化和执行查询的机制。一旦理解了这些&#xff0c;大部分的查询优化是有据可循的&#xff0c;从而使得整个查询优化的过程更有逻辑性。下图展示了 MySQL 执行查询的过程&#xff1a; 客户端…...

pytorch线性回归模型预测房价例子

import torch import torch.nn as nn import torch.optim as optim import numpy as np# 1. 创建线性回归模型类 class LinearRegressionModel(nn.Module):def __init__(self):super(LinearRegressionModel, self).__init__()self.linear nn.Linear(1, 1) # 1个输入特征&…...

UE AController

定义和功能 AController是一种特定于游戏的控制器&#xff0c;在UE框架中用于定义玩家和AI的控制逻辑。AController负责处理玩家输入&#xff0c;并根据这些输入驱动游戏中的角色或其他实体的行为。设计理念 AController设计用于分离控制逻辑与游戏角色&#xff0c;增强游戏设计…...

选择的阶段性质疑

条条大路通罗马&#xff0c;每个人选择的道路&#xff0c;方向并不一样&#xff0c;但不妨碍都可以到达终点&#xff0c;而往往大家会更推崇自己走过的路径。 自己靠什么走向成功&#xff0c;自己用了什么方法&#xff0c;奉行什么原则或者理念&#xff0c;也会尽可能传播这种&…...

Git进阶之旅:Git 配置信息 Config

Git 配置级别&#xff1a; 仓库级别&#xff1a;local [ 优先级最高 ]用户级别&#xff1a;global [ 优先级次之 ]系统级别&#xff1a;system [ 优先级最低 ] 配置文件位置&#xff1a; git 仓库级别对应的配置文件是当前仓库下的 .git/configgit 用户级别对应的配置文件时用…...

51单片机开发:定时器中断

目标&#xff1a;利用定时器中断&#xff0c;每隔1s开启/熄灭LED1灯。 外部中断结构图如下图所示&#xff0c;要使用定时器中断T0&#xff0c;须开启TE0、ET0。&#xff1a; 系统中断号如下图所示&#xff1a;定时器0的中断号为1。 定时器0的工作方式1原理图如下图所示&#x…...

ultralytics 是什么?

ultralytics 是一个用于计算机视觉任务的 Python 库&#xff0c;专注于提供高效、易用的目标检测、实例分割和图像分类工具。它最著名的功能是实现 YOLO&#xff08;You Only Look Once&#xff09; 系列模型&#xff0c;特别是最新的 YOLOv8。 1. YOLO 是什么&#xff1f; YO…...

Qt调用FFmpeg库实时播放UDP组播视频流

基于以下参考链接&#xff0c;通过改进实现实时播放UDP组播视频流 https://blog.csdn.net/u012532263/article/details/102736700 源码在windows&#xff08;qt-opensource-windows-x86-5.12.9.exe&#xff09;、ubuntu20.04.6(x64)(qt-opensource-linux-x64-5.12.12.run)、以…...

Python学习之旅:进阶阶段(五)数据结构-双端队列(collections.deque)

在 Python 的进阶学习过程中,数据结构的掌握至关重要。今天要介绍的双端队列(deque,即 double-ended queue),是一种非常实用的数据结构,Python 的collections模块中的deque类为我们提供了强大的双端队列操作功能。接下来,就一起深入了解双端队列吧。 一、什么是双端队列…...

selenium自动化测试框架——面试题整理

目录 1. 什么是 Selenium&#xff1f;它的工作原理是什么&#xff1f; 2. Selenium 主要组件 3. 常见 WebDriver 驱动 4. Selenium 如何驱动浏览器&#xff1f; 5. WebDriver 协议是什么&#xff1f; 6. Page Object 模式与 Page Factory 7. 如何判断元素是否可见&#x…...

第19篇:python高级编程进阶:使用Flask进行Web开发

第19篇&#xff1a;python高级编程进阶&#xff1a;使用Flask进行Web开发 内容简介 在第18篇文章中&#xff0c;我们介绍了Web开发的基础知识&#xff0c;并使用Flask框架构建了一个简单的Web应用。本篇文章将深入探讨Flask的高级功能&#xff0c;涵盖模板引擎&#xff08;Ji…...

深度学习框架应用开发:基于 TensorFlow 的函数求导分析

深度学习框架应用开发&#xff1a;基于 TensorFlow 的函数求导分析 在深度学习的世界里&#xff0c;梯度计算是优化算法的核心。而 TensorFlow 作为一款强大的深度学习框架&#xff0c;为我们提供了简洁而强大的工具来进行自动求导操作&#xff0c;这极大地简化了深度学习模型的…...

【学术会议征稿-第二届生成式人工智能与信息安全学术会议(GAIIS 2025)】人工智能与信息安全的魅力

重要信息 时间&#xff1a;2025年2月21日-23日 地点&#xff1a;中国杭州 官网&#xff1a;http://www.ic-gaiis.org 简介 2025年第二届生成式人工智能与信息安全将于 2025年2月21日-23日在中国杭州举行。主要围绕“生成式人工智能与信息安全”的最新研究展开&#xff0c;…...

2025春晚刘谦魔术揭秘魔术过程

2025春晚刘谦魔术揭秘魔术过程 首先来看全过程 将杯子&#xff0c;筷子&#xff0c;勺子以任意顺序摆成一排 1.筷子和左边物体交换位置 2.杯子和右边物体交换位置 3.勺子和左边物体交换位置 最终魔术的结果是右手出现了杯子 这个就是一个简单的分类讨论的问题。 今年的魔术…...

postgresql的用户、数据库和表

在 PostgreSQL 中&#xff0c;用户、数据库和表是关系型数据库系统的基本组成部分。理解这些概念对数据库管理和操作至关重要。下面是对这些概念的详细解释&#xff1a; 1. 用户&#xff08;User&#xff09; 在 PostgreSQL 中&#xff0c;用户&#xff08;也称为 角色&#…...