小柒味来 - 用户下单相关功能开发

一、地址簿功能

1.1 需求分析

地址簿用于存储用户的收货地址信息。用户登录后可以有多个地址,但仅允许一个默认地址。

1.1.1 产品原型

1.1.2 接口设计

功能需求:

  • 地址的CRUD
  • 设置默认地址
  • 查询默认地址

根据需求:

  1. 新增地址

接口地址:/user/addressBook

请求方式:POST

请求数据类型:application/json

请求示例:

{
  "cityCode": "",
  "cityName": "",
  "consignee": "",
  "detail": "",
  "districtCode": "",
  "districtName": "",
  "id": 0,
  "isDefault": 0,
  "label": "",
  "phone": "",
  "provinceCode": "",
  "provinceName": "",
  "sex": "",
  "userId": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
addressBook addressBook body true AddressBook AddressBook
cityCode false string
cityName false string
consignee false string
detail false string
districtCode false string
districtName false string
id false integer(int64)
isDefault false integer(int32)
label false string
phone false string
provinceCode false string
provinceName false string
sex false string
userId false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
  1. 查询当前用户所有地址

接口地址:/user/addressBook/list

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array AddressBook
cityCode string
cityName string
consignee string
detail string
districtCode string
districtName string
id integer(int64)
isDefault integer(int32)
label string
phone string
provinceCode string
provinceName string
sex string
userId integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "cityCode": "",
            "cityName": "",
            "consignee": "",
            "detail": "",
            "districtCode": "",
            "districtName": "",
            "id": 0,
            "isDefault": 0,
            "label": "",
            "phone": "",
            "provinceCode": "",
            "provinceName": "",
            "sex": "",
            "userId": 0
        }
    ],
    "msg": ""
}
  1. 查询默认地址

接口地址:/user/addressBook/default

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data AddressBook AddressBook
cityCode string
cityName string
consignee string
detail string
districtCode string
districtName string
id integer(int64)
isDefault integer(int32)
label string
phone string
provinceCode string
provinceName string
sex string
userId integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": {
        "cityCode": "",
        "cityName": "",
        "consignee": "",
        "detail": "",
        "districtCode": "",
        "districtName": "",
        "id": 0,
        "isDefault": 0,
        "label": "",
        "phone": "",
        "provinceCode": "",
        "provinceName": "",
        "sex": "",
        "userId": 0
    },
    "msg": ""
}
  1. 修改地址

接口地址:/user/addressBook

请求方式:PUT

请求数据类型:application/json

请求示例:

{
  "cityCode": "",
  "cityName": "",
  "consignee": "",
  "detail": "",
  "districtCode": "",
  "districtName": "",
  "id": 0,
  "isDefault": 0,
  "label": "",
  "phone": "",
  "provinceCode": "",
  "provinceName": "",
  "sex": "",
  "userId": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
addressBook addressBook body true AddressBook AddressBook
cityCode false string
cityName false string
consignee false string
detail false string
districtCode false string
districtName false string
id false integer(int64)
isDefault false integer(int32)
label false string
phone false string
provinceCode false string
provinceName false string
sex false string
userId false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
  1. 根据 ID 删除地址

接口地址:/user/addressBook

请求方式:DELETE

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
id id query false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
  1. 根据 ID 查询地址

接口地址:/user/addressBook/{id}

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
id id path true integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data AddressBook AddressBook
cityCode string
cityName string
consignee string
detail string
districtCode string
districtName string
id integer(int64)
isDefault integer(int32)
label string
phone string
provinceCode string
provinceName string
sex string
userId integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": {
        "cityCode": "",
        "cityName": "",
        "consignee": "",
        "detail": "",
        "districtCode": "",
        "districtName": "",
        "id": 0,
        "isDefault": 0,
        "label": "",
        "phone": "",
        "provinceCode": "",
        "provinceName": "",
        "sex": "",
        "userId": 0
    },
    "msg": ""
}
  1. 设置默认地址

接口地址:/user/addressBook/default

请求方式:PUT

请求数据类型:application/json

请求示例:

{
  "cityCode": "",
  "cityName": "",
  "consignee": "",
  "detail": "",
  "districtCode": "",
  "districtName": "",
  "id": 0,
  "isDefault": 0,
  "label": "",
  "phone": "",
  "provinceCode": "",
  "provinceName": "",
  "sex": "",
  "userId": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
addressBook addressBook body true AddressBook AddressBook
cityCode false string
cityName false string
consignee false string
detail false string
districtCode false string
districtName false string
id false integer(int64)
isDefault false integer(int32)
label false string
phone false string
provinceCode false string
provinceName false string
sex false string
userId false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}

1.2 功能实现

1.2.1 Controller 层

