该模块分为地址表的增删改查、用户下单、订单支付三个部分。
第一部分地址表的增删改查无非就是对于单表的增删改查,较基础,因此直接导入代码。
地址表
一个用户可以有多个地址,同时有一个地址为默认地址。用户还可为地址添加例如"公司"、"学校"、"家"之类的标签。项目中的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格式提交请求参数。
java">// 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获取地址信息并返回。
java">// 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获取地址信息并返回。
java">// 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。
java">// 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文件有误,无法修改省份城市和区域)。
java">// 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。
java">// 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格式提交请求。
java">// 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:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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的名字。
java">// 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格式数据:
javascript">{// 商户注册所得的商户号"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,有效期为两小时:
java">{"prepay_id" : "wx201410272009395522657a690389285100"
}
二、通过小程序下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法wx.requestPayment(OBJECT)调起微信支付。以下为Object请求参数。
其内部还包含了三个回调函数,分别代表不同的结果:
javascript">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文件(该步骤只需执行一次,即生成文件后便不需要再次这样操作):
javascript">//cmd窗口弹出生成的yml文件存储位置
Authtoken saved to configuration file: C:\Users\chn/.cpolar/cpolar.yml
执行完上述步骤后,回到cmd窗口,输入cpolar.exe http 8080(此处的8080为后端接口,需与自己的后端接口对应),我们就可以启动服务获取临时ip地址。cmd窗口弹出:
javascript">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,我们直接使用即可。
代码导入
因为微信支付的代码较为固定,因此我们直接导入即可。
首先配置微信支付所需的配置项:
javascript">//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中添加相关方法
java">// 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类复制进去(该类用于接收微信端推送的支付结果):
java">//支付回调相关接口
@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分别代表页面、每页记录数、订单状态。
java">// 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。
java">// 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订单已退款来代替微信退款。取消订单后需要将订单状态修改为“已取消”。
java">// 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。再来一单意为将原订单中的商品重新加入到购物车中。
小程序会先发起清空购物车的请求,然后再发起再来一单的请求,后台响应请求后,小程序再跳转到点餐页并读取购物车中的数据。
java">// 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并编写代码:
java">// 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分别意为待派送数量、派送中数量、待接单数量。
java">// 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);并返回结果即可。
java">// 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格式传参?好怪)
java">// 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类来接收。
java">// 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类来接收。
java">// 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。
java">// 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。
java">// 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服务时进行身份验证,确保只有经过授权的用户才能使用这些服务。
回到项目配置相关信息:
javascript">//application.yml——————————————————————————
sky:......shop:address: ${sky.shop.address}baidu:ak: ${sky.baidu.ak}
//application-dev.yml——————————————————————
sky:......shop:address: 北京市海淀区上地十街10号baidu:ak: 刚刚获取的ak
然后回到OrderServiceImpl中注入上面的配置项,并编写校验方法:
java">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方法中,各种业务异常处理之后,构造订单数据之前添加条件判断语句:
java"> @Override@Transactional // 事务注解,确保方法内所有操作在同一个事务中public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) {//各种业务异常处理......//检查用户的收货地址是否超出配送范围checkOutOfRange(address:addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());......// 构造订单数据}
此时如果距离过远会报错,距离足够则正常下单。但小程序端因为代码问题不会出提示,如果我们想要实现微信小程序的距离提醒功能可以参考该博客:
苍穹外卖超出配送范围前端不提示问题解决方法https://blog.csdn.net/qq_65993561/article/details/143636095 总之就是非常麻烦,晚安,好梦。一篇文章写了四万字,浏览器都开始卡了。