苍穹外卖—订单模块

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

地址表

        一个用户可以有多个地址,同时有一个地址为默认地址。用户还可为地址添加例如"公司"、"学校"、"家"之类的标签。项目中的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 {

    @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:

字段名数据类型说明备注
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 {

    @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        总之就是非常麻烦,晚安,好梦。一篇文章写了四万字,浏览器都开始卡了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/958750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从ChatGPT热潮看智算崛起

2025年1月7日&#xff0c;科智咨询发布《2025年IDC产业七大发展趋势》&#xff0c;其中提到“ChatGPT开启生成式AI热潮&#xff0c;智能算力需求暴涨&#xff0c;算力供给结构发生转变”。 【图片来源于网络&#xff0c;侵删】 为何会以ChatGPT发布为节点呢&#xff1f;咱们一起…

【Uniapp-Vue3】setTabBar设置TabBar和下拉刷新API

一、setTabBar设置 uni.setTabBarItem({ index:"需要修改第几个", text:"修改后的文字内容" }) 二、tabBar的隐藏和显式 // 隐藏tabBar uni.hideTabBar(); // 显示tabBar uni.showTabBar(); 三、为tabBar右上角添加文本 uni.setTabBarBadge({ index:"…

【express-generator】06-RESTFUL API设计(第二阶段)

前言&#xff1a; 前面我们学习了第一阶段的express-generator内容以及做了对应练习&#xff0c;现在我们正式开始第二阶段的学习以及练习。本篇介绍的内容是RESTFUL API设计。 第二阶段的大纲如下&#xff1a; RESTful API 设计&#xff1a; 学习如何设计符合 REST 原则的 …

Python 预训练:打通视觉与大语言模型应用壁垒——Python预训练视觉和大语言模型

大语言模型是一种由包含数百亿甚至更多参数的深度神经网络构建的语言模型&#xff0c;通常使用自监督学习方法通过大量无标签文本进行训练&#xff0c;是深度学习之后的又一大人工智能技术革命。 大语言模型的发展主要经历了基础模型阶段(2018 年到2021年)、能力探索阶段(2019年…

【深度学习】2.视觉问题与得分函数

计算机视觉任务 可以通过神经网络搜索是什么类别的动物。 图像实际就是含有数值的三维矩阵。 像素值从0-255可以表示亮度递增的参数。数字越大&#xff0c;像素点越亮。 最后的3表示三个颜色通道&#xff0c;常见的如JPG、RGB等。 现实场景容易发生各种遮蔽现象。 计算机判断…

1.CSS的三大特性

css有三个非常重要的三个特性&#xff1a;层叠性、继承性、优先级 1.1 层叠性 想通选择器给设置想听的样式&#xff0c;此时一个样式就会覆盖&#xff08;层叠&#xff09;另一个冲突的样式。层叠性主要是解决样式冲突的问题。 <!DOCTYPE html> <html lang"en&…

使用Edge打开visio文件

使用Edge打开visio文件 打开Edge浏览器搜索‘vsdx edge’ 打开第一个搜索结果 Microsoft Support 根据上述打开的页面进行操作 第一步&#xff1a;安装Visio Viewer 第二步&#xff1a;添加注册表 桌面新增文本文件&#xff0c;将下面的内容放入新建文本中&#xff0c;修…

基于微信小程序的健身管理系统设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

GPS信号生成:C/A码序列生成【MATLAB实现】

GPS C/A码序列生成【MATLAB实现】 在本文中&#xff0c;将简要介绍GPS C/A码及其生成原理&#xff0c;并且用MATLAB代码实现。 GPS信号与C/A码 GPS的信号主要有三类&#xff1a;载波&#xff08;carrier&#xff09;、测距码&#xff08;也可以说是伪随机噪声码&#xff0c;…

redis离线安装部署详解(包括一键启动)

像上文一样 因为在学习的过程中没有查到一个详细的离线部署方案 所以在自己学习之后想要自己写一个文章 希望可以帮助后续学习redis离线部署的朋友少走一线弯路 首先就是下载安装包 可以自己在本地下载再传到机器上&#xff08;通过xftp或lrzsz都可&#xff09; http://d…

Hadoop•搭建完全分布式集群

听说这里是目录哦 一、安装Hadoop&#x1f955;二、配置Hadoop系统环境变量&#x1f96e;三、验证Hadoop系统环境变量是否配置成功&#x1f9c1;四、修改Hadoop配置文件&#x1f36d;五、分发Hadoop安装目录&#x1f9cb;六、分发系统环境变量文件&#x1f368;七、格式化HDFS文…

安卓动态设置Unity图形API

命令行方式 Unity图像api设置为自动,安卓动态设置Vulkan、OpenGLES Unity设置 安卓设置 创建自定义活动并将其设置为应用程序入口点。 在自定义活动中,覆盖字符串UnityPlayerActivity。updateunitycommandlineararguments (String cmdLine)方法。 在该方法中,将cmdLine…

Java春招面试指南前言

在当今竞争激烈的就业市场中&#xff0c;对于即将踏入职场的Java开发者而言&#xff0c;春招是一次宝贵的机会。本博客专栏旨在为大家提供一份全面且实用的Java春招面试指南&#xff0c;助力大家顺利通过面试&#xff0c;开启职业生涯的新篇章。 无论你是初出茅庐的应届生&…

记录一次k8s起不来的排查过程

我在k8s集群&#xff0c;重启了一个node宿主机&#xff0c;竟然发现kubelet起不来了&#xff01;报错如下 这个报错很模糊&#xff0c;怎么排查呢。这样&#xff0c;开两个界面&#xff0c;一个重启kubelet&#xff0c;一个看系统日志(/var/log/message:centos&#xff0c;/va…

利用Qt5.15.2编写Android程序时遇到的问题及解决方法

文章目录 背景1.文件读写 背景 目前我用的是Qt5.15.2来编写Qt程序&#xff0c;环境的配置看我这篇文章【Qt5.15.2配置Android开发环境】 项目中的一些配置的截图&#xff1a; 1.文件读写 假如直接用 QFileDialog::getExistingDirectory来获取路径的话&#xff0c;会得到类…

JVM深入学习(一)

目录 一.JVM概述 1.1 为什么要学jvm&#xff1f; 1.2 jvm的作用 1.3 jvm内部构造 二.JVM类加载 2.1类加载过程 2.2类加载器 2.3类加载器的分类 2.4双亲委派机制 三.运行时数据区 堆空间区域划分&#xff08;堆&#xff09; 为什么分区(代)&#xff1f;&#xff08…

o1 医学推理:基于推断时长扩展与旅程学习,仅用 500 条蒸馏示例,实现 6%~11% 性能提升

o1 医学推理&#xff1a;基于推断时长扩展与旅程学习&#xff0c;仅用 500 条蒸馏示例&#xff0c;实现 6%&#xff5e;11% 性能提升 论文大纲1. 提出背景是什么&#xff1f;2. 概念的性质是什么&#xff1f;是什么导致这个性质&#xff1f;3. 请举一个正例、一个反例&#xff…

微信小程序隐藏右侧胶囊按钮,分享和关闭即右侧三个点和小圆圈按钮

在微信小程序开发过程中&#xff0c;可能需要将右侧的胶囊按钮、即右侧的三个点和小圆圈按钮关闭掉。如图&#xff1a; 这时&#xff0c;我们只需在该页面的json文件中进行相关配置即可 {"navigationBarTitleText": "商品详情页","navigationStyle&q…

chrome小插件:长图片等分切割

前置条件&#xff1a; 安装有chrome谷歌浏览器的电脑 使用步骤&#xff1a; 1.打开chrome扩展插件 2.点击管理扩展程序 3.加载已解压的扩展程序 4.选择对应文件夹 5.成功后会出现一个扩展小程序 6.点击对应小程序 7.选择图片进行切割&#xff0c;切割完成后会自动保存 代码…

检测到联想鼠标自动调出运行窗口,鼠标自己作为键盘操作

联想鼠标会自动时不时的调用“运行”窗口 然后鼠标自己作为键盘输入 然后打开这个网页 &#xff08;不是点击了什么鼠标外加按键&#xff0c;这个鼠标除了左右和中间滚轮&#xff0c;没有其他按键了&#xff09;