瑞吉外卖-基础篇
1. 项目概述
1.1 软件开发整体介绍
- 软件开发流程
- 角色分工
- 软件环境
1.2 瑞吉外卖项目介绍
- 项目介绍
- 产品原型展示
- 技术选型
- 功能架构
- 角色
1.3 开发环境搭建
- 数据库环境搭建
- maven项目环境搭建
2. 后台登录功能开发
2.1 后台系统登录功能
2022/11/1
后台登录功能开发处理逻辑:
- 将页面提交的密码password进行md5加密处理
- 根据页面提交的用户名username查询数据库
- 如果没有查询到则返回登录失败结果
- 密码比对,如果不一致则返回登录失败结果
- 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
- 登录成功,将员工id存入Session并返回登录成功的结果
controller层处理逻辑代码:
1 |
|
2.2 后台退出功能开发
2022/11/2
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST
只需要在controller中创建对应的处理方法即可,具体的处理逻辑为:
- 清理session中的用户id
- 返回结果
处理逻辑代码:
1 |
|
3. 员工管理业务开发
3.1 完善登录功能
问题分析
前面已近完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以直接访问。这种设计是不合理的,希望看到的效果是:只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。
具体实现
使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
代码实现
实现步骤:
- 创建自定义过滤器LoginCheckFilter
- 在启动类上加入注解@ServletComponentScan //扫描过滤器
- 完善过滤器的处理逻辑
具体代码:
1 |
|
3.2 新增员工
新增员工程序的执行过程:
- 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
- 服务端controller接收页面提交的数据并调用service将数据进行保存
- service调用mapper操作数据库,保存数据
具体逻辑处理代码:
1 |
|
全局异常捕获处理
前面程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常。
此时需要我们的程序进行异常捕获,通常有两种处理方法:
- 在controller中加入try、catch进行异常捕获
- 使用异常处理器进行全局异常捕获
1 |
|
3.3 员工信息分页查询
2022/11/3
员工信息分页查询整个程序的执行过程:
- 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
- 服务端controller接收页面提交的数据并调用service查询数据
- service调用mapper操作数据库,查询分页数据
- controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementsUI的Table组件展示到页面上
员工信息分页查询:
1 |
|
3.4 启用/禁用员工账号
启用或禁用员工账号程序的执行过程:
- 页面发送ajax请求,将参数(id、status)提交到服务端
- 服务端controller接收页面提交的数据并调用service更新数据
- service调用mapper操作数据库
启用/禁用员工账号:
1 |
|
功能测试:
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有发生变化。
具体原因:
js处理前端Long型员工id,精度丢失(16位之后精度丢失) Long=>String
代码修复:
- 提供对象转换器JacksonObjectMapper,基于Jackson进行java数据到json数据的转换
- 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json对象的转换
3.5 编辑员工信息
编辑员工信息操作过程和对应的程序执行流程:
点击编辑按钮时,页面跳转到add.html,并在url中携带参数(员工id)
在add.html页面获取url中的参数(员工id)
发送ajax请求,请求服务端,同时提交员工id参数
服务端接收请求,根据员工di查询员工信息,将员工信息以json形式响应给页面
1
2
3
4
5
6
7
8
9
10
11
12
13/*
* 根据id查询员工信息
* @author linsuwen
* @date 2022/11/3 22:21
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息!");
}页面接收服务端响应的json数据,通过vue的数据绑定进行员工信息回显
点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
服务端接收员工信息,并进行处理,完成后给页面响应
页面接收到服务端响应信息后进行相应处理
4. 分类管理业务开发
4.1 公共字段自动填充
2022/11/6
问题分析
在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段
对于这些公共字段可以使用MybatisPlus框架提供的公共字段自动填充功能在某个地方统一处理来简化开发。
MybatisPlus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段处理,避免了重复代码。
实现步骤:
在实体类的属性上加入@TableField注解,指定自动填充策略
按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/*
* 自定义元数据对象处理器(自动填充配置)
* @author linsuwen
* @date 2022/11/6 16:29
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/*
* 插入操作自动填充
* @author linsuwen
* @date 2022/11/6 16:18
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("insert公共字段自动填充...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
/*
* 更新操作自动填充
* @author linsuwen
* @date 2022/11/6 16:18
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("update公共字段自动填充...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
4.2 新增分类
新增分类:
1 |
|
4.3 分类信息分页查询
分类信息分页查询:
1 |
|
4.4 删除分类
需求分析:
在分类管理列表页面,可以对某个分类进行删除操作,需要注意的是当分类关联了某个菜品或者套餐时,此分类不允许删除。
删除分类:
1 |
|
4.5 修改分类
修改分类:
1 |
|
5. 菜品管理业务开发
5.1 文件上传下载
2022/12/5
文件上传代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/*
* 文件上传
* @author linsuwen
* @date 2022/12/5 18:14
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){ //注意参数名有要求
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix; //动态拼接文件名后缀
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在需要创建
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
return R.success(fileName);
}文件下载代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/*
* 文件下载
* @author linsuwen
* @date 2022/12/5 18:56
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
//输出流,通过输出流将文件写会浏览器,在浏览器中展示图片
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg"); //图片文件
int len = 0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
fileInputStream.close();
outputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
5.2 新增菜品
新增菜品时前端页面和服务端的交互过程:
- 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这四次请求即可。
实现步骤:
页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/*
* 根据条件查询分类数据
* @author linsuwen
* @date 2022/12/5 21:18
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}页面发送请求进行图片上传,请求服务端将图片保存到服务器
5.1 文件上传已经完成该部分功能
页面发送请求进行图片下载,将上传的图片进行回显
5.1 文件下载已经完成该部分功能
点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
编写DishDTO,用于封装页面提交的数据(因为传输的数据和实体类不是一一对应的)
DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。
1
2
3
4
5
6@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}服务端接收并处理数据
1
2
3
4
5
6
7
8
9
10
11/*
* 新增菜品
* @author linsuwen
* @date 2022/12/6 15:56
*/
@PostMapping
public R<String> save(@NotNull @RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto); //service层
return R.success("新增菜品成功!");
}
5.3 菜品信息分页查询
2022/12/6
菜品分页查询时前端页面和服务端交互过程:
- 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务端,获取分页数据。
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示。
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这两次请求即可。
菜品信息分页查询:
1 |
|
5.4 修改菜品
修改菜品时前端页面(add.html)和服务端的交互过程:
- 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中的数据展示
- 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
- 页面发送请求,请求服务端进行图片下载,用于页面回显
- 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
实现步骤:
页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中的数据展示
5.2 新增菜品时已经完成该部分功能
页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
1
2
3
4
5
6
7
8
9
10/*
* 根据id查询菜品信息和对应口味信息(数据回显)
* @author linsuwen
* @date 2022/12/6 21:09
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id); //主要逻辑封装在了service层
return R.success(dishDto);
}页面发送请求,请求服务端进行图片下载,用于页面回显
5.1 文件上传下载时已经完成该部分功能
点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
1
2
3
4
5
6
7
8
9
10
11/*
* 修改菜品
* @author linsuwen
* @date 2022/12/6 21:51
*/
@PutMapping
public R<String> update(@NotNull @RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto); //主要逻辑封装在了service层
return R.success("菜品信息修改成功!");
}
6. 套餐管理业务开发
6.1 新增套餐
2022/12/7
新增套餐时前端页面和服务端的交互过程:
- 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
- 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
- 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将套餐相关数据以json显示提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
实现步骤:
页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
前端页面发送的ajax请求:http://localhost:8081/category/list?type=2
5.2 新增菜品时已经完成该部分功能
页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
前端页面发送的ajax请求:http://localhost:8081/category/list?type=1
5.2 新增菜品时已经完成该部分功能
页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
前端页面发送的ajax请求:http://localhost:8081/dish/list?categoryId=1397844263642378242
服务端处理请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*
* 根据条件查询对应菜品数据
* @author linsuwen
* @date 2022/12/7 16:49
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}页面发送请求进行图片上传,请求服务端将图片保存到服务器
5.1 文件上传下载时已经完成该部分功能
页面发送请求进行图片下载,将上传的图片进行回显
5.1 文件上传下载时已经完成该部分功能
点击保存按钮,发送ajax请求,将套餐相关数据以json显示提交到服务端
1
2
3
4
5
6
7
8
9
10
11/*
* 新增套餐
* @author linsuwen
* @date 2022/12/7 18:49
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto); //底层逻辑代码封装在了Service层
return R.success("新增套餐成功!");
}
6.2 套餐信息分页查询
套餐分页查询时前端页面和服务端的交互过程:
- 页面发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务端,获取分页数据
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的两次请求即可。
套餐信息分页查询:
1 |
|
6.3 删除套餐
删除套餐时前端页面和服务端的交互过程:
- 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
- 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应 套餐
开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递id的个数,所以在服务端可以提供一个方法来统一处理。
删除套餐:
删除套餐时要同时操作数据库中两张表的delete操作,底层逻辑代码封装在了service层
1 |
|
7. 手机验证码登录
7.1 短信发送
2022/12/8
7.2 手机验证码登录
登录时前端页面和服务端的交互过程:
- 在登录页面(front/page/login.html)输入手机号,点击 “获取验证码” 按钮发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
- 在登录页面输入验证码,点击 “登录” 按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
8. 菜品展示购物车下单
8.1 导入用户地址簿
2022/12/8
地址簿,指的是移动端消费用户的地址信息,用户登录成功后可以维护自己的地址信息。同时一个用户可以有多个地址信息,但是只能有一个地址信息。
8.2 菜品展示
菜品展示时前端页面和服务端的交互过程:
页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
页面发送ajax请求,获取第一个分类下的菜品或者套餐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38//为了前端页面可以显示菜品规格和口味信息,对改方法进行改进
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId(); //分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
List<DishFlavor> dishFlavors = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavors);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这两次请求即可。
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,当两次请求全部响应成功后前端页面才会进行渲染并加载显示。
8.3 购物车
2022/12/9
购物车操作时前端页面和服务端的交互过程:
- 点击 “加入购物车” 或者 “+” 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐加入到购物车
- 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
- 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。
实现步骤:
点击 “加入购物车” 或者 “+” 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐加入到购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/*
* 添加购物车
* @author linsuwen
* @date 2022/12/9 21:01
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setId(currentId);
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
if(dishId != null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else{
//添加到购物车的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if(cartServiceOne != null){
//如果已经存在,就在原来数量的基础上加1
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number+1);
shoppingCartService.updateById(cartServiceOne);
}else{
//如果不存在则添加到购物车,数量默认就是1
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/*
* 查看购物车
* @author linsuwen
* @date 2022/12/11 21:23
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查询购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
1
2
3
4
5
6
7
8
9
10
11
12/*
* 清空购物车
* @author linsuwen
* @date 2022/12/11 22:06
*/
@DeleteMapping("/clean")
public R<String> clean(){
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功!");
}
8.4 下单
用户下单操作时前端页面和服务端的交互过程:
- 在购物车中点击 “去结算” 按钮,页面跳转到订单确认页面
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
- 在订单确认页面点击 “去支付” 按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。