瑞吉外卖
项目源码地址:https://gitee.com/sjd75/takeaway
1. 准备阶段
角色
- 后台系统管理员:登录后台管理系统,拥有后台管理系统中的所有操作权限。
- 后台系统普通员工:登录后台管理系统,仅能对菜品、套餐、订单等模块进行操作。
- C 端用户:登录移动端应用,浏览菜品、添加菜品到购物车、设置地址、在线下单等。
1.1 数据库准备
员工表 employee
-- 员工信息
employee:
+ columns
id: bigint NN
-- 主键(USING BTREE)
name: varchar(32) NN
-- 姓名
username: varchar(32) NN
-- 用户名(UNIQUE KEY)
password: varchar(64) NN
-- 密码
phone: varchar(11) NN
-- 手机号
sex: varchar(2) NN
-- 性别
id_number: varchar(18) NN
-- 身份证号
status: int NN default 1
-- 状态(0 禁用,1 正常)
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
菜品及套餐分类表 category
-- 菜品及套餐分类
category:
+ columns
id: bigint NN
-- 主键(USING BTREE)
type: int
-- 类型(1 菜品分类,2 套餐分类)
name: varchar(64) NN
-- 分类名称(UNIQUE KEY)
sort: int NN default 0
-- 顺序
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
菜品表 dish
-- 菜品管理
dish:
+ columns
id: bigint NN
-- 主键(USING BTREE)
name: varchar(64) NN
-- 菜品名称(UNIQUE KEY)
category_id: bigint NN
-- 菜品分类id
price: decimal(10,2)
-- 菜品价格
code: varchar(64) NN
-- 商品码
image: varchar(200) NN
-- 图片
description: varchar(400)
-- 描述信息
status: int NN default 1
-- 0 停售 1 起售
sort: int NN default 0
-- 顺序
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
is_deleted: int NN default 0
-- 是否删除
套餐表 setmeal
-- 套餐
setmeal:
+ columns
id: bigint NN
-- 主键(USING BTREE)
category_id: bigint NN
-- 菜品分类id
name: varchar(64) NN
-- 套餐名称(UNIQUE KEY)
price: decimal(10,2) NN
-- 套餐价格
status: int
-- 状态 0:停用 1:启用
code: varchar(32)
-- 编码
description: varchar(512)
-- 描述信息
image: varchar(255)
-- 图片
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
is_deleted: int NN default 0
-- 是否删除
套餐菜品关系表 setmeal_dish
-- 套餐菜品关系
setmeal_dish:
+ columns
id: bigint NN
-- 主键(USING BTREE)
setmeal_id: varchar(32) NN
-- 套餐id
dish_id: varchar(32) NN
-- 菜品id
name: varchar(32)
-- 菜品名称 (冗余字段)
price: decimal(10,2)
-- 菜品原价(冗余字段)
copies: int NN
-- 份数
sort: int NN default 0
-- 排序
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
is_deleted: int NN default 0
-- 是否删除
菜品口味关系表 dish_flavor
-- 菜品口味关系表
dish_flavor:
+ columns
id: bigint NN
-- 主键(USING BTREE)
dish_id: bigint NN
-- 菜品
name: varchar(64) NN
-- 口味名称
value: varchar(500)
-- 口味数据list
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
is_deleted: int NN default 0
-- 是否删除
C 端用户表 user
-- 用户信息
user:
+ columns
id: bigint NN
-- 主键(USING BTREE)
name: varchar(50)
-- 姓名
phone: varchar(100) NN
-- 手机号
sex: varchar(2)
-- 性别
id_number: varchar(18)
-- 身份证号
avatar: varchar(500)
-- 头像
status: int default 0
-- 状态 0:禁用,1:正常
地址簿表 address_book
-- 地址管理
address_book:
+ columns
id: bigint NN
-- 主键(USING BTREE)
user_id: bigint NN
-- 用户id
consignee: varchar(50) NN
-- 收货人
sex: tinyint NN
-- 性别 0 女 1 男
phone: varchar(11) NN
-- 手机号
province_code: varchar(12) collate utf8mb4_0900_ai_ci
-- 省级区划编号
province_name: varchar(32) collate utf8mb4_0900_ai_ci
-- 省级名称
city_code: varchar(12) collate utf8mb4_0900_ai_ci
-- 市级区划编号
city_name: varchar(32) collate utf8mb4_0900_ai_ci
-- 市级名称
district_code: varchar(12) collate utf8mb4_0900_ai_ci
-- 区级区划编号
district_name: varchar(32) collate utf8mb4_0900_ai_ci
-- 区级名称
detail: varchar(200) collate utf8mb4_0900_ai_ci
-- 详细地址
label: varchar(100) collate utf8mb4_0900_ai_ci
-- 标签
is_default: tinyint(1) NN default 0
-- 默认 0 否 1是
create_time: datetime NN
-- 创建时间
update_time: datetime NN
-- 更新时间
create_user: bigint NN
-- 创建人
update_user: bigint NN
-- 修改人
is_deleted: int NN default 0
-- 是否删除
购物车表 shopping_cart
-- 购物车
shopping_cart:
+ columns
id: bigint NN
-- 主键(USING BTREE)
name: varchar(50)
-- 名称
image: varchar(100)
-- 图片
user_id: bigint NN
-- 主键
dish_id: bigint
-- 菜品id
setmeal_id: bigint
-- 套餐id
dish_flavor: varchar(50)
-- 口味
number: int NN default 1
-- 数量
amount: decimal(10,2) NN
-- 金额
create_time: datetime
-- 创建时间
订单表 order
-- 订单表
orders:
+ columns
id: bigint NN
-- 主键(USING BTREE)
number: varchar(50)
-- 订单号
status: int NN default 1
-- 订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
user_id: bigint NN
-- 下单用户
address_book_id: bigint NN
-- 地址id
order_time: datetime NN
-- 下单时间
checkout_time: datetime NN
-- 结账时间
pay_method: int NN default 1
-- 支付方式 1微信,2支付宝
amount: decimal(10,2) NN
-- 实收金额
remark: varchar(100)
-- 备注
phone: varchar(255)
address: varchar(255)
user_name: varchar(255)
consignee: varchar(255)
订单明细表 order_detail
-- 订单明细表
order_detail:
+ columns
id: bigint NN
-- 主键(USING BTREE)
name: varchar(50)
-- 名字
image: varchar(100)
-- 图片
order_id: bigint NN
-- 订单id
dish_id: bigint
-- 菜品id
setmeal_id: bigint
-- 套餐id
dish_flavor: varchar(50)
-- 口味
number: int NN default 1
-- 数量
amount: decimal(10,2) NN
-- 金额
1.2 POM 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>com.sun.takeaway</groupId>
<artifactId>takeaway</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!-- Tools -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
1.3 application.yml
server:
port: 8080
spring:
application:
name: takeaway
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/takeaway?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
logic-delete-field: is_delete
logic-delete-value: 1
logic-not-delete-value: 0
1.4 前端项目导入
将 backend、frontend 导入到 Maven 项目中的 resource
目录下,并设置静态资源映射。
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 设置静态资源映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 访问 /backend、/frontend 开头的任意路径,都会被映射到类路径下的 backend 和 frontend 目录
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
1.5 COMMON
统一返回
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
@Data
public class CommonResult<T> implements Serializable {
private int code;
private T data;
private String message;
public CommonResult(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(1, data, "ok");
}
public static <T> CommonResult<T> error(ErrorCode errorCode) {
return new CommonResult<>(errorCode.getCode(), null, errorCode.getMessage());
}
public static <T> CommonResult<T> error(int code, String message) {
return new CommonResult<>(code, null, message);
}
public static <T> CommonResult<T> error(ErrorCode errorCode, String message) {
return new CommonResult<>(errorCode.getCode(), null, message);
}
}
EXCEPTION
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(String message, int code) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public CommonResult<?> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return CommonResult.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public CommonResult<?> runtimeExceptionHandler(RuntimeException e) {
log.error("RuntimeException", e);
return CommonResult.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}
public class ThrowUtils {
public static void throwIf(boolean condition, RuntimeException runtimeException) {
if (condition) {
throw runtimeException;
}
}
public static void throwIf(boolean condition, ErrorCode errorCode) {
throwIf(condition, new BusinessException(errorCode));
}
public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
throwIf(condition, new BusinessException(errorCode, message));
}
}
CONSTANT
public interface LOGINConstant {
/**
* 员工登录态的 Key
*/
String EMPLOYEE_LOGIN_STATE = "employee_login";
/**
* 用户登录态的 Key
*/
String USER_LOGIN_STATE = "user_login";
}
public interface CommonConstant {
/**
* 可用
*/
int AVAILABLE = 1;
/**
* 禁用
*/
int BAN = 0;
/**
* 1
*/
int ONE = 1;
/**
* 0
*/
int ZERO = 0;
}
MODEL
@Data
public class EmployeeLoginRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
}
@Data
public class LoginEmployeeVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
// 用户名(Unique)
private String username;
// 姓名
private String name;
// 手机号
private String phone;
// 性别
private String sex;
// 身份证号
private String idNumber;
// 状态 (0 禁用,1 正常)
private Integer status;
// 创建时间
private LocalDateTime createTime;
// 更新时间
private LocalDateTime updateTime;
// 创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
// 修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
1.6 新增消息转换器
/**
* 对象映射器:基于 Jackson 将 Java 对象序列化为 JSON,或者将 JSON 反序列化为 Java 对象
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
// 收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
// 反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
// 注册功能模块(可以添加自定义序列化器和反序列化器)
this.registerModule(simpleModule);
}
}
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 设置静态资源映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 访问 /backend、/frontend 开头的任意路径,都会被映射到类路径下的 backend 和 frontend 目录
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 添加自定义的消息转换器
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 设置消息转换器
converter.setObjectMapper(new JacksonObjectMapper());
//