@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端地址簿接口")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 查询当前登录用户的所有地址信息
     *
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询当前登录用户的所有地址信息")
    public Result<List<AddressBook>> list() {
        AddressBook addressBook = new AddressBook();
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);
        return Result.success(list);
    }

    /**
     * 新增地址
     *
     * @param addressBook
     * @return
     */
    @PostMapping
    @ApiOperation("新增地址")
    public Result save(@RequestBody AddressBook addressBook) {
        addressBookService.save(addressBook);
        return Result.success();
    }

    @GetMapping("/{id}")
    @ApiOperation("根据id查询地址")
    public Result<AddressBook> getById(@PathVariable Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        return Result.success(addressBook);
    }

    /**
     * 根据id修改地址
     *
     * @param addressBook
     * @return
     */
    @PutMapping
    @ApiOperation("根据id修改地址")
    public Result update(@RequestBody AddressBook addressBook) {
        addressBookService.update(addressBook);
        return Result.success();
    }

    /**
     * 设置默认地址
     *
     * @param addressBook
     * @return
     */
    @PutMapping("/default")
    @ApiOperation("设置默认地址")
    public Result setDefault(@RequestBody AddressBook addressBook) {
        addressBookService.setDefault(addressBook);
        return Result.success();
    }

    /**
     * 根据id删除地址
     *
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("根据id删除地址")
    public Result deleteById(Long id) {
        addressBookService.deleteById(id);
        return Result.success();
    }

    /**
     * 查询默认地址
     */
    @GetMapping("default")
    @ApiOperation("查询默认地址")
    public Result<AddressBook> getDefault() {
        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = new AddressBook();
        addressBook.setIsDefault(1);
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);

        if (list != null && list.size() == 1) {
            return Result.success(list.get(0));
        }
        return Result.error("没有查询到默认地址");
    }
}

1.2.2 Service 层

public interface AddressBookService {

    List<AddressBook> list(AddressBook addressBook);

    void save(AddressBook addressBook);

    AddressBook getById(Long id);

    void update(AddressBook addressBook);

    void setDefault(AddressBook addressBook);

    void deleteById(Long id);
}

1.2.3 Service 实现类

AddressBookService.java
public interface AddressBookService {
    List<AddressBook> list(AddressBook addressBook);
    void save(AddressBook addressBook);
    AddressBook getById(Long id);
    void update(AddressBook addressBook);
    void setDefault(AddressBook addressBook);
    void deleteById(Long id);
}
AddressBookServiceImpl.java
@Service
@Slf4j
public class AddressBookServiceImpl implements AddressBookService {
    @Autowired
    private AddressBookMapper addressBookMapper;

    public List<AddressBook> list(AddressBook addressBook) { ... }

    public void save(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBook.setIsDefault(0);
        addressBookMapper.insert(addressBook);
    }

    public AddressBook getById(Long id) { ... }

    public void update(AddressBook addressBook) { ... }

    @Transactional
    public void setDefault(AddressBook addressBook) {
        // 全部设为非默认
        addressBook.setIsDefault(0);
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBookMapper.updateIsDefaultByUserId(addressBook);
        // 当前地址设为默认
        addressBook.setIsDefault(1);
        addressBookMapper.update(addressBook);
    }

    public void deleteById(Long id) { ... }
}

1.2.3 Mapper 层

@Mapper
public interface AddressBookMapper {
    List<AddressBook> list(AddressBook addressBook);
    @Insert("insert into address_book ...")
    void insert(AddressBook addressBook);
    @Select("select * from address_book where id = #{id}")
    AddressBook getById(Long id);
    void update(AddressBook addressBook);
    @Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")
    void updateIsDefaultByUserId(AddressBook addressBook);
    @Delete("delete from address_book where id = #{id}")
    void deleteById(Long id);
}
AddressBookMapper.xml
<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>
    <update id="update" parameterType="addressBook">
        update address_book
        <set>
            <!-- 按需更新字段 -->
        </set>
        where id = #{id}
    </update>
</mapper>

AddressBookMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<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>

    <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="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="phone != null">
                phone = #{phone},
            </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>
</mapper>

1.3 联调测试

1.3.1 新增收货地址

添加两条收货地址:

数据库验证:

1.3.2 删除收货地址

编辑地址:

删除地址:


二、用户下单

2.1 需求分析

2.1.1 产品原型

业务说明
在电商系统中,用户通过下单的方式通知商家自己已购买商品,商家需根据订单进行备货和发货。
用户下单后会产生订单相关数据,订单需包含以下信息:

  • 商品信息
  • 收货信息
  • 支付方式与状态
  • 订单状态与金额等

业务流程:

  1. 用户将菜品或套餐加入购物车
  2. 点击购物车中的 “去结算” 按钮,跳转至订单确认页面
  3. 点击 “去支付” 按钮,完成下单操作

用户点餐业务流程图(效果图):


2.1.2 接口设计

