苍穹外卖
最适合新手入门的SpringBoot+SSM企业级实战项目
SSM 是 Java 后端入门的核心技术栈,指 Spring + SpringMVC + MyBatis,是传统企业开发的主流组合
Spring Boot 是对 SSM 的简化和增强(自动配置、快速启动),是现在企业开发的主流框架
开发方式:前后端分离;用户端实现微信小程序;服务端由有状态升级为无状态
基础数据模块、点餐业务模块、统计报表模块
项目效果展示

软件开发整体介绍
软件开发流程
需求分析:需求规格说明书(word文档)、产品原型(静态网页)
设计:UI设计(人工交互页面)、数据库设计(表)、接口设计(登录功能等)
编码:项目代码、单元测试(测试自己写的代码)
测试:测试用例、测试报告
上线运维:软件环境安装、配置
角色分工
项目经理:对整个项目负责
产品经理:需求调研,产品原型
UI设计师:界面效果图
架构师:整体架构设计,技术选型
开发工程师:代码实现(我们的工作)
测试工程师:测试用例
运维工程师:环境搭建、项目上线
软件环境
开发环境:开发人员在开发阶段使用的环境,本地环境
测试环境:给测试人员使用的环境,测试服务器
生产环境:线上环境,正式提供对外服务的环境
苍穹外卖项目介绍
项目介绍
定位:专门为餐饮企业定制的一款软件产品(管理端:外卖商家使用;用户端:点餐用户使用)
功能架构:业务功能模块(管理端:员工管理、分类管理、菜品管理、套餐管理、订单管理、工作台、数据统计、来单提醒;用户端:微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单)
产品原型
产品原型:展示项目的业务功能(html文件)

管理端:

用户端:

技术选型
技术选型:技术框架和中间件

开发环境搭建
前后端分离
前端环境搭建
整体结构:
前端:管理端(Web),用户端(小程序)
后端:后端服务(Java)
前端工程基于nginx运行,发好的前端代码部署到 Nginx 服务器上,由 Nginx 来负责接收用户的请求、处理并返回这些前端资源。前端开发完成后,会通过打包工具生成静态资源包,这些文件本身不能直接被用户访问,需要一个 "中间人" 来处理网络请求 ------Nginx 就是这个核心的 "中间人"


前端工程打包之后的效果
注:Nginx目录必须放在没有中文的目录中才能正常运行
双击nginx.exe文件即可运行:


后端环境搭建
熟悉项目结构
后端工程基于maven进行项目构建,并且进行分模块开发,Maven 是 Java 后端领域最常用的项目构建和依赖管理工具,用 Maven 这个工具来管理后端项目的依赖、编译、打包等全流程,同时把一个大的后端项目拆分成多个功能独立的小模块





做项目时是在有一部分基础代码上,去开发业务工程
使用Git进行版本控制
git是通过仓库来管理代码的,分为本地仓库和远程仓库
IntelliJ IDEA 是一款广受欢迎的集成开发环境(IDE),专为Java编程语言设计,但也支持其他语言,它由JetBrains公司开发,Community Edition(社区版):作为一个开源项目,社区版是免费的,但功能相对有限,主要支持Java和Kotlin开发
gitignore文件:
**/target/
.idea
*.iml
*.class
*Test.java
**/test/
Git 哪些文件 / 文件夹不需要被追踪(即不上传到代码仓库)

- 创建Git本地仓库:VCS版本控制系统

创建Git仓库:sky-take-out

提交到本地仓库:

- 创建Git远程仓库:

- 创建一个远程的仓库
- 将本地文件推送到Git远程仓库


数据库环境搭建
通过数据库建表语句创建数据库表结构:


导入sky.sql文件:

前后端联调
后端的初始工程已经实现了登录功能,直接进行前后端联调测试即可

- 浏览器 → Controller(控制层):浏览器发起请求后,Controller 作为入口层,主要做三件事:
- 接收并封装参数:获取前端传来的用户名、密码等登录数据。
- 调用 Service 方法:把业务逻辑的处理交给 Service 层,自己不直接操作数据库。
- 封装结果并响应:拿到 Service 返回的结果后,整理成前端能识别的格式(如 JSON)并返回。
- Controller → Service(业务层):Service 是核心业务逻辑层,负责处理具体业务规则:
- 调用 Mapper 查询数据库:根据用户名去数据库查询用户信息。
- 密码比对:将前端传来的密码和数据库中查询到的加密密码进行校验。
- 返回结果:把校验结果(登录成功 / 失败)返回给 Controller。
- Service → Mapper(数据访问层):Mapper 是直接和数据库交互的层:
- 执行 SQL 语句:select * from employee where username = ?
- 把查询结果封装成 Java 对象,返回给 Service 层。
- Mapper → 数据库:数据库执行 SQL 查询,返回匹配的员工数据,再依次回传给 Mapper、Service、Controller,最后由 Controller 响应给浏览器。
maven中编译:

