项目导航站开发笔记
一、在公司做项目流程总览
- 需求分析
- 设计(概要 + 详细)
- 技术选型
- 项目初始化(新项目或在老项目中引入)
- Demo 编写
- 业务逻辑开发
- 单元测试
- 代码提交 & 评审
- 部署
- 上线发布
以上流程基于网络收集,不确保真实性
二、核心功能需求
- 登录 / 注册
- 用户管理(仅管理员可见)
- 注册用户校验(//TODO 部分用户限制)
- 项目展示与其增删改查
- 产品原型图及其开发文档请参阅
三、技术选型
前端:HTML、CSS、JavaScript;Vue3; Ant Design Pro of Vue;Pinia;
后端: Spring Boot;MyBatis-Plus;MySQL;JUnit 单元测试
部署平台:容器化部署
四、实现阶段
一:环境初始化 + 基础功能
1. 前端
- 安装 Node.js(建议 ≥14)
- 切换 npm 镜像源到腾讯源
npm config get registry #get命令查看registry
npm config set registry https://mirrors.cloud.tencent.com/npm/ #用set换为腾讯镜像
- 安装脚手架
npm install -g @vue/cli
- 检查是否安装成功
vue -V
- 创建项目
vue create navi-center-frontend-ui
- 选择自定义特性
- 选择以下特性
- 到这里就成功创建前端工程了
- 引入Ant Design Vue组件库
cd navi-center-frontend-ui
npm i --save ant-design-vue@4.x
- 改变入口文件main.ts,注册全局组件,到这里初始化就完成了:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/reset.css";
createApp(App).use(Antd).use(router).mount("#app");
2. 后端(本项目使用Java1.8)
配置 MySQL 8.0,并用 IDEA 验证连接
通过 Spring Initializr 或直接在 IDEA 中生成 Spring Boot 项目
添加依赖:
- Lombok(注解工具,生成 Get、Set方法等)
- Spring Boot DevTools(热部署)
- Spring Configuration Processor (可以支持 Spring 的ConfigrationProperties 等注解,读取属性文件时使用)
- MySQL Driver (MySQL 驱动)
- Spring Web (增加 Web 访问能力)
- 引入 MyBatis‑Plus,并配置 Mapper 扫描、数据库表结构
依赖项一览:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
<scope>compile</scope>
</dependency>
</dependencies>
- 配置项一览:
# 公共配置文件
spring:
application:
name: navi-center-backend
# DataSource Config
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user_center
username: root
password: root
# session 失效时间
session:
timeout: 86400
server:
port: 7002
servlet:
context-path: /api
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: isDelete
logic-delete-value: 1
logic-not-delete-value: 0
alioss:
endpoint: oss-cn.aliyuncs.com
access-key-id: LTAI5t***********919HFJ
access-key-secret: oWBu*********Kb85gr0F
bucket-name: qixiaoran-bucket
二:后端实现
1. 设计用户表结构
create table user
(
userName varchar(64) null comment '用户昵称',
id bigint auto_increment comment 'id'
primary key,
userAccount varchar(64) null comment '账号',
avatarUrl varchar(1024) null comment '用户头像',
gender tinyint null comment '性别',
userPassword varchar(128) not null comment '密码',
phone varchar(20) null comment '电话',
email varchar(128) null comment '邮箱',
userStatus int default 0 not null comment '状态 0 - 正常',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除',
userRole int default 0 not null comment '用户角色 0 - 普通用户 1 - 管理员'
)
comment '用户' charset = utf8mb4;
2. 后端项目基本结构搭建
2.1 项目目录结构整理
2.2 通过MyBatisX插件自动生成domain实体、mapper、mapper.xml、service、serviceImpl
2.3 右键表 => MyBatisX-Generator => Next => 勾选 MyBatis-Plus3 => 勾选 Comment(注释)、Actual Column(每一个列的实际列名)、 Model(生成 domain ) 、 Lombok(使用 Lombok 注解) => 勾选 mybatis-plus3 => Finish
2.4 注册流程:账户校验、密码加盐 md5 加密、写库
//篇幅有限,注册逻辑参考功能文档,这里不再赘述
- 注册接口
@PostMapping("/register")
public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
// 校验
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
String userName = userRegisterRequest.getUserName();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, userName)) {
return null;
}
long result = userService.userRegister(userAccount, userPassword, checkPassword, userName);
return ResultUtils.success(result);
}
controller层倾向于对请求参数本身的校验,不涉及业务逻辑(越少越好)
service层是对业务逻辑的校验(有可能被controller之外的类调用)
- service实现
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword, String userName) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, userName)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
}
if (userName.length() > 20) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名过长");
}
// 账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return -1;
}
// 密码和校验密码相同
if (!userPassword.equals(checkPassword)) {
return -1;
}
// 账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 3. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
user.setUserName(userName);
//设置默认头像
user.setAvatarUrl("https://qixiaoran-bucket.oss-cn-fuzhou.aliyuncs.com/134593c1-d405-49e8-a9bd-ec4445efab43.jpg");
boolean saveResult = this.save(user);
if (!saveResult) {
return -1;
}
return user.getId();
}
- 单元测试
@Test
void userRegister() {
//测试密码为空
String userAccount = "qixiaoran";
String userPassword = "";
String checkPassword = "12345678";
String userName = "柒小染"
long result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertEquals(-1, result);
//测试账号小于4位
userAccount = "la";
userPassword = "12345678";
checkPassword = "12345678";
userName = "柒小染"
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertEquals(-1, result);
//测试密码小于8位
userAccount = "qixiaoran";
userPassword = "123456";
checkPassword = "123456";
userName = "柒小染"
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertEquals(-1, result);
//测试账号含有特殊字符
String userAccount = "qixiaoran@ :;";
String userPassword = "12345678";
String checkPassword = "12345678";
String userName = "柒小染"
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertEquals(-1, result);
//测试密码和校验密码不相同
userAccount = "qixiaoran";
userPassword = "12345678";
checkPassword = "1234568889910";
userName = "柒小染"
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertEquals(-1, result);
//测试是否可以注册成功
userAccount = "qixiaoran";
userPassword = "12345678";
checkPassword = "12345678";
userName = "柒小染"
result = userService.userRegister(userAccount, userPassword, checkPassword);
Assert.assertTrue(result > 0);
}
2.5 登录流程:校验合法性、密码匹配、脱敏、添加 Admin 权限鉴权、登录态 session 存储
- 为了方便使用Session鉴权,添加常量类
- 新建包contant,新建UserContant接口(接口中的属性默认为public static),将常量都写到这里
/**
* 用户常量
*/
public interface UserConstant {
/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";
// 权限
/**
* 默认权限
*/
int DEFAULT_ROLE = 0;
/**
* 管理员权限
*/
int ADMIN_ROLE = 1;
}
设置登录态过期时间:
spring:
#session失效时间:一天
session:
timeout: 86400
- 登录接口
@PostMapping("/login")
public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR);
}
User user = userService.userLogin(userAccount, userPassword, request);
if (user == null) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
}
return ResultUtils.success(user);
}
- service实现
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 8) {
return null;
}
// 账户不能包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
return null;
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
queryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
// 用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
// 3. 用户脱敏
User safetyUser = getSafetyUser(user);
// 4. 记录用户的登录态
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, safetyUser);
return safetyUser;
}
- ①逻辑删除配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
- ②实体类字段加@TableLogic注解
/**
* 是否删除
*/
@TableLogic
private Integer isDelete;
接口测试
- 使用 IDEA 自带的测试工具
- 这里我使用apifox

2.6 后端用户管理接口CRUD(MyBatis-plus实现)
- 查询用户接口
@GetMapping("/search")
public BaseResponse<List<User>> searchUsers(String userName, HttpServletRequest request) {
if (!isAdmin(request)) {
// throw new BusinessException(ErrorCode.PARAMS_ERROR);
throw new BusinessException(ErrorCode.NO_AUTH);
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(userName)) {
queryWrapper.like("userName", userName);
}
List<User> userList = userService.list(queryWrapper);
List<User> list = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
return ResultUtils.success(list);
}
- 修改用户
@PutMapping("/edit")
public BaseResponse<Boolean> editUser(@RequestBody User user, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
if (user == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = userService.updateUser(user);
return ResultUtils.success(result);
}
- 文件删除
@Autowired
private AliOssUtil aliOssUtil;
/*文件上传*/
@PostMapping("/upload")
// @ApiOperation("文件上传")
public BaseResponse<String> upload(MultipartFile file) {
log.info("文件上传:{}", file);
try {
String originalFilename = file.getOriginalFilename();
String substring = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectString = UUID.randomUUID() + substring;
String upload = aliOssUtil.upload(file.getBytes(), objectString);
return ResultUtils.success(upload);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}
throw new BusinessException(ErrorCode.UPLOAD_FAILED);
}
- 用户删除
@PostMapping("/delete")
public BaseResponse<Boolean> deleteUser(@RequestBody long id, HttpServletRequest request) {
if (!isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = userService.removeById(id);
return ResultUtils.success(b);
}
2.7 总结
- 先做设计
- 代码实现
- 复用代码、提取公共逻辑/常量
三:前端实现
由于前端内容较多,请移步项目导航站开发笔记之前端实现
四:返回结构优化 + 异常统一处理
- 定义如下通用对象
- 通用返回工具类
ResultUtils封装 success 和 error
/**
* 返回工具类
*
*/
public class ResultUtils {
/**
* 成功
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}
/**
* 失败
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*/
public static BaseResponse error(int code, String message, String description) {
return new BaseResponse(code, null, message, description);
}
/**
* 失败
*/
public static BaseResponse error(ErrorCode errorCode, String message, String description) {
return new BaseResponse(errorCode.getCode(), null, message, description);
}
/**
* 失败
*/
public static BaseResponse error(ErrorCode errorCode, String description) {
return new BaseResponse(errorCode.getCode(), errorCode.getMessage(), description);
}
}
- 定义枚举
ErrorCode管理错误码和提示
/**
* 错误码
*/
public enum ErrorCode {
SUCCESS(0, "ok", ""),
PARAMS_ERROR(40000, "请求参数错误", ""),
NOT_ADMIN(40002, "当前用户非管理员", ""),
NULL_ERROR(40001, "请求数据为空", ""),
NOT_LOGIN(40100, "未登录", ""),
NO_AUTH(40101, "无权限", ""),
SYSTEM_ERROR(50000, "系统内部异常", ""),
UPLOAD_FAILED(40003, "文件上传失败","" );
private final int code;
/**
* 状态码信息
*/
private final String message;
/**
* 状态码描述(详情)
*/
private final String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}
- 通用返回体
BaseResponse<T>包含 code、data、msg、desc
/**
* 通用返回类
*/
@Data
public class BaseResponse<T> implements Serializable {
/**
* 状态码
*/
private int code;
/**
* 数据
*/
private T data;
/**
* 消息
*/
private String message;
/**
* 描述
*/
private String description;
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
public BaseResponse(int code, T data, String message) {
this(code, data, message, "");
}
public BaseResponse(int code, T data) {
this(code, data, "", "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getDescription());
}
}
实现全局异常拦截器,统一处理
BusinessException和RuntimeException- 业务异常类
public class BusinessException extends RuntimeException { /** * 异常码 */ private final int code; /** * 描述 */ private final String description; public BusinessException(String message, int code, String description) { super(message); this.code = code; this.description = description; } public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); this.description = errorCode.getDescription(); } public BusinessException(ErrorCode errorCode, String description) { super(errorCode.getMessage()); this.code = errorCode.getCode(); this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } }- 全局异常处理器
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public BaseResponse<?> businessExceptionHandler(BusinessException e) { log.error("businessException: " + e.getMessage(), e); return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription()); } @ExceptionHandler(RuntimeException.class) public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) { log.error("runtimeException", e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), ""); } }小知识:全局异常是如何工作?
- 容器启动时扫描所有
@ControllerAdvice类- 利用反射找出含有
@ExceptionHandler注解的方法 - 缓存到内部的
ExceptionHandlerMethodResolver映射表中
- 利用反射找出含有
- 异常发生时
- 遍历
handlerExceptionResolvers - 找到
ExceptionHandlerExceptionResolver处理该异常 - 根据异常类型,在映射表中找到对应的方法进行调用
- 遍历
- 方法被调用,返回值包装成 ResponseEntity 或 JSON
- 支持返回
ResponseEntity<?>、@ResponseBody等类型 - 如果是
@RestControllerAdvice注解,默认是 JSON 格式返回
- 支持返回
五:环境管理 + 部署
区分多个环境:本地、开发、测试、预发布、生产
为什么需要区分多个环境?
- 每个环节互不影响
- 为了区分不同的阶段:开发、测试、生产
- 对项目进行优化
- 本地日志级别
- 精简依赖(节省项目体积)
- 项目的环境/参数可以调整,比如JVM参数
针对不同环境做不同的事情。
多环境分类:
- 本地开发(自己的电脑):localhost(127.0.0.1)
- 开发环境(远程开发):大家连同一台机器,为了大家开发方便
- 测试环境(测试):开发/测试/产品人员使用,单元测试/性能测试/功能测试/系统集成测试,独立的数据库、独立的服务器
- 沙箱环境(实验环境):完全隔离的环境,为了做实验,测试某个功能
- 预发布环境(类似内测/体验服):基本和正式环境一致,使用正式数据库,更严谨、查出更多问题
- 正式环境(线上、公开对外访问的项目):尽量不要改动,保证上线前的代码是“完美”运行的
前端项目中使用
process.env.NODE_ENV环境变量来区分开发和生产环境,在request.ts中配置环境,如果需要添加环境变量,可以创建.env、.env.development或.env.production文件。
const myAxios = axios.create({
baseURL: process.env.NODE_ENV === "development" ? "http://localhost:7002" : "https://user.api.onavi.icu",
timeout: 10000,
withCredentials: true,
});
后端使用 Spring Profiles(
application-prod.yml)切换环境可在启动项目时添加启动变量
java -jar .\xxxx.jar --spring.profiles.active=prod
部署方式:
原始部署(NGINX + 后端 JAR 包)
- 在服务器root目录下创建services目录
mkdir services - 执行命令
curl -o nginx-1.22.1.tar.gz http://nginx.org/download/nginx-1.22.1.tar.gz下载到服务器 - 执行命令
tar -zvxf nginx-1.22.1.tar.gz解压 - 进入到
cd nginx-1.22.1/执行configure./configure - 安装https
yum install openssl openssl-devel -y - 再次执行执行configure
./configure - 设置系统配置参数
./configure --with-http_ssl_module --with-http_v2_module --with-stream - 编译,执行
make - 安装,
make install - 配置环境变量
vim /etc/profile在最后一行添加export PATH=$PATH:/usr/local/nginx/sbin - 重启:
source /etc/profile - 执行
netstat-ntlp或netstat -tunpl查看端口占用情况,可以发现nginx已经启动 - 修改
/usr/local/nginx/conf下的配置文件(最好先不要修改原始配置文件):复制一份配置文件cp nginx.conf nginx.default.conf - 前端项目build打包,压缩为dist.zip,上传到服务器,解压缩
unzip dist.zip - 修改配置文件
vim nginx.conf
server { listen 7001; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /root/services/dist; index index.html index.htm; }- 记得修改用户权限
chmod 755 /root chmod 755 /root/services chmod 755 /root/services/dist chmod 644 /root/services/dist/index.html- 重新加载nginx配置
nginx -s reload - 后端部分:
- 执行mvn编译打包构建:
mvn package-DskipTests(-DskipTests跳过测试) - 通过Xshell把
navi-center-backend-0.0.1-SNAPSHOT.jar复制到服务器上(或使用scp命令等) - 改变权限:
chmod a+x navi-center-backend-0.0.1-SNAPSHOT.jar - 执行:
java -jar ./navi-center-backend-0.0.1-SNAPSHOT.jar --spring.profile.active=prod - 后台执行:
nohup java -jar ./navi-center-backend-0.0.1-SNAPSHOT.jar --spring.profile.active=prod & - 执行
jobs命令即可看到当前运行的项目 - 执行
jps命令可以查看所有已经运行的java程序 - 执行
netstat -ntlp可以查看运行的端口和进程号
- 在服务器root目录下创建services目录
容器化部署
- 安装 Docker & Docker Compose
# 安装 Docker curl -fsSL https://get.docker.com | bash -s docker # 安装 Docker Compose(适用于 Linux) sudo curl -L "https://github.com/docker/compose/releases/download/2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # 验证 docker -v docker-compose -v- 目录结构(方便使用docker-compose配置)
project-root/ ├── backend/ │ ├── Dockerfile │ └── navi-center-backend-0.0.1-SNAPSHOT.jar ├── frontend/ │ ├── Dockerfile │ └── dist/ # 解压 dist.zip 后的目录 ├── nginx/ │ └── default.conf └── docker-compose.yml- 后端
Dockerfile
FROM openjdk:1.8.0-jdk-alpine VOLUME /tmp ARG JAR_FILE=navi-center-backend-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]- 前端
Dockerfile
FROM nginx:1.22.1 COPY dist/ /usr/share/nginx/html/ COPY ../nginx/default.conf /etc/nginx/conf.d/default.conf- 修改配置文件
vim nginx/default.conf
server { listen 7001; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:7002/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }docker-compose配置文件
version: '1' services: backend: build: context: ./backend container_name: backend ports: - "7002:7002" restart: always frontend: build: context: ./frontend container_name: frontend ports: - "80:7001" depends_on: - backend restart: always构建启动容器
docker-compose up -d --build验证运行状态
docker ps
1Panel面板部署
看完了上面的两种部署方式想必也是一个头两个大吧,许多东西完全不知道怎么配置又有什么意义,接下来的1Panel面板则是完全简化了这些部分
一台已安装 1Panel 的服务器(安装教程)
上传项目资源
- 登录 1Panel 后台
打开“系统->文件”,新建一个存放的文件夹上传:
navi-center-backend-0.0.1-SNAPSHOT.jardist/文件夹
打开「运行环境」 → 「Java」 → 「创建运行环境」
填写如下信息
- 打开「网站」 → 「网站」→ 「创建网站」→ 「静态网站」,填写如下信息
- 创建好后点击配置,进入网站目录,打开root目录,上传dist目录
运行目录设置为上传的
dist目录到这里想必你对三种部署方式都有了一定了解,这其中的知识我会在后面的文章中慢慢补充
欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com