接口地址:/user/order/submit

请求方式:POST

请求数据类型:application/json

请求示例:

{
  "addressBookId": 0,
  "amount": 0,
  "deliveryStatus": 0,
  "estimatedDeliveryTime": "yyyy-MM-dd HH:mm:ss",
  "packAmount": 0,
  "payMethod": 0,
  "remark": "",
  "tablewareNumber": 0,
  "tablewareStatus": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
ordersSubmitDTO ordersSubmitDTO body true OrdersSubmitDTO OrdersSubmitDTO
addressBookId false integer(int64)
amount false number
deliveryStatus false integer(int32)
estimatedDeliveryTime false string
packAmount false integer(int32)
payMethod false integer(int32)
remark false string
tablewareNumber false integer(int32)
tablewareStatus false integer(int32)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data OrderSubmitVO OrderSubmitVO
id integer(int64)
orderAmount number
orderNumber string
orderTime string(date-time)
msg string

响应示例:

{
    "code": 0,
    "data": {
        "id": 0,
        "orderAmount": 0,
        "orderNumber": "",
        "orderTime": ""
    },
    "msg": ""
}

2.2 功能实现

2.2.1 设计DTO

OrdersSubmitDTO.java

@Data
public class OrdersSubmitDTO implements Serializable {
    private Long addressBookId; // 地址簿id
    private int payMethod; // 付款方式
    private String remark; // 备注
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime estimatedDeliveryTime; // 预计送达时间
    private Integer deliveryStatus; // 配送状态
    private Integer tablewareNumber; // 餐具数量
    private Integer tablewareStatus; // 餐具数量状态
    private Integer packAmount; // 打包费
    private BigDecimal amount; // 总金额
}

2.2.2 设计VO

OrderSubmitVO.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    private Long id;
    private String orderNumber;
    private BigDecimal orderAmount;
    private LocalDateTime orderTime;
}

2.2.3 Controller 层

@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "C端-订单接口")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/submit")
    @ApiOperation("用户下单")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
        log.info("用户下单:{}", ordersSubmitDTO);
        OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
        return Result.success(orderSubmitVO);
    } 
}

2.2.4 Service 层

public interface OrderService {
    OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}

2.2.5 Service 实现类

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;

    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
        //异常情况的处理(收货地址为空、超出配送范围、购物车为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        Long userId = BaseContext.getCurrentId();
        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 order = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO,order);
        order.setPhone(addressBook.getPhone());
        order.setAddress(addressBook.getDetail());
        order.setConsignee(addressBook.getConsignee());
        order.setNumber(String.valueOf(System.currentTimeMillis()));
        order.setUserId(userId);
        order.setStatus(Orders.PENDING_PAYMENT);
        order.setPayStatus(Orders.UN_PAID);
        order.setOrderTime(LocalDateTime.now());
        //向订单表插入1条数据
        orderMapper.insert(order);
        //订单明细数据
        List<OrderDetail> orderDetailList = new ArrayList<>();
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail();
            BeanUtils.copyProperties(cart, orderDetail);
            orderDetail.setOrderId(order.getId());
            orderDetailList.add(orderDetail);
        }
        //向明细表插入n条数据
        orderDetailMapper.insertBatch(orderDetailList);
        //清理购物车中的数据
        shoppingCartMapper.deleteByUserId(userId);
        //封装返回结果
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(order.getId())
                .orderNumber(order.getNumber())
                .orderAmount(order.getAmount())
                .orderTime(order.getOrderTime())
                .build();

        return orderSubmitVO;
    }  
}

2.2.6 Mapper层

OrderMapper.java

@Mapper
public interface OrderMapper {
    void insert(Orders order);
}

OrderMapper.xml

<mapper namespace="com.qi.mapper.OrderMapper">

    <insert id="insert" parameterType="Orders" 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, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>
</mapper>

OrderDetailMapper.java

@Mapper
public interface OrderDetailMapper {
    void insertBatch(List<OrderDetail> orderDetails);
}

OrderDetailMapper.xml

<mapper namespace="com.qi.mapper.OrderDetailMapper">
    <insert id="insertBatch" parameterType="list">
        insert into order_detail
        (name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)
        values
        <foreach collection="orderDetails" item="od" separator=",">
            (#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},
             #{od.number},#{od.amount},#{od.image})
        </foreach>
    </insert>
</mapper>

2.3 联调测试

  1. 登录小程序
  2. 完成下单操作(此时会删除购物车中的数据)
  3. 检查 shopping_cart 表数据是否清空
  4. 测试流程:去结算 → 去支付

测试截图占位:

  • 购物车页面
  • 购物车表
  • 支付页面
  • order_detail 表
  • 确定清空购物车表

关于下单的相关部分就基本实现了,下一章小柒味来-订单支付实现


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub