1. 蒼穹外賣項目介紹 1.1 項目介紹 1)管理端功能 員工登錄/退出 , 員工信息管理 , 分類管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 訂單管理 ,數據統計,來單提醒。 2)用戶端功能 微信登錄 , 收件人地址管理 , 用戶歷史訂單查詢 , 菜品規格查詢 , 購物車功能 , 下單 ...
1. 蒼穹外賣項目介紹
1.1 項目介紹
1)管理端功能
員工登錄/退出 , 員工信息管理 , 分類管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 訂單管理 ,數據統計,來單提醒。
2)用戶端功能
微信登錄 , 收件人地址管理 , 用戶歷史訂單查詢 , 菜品規格查詢 , 購物車功能 , 下單 , 支付、分類及菜品瀏覽。
1.2 產品原型
1)管理端
餐飲企業內部員工使用。 主要功能有:
模塊 | 描述 |
---|---|
登錄/退出 | 內部員工必須登錄後,才可以訪問系統管理後臺 |
員工管理 | 管理員可以在系統後臺對員工信息進行管理,包含查詢、新增、編輯、禁用等功能 |
分類管理 | 主要對當前餐廳經營的 菜品分類 或 套餐分類 進行管理維護, 包含查詢、新增、修改、刪除等功能 |
菜品管理 | 主要維護各個分類下的菜品信息,包含查詢、新增、修改、刪除、啟售、停售等功能 |
套餐管理 | 主要維護當前餐廳中的套餐信息,包含查詢、新增、修改、刪除、啟售、停售等功能 |
訂單管理 | 主要維護用戶在移動端下的訂單信息,包含查詢、取消、派送、完成,以及訂單報表下載等功能 |
數據統計 | 主要完成對餐廳的各類數據統計,如營業額、用戶數量、訂單等 |
2)用戶端
移動端應用主要提供給消費者使用。主要功能有:
模塊 | 描述 |
---|---|
登錄/退出 | 用戶需要通過微信授權後登錄使用小程式進行點餐 |
點餐-菜單 | 在點餐界面需要展示出菜品分類/套餐分類, 並根據當前選擇的分類載入其中的菜品信息, 供用戶查詢選擇 |
點餐-購物車 | 用戶選中的菜品就會加入用戶的購物車, 主要包含 查詢購物車、加入購物車、刪除購物車、清空購物車等功能 |
訂單支付 | 用戶選完菜品/套餐後, 可以對購物車菜品進行結算支付, 這時就需要進行訂單的支付 |
個人信息 | 在個人中心頁面中會展示當前用戶的基本信息, 用戶可以管理收貨地址, 也可以查詢歷史訂單數據 |
1.3 技術選型
關於本項目的技術選型, 我們將會從 用戶層、網關層、應用層、數據層 這幾個方面進行介紹,主要用於展示項目中使用到的技術框架和中間件等。
- 用戶層 node.js VUE.js ElementUI 微信小程式 apache echarts
- 網關層 Nginx
- 應用層 SpringBoot SpringMVC SpringTask SpringCache JWT 阿裡雲OSS httpclient Swagger POI WebSocket
- 數據層 MySQL Redis mybatis
- 工具 Git maven Junit postman pagehelper springdataredis
1)用戶層
後臺的前端頁面用到H5、Vue.js、ElementUI、apache echarts(展示圖表)等技術。移動端應用使用到微信小程式。
2)網關層
Nginx是一個伺服器,主要用來作為Http伺服器,部署靜態資源,訪問性能高。在Nginx中還有兩個比較重要的作用: 反向代理和負載均衡, 在進行項目部署時,要實現Tomcat的負載均衡,就可以通過Nginx來實現。
3)應用層
- SpringBoot: 快速構建Spring項目, 採用 "約定優於配置" 的思想, 簡化Spring項目的配置開發。
- SpringMVC:SpringMVC是spring框架的一個模塊,springmvc和spring無需通過中間整合層進行整合,可以無縫集成。
- Spring Task: 由Spring提供的定時任務框架。
- httpclient: 主要實現了對http請求的發送。
- Spring Cache: 由Spring提供的數據緩存框架
- JWT: 用於對應用程式上的用戶進行身份驗證的標記。
- 阿裡雲OSS: 對象存儲服務,在項目中主要存儲文件,如圖片等。
- Swagger: 可以自動的幫助開發人員生成介面文檔,並對介面進行測試。
- POI: 封裝了對Excel表格的常用操作。
- WebSocket: 一種通信網路協議,使客戶端和伺服器之間的數據交換更加簡單,用於項目的來單、催單功能實現。
4)數據層
- MySQL: 關係型資料庫, 本項目的核心業務數據都會採用MySQL進行存儲。
- Redis: 基於key-value格式存儲的記憶體資料庫, 訪問速度快, 經常使用它做緩存。
- Mybatis: 本項目持久層將會使用Mybatis開發。
- pagehelper: 分頁插件。
- spring data redis: 簡化java代碼操作Redis的API。
5)工具
- git: 版本控制工具, 在團隊協作中, 使用該工具對項目中的代碼進行管理。
- maven: 項目構建工具。
- junit:單元測試工具,開發人員功能實現完畢後,需要通過junit對功能進行單元測試。
- postman: 介面測工具,模擬用戶發起的各類HTTP請求,獲取對應的響應結果。
2. 開發環境搭建
2.1 熟悉項目結構
對工程的每個模塊作用說明:
序號 | 名稱 | 說明 |
---|---|---|
1 | sky-take-out | maven父工程,統一管理依賴版本,聚合其他子模塊 |
2 | sky-common | 子模塊,存放公共類,例如:工具類、常量類、異常類等 |
3 | sky-pojo | 子模塊,存放實體類、VO、DTO等 |
4 | sky-server | 子模塊,後端服務,存放配置文件、Controller、Service、Mapper等 |
-
sky-common: 模塊中存放的是一些公共類,可以供其他模塊使用
分析sky-common模塊的每個包的作用:
名稱 說明 constant 存放相關常量類 context 存放上下文類 enumeration 項目的枚舉類存儲 exception 存放自定義異常類 json 處理json轉換的類 properties 存放SpringBoot相關的配置屬性類 result 返回結果類的封裝 utils 常用工具類 -
sky-pojo: 模塊中存放的是一些 entity、DTO、VO
分析sky-pojo模塊的每個包的作用:
名稱 說明 Entity 實體,通常和資料庫中的表對應 DTO 數據傳輸對象,通常用於程式中各層之間傳遞數據 VO 視圖對象,為前端展示數據提供的對象 POJO 普通Java對象,只有屬性和對應的getter和setter -
sky-server: 模塊中存放的是 配置文件、配置類、攔截器、controller、service、mapper、啟動類等
分析sky-server模塊的每個包的作用:
名稱 說明 config 存放配置類 controller 存放controller類 interceptor 存放攔截器類 mapper 存放mapper介面 service 存放service類 SkyApplication 啟動類
2.2 資料庫環境搭建
序號 | 表名 | 中文名 |
---|---|---|
1 | employee | 員工表 |
2 | category | 分類表 |
3 | dish | 菜品表 |
4 | dish_flavor | 菜品口味表 |
5 | setmeal | 套餐表 |
6 | setmeal_dish | 套餐菜品關係表 |
7 | user | 用戶表 |
8 | address_book | 地址表 |
9 | shopping_cart | 購物車表 |
10 | orders | 訂單表 |
11 | order_detail | 訂單明細表 |
2.3 前後端聯調
1.Controller層
在sky-server模塊中,com.sky.controller.admin.EmployeeController
/**
* 登錄
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("員工登錄:{}", employeeLoginDTO);
//調用service方法查詢資料庫
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);
}
2.Service層
在sky-server模塊中,com.sky.service.impl.EmployeeServiceImpl
/**
* 員工登錄
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根據用戶名查詢資料庫中的數據
Employee employee = employeeMapper.getByUsername(username);
//2、處理各種異常情況(用戶名不存在、密碼不對、賬號被鎖定)
if (employee == null) {
//賬號不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密碼比對
if (!password.equals(employee.getPassword())) {
//密碼錯誤
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//賬號被鎖定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回實體對象
return employee;
}
3.Mapper層
在sky-server模塊中,com.sky.mapper.EmployeeMapper
package com.sky.mapper;
import com.sky.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface EmployeeMapper {
/**
* 根據用戶名查詢員工
* @param username
* @return
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
}
註:可以通過斷點調試跟蹤後端程式的執行過程
2.4 nginx反向代理和負載均衡
對登錄功能測試完畢後,接下來一個問題:前端發送的請求,是如何請求到後端服務的?
前端請求地址:http://localhost/api/employee/login
後端介面地址:http://localhost:8080/admin/employee/login
很明顯,兩個地址不一致,那是如何請求到後端服務的呢?
1)nginx反向代理
nginx 反向代理,就是將前端發送的動態請求由 nginx 轉發到後端伺服器
那為什麼不直接通過瀏覽器直接請求後臺服務端,需要通過nginx反向代理呢?
nginx 反向代理的好處:
-
提高訪問速度
因為nginx本身可以進行緩存,如果訪問的同一介面,並且做了數據緩存,nginx就直接可把數據返回,不需要真正地訪問服務端,從而提高訪問速度。 -
進行負載均衡
所謂負載均衡,就是把大量的請求按照我們指定的方式均衡的分配給集群中的每台伺服器。 -
保證後端服務安全
因為一般後臺服務地址不會暴露,所以使用瀏覽器不能直接訪問,可以把nginx作為請求訪問的入口,請求到達nginx後轉發到具體的服務中,從而保證後端服務的安全。
nginx 反向代理的配置方式:
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://localhost:8080/admin/; #反向代理
}
}
proxy_pass:該指令是用來設置代理伺服器的地址,可以是主機名稱,IP地址加埠號等形式。
如上代碼的含義是:監聽80埠號, 然後當我們訪問 http://localhost:80/api/../..這樣的介面的時候,它會通過 location /api/ {} 這樣的反向代理到 http://localhost:8080/admin/上來。
接下來,進到nginx-1.20.2\conf,打開nginx配置
# 反向代理,處理管理端發送的請求
location /api/ {
proxy_pass http://localhost:8080/admin/;
#proxy_pass http://webservers/admin/;
}
當在訪問http://localhost/api/employee/login,nginx接收到請求後轉到http://localhost:8080/admin/,故最終的請求地址為http://localhost:8080/admin/employee/login,和後臺服務的訪問地址一致
2)nginx 負載均衡
當如果服務以集群的方式進行部署時,那nginx在轉發請求到伺服器時就需要做相應的負載均衡。其實,負載均衡從本質上來說也是基於反向代理來實現的,最終都是轉發請求。
nginx 負載均衡的配置方式:
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://webservers/admin;#負載均衡
}
}
upstream:如果代理伺服器是一組伺服器的話,我們可以使用upstream指令配置後端伺服器組。
如上代碼的含義是:監聽80埠號, 然後當我們訪問 http://localhost:80/api/../..這樣的介面的時候,它會通過 location /api/ {} 這樣的反向代理到 http://webservers/admin,根據webservers名稱找到一組伺服器,根據設置的負載均衡策略(預設是輪詢)轉發到具體的伺服器。
註:upstream後面的名稱可自定義,但要上下保持一致。
nginx 負載均衡策略:
名稱 | 說明 |
---|---|
輪詢 | 預設方式 |
weight | 權重方式,預設為1,權重越高,被分配的客戶端請求就越多 |
ip_hash | 依據ip分配方式,這樣每個訪客可以固定訪問一個後端服務 |
least_conn | 依據最少連接方式,把請求優先分配給連接數少的後端服務 |
url_hash | 依據url分配方式,這樣相同的url會被分配到同一個後端服務 |
fair | 依據響應時間方式,響應時間短的服務將會被優先分配 |
具體配置方式:
輪詢:
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
weight:
upstream webservers{
server 192.168.100.128:8080 weight=90;
server 192.168.100.129:8080 weight=10;
}
ip_hash:
upstream webservers{
ip_hash;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
least_conn:
upstream webservers{
least_conn;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
url_hash:
upstream webservers{
hash &request_uri;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
fair:
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
fair;
}
3. 完善登錄功能
問題:員工表中的密碼是明文存儲,安全性太低。
解決思路:
- 將密碼加密後存儲,提高安全性
- 使用MD5加密方式對明文密碼加密
實現步驟:
-
修改資料庫中明文密碼,改為MD5加密後的密文
打開employee表,修改密碼 -
修改Java代碼,前端提交的密碼進行MD5加密後再跟資料庫中密碼比對
打開EmployeeServiceImpl.java,修改比對密碼
/**
* 員工登錄
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
//1、根據用戶名查詢資料庫中的數據
//2、處理各種異常情況(用戶名不存在、密碼不對、賬號被鎖定)
//.......
//密碼比對
// TODO 後期需要進行md5加密,然後再進行比對
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密碼錯誤
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
//3、返回實體對象
return employee;
}
4. Swagger
4.1 介紹
Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務(https://swagger.io/)。 它的主要作用是:
- 使得前後端分離開發更加方便,有利於團隊協作
- 介面的文檔線上自動生成,降低後端開發人員編寫介面文檔的負擔
- 功能測試
Spring已經將Swagger納入自身的標準,建立了Spring-swagger項目,現在叫Springfox。通過在項目中引入Springfox ,即可非常簡單快捷的使用Swagger。
knife4j是為Java MVC框架集成Swagger生成Api文檔的增強解決方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一樣小巧,輕量,並且功能強悍!
目前,一般都使用knife4j框架。
4.2 使用步驟
- 導入 knife4j 的maven坐標
在pom.xml中添加依賴
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
- 在配置類中加入 knife4j 相關配置
WebMvcConfiguration.java
/**
* 通過knife4j生成介面文檔
* @return
*/
@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;
}
- 設置靜態資源映射,否則介面文檔頁面無法訪問
WebMvcConfiguration.java
/**
* 設置靜態資源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
- 訪問測試
介面文檔訪問路徑為 http://ip:port/doc.html ---> http://localhost:8080/doc.html
4.3 常用註解
通過註解可以控制生成的介面文檔,使介面文檔擁有更好的可讀性,常用註解如下:
註解 | 說明 |
---|---|
@Api | 用在類上,例如Controller,表示對類的說明 |
@ApiModel | 用在類上,例如entity、DTO、VO |
@ApiModelProperty | 用在屬性上,描述屬性信息 |
@ApiOperation | 用在方法上,例如Controller的方法,說明方法的用途、作用 |
接下來,使用上述註解,生成可讀性更好的介面文檔
在sky-pojo模塊中
EmployeeLoginDTO.java
package com.sky.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "員工登錄時傳遞的數據模型")
public class EmployeeLoginDTO implements Serializable {
@ApiModelProperty("用戶名")
private String username;
@ApiModelProperty("密碼")
private String password;
}
EmployeeLoginVo.java
package com.sky.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@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;
}
在sky-server模塊中
EmployeeController.java
package com.sky.controller.admin;
import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.EmployeeService;
import com.sky.utils.JwtUtil;
import com.sky.vo.EmployeeLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 員工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "員工相關介面")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登錄
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "員工登錄")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
//
}
/**
* 退出
*
* @return
*/
@PostMapping("/logout")
@ApiOperation("員工退出")
public Result<String> logout() {
return Result.success();
}
}
啟動服務:訪問http://localhost:8080/doc.html