下载jdk:

启动:

SkyApplication:启动类

// Spring Boot 项目的启动入口类,运行这个类就能启动整个后端程序
package com .sky ;
// 包声明:给这个类归个文件夹,避免和其他类重名;com.sky 是项目的根包名,所有代码都放在这个包下
import lombok .extern .slf4j .Slf4j ;
// 导入日志工具,用来打印运行信息
import org .springframework .boot .SpringApplication ;
import org .springframework .boot .autoconfigure .SpringBootApplication ;
// Spring Boot 的核心工具,负责启动项目、自动配置
import org .springframework .transaction .annotation .EnableTransactionManagement ;
// 用来开启数据库事务
@SpringBootApplication
// Spring Boot 的总注解,三合一功能:自动扫描项目中的所有组件;自动配置 Spring 环境;排除不需要的配置.加了这个注解,项目才能正常启动
@EnableTransactionManagement
//开启注解方式的事务管理:操作数据库时,如果出错,数据会自动回滚,不会乱掉
@Slf4j
// 日志注解,加了之后可以直接用 log.info() 打印日志
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
// main 方法:Java 程序的固定入口,运行代码必须从这里开始
// SpringApplication.run(...):真正启动 Spring Boot 服务的代码
// log.info("server started");:打印日志:服务已启动
启动前端:
双击nginx.exe,或在 nginx-1.20.2 文件夹的地址栏输入 cmd,回车打开命令提示符,输入
启动命令
start nginx
验证是否启动成功
tasklist | findstr nginx

打开浏览器,输入地址,访问前端页面

admin 123456
MySQL 数据库插入语句:
往员工表(employee)里添加一条管理员数据
INSERT INTO `employee` VALUES (1,'管理员','admin','123456','13812312312','1','110101199001010047',1,'2022-02-15 15:51:20','2022-02-17 09:16:20',10,1);
select * from sky_take_out.employee;

application-dev.yml文件:
sky:
datasource:
driver-class -name: com.mysql.cj.jdbc.Driver
host: localhost
// 数据库在本机
port: 3306
// MySQL固定端口
database: sky_take_out
username: root
password: 123456
// 数据库名字、用户名和密码

断点调试跟踪代码:debug(小虫子)
controller.admin文件中的EmployeeController.java文件
// 后端登录的控制器
// 作用:专门处理管理端员工的登录和退出:接收前端传来的账号密码;调用 Service 层校验登录;登录成功 → 生成 JWT 令牌(登录凭证);返回给前端:员工信息 + token;提供退出接口
/**
* 员工管理
*/
@RestController
// 声明这是接口控制器,返回 JSON 数据
@RequestMapping("/admin/employee")
// 接口前缀,所有接口都以这个开头
@Slf4j
// 可以用 log.info() 打印日志,方便排查问题
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// 员工业务逻辑(校验账号密码)
@Autowired
private JwtProperties jwtProperties;
// JWT配置(密钥、过期时间)
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
// 打印前端传过来的登录信息
Employee employee = employeeService.login(employeeLoginDTO);
// 调用Service,校验用户名密码,返回员工对象
//登录成功后,生成jwt令牌(前端以后靠这个证明已登录)
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); // 把员工ID放进token
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(), // 密钥
jwtProperties.getAdminTtl(), // 过期时间
claims); // 存放的数据
// 封装要返回给前端的数据
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token) // 把token返回给前端
.build();
// 统一返回格式:成功 + 数据
return Result.success(employeeLoginVO);
}
/**
* 退出
*
* @return
*/
// 退出只需要前端删除 token 就行,后端直接返回成功即可
@PostMapping("/logout")
public Result<String> logout() {
return Result.success();
}
}
单步调试:Fn+F8
接收前端传过来的登录信息:

调用Service,校验用户名密码,返回员工对象:
ctrl+鼠标:接口:规定登录规则
// Employee employee = employeeService.login(employeeLoginDTO);
// EmployeeService 接口:传入:前端给的账号密码(EmployeeLoginDTO);返回:查询到的员工对象(Employee);给我账号密码,我给你返回登录成功的员工信息
public interface EmployeeService {
/**
* 员工登录
* @param employeeLoginDTO
* @return
*/
Employee login(EmployeeLoginDTO employeeLoginDTO);
}
ctrl+alt+鼠标:实现类:真正实现登录
// Employee employee = employeeService.login(employeeLoginDTO);
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
/**
* 员工登录
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
// 拿到前端传的账号、密码
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
// 根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
// 处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null ) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
// 密码比对
// TODO 后期需要进行md5加密,然后再进行比对
if (!password.equals(employee.getPassword())) {
// 密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
// 账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
// 全部正确 → 返回员工信息
return employee;
}
}
// EmployeeMapper.java:根据用户名来查询员工信息
// 使用sql语句的方法
@Mapper
public interface EmployeeMapper {
/**
* 根据用户名查询员工
* @param username
* @return
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
}
// AccountNotFoundException继承BaseException(全局异常)
/**
* 账号不存在异常
*/
public class AccountNotFoundException extends BaseException {
public AccountNotFoundException() {
}
public AccountNotFoundException(String msg) {
super (msg);
}
}
// PasswordErrorException继承BaseException(全局异常)
/**
* 密码错误异常
*/
public class PasswordErrorException extends BaseException {
public PasswordErrorException() {
}
public PasswordErrorException(String msg) {
super (msg);
}
}
//sky-common的exception的BaseException(全局异常)
/**
* 业务异常
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super (msg);
}
}
// handler的GlobalExceptionHandler.java文件
// 全局异常处理器,捕获项目中抛出的业务异常,给前端页面返回异常结果
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
// StatusConstant.java文件
/**
* 状态常量,启用或者禁用
*/
public class StatusConstant {
//启用
public static final Integer ENABLE = 1;
//禁用
public static final Integer DISABLE = 0;
}
登录成功后,生成jwt令牌(前端以后靠这个证明已登录)
//登录成功后,生成jwt令牌(前端以后靠这个证明已登录)
Map<String, Object> claims = new HashMap<>(); // 创建一个空盒子,用来存放要放进 JWT 里的数据
claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); // 把员工ID放进token
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(), // 密钥
jwtProperties.getAdminTtl(), // 过期时间
claims); // 存放的数据
// 封装要返回给前端的数据
// Builder:建造器,用来快速创建对象
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token) // 把token返回给前端
.build();
// JWT Token 里的自定义字段(Claims)名称
public class JwtClaimsConstant {
public static final String EMP_ID = "empId";
public static final String USER_ID = "userId";
public static final String PHONE = "phone";
public static final String USERNAME = "username";
public static final String NAME = "name";
}
// sky-common的properties的JwtProperties
// JWT 配置类,专门用来从配置文件(application.yml文件)里读取 JWT 相关的配置信息,然后封装成一个 Java 对象
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成 jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成 jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
// 配置文件:resources的application.yml文件
sky:
jwt:
设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
设置jwt过期时间
admin-ttl: 7200000
设置前端传递过来的令牌名称
admin-token-name: token
// 给前端返回登录成功数据的模板类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {
@ApiModelProperty("主键值")
private Long id;
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("jwt令牌")
private String token;
}
前端发送的请求,是如何请求到后端服务的?
Ctrl+Shift+i:打开控制面板
前端:

后端:
// EmployeeController.java文件
@RequestMapping("/admin/employee")
@PostMapping("/login")
// application.yml文件
server:
port: 8080








nginx.conf文件
用户访问前端页面时,Nginx 自动把 /user/ 开头的请求,转发给后端 Java 服务(127.0.0.1:8080)
upstream webservers{
server 127.0.0.1:8080 weight=90 ;
}
反向代理,处理用户端发送的请求
location /user/ {
proxy_pass http://webservers/user/;
}
完善登录功能


MD5加密方式:(不可逆)



修改数据库密码:

提交即可
修改java代码:
todo面板:View → Tool Windows → TODO 打开
//进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
导入接口文档
开发方式基于前后端分离的开发方式,所以需要将接口定义后,为后面的业务开发做好准备
接口的设计其实需要长期讨论
前后端分离开发流程

操作步骤

管理端接口和用户端接口

两个json文件,所以两个项目

Swagger
帮助后端生成接口文档,并且进行在线接口测试
介绍

使用方式



// sky-server的config
// 通过knife4j生成接口文档
@Bean
public Docket docket(){
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
//指定生成接口需要扫描的包
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
// 设置静态资源映射
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
访问地址:http://localhost:8080/doc.html

// 登入接口和登出接口
/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
/**
* 退出
*
* @return
*/
@PostMapping("/logout")
public Result<String> logout() {
return Result.success();
}
}


测试:


常用